@piagent/platform 1.5.5 → 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 +166 -0
- package/README.md +25 -21
- package/README.vi.md +34 -5
- package/adapters/web-frontend/profile.json +2 -4
- package/architecture/layers.json +18 -3
- package/benchmarks/deep-logic-v1/SPECIALIST_PROTOCOL.vi.md +122 -0
- package/benchmarks/deep-logic-v1/grade.mjs +272 -10
- package/benchmarks/deep-logic-v1/project/apps/admin/src/billing-summary.js +3 -0
- package/benchmarks/deep-logic-v1/project/benchmark-contract/public-smoke.test.js +166 -0
- package/benchmarks/deep-logic-v1/project/package.json +1 -1
- package/benchmarks/deep-logic-v1/project/packages/billing/src/close-period.js +3 -0
- package/benchmarks/deep-logic-v1/project/packages/billing/src/plan-timeline.js +3 -0
- package/benchmarks/deep-logic-v1/project/test/smoke.test.js +7 -1
- package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +3 -1
- package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/temporal-usage-billing-close.md +123 -0
- package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +2 -0
- package/benchmarks/deep-logic-v1/suite.json +51 -3
- package/benchmarks/deep-logic-v1/variant.mjs +22 -1
- package/benchmarks/production-v1/spend-control.v1.json +92 -0
- package/benchmarks/production-v1/suite.json +46 -3
- package/catalog/capabilities.json +18 -18
- package/docs/authority-manifest.md +1 -1
- package/docs/auto-delegation-policy.md +38 -73
- package/docs/command-reference-vietnamese.md +27 -35
- package/docs/context-engine.md +12 -4
- package/docs/context-window-policy.md +37 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +27 -13
- package/docs/languages.json +4 -4
- package/docs/model-options.md +8 -5
- package/docs/operator-manual-vietnamese.md +27 -45
- package/docs/project-adapters.md +2 -2
- package/docs/publishing-for-teams.md +10 -0
- package/docs/quality-benchmark.md +427 -62
- package/docs/release-install-policy.md +10 -9
- package/docs/runtime-harness-standard.md +3 -3
- package/docs/runtime-policy-design.md +4 -4
- package/docs/runtime-quality-baseline.md +13 -6
- package/docs/security-threat-model.md +2 -2
- 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 +3 -3
- package/docs/team-onboarding.md +2 -2
- package/docs/usage-observability.md +7 -2
- package/docs/vi/architecture.md +27 -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/session-live-state-v1.invalid.json +18 -0
- package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
- 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 +3 -3
- 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 +7 -6
- package/evals/fs5-pilot-protocol.v5.json +9 -7
- 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 +3 -1
- package/packages/piagent-core/README.md +13 -4
- package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
- package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
- package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
- package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
- package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
- package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
- package/packages/piagent-core/benchmark/benchmark-core.js +736 -664
- package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
- package/packages/piagent-core/benchmark/benchmark-forensics.js +441 -4
- package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
- package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
- package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
- package/packages/piagent-core/benchmark/benchmark-preflight.js +5 -1
- package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
- package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
- package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.js +252 -0
- package/packages/piagent-core/benchmark/benchmark-report.js +130 -12
- package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
- package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
- package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
- package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +979 -0
- package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
- package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
- package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
- package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
- package/packages/piagent-core/benchmark/benchmark-suite.js +149 -2
- package/packages/piagent-core/benchmark/benchmark-summary-support.js +380 -0
- package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
- package/packages/piagent-core/benchmark/benchmark-usage.js +322 -0
- package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
- package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
- package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
- package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
- package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
- package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
- package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
- package/packages/piagent-core/extensions/acceptance-receipt.js +123 -59
- package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
- package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
- package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
- package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
- package/packages/piagent-core/extensions/context-engine.js +149 -156
- package/packages/piagent-core/extensions/context-evidence.js +48 -0
- package/packages/piagent-core/extensions/context-import-links.js +296 -0
- 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/context-telemetry-integrity.js +65 -0
- package/packages/piagent-core/extensions/criterion-context-pack.js +435 -7
- package/packages/piagent-core/extensions/criterion-graph.js +82 -9
- package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
- package/packages/piagent-core/extensions/guard-types.ts +6 -0
- package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +420 -260
- package/packages/piagent-core/extensions/task-contract-view.js +17 -3
- package/packages/piagent-core/extensions/task-lifecycle.js +39 -2
- package/packages/piagent-core/extensions/task-state.js +109 -142
- package/packages/piagent-core/extensions/verification-intelligence.js +52 -13
- package/packages/piagent-core/extensions/workspace-evidence-roots.js +193 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +44 -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/context/context-delivery.ts +103 -0
- package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
- package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
- package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
- package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
- package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
- package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
- package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
- package/packages/piagent-core/runtime/hooks/completion-hook.ts +12 -12
- package/packages/piagent-core/runtime/hooks/input-hook.ts +17 -1
- package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +107 -3
- package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +134 -166
- package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
- package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +4 -2
- package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -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-compaction-history.ts +1 -1
- package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
- package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
- package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +70 -0
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +73 -9
- package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
- package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
- package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
- 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/activity-inspector.ts +7 -2
- package/packages/piagent-core/runtime/product/efficiency-metrics.ts +7 -2
- package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
- package/packages/piagent-core/runtime/quality/exact-output-contract.ts +22 -11
- package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +9 -5
- package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
- package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
- package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
- 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 +60 -13
- package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
- package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
- package/packages/piagent-core/runtime/registration/context-commands.ts +82 -7
- 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 +50 -15
- package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
- package/packages/piagent-core/runtime/registration/task-completion-tools.ts +61 -25
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
- package/packages/piagent-core/runtime/registration/task-start-guidance.ts +169 -0
- package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
- package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +108 -103
- 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 +161 -5
- 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/apply-patch-tool.ts +500 -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/trajectory/trajectory-runtime.ts +2 -1
- 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 +207 -38
- package/packages/piagent-core/security/sensitive-data.js +13 -218
- package/packages/piagent-core/security/sensitive-project-file.js +42 -0
- package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
- package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
- package/packages/piagent-core/security/sensitive-text.js +234 -0
- 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 +7 -5
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-B4XehTCU.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-CgOEyRPG.js +176 -0
- 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-events.ts +109 -5
- package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +122 -20
- package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
- package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
- package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
- 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-start.ts +5 -1
- package/packages/piagent-webui/gateway/session-operation-watchdog.ts +165 -0
- package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +141 -125
- 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/loopback-server.ts +8 -0
- package/packages/piagent-webui/server/transcript-projection.ts +113 -9
- package/packages/piagent-webui/shared/text-visibility.ts +10 -0
- package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
- package/schemas/piagent-webui/catalog-v1.json +9 -0
- package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
- package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
- package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
- 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 +27 -1
- package/scripts/benchmark-runner-core.mjs +367 -363
- package/scripts/benchmark-runner-finalization.mjs +313 -0
- package/scripts/benchmark-runner-host-readiness.mjs +94 -0
- package/scripts/benchmark-runner-support.mjs +200 -0
- package/scripts/benchmark-session.mjs +95 -41
- package/scripts/configure-model-scope.sh +4 -3
- package/scripts/configure-subagents.sh +42 -34
- package/scripts/helper-evaluation.mjs +2 -2
- package/scripts/install-global.sh +2 -2
- package/scripts/pi-usage-history.mjs +37 -10
- package/scripts/setup.sh +2 -2
- package/scripts/team-doctor.sh +1 -1
- package/scripts/verify-local.sh +10 -1
- package/templates/global/settings.json +2 -1
- package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
import { currentFileContentDigests } from "./model-mutation-proof.ts";
|
|
5
|
+
|
|
6
|
+
export type EditFreshnessMode = "off" | "observe" | "enforce";
|
|
7
|
+
export type EditFreshnessSource = "read" | "mutation";
|
|
8
|
+
export type EditFreshnessEvaluation = {
|
|
9
|
+
decision: "off" | "unobserved" | "current" | "stale";
|
|
10
|
+
checkedPaths: string[];
|
|
11
|
+
stalePaths: string[];
|
|
12
|
+
enforce: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type SessionContext = Pick<ExtensionContext, "cwd" | "sessionManager">;
|
|
16
|
+
type Snapshot = { digest: string; taskRunId: string; source: EditFreshnessSource; recordedAt: string };
|
|
17
|
+
type SessionState = { identity: string; snapshots: Map<string, Snapshot> };
|
|
18
|
+
|
|
19
|
+
const MAX_SNAPSHOTS_PER_SESSION = 256;
|
|
20
|
+
|
|
21
|
+
function normalizeProjectPath(cwd: string, candidate: string): string | undefined {
|
|
22
|
+
if (!candidate || candidate.includes("\0")) return undefined;
|
|
23
|
+
const absolute = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(cwd, candidate);
|
|
24
|
+
const relative = path.relative(cwd, absolute).split(path.sep).join("/");
|
|
25
|
+
return relative && relative !== "." && relative !== ".." && !relative.startsWith("../") && !path.isAbsolute(relative)
|
|
26
|
+
? relative
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function editFreshnessModeFromEnvironment(value: unknown): EditFreshnessMode {
|
|
31
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
32
|
+
if (["0", "false", "no", "off", "disabled"].includes(normalized)) return "off";
|
|
33
|
+
if (["observe", "shadow", "advisory"].includes(normalized)) return "observe";
|
|
34
|
+
return "enforce";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Binds an edit to the complete file version last delivered by a successful
|
|
39
|
+
* read (or produced by a successful Piagent mutation). Files that have no
|
|
40
|
+
* observed snapshot retain the host tool's existing exact-anchor policy; the
|
|
41
|
+
* pilot only rejects proven drift, so it cannot invent a read requirement.
|
|
42
|
+
*/
|
|
43
|
+
export class EditFreshnessGuard {
|
|
44
|
+
readonly #mode: EditFreshnessMode;
|
|
45
|
+
readonly #sessions = new WeakMap<SessionContext["sessionManager"], SessionState>();
|
|
46
|
+
|
|
47
|
+
constructor(mode: EditFreshnessMode = "enforce") { this.#mode = mode; }
|
|
48
|
+
|
|
49
|
+
#identity(ctx: SessionContext): string {
|
|
50
|
+
return `${path.resolve(ctx.cwd)}\0${ctx.sessionManager.getSessionId()}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#session(ctx: SessionContext): SessionState {
|
|
54
|
+
const identity = this.#identity(ctx), existing = this.#sessions.get(ctx.sessionManager);
|
|
55
|
+
if (existing?.identity === identity) return existing;
|
|
56
|
+
const created = { identity, snapshots: new Map<string, Snapshot>() };
|
|
57
|
+
this.#sessions.set(ctx.sessionManager, created);
|
|
58
|
+
return created;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
observe(
|
|
62
|
+
ctx: SessionContext,
|
|
63
|
+
taskRunId: string | undefined,
|
|
64
|
+
targetPaths: string[],
|
|
65
|
+
source: EditFreshnessSource,
|
|
66
|
+
recordedAt = new Date().toISOString()
|
|
67
|
+
): string[] {
|
|
68
|
+
if (this.#mode === "off" || !taskRunId) return [];
|
|
69
|
+
const normalized = [...new Set(targetPaths.map((item) => normalizeProjectPath(ctx.cwd, item)).filter(Boolean) as string[])];
|
|
70
|
+
const digests = currentFileContentDigests(ctx.cwd, normalized), session = this.#session(ctx), observed: string[] = [];
|
|
71
|
+
for (const file of normalized) {
|
|
72
|
+
const digest = digests[file];
|
|
73
|
+
if (!digest) continue;
|
|
74
|
+
session.snapshots.delete(file);
|
|
75
|
+
session.snapshots.set(file, { digest, taskRunId, source, recordedAt });
|
|
76
|
+
observed.push(file);
|
|
77
|
+
}
|
|
78
|
+
while (session.snapshots.size > MAX_SNAPSHOTS_PER_SESSION) {
|
|
79
|
+
session.snapshots.delete(session.snapshots.keys().next().value as string);
|
|
80
|
+
}
|
|
81
|
+
return observed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
evaluate(ctx: SessionContext, taskRunId: string | undefined, targetPaths: string[]): EditFreshnessEvaluation {
|
|
85
|
+
if (this.#mode === "off") return { decision: "off", checkedPaths: [], stalePaths: [], enforce: false };
|
|
86
|
+
if (!taskRunId) return { decision: "unobserved", checkedPaths: [], stalePaths: [], enforce: this.#mode === "enforce" };
|
|
87
|
+
const session = this.#session(ctx);
|
|
88
|
+
const tracked = [...new Set(targetPaths.map((item) => normalizeProjectPath(ctx.cwd, item)).filter(Boolean) as string[])]
|
|
89
|
+
.filter((file) => session.snapshots.get(file)?.taskRunId === taskRunId);
|
|
90
|
+
if (tracked.length === 0) return { decision: "unobserved", checkedPaths: [], stalePaths: [], enforce: this.#mode === "enforce" };
|
|
91
|
+
const current = currentFileContentDigests(ctx.cwd, tracked);
|
|
92
|
+
const stalePaths = tracked.filter((file) => current[file] !== session.snapshots.get(file)?.digest);
|
|
93
|
+
return {
|
|
94
|
+
decision: stalePaths.length > 0 ? "stale" : "current",
|
|
95
|
+
checkedPaths: tracked,
|
|
96
|
+
stalePaths,
|
|
97
|
+
enforce: this.#mode === "enforce"
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
clear(ctx: SessionContext): void { this.#sessions.delete(ctx.sessionManager); }
|
|
102
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { durableContextEvidenceEntries } from "../../extensions/context-evidence.js";
|
|
4
5
|
import type { TaskContract } from "../../extensions/guard-types.js";
|
|
6
|
+
import { operatorRequestDigest } from "../../extensions/task-state.js";
|
|
5
7
|
|
|
6
8
|
const MAX_EVIDENCE_BYTES = 256 * 1024;
|
|
7
9
|
const EXACT_OUTPUT_TEMPLATE = /\b([A-Z][A-Z0-9_]{2,63})\s*=\s*<([a-z][a-z0-9_-]{1,31})>/g;
|
|
@@ -10,6 +12,7 @@ export type ExactOutputContractEvaluation = {
|
|
|
10
12
|
applicable: boolean;
|
|
11
13
|
passed: boolean;
|
|
12
14
|
key?: string;
|
|
15
|
+
reason?: "exact-output-evidence-missing" | "exact-output-evidence-ambiguous";
|
|
13
16
|
expectedLines: string[];
|
|
14
17
|
evidencePaths: string[];
|
|
15
18
|
};
|
|
@@ -29,6 +32,15 @@ function exactOutputDirective(text: string): { key: string; placeholder: string
|
|
|
29
32
|
return undefined;
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
type ExactOutputTaskText = Pick<TaskContract, "summary" | "expectedOutput" | "acceptanceCriteria" | "operatorRequest" | "operatorRequestDigest">;
|
|
36
|
+
|
|
37
|
+
function authoritativeTaskText(input: string | ExactOutputTaskText): string {
|
|
38
|
+
if (typeof input === "string") return input;
|
|
39
|
+
if (typeof input.operatorRequest === "string"
|
|
40
|
+
&& input.operatorRequestDigest === operatorRequestDigest(input.operatorRequest)) return input.operatorRequest;
|
|
41
|
+
return [input.summary, input.expectedOutput, ...(input.acceptanceCriteria ?? [])].filter(Boolean).join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
function safeObservedFile(cwd: string, candidate: string): { path: string; text: string } | undefined {
|
|
33
45
|
try {
|
|
34
46
|
const absolute = path.resolve(cwd, candidate);
|
|
@@ -49,29 +61,25 @@ function valuesForKey(key: string, text: string): string[] {
|
|
|
49
61
|
return [...text.matchAll(assignment)].map((match) => match[1]);
|
|
50
62
|
}
|
|
51
63
|
|
|
52
|
-
export function exactFinalOutputGuidance(taskText: string): string[] {
|
|
53
|
-
const directive = exactOutputDirective(taskText);
|
|
64
|
+
export function exactFinalOutputGuidance(taskText: string | ExactOutputTaskText): string[] {
|
|
65
|
+
const directive = exactOutputDirective(authoritativeTaskText(taskText));
|
|
54
66
|
if (!directive) return [];
|
|
55
67
|
return [
|
|
56
|
-
`Exact final-output contract: make ${directive.key}=<${directive.placeholder}> the last non-empty response line. Copy the complete value verbatim from observed
|
|
68
|
+
`Exact final-output contract: make ${directive.key}=<${directive.placeholder}> the last non-empty response line. Copy the complete value verbatim from observed project evidence and self-check every character before handoff.`
|
|
57
69
|
];
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
export function evaluateExactFinalOutputContract(
|
|
61
|
-
task: Pick<TaskContract, "summary" | "expectedOutput" | "acceptanceCriteria" | "contextManifest">,
|
|
73
|
+
task: Pick<TaskContract, "summary" | "expectedOutput" | "acceptanceCriteria" | "contextManifest" | "operatorRequest" | "operatorRequestDigest">,
|
|
62
74
|
responseText: string,
|
|
63
75
|
cwd: string
|
|
64
76
|
): ExactOutputContractEvaluation {
|
|
65
|
-
const directive = exactOutputDirective(
|
|
66
|
-
task.summary,
|
|
67
|
-
task.expectedOutput,
|
|
68
|
-
...(task.acceptanceCriteria ?? [])
|
|
69
|
-
].filter(Boolean).join("\n"));
|
|
77
|
+
const directive = exactOutputDirective(authoritativeTaskText(task));
|
|
70
78
|
if (!directive) return { applicable: false, passed: true, expectedLines: [], evidencePaths: [] };
|
|
71
79
|
|
|
72
80
|
const evidencePaths: string[] = [];
|
|
73
81
|
const observedValues = new Set<string>();
|
|
74
|
-
for (const entry of task
|
|
82
|
+
for (const entry of durableContextEvidenceEntries(task)) {
|
|
75
83
|
const observed = safeObservedFile(cwd, entry.path);
|
|
76
84
|
if (!observed) continue;
|
|
77
85
|
const values = valuesForKey(directive.key, observed.text);
|
|
@@ -80,10 +88,13 @@ export function evaluateExactFinalOutputContract(
|
|
|
80
88
|
for (const value of values) observedValues.add(value);
|
|
81
89
|
}
|
|
82
90
|
if (observedValues.size === 0) {
|
|
83
|
-
return { applicable:
|
|
91
|
+
return { applicable: true, passed: false, key: directive.key, reason: "exact-output-evidence-missing", expectedLines: [], evidencePaths: [] };
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
const expectedLines = [...observedValues].sort().map((value) => `${directive.key}=${value}`);
|
|
95
|
+
if (expectedLines.length !== 1) {
|
|
96
|
+
return { applicable: true, passed: false, key: directive.key, reason: "exact-output-evidence-ambiguous", expectedLines, evidencePaths: [...new Set(evidencePaths)].sort() };
|
|
97
|
+
}
|
|
87
98
|
const finalLine = String(responseText ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).at(-1) ?? "";
|
|
88
99
|
return {
|
|
89
100
|
applicable: true,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const GRAPH_ORDER_PATTERNS = [
|
|
2
|
+
/\b(?:(?:dependenc(?:y|ies)|prerequisites?)[-\s]+(?:dag|graph|order(?:ing)?|relation(?:ship)?s?|direction|edges?|chains?)|(?:topological(?:ly)?\s+(?:sort(?:ed|ing)?|order(?:ed|ing)?)|dag(?:\s+(?:order(?:ing)?|edges?|schedule))?)|first[-\s]+(?:observed|seen|encountered)[-\s]+order|replay order|deduplicat(?:e|ion)[^.!?\n]{0,48}\border|before (?:its|their|the) dependents?)\b/i,
|
|
3
|
+
/\b(?:preserve|maintain|follow|respect|use)\b[^.!?\n]{0,24}\b(?:stable (?:input )?order|input order|stable order)\b[^.!?\n]{0,48}\b(?:schedul(?:e|ed|ing)|jobs?|tasks?|nodes?|steps?|dependents?|dependenc(?:y|ies)|prerequisites?|ranks?|ranked|priorit(?:y|ies)|ties?|tie[-\s]?break)\b|\b(?:schedul(?:e|ed|ing)|emit(?:s|ted|ting)?|process(?:es|ed|ing)?|ranks?|ranked|priorit(?:y|ies)|ties?|tie[-\s]?break)\b[^.!?\n]{0,48}\b(?:jobs?|tasks?|nodes?|steps?|dependents?)?[^.!?\n]{0,32}\b(?:stable (?:input )?order|input order|stable order)\b/i,
|
|
4
|
+
/\b(?:dependenc(?:y|ies)|prerequisites?|dag|graph|scheduler|jobs?|tasks?|nodes?)\b[^.!?\n]{0,64}\b(?:acyclic|cyclic|cycle detection|(?:detect|reject|prevent|avoid|report|forbid|form|contain|create)(?:s|ed|ing)?\s+cycles?)\b|\b(?:acyclic|cyclic|cycle detection|(?:detect|reject|prevent|avoid|report|forbid|form|contain|create)(?:s|ed|ing)?\s+cycles?|cycles?\s+(?:in|among|through))\b[^.!?\n]{0,64}\b(?:dependenc(?:y|ies)|prerequisites?|dag|graph|scheduler|jobs?|tasks?|nodes?)\b/i,
|
|
5
|
+
/\b(?:run|execute|start|schedule|process|emit|appear)\b[^.!?\n]{0,48}\bafter\b[^.!?\n]{0,48}\b(?:(?:all|each|every|its|their|the)\s+)?(?:dependenc(?:y|ies)|prerequisites?)\s+(?:(?:must|should|shall|has|have|had)\s+)?(?:complete[sd]?|finish(?:ed|es)?|resolve[sd]?|ran|executed)\b/i,
|
|
6
|
+
/\b(?:run|execute|start|schedule|process|emit|begin|appear)\b[^.!?\n]{0,48}\b(?:only\s+)?(?:once|when)\b[^.!?\n]{0,48}\b(?:all|each|every|its|their|the)?\s*(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,32}\b(?:complete[sd]?|finish(?:ed|es)?|resolve[sd]?|ran|executed)\b/i,
|
|
7
|
+
/\b(?:run|execute|start|schedule|process|emit|begin|appear)\b[^.!?\n]{0,48}\buntil\b[^.!?\n]{0,48}\b(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,32}\b(?:complete[sd]?|finish(?:ed|es)?|resolve[sd]?|ran|executed)\b/i,
|
|
8
|
+
/\b(?:jobs?|tasks?|nodes?|steps?|dependents?)\b[^.!?\n]{0,48}\bwait(?:s|ed|ing)?\s+for\b[^.!?\n]{0,32}\b(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,32}\b(?:complete|finish|resolve)\b/i,
|
|
9
|
+
/\bwait\s+for\b[^.!?\n]{0,32}\b(?:(?:all|each|every|its|their|the)\s+)?(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,32}\bbefore\b[^.!?\n]{0,32}\b(?:run|running|execute|executing|start|starting|schedule|scheduling|process|processing|emit|emitting)\b[^.!?\n]{0,32}\b(?:jobs?|tasks?|nodes?|steps?|dependents?)\b/i,
|
|
10
|
+
/\b(?:(?:each|every|all|the)\s+)?(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,48}\b(?:complete|finish|resolve)(?:s|d|ed)?\b[^.!?\n]{0,24}\b(?:before|prior\s+to)\b[^.!?\n]{0,24}\b(?:(?:run|running|execute|executing|start|starting|schedule|scheduling|process|processing|emit|emitting)\s+)?(?:the\s+)?(?:(?:dependent\s+)?(?:job|task|node|step)s?|dependents?)\b/i,
|
|
11
|
+
/\b(?:jobs?|tasks?|nodes?|steps?|dependents?)\b[^.!?\n]{0,32}\b(?:eligible|ready)\b[^.!?\n]{0,24}\b(?:when|once)\b[^.!?\n]{0,32}\b(?:each|every|all|the|its|their)?\s*(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,24}\b(?:is|are|be|become(?:s)?|remain(?:s)?)?\s*(?:done|complete[sd]?|finish(?:ed|es)?|resolve[sd]?)\b/i,
|
|
12
|
+
/\b(?:run|execute|process|resolve|complete|schedule)\s+(?:each|every|all|the)\s+(?:dependenc(?:y|ies)|prerequisites?)\s+first\b/i,
|
|
13
|
+
/\b(?:(?:each|every|all|the)\s+)?(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,48}\b(?:precede(?:s)?|come(?:s)?\s+before|run(?:s)?\s+before|execute(?:s)?\s+before|finish(?:es)?\s+before|complete(?:s|d)?\s+before)\b[^.!?\n]{0,32}\b(?:the\s+)?(?:(?:dependent\s+)?(?:job|task|node|step)s?|dependents?)\b/i,
|
|
14
|
+
/\bdepends?\s+on\b[^.!?\n]{0,64}\b(?:run|runs|ran|execute|executes|executed|start|starts|started|schedule|schedules|scheduled|process|processes|processed|emit|emits|emitted)\b[^.!?\n]{0,24}\b(?:after|afterward|afterwards|later)\b/i,
|
|
15
|
+
/\bdepends?\s+on\b[^.!?\n]{0,64}\b(?:run|execute|start|schedule|process|emit)(?:s|d|ed|ing)?\b[^.!?\n]{0,24}\b[^.!?\n]{0,24}\bfirst\b/i,
|
|
16
|
+
/\b(?:run|execute|start|schedule|process|emit)(?:s|d|ed|ing)?\b[^.!?\n]{0,32}\b(?:dependenc(?:y|ies)|prerequisites?)\b[^.!?\n]{0,24}\bfollowed\s+by\b[^.!?\n]{0,24}\b(?:(?:the\s+)?dependent|(?:the\s+)?(?:job|task|node|step)s?)\b/i,
|
|
17
|
+
/\b(?:run|execute|start|schedule|process|emit)(?:s|d|ed|ing)?\b[^.!?\n]{0,48}\bbefore\b[^.!?\n]{0,48}\bdepends?\s+on\b/i,
|
|
18
|
+
/\b(?:do\s+not|don't|never)\s+(?:run|execute|start|schedule|process|emit|begin)\b[^.!?\n]{0,48}\b(?:jobs?|tasks?|nodes?|steps?|dependents?)\b[^.!?\n]{0,48}\bbefore\b[^.!?\n]{0,32}\b(?:dependenc(?:y|ies)|prerequisites?)\b/i
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function hasGraphOrderContract(request: string): boolean {
|
|
22
|
+
return GRAPH_ORDER_PATTERNS.some((pattern) => pattern.test(request));
|
|
23
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { TaskContract } from "../../extensions/guard-types.ts";
|
|
|
2
2
|
import { acceptanceBoundaryProofGuidance } from "../../extensions/acceptance-boundary-guidance.js";
|
|
3
3
|
import { normalizePathCandidate } from "../../extensions/policy-core.js";
|
|
4
4
|
import { acceptanceLanguageAdapterStatus } from "../../extensions/acceptance-language-adapters.js";
|
|
5
|
+
import { hasGraphOrderContract } from "./performance-assurance-signals.ts";
|
|
5
6
|
import {
|
|
6
7
|
MAX_PERFORMANCE_MUTATIONS_PER_REVISION,
|
|
7
8
|
MAX_PERFORMANCE_REPAIR_PATHS,
|
|
@@ -55,8 +56,7 @@ const SIGNALS: Array<{ code: string; pattern: RegExp }> = [
|
|
|
55
56
|
{ code: "boundary-contract", pattern: /\b(?:backpressure|boundary|budget|capacity|ceil(?:ing)?|clamp|inclusive|minimum|maximum|maxchars|overflow|round(?:ing)?|safe[-\s]+integer|basis points?|positive integer|non-negative|falsey|falsy|nullish)\b/i },
|
|
56
57
|
{ code: "return-shape-contract", pattern: /\b(?:return shape|returned? (?:element|object|value|representation)s?|preserve (?:the )?return)\b/i },
|
|
57
58
|
{ code: "public-api-contract", pattern: /\b(?:exported api|public api|do not change (?:the )?(?:api|signature)|preserve (?:the )?(?:api|input|output|return)|without changing unrelated behavior)\b/i },
|
|
58
|
-
{ code: "
|
|
59
|
-
{ code: "identity-isolation-contract", pattern: /\b(?:tenant|authorization|permission|cache[- ]?key|collision|isolation|same tuple|cross[- ]tenant)\b/i },
|
|
59
|
+
{ code: "identity-isolation-contract", pattern: /\b(?:auth(?:orization)?|unauthoriz(?:ed|ation)|permission|cache[- ]?key|collision|same tuple|cross[- ]tenant|same[- ]tenant|tenant boundary|tenant[- ]scoped (?:cache|storage))\b/i },
|
|
60
60
|
{ code: "concurrency-contract", pattern: /\b(?:concurren|race condition|stale response|bounded retry|idempot(?:ent|ency)?|lock)\b/i },
|
|
61
61
|
{ code: "data-migration-contract", pattern: /\b(?:migration|schema|money|invoice|ledger|transaction|data loss)\b/i },
|
|
62
62
|
{ code: "non-mutation-contract", pattern: /\b(?:do not mutate|must not mutate|without mutating|input unchanged|preserve input)\b/i }
|
|
@@ -97,6 +97,10 @@ export function analyzePerformanceAssurance(input: AssuranceInput): PerformanceA
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const reasonCodes = SIGNALS.filter((signal) => signal.pattern.test(request)).map((signal) => signal.code);
|
|
100
|
+
if (hasGraphOrderContract(request)) reasonCodes.push("graph-order-contract");
|
|
101
|
+
if (/\b(?:tenants?|tenantid)\b/i.test(request) && /\bisolat(?:e|ed|es|ing|ion)\b/i.test(request)) {
|
|
102
|
+
reasonCodes.push("identity-isolation-contract");
|
|
103
|
+
}
|
|
100
104
|
if (request.length >= 320) reasonCodes.push("long-contract");
|
|
101
105
|
if (namedEntrypoints(request) > 1) reasonCodes.push("multiple-entrypoints");
|
|
102
106
|
const exactnessSignals = reasonCodes.filter((code) => [
|
|
@@ -176,7 +180,7 @@ export function performanceReviewGuidance(task: TaskContract): string[] {
|
|
|
176
180
|
? [`Review the pre-change and current behavior for: ${changedFiles.slice(0, 12).join(", ")}.`]
|
|
177
181
|
: []),
|
|
178
182
|
...plan.reviewChecks,
|
|
179
|
-
"If no contradiction is found, stop reviewing and complete immediately. If one is found, name it, make the coordinated
|
|
183
|
+
"If no contradiction is found, stop reviewing and complete immediately. If one is found, name it, make the coordinated evidence-backed source/test edits required for one bounded revision, then run the exact configured verifier. A high-confidence verifier failure may open one final corrective revision."
|
|
180
184
|
];
|
|
181
185
|
}
|
|
182
186
|
|
|
@@ -485,11 +489,11 @@ export function performanceReviewToolDecision(input: PerformanceReviewToolInput)
|
|
|
485
489
|
}
|
|
486
490
|
return {
|
|
487
491
|
block: true,
|
|
488
|
-
reason: "The failed verifier opened a bounded corrective revision. Apply only the evidence-backed
|
|
492
|
+
reason: "The failed verifier opened a bounded corrective revision. Apply only the evidence-backed correction before re-running the exact verifier."
|
|
489
493
|
};
|
|
490
494
|
}
|
|
491
495
|
return {
|
|
492
496
|
block: true,
|
|
493
|
-
reason: "This semantic-review continuation is limited to bounded diff/read inspection, reviewed
|
|
497
|
+
reason: "This semantic-review continuation is limited to bounded diff/read inspection, reviewed evidence-backed edits, and one exact verifier per bounded revision."
|
|
494
498
|
};
|
|
495
499
|
}
|
|
@@ -52,7 +52,7 @@ function normalizedEvidenceDigest(value: string | null | undefined): string | nu
|
|
|
52
52
|
|
|
53
53
|
export function continuationClassForRecovery(decision: RecoveryDecision): ContinuationClass {
|
|
54
54
|
if (["permission-policy", "scope-protected-path", "environment"].includes(decision.failureCategory)
|
|
55
|
-
|| decision.reasonCodes.some((code) => ["permission-expansion-forbidden", "scope-replan-required", "operator-environment-action", "dependency-mutation-not-authorized"].includes(code))) return "policy-blocked";
|
|
55
|
+
|| decision.reasonCodes.some((code) => ["permission-expansion-forbidden", "protected-path-forbidden", "scope-replan-required", "operator-environment-action", "dependency-mutation-not-authorized"].includes(code))) return "policy-blocked";
|
|
56
56
|
if (decision.action === "repair") return "source-repair";
|
|
57
57
|
if (decision.failureCategory === "provider-network") return "model-retry";
|
|
58
58
|
if (decision.failureCategory === "flaky-infrastructure") return "infrastructure-retry";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { shouldIndexPath } from "../../extensions/context-engine.js";
|
|
6
|
+
import { matchesProtectedPath } from "../../extensions/policy-core.js";
|
|
7
|
+
import { redactSensitiveProjectFileText, redactSensitiveText } from "../../security/sensitive-data.js";
|
|
8
|
+
import type { ToolFailureReasonCode } from "../inspection/tool-failure-classification.ts";
|
|
9
|
+
|
|
10
|
+
const MAX_RECOVERY_FILE_BYTES = 8_192;
|
|
11
|
+
const PRIVATE_PATH = /(?:^|\/)(?:\.git|\.pi|node_modules)(?:\/|$)|(?:^|\/)\.env(?:\.|$)|(?:^|\/)(?:auth|credentials?|secrets?|tokens?)\.json$/i;
|
|
12
|
+
|
|
13
|
+
export type EditRecoveryContext = {
|
|
14
|
+
key: string;
|
|
15
|
+
targetPath: string;
|
|
16
|
+
text: string;
|
|
17
|
+
contentHash: string;
|
|
18
|
+
originalChars: number;
|
|
19
|
+
redacted: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function projectRelativeTarget(cwd: string, supplied: string): { root: string; absolute: string; relative: string } | undefined {
|
|
23
|
+
try {
|
|
24
|
+
const root = fs.realpathSync.native(cwd);
|
|
25
|
+
const suppliedAbsolute = path.resolve(root, supplied);
|
|
26
|
+
const suppliedRelative = path.relative(root, suppliedAbsolute).split(path.sep).join("/");
|
|
27
|
+
if (!suppliedRelative || suppliedRelative === "." || suppliedRelative === ".."
|
|
28
|
+
|| suppliedRelative.startsWith("../") || path.isAbsolute(suppliedRelative)) return undefined;
|
|
29
|
+
let cursor = root;
|
|
30
|
+
for (const segment of suppliedRelative.split("/")) {
|
|
31
|
+
cursor = path.join(cursor, segment);
|
|
32
|
+
if (fs.lstatSync(cursor).isSymbolicLink()) return undefined;
|
|
33
|
+
}
|
|
34
|
+
const absolute = fs.realpathSync.native(suppliedAbsolute);
|
|
35
|
+
const relative = path.relative(root, absolute).split(path.sep).join("/");
|
|
36
|
+
if (!relative || relative === "." || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) return undefined;
|
|
37
|
+
return { root, absolute, relative };
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stableSmallTextFile(root: string, absolute: string): string | undefined {
|
|
44
|
+
let descriptor: number | undefined;
|
|
45
|
+
try {
|
|
46
|
+
const initial = fs.lstatSync(absolute);
|
|
47
|
+
if (!initial.isFile() || initial.isSymbolicLink() || initial.size > MAX_RECOVERY_FILE_BYTES) return undefined;
|
|
48
|
+
const real = fs.realpathSync.native(absolute);
|
|
49
|
+
if (real !== root && !real.startsWith(`${root}${path.sep}`)) return undefined;
|
|
50
|
+
descriptor = fs.openSync(real, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
51
|
+
const observed = fs.fstatSync(descriptor);
|
|
52
|
+
if (!observed.isFile() || observed.dev !== initial.dev || observed.ino !== initial.ino
|
|
53
|
+
|| observed.size !== initial.size || observed.mtimeMs !== initial.mtimeMs || observed.ctimeMs !== initial.ctimeMs
|
|
54
|
+
|| observed.size > MAX_RECOVERY_FILE_BYTES) return undefined;
|
|
55
|
+
const buffer = Buffer.alloc(observed.size + 1);
|
|
56
|
+
let bytesRead = 0;
|
|
57
|
+
while (bytesRead < buffer.length) {
|
|
58
|
+
const count = fs.readSync(descriptor, buffer, bytesRead, buffer.length - bytesRead, null);
|
|
59
|
+
if (count === 0) break;
|
|
60
|
+
bytesRead += count;
|
|
61
|
+
}
|
|
62
|
+
const afterRead = fs.fstatSync(descriptor);
|
|
63
|
+
if (bytesRead !== observed.size || afterRead.dev !== observed.dev || afterRead.ino !== observed.ino
|
|
64
|
+
|| afterRead.size !== observed.size || afterRead.mtimeMs !== observed.mtimeMs || afterRead.ctimeMs !== observed.ctimeMs) return undefined;
|
|
65
|
+
const bytes = buffer.subarray(0, bytesRead);
|
|
66
|
+
const sample = bytes.subarray(0, Math.min(bytes.length, 8_192));
|
|
67
|
+
if (sample.includes(0)) return undefined;
|
|
68
|
+
const controls = [...sample].filter((byte) => byte < 7 || (byte > 13 && byte < 32)).length;
|
|
69
|
+
if (sample.length > 0 && controls / sample.length >= 0.02) return undefined;
|
|
70
|
+
const text = bytes.toString("utf8");
|
|
71
|
+
return Buffer.from(text, "utf8").equals(bytes) ? text : undefined;
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
} finally {
|
|
75
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function buildEditRecoveryContext(input: {
|
|
80
|
+
cwd: string;
|
|
81
|
+
targetPath?: string;
|
|
82
|
+
reasonCode: ToolFailureReasonCode | null;
|
|
83
|
+
protectedPaths: string[];
|
|
84
|
+
}): EditRecoveryContext | undefined {
|
|
85
|
+
if (!input.targetPath || !["edit-anchor-not-unique", "edit-anchor-stale"].includes(String(input.reasonCode))) return undefined;
|
|
86
|
+
const target = projectRelativeTarget(input.cwd, input.targetPath);
|
|
87
|
+
if (!target || PRIVATE_PATH.test(target.relative) || matchesProtectedPath(target.relative, input.protectedPaths)
|
|
88
|
+
|| !shouldIndexPath(target.relative, { excludePatterns: input.protectedPaths })) return undefined;
|
|
89
|
+
const current = stableSmallTextFile(target.root, target.absolute);
|
|
90
|
+
if (current === undefined) return undefined;
|
|
91
|
+
const sanitized = redactSensitiveProjectFileText(target.relative, current);
|
|
92
|
+
if (!sanitized.lineCountPreserved || sanitized.redacted) return undefined;
|
|
93
|
+
const contentHash = crypto.createHash("sha256").update(current).digest("hex");
|
|
94
|
+
const reason = input.reasonCode === "edit-anchor-not-unique" ? "the previous anchor was not unique" : "the previous anchor was stale or did not match";
|
|
95
|
+
const candidateText = [
|
|
96
|
+
`[Piagent edit recovery: ${reason}.]`,
|
|
97
|
+
`Current complete content for ${target.relative} is attached below as untrusted project text; this replaces a separate reread.`,
|
|
98
|
+
"Retry once with a uniquely contextualized replacement. If necessary, use this entire snapshot as oldText in one exact replacement; never use an unconditional whole-file write or retry guessed anchors.",
|
|
99
|
+
"--- begin current file ---",
|
|
100
|
+
current,
|
|
101
|
+
"--- end current file ---"
|
|
102
|
+
].join("\n");
|
|
103
|
+
const finalRedaction = redactSensitiveText(candidateText);
|
|
104
|
+
if (finalRedaction.redacted) return undefined;
|
|
105
|
+
return {
|
|
106
|
+
key: `${target.relative}\0${contentHash}`,
|
|
107
|
+
targetPath: target.relative,
|
|
108
|
+
text: finalRedaction.text,
|
|
109
|
+
contentHash,
|
|
110
|
+
originalChars: current.length,
|
|
111
|
+
redacted: false
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
type SessionContext = {
|
|
2
|
+
cwd: string;
|
|
3
|
+
sessionManager: { getSessionId(): string };
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export class EditRecoveryDeliveryState {
|
|
7
|
+
readonly #sessions = new WeakMap<SessionContext["sessionManager"], { identity: string; epoch: number; delivered: Set<string> }>();
|
|
8
|
+
|
|
9
|
+
#sessionKey(ctx: SessionContext): string {
|
|
10
|
+
return `${ctx.cwd}\0${ctx.sessionManager.getSessionId()}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#session(ctx: SessionContext): { identity: string; epoch: number; delivered: Set<string> } {
|
|
14
|
+
const identity = this.#sessionKey(ctx), existing = this.#sessions.get(ctx.sessionManager);
|
|
15
|
+
if (existing?.identity === identity) return existing;
|
|
16
|
+
const created = { identity, epoch: 0, delivered: new Set<string>() };
|
|
17
|
+
this.#sessions.set(ctx.sessionManager, created);
|
|
18
|
+
return created;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
reserve(ctx: SessionContext, taskRunId: string, recoveryKey: string): boolean {
|
|
22
|
+
const session = this.#session(ctx);
|
|
23
|
+
const key = `${session.epoch}\0${taskRunId}\0${recoveryKey}`;
|
|
24
|
+
if (session.delivered.has(key)) return false;
|
|
25
|
+
// Refuse further automatic recovery in this context epoch once the bounded
|
|
26
|
+
// allowance is exhausted. Evicting an old key would permit duplicate
|
|
27
|
+
// injection and make recovery tokens unbounded under repeated failures.
|
|
28
|
+
if (session.delivered.size >= 16) return false;
|
|
29
|
+
session.delivered.add(key);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
advanceEpoch(ctx: SessionContext): void {
|
|
34
|
+
const session = this.#session(ctx);
|
|
35
|
+
session.epoch += 1;
|
|
36
|
+
session.delivered.clear();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
clearSession(ctx: SessionContext): void {
|
|
40
|
+
this.#sessions.delete(ctx.sessionManager);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -211,6 +211,9 @@ function authority(recovery: RecoveryDecision | null): HandoffProjection["requir
|
|
|
211
211
|
const reasons = strings(recovery.reasonCodes, 20, 120);
|
|
212
212
|
if (recovery.action === "fresh-session") return { required: true, kind: "fresh-session", reasonCodes: reasons };
|
|
213
213
|
if (recovery.reasonCodes.includes("permission-expansion-forbidden")) return { required: true, kind: "permission", reasonCodes: reasons };
|
|
214
|
+
if (recovery.reasonCodes.includes("protected-path-forbidden")) return { required: true, kind: "permission", reasonCodes: reasons };
|
|
215
|
+
// Backward-compatible projection for recovery records written before task
|
|
216
|
+
// scope became advisory.
|
|
214
217
|
if (recovery.reasonCodes.includes("scope-replan-required")) return { required: true, kind: "scope", reasonCodes: reasons };
|
|
215
218
|
if (recovery.action === "ask-operator") return { required: true, kind: "operator", reasonCodes: reasons };
|
|
216
219
|
return { required: false, kind: "none", reasonCodes: reasons };
|
|
@@ -292,7 +295,7 @@ export function buildHandoffProjection(
|
|
|
292
295
|
decisionsAndInvariants: [
|
|
293
296
|
"Task Contract v2 is authoritative for task outcome.",
|
|
294
297
|
"Failure classification never authorizes source mutation by itself.",
|
|
295
|
-
"Source mutation still requires task scope
|
|
298
|
+
"Source mutation still requires hook authorization; task scope is an advisory retrieval/review focus.",
|
|
296
299
|
"Verifier evidence is current only when its namespaced working-tree digest matches the current tree."
|
|
297
300
|
],
|
|
298
301
|
contextReferences: {
|
|
@@ -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);
|