@piagent/platform 1.5.5 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +117 -0
- package/README.md +9 -8
- package/README.vi.md +9 -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 +68 -0
- package/benchmarks/production-v1/suite.json +42 -3
- package/catalog/capabilities.json +6 -6
- package/docs/command-reference-vietnamese.md +6 -6
- package/docs/context-engine.md +7 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +1 -0
- package/docs/languages.json +4 -4
- package/docs/model-options.md +8 -5
- package/docs/operator-manual-vietnamese.md +13 -11
- package/docs/project-adapters.md +2 -2
- package/docs/publishing-for-teams.md +5 -0
- package/docs/quality-benchmark.md +271 -62
- package/docs/release-install-policy.md +10 -9
- 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/task-lifecycle-vietnamese.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs/usage-observability.md +7 -2
- package/docs/vi/architecture.md +1 -0
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +1 -1
- 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 +1 -1
- 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 +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +1 -1
- 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 +1 -1
- 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 +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.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/fs4-readiness-matrix.v1.json +2 -2
- package/evals/fs5-causal-arm.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v4.json +6 -5
- package/evals/fs5-pilot-protocol.v5.json +7 -6
- package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
- package/package.json +3 -1
- 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.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
- package/packages/piagent-core/benchmark/benchmark-core.js +732 -661
- package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
- package/packages/piagent-core/benchmark/benchmark-forensics.js +439 -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 +3 -1
- package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
- package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.js +183 -0
- package/packages/piagent-core/benchmark/benchmark-report.js +116 -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-stage-diagnostic.js +782 -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 +128 -2
- package/packages/piagent-core/benchmark/benchmark-summary-support.js +376 -0
- package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
- package/packages/piagent-core/benchmark/benchmark-usage.js +311 -0
- package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
- package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -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 +34 -11
- 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 +148 -145
- 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-telemetry-integrity.js +65 -0
- package/packages/piagent-core/extensions/criterion-context-pack.js +422 -7
- package/packages/piagent-core/extensions/criterion-graph.js +78 -8
- package/packages/piagent-core/extensions/guard-types.ts +2 -0
- package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +233 -41
- package/packages/piagent-core/extensions/task-contract-view.js +17 -3
- package/packages/piagent-core/extensions/task-lifecycle.js +7 -2
- package/packages/piagent-core/extensions/task-state.js +40 -134
- package/packages/piagent-core/extensions/verification-intelligence.js +3 -1
- package/packages/piagent-core/extensions/workspace-evidence-roots.js +157 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +2 -0
- 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 +5 -6
- package/packages/piagent-core/runtime/hooks/input-hook.ts +11 -1
- package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +7 -2
- package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +84 -102
- 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/task-compaction-history.ts +1 -1
- package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
- package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +53 -0
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +34 -6
- 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/role-policy.ts +1 -1
- package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
- package/packages/piagent-core/runtime/product/efficiency-metrics.ts +4 -2
- package/packages/piagent-core/runtime/quality/exact-output-contract.ts +2 -1
- package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -2
- 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/semantic-repair-handshake.ts +55 -2
- package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
- package/packages/piagent-core/runtime/registration/context-commands.ts +66 -5
- package/packages/piagent-core/runtime/registration/policy-tools.ts +46 -11
- package/packages/piagent-core/runtime/registration/task-completion-tools.ts +49 -16
- package/packages/piagent-core/runtime/registration/task-start-guidance.ts +163 -0
- package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +67 -62
- package/packages/piagent-core/runtime/session/runtime-state.ts +154 -2
- package/packages/piagent-core/runtime/session/system-prompt.ts +4 -4
- package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
- package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
- package/packages/piagent-core/runtime/workflows/task-intake.ts +43 -2
- 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-worker.md +1 -1
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-CxeZqTte.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-DynIMe8I.js +176 -0
- package/packages/piagent-webui/dist/client/index.html +1 -1
- 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 +93 -7
- 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-start.ts +5 -1
- package/packages/piagent-webui/gateway/session-operation-watchdog.ts +158 -0
- package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +139 -124
- package/packages/piagent-webui/server/loopback-server.ts +8 -0
- package/packages/piagent-webui/server/transcript-projection.ts +22 -4
- package/packages/piagent-webui/shared/text-visibility.ts +10 -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/session-live-state-v1.schema.json +76 -0
- package/schemas/task-contract.schema.json +12 -1
- package/scripts/benchmark-runner-core.mjs +355 -359
- package/scripts/benchmark-runner-finalization.mjs +309 -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/helper-evaluation.mjs +1 -1
- package/scripts/install-global.sh +2 -2
- package/scripts/pi-usage-history.mjs +37 -10
- package/scripts/setup.sh +2 -2
- package/scripts/verify-local.sh +5 -1
- package/templates/global/settings.json +2 -1
- package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
4
|
import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
|
|
6
5
|
import { appendTaskJournalEvent, readTaskJournal } from "./task-journal.js";
|
|
7
6
|
import {
|
|
@@ -28,12 +27,25 @@ import {
|
|
|
28
27
|
unavailableWorkingTreeHash,
|
|
29
28
|
versionWorkingTreeHash
|
|
30
29
|
} from "./working-tree-digest.js";
|
|
30
|
+
import {
|
|
31
|
+
directChildGitEvidenceRoots,
|
|
32
|
+
gitEvidenceRootDetails,
|
|
33
|
+
gitEvidenceRoots,
|
|
34
|
+
gitOutput,
|
|
35
|
+
isGitWorkingTree,
|
|
36
|
+
nonGitWorkspaceFileDetails,
|
|
37
|
+
nonGitWorkspaceFiles,
|
|
38
|
+
normalizedGitPath,
|
|
39
|
+
pathWithinNonGitWorkspaceEvidenceRoot,
|
|
40
|
+
prefixedGitPath
|
|
41
|
+
} from "./workspace-evidence-roots.js";
|
|
42
|
+
export { isGitWorkingTree } from "./workspace-evidence-roots.js";
|
|
31
43
|
|
|
32
44
|
export const TASK_CONTRACT_SCHEMA_VERSION = 2; export const DEFAULT_MAX_TASK_ATTEMPTS = 3;
|
|
33
45
|
const TASK_OUTCOMES = ["pending", "completed", "blocked", "partial", "failed"], TERMINAL_TASK_OUTCOMES = new Set(["completed", "blocked", "partial", "failed"]);
|
|
34
46
|
const REVIEW_LENSES = ["correctness", "tests", "scope", "security", "docs", "release", "package"];
|
|
35
47
|
const TASK_CONTRACT_FIELDS = new Set([
|
|
36
|
-
"schemaVersion", "taskRunId", "taskId", "sessionId", "sessionName", "changeMode", "attempt", "maxAttempts",
|
|
48
|
+
"schemaVersion", "taskRunId", "taskId", "sessionId", "sessionName", "changeMode", "mutationPolicy", "attempt", "maxAttempts",
|
|
37
49
|
"previousAttempts", "summary", "riskLane", "intakeMode", "expectedOutput", "acceptanceCriteria", "criterionGraph", "scope", "outOfScope",
|
|
38
50
|
"protectedPaths", "requiredContext", "contextManifest", "memoryCitations", "mcpCapabilities", "verifyGroup",
|
|
39
51
|
"verifyCommands", "workPlan", "reviewLenses", "orchestration", "acceptanceReceipt", "authoritySnapshot", "workingTreeDigestAlgorithm", "workingTreeDigestMigration", "baselineChangedFiles", "baselineFileDigests",
|
|
@@ -47,14 +59,6 @@ const ORCHESTRATION_FIELDS = new Set(["mode", "subagents", "reason", "fieldGuide
|
|
|
47
59
|
const MODEL_ROLE_FIELDS = new Set(["planner", "worker", "reviewer", "watchdog"]);
|
|
48
60
|
const VERIFY_EVIDENCE_FIELDS = new Set(["command", "exitCode", "summary", "recordedAt", "observed", "observedAt", "isError", "matchedProfileCommand", "preWorkingTreeDigest", "workingTreeDigest"]);
|
|
49
61
|
const TRACE_FIELDS = new Set(["outcome", "friction", "notes", "recordedAt"]);
|
|
50
|
-
const WORKSPACE_EVIDENCE_MAX_FILES = 2000;
|
|
51
|
-
const WORKSPACE_EVIDENCE_SKIP_NAMES = new Set([
|
|
52
|
-
".git", ".hg", ".svn", ".pi", "node_modules", "dist", "build", ".next", "coverage"
|
|
53
|
-
]);
|
|
54
|
-
const WORKSPACE_EVIDENCE_PRIORITY_NAMES = new Map([
|
|
55
|
-
["plans", 0]
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
62
|
export function safeTaskId(value) {
|
|
59
63
|
const normalized = String(value ?? "")
|
|
60
64
|
.trim()
|
|
@@ -198,6 +202,8 @@ export function taskContractValidationErrors(input) {
|
|
|
198
202
|
}
|
|
199
203
|
if (input.failedAt !== undefined && !["research", "plan", "execute", "verify", "review"].includes(input.failedAt)) errors.push("failedAt is invalid");
|
|
200
204
|
if (!["source-change", "read-only"].includes(input.changeMode)) errors.push("changeMode is invalid");
|
|
205
|
+
if (input.mutationPolicy !== undefined && !["required", "forbidden"].includes(input.mutationPolicy)) errors.push("mutationPolicy is invalid");
|
|
206
|
+
if (input.changeMode === "read-only" && input.mutationPolicy === "required") errors.push("read-only tasks cannot require mutation");
|
|
201
207
|
if (!["tiny", "normal", "high-risk"].includes(input.riskLane)) errors.push("riskLane is invalid");
|
|
202
208
|
if (input.intakeMode !== undefined && !["model", "runtime"].includes(input.intakeMode)) errors.push("intakeMode is invalid");
|
|
203
209
|
errors.push(...taskDigestContractValidationErrors(input));
|
|
@@ -227,6 +233,7 @@ export function taskContractValidationErrors(input) {
|
|
|
227
233
|
}
|
|
228
234
|
if (Array.isArray(input.acceptanceCriteria) && input.acceptanceCriteria.length === 0) errors.push("acceptanceCriteria must not be empty");
|
|
229
235
|
if (Array.isArray(input.scope) && input.scope.length === 0) errors.push("scope must not be empty");
|
|
236
|
+
if (Array.isArray(input.scope) && input.scope.length > 2000) errors.push("scope must contain at most 2000 entries");
|
|
230
237
|
if (Array.isArray(input.previousAttempts) && input.previousAttempts.length > 10) errors.push("previousAttempts must contain at most 10 entries");
|
|
231
238
|
if (Array.isArray(input.workPlan) && input.workPlan.length > 12) errors.push("workPlan must contain at most 12 entries");
|
|
232
239
|
if (Array.isArray(input.reviewLenses)) {
|
|
@@ -390,6 +397,7 @@ export function normalizeTaskContract(input, options = {}) {
|
|
|
390
397
|
sessionId,
|
|
391
398
|
sessionName: String(input.sessionName ?? options.sessionName ?? "").trim() || undefined,
|
|
392
399
|
changeMode: input.changeMode === "read-only" ? "read-only" : "source-change",
|
|
400
|
+
mutationPolicy: input.changeMode === "read-only" || input.mutationPolicy === "forbidden" ? "forbidden" : "required",
|
|
393
401
|
attempt: positiveInteger(input.attempt, 1, 100),
|
|
394
402
|
maxAttempts: positiveInteger(input.maxAttempts, DEFAULT_MAX_TASK_ATTEMPTS, 10),
|
|
395
403
|
previousAttempts: Array.isArray(input.previousAttempts) ? input.previousAttempts.slice(-10).map((item) => pickFields(item, PREVIOUS_ATTEMPT_FIELDS)) : [],
|
|
@@ -576,13 +584,6 @@ export function summarizeAttempt(task) {
|
|
|
576
584
|
};
|
|
577
585
|
}
|
|
578
586
|
|
|
579
|
-
function gitOutput(cwd, args, options = {}) {
|
|
580
|
-
return execFileSync("git", ["-C", cwd, ...args], {
|
|
581
|
-
encoding: options.encoding ?? "utf8",
|
|
582
|
-
maxBuffer: options.maxBuffer ?? 16 * 1024 * 1024,
|
|
583
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
587
|
function gitHasHead(cwd) {
|
|
587
588
|
try {
|
|
588
589
|
gitOutput(cwd, ["rev-parse", "--verify", "HEAD"]);
|
|
@@ -591,20 +592,6 @@ function gitHasHead(cwd) {
|
|
|
591
592
|
return false;
|
|
592
593
|
}
|
|
593
594
|
}
|
|
594
|
-
export function isGitWorkingTree(cwd) {
|
|
595
|
-
try {
|
|
596
|
-
return gitOutput(cwd, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
|
|
597
|
-
} catch {
|
|
598
|
-
return false;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
function normalizedGitPath(value) {
|
|
602
|
-
return String(value ?? "").replaceAll("\\", "/").replace(/^\/+/, "");
|
|
603
|
-
}
|
|
604
|
-
function prefixedGitPath(prefix, file) {
|
|
605
|
-
const normalized = normalizedGitPath(file);
|
|
606
|
-
return prefix ? `${prefix}/${normalized}` : normalized;
|
|
607
|
-
}
|
|
608
595
|
|
|
609
596
|
function workingTreeFilesForGitRoot(cwd, observeRename) {
|
|
610
597
|
if (!isGitWorkingTree(cwd)) return [];
|
|
@@ -615,90 +602,8 @@ function workingTreeFilesForGitRoot(cwd, observeRename) {
|
|
|
615
602
|
return [...new Set([...changed, ...untracked].map(normalizedGitPath).filter((item) => item && !item.startsWith(".pi/piagent-state/")))].sort();
|
|
616
603
|
}
|
|
617
604
|
|
|
618
|
-
function directChildGitEvidenceRoots(cwd) {
|
|
619
|
-
const roots = [];
|
|
620
|
-
let entries;
|
|
621
|
-
try {
|
|
622
|
-
entries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
623
|
-
} catch {
|
|
624
|
-
return roots;
|
|
625
|
-
}
|
|
626
|
-
for (const entry of entries) {
|
|
627
|
-
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
628
|
-
const child = path.join(cwd, entry.name);
|
|
629
|
-
try {
|
|
630
|
-
if (fs.lstatSync(child).isSymbolicLink()) continue;
|
|
631
|
-
if (!isGitWorkingTree(child)) continue;
|
|
632
|
-
const topLevel = gitOutput(child, ["rev-parse", "--show-toplevel"]).trim();
|
|
633
|
-
if (fs.realpathSync(topLevel) !== fs.realpathSync(child)) continue;
|
|
634
|
-
roots.push({ cwd: child, prefix: normalizedGitPath(entry.name) });
|
|
635
|
-
} catch {
|
|
636
|
-
// Ignore unreadable or non-standard child folders; source-change start
|
|
637
|
-
// only needs at least one reliable evidence root.
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
return roots.sort((left, right) => left.prefix.localeCompare(right.prefix));
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function gitEvidenceRoots(cwd) {
|
|
644
|
-
if (isGitWorkingTree(cwd)) return [{ cwd, prefix: "" }];
|
|
645
|
-
return directChildGitEvidenceRoots(cwd);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function nonGitWorkspaceFiles(cwd, gitRootPrefixes) {
|
|
649
|
-
const files = [];
|
|
650
|
-
const gitRoots = new Set(gitRootPrefixes.filter(Boolean));
|
|
651
|
-
function orderedEntries(entries) {
|
|
652
|
-
return [...entries].sort((left, right) => {
|
|
653
|
-
const leftPriority = WORKSPACE_EVIDENCE_PRIORITY_NAMES.get(left.name.toLowerCase()) ?? 10;
|
|
654
|
-
const rightPriority = WORKSPACE_EVIDENCE_PRIORITY_NAMES.get(right.name.toLowerCase()) ?? 10;
|
|
655
|
-
if (leftPriority !== rightPriority) return leftPriority - rightPriority;
|
|
656
|
-
return left.name.localeCompare(right.name, "en-US");
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
function visit(directory, relativeDirectory) {
|
|
660
|
-
if (files.length >= WORKSPACE_EVIDENCE_MAX_FILES) return;
|
|
661
|
-
let entries;
|
|
662
|
-
try {
|
|
663
|
-
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
664
|
-
} catch {
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
for (const entry of orderedEntries(entries)) {
|
|
668
|
-
if (files.length >= WORKSPACE_EVIDENCE_MAX_FILES) return;
|
|
669
|
-
if (WORKSPACE_EVIDENCE_SKIP_NAMES.has(entry.name)) continue;
|
|
670
|
-
const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
|
|
671
|
-
const topSegment = relative.split("/")[0];
|
|
672
|
-
if (gitRoots.has(topSegment)) continue;
|
|
673
|
-
const absolute = path.join(directory, entry.name);
|
|
674
|
-
try {
|
|
675
|
-
if (fs.lstatSync(absolute).isSymbolicLink()) continue;
|
|
676
|
-
} catch {
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
if (entry.isDirectory()) {
|
|
680
|
-
visit(absolute, relative);
|
|
681
|
-
} else if (entry.isFile()) {
|
|
682
|
-
files.push(normalizedGitPath(relative));
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
visit(cwd, "");
|
|
687
|
-
return files.sort();
|
|
688
|
-
}
|
|
689
|
-
|
|
690
605
|
function nonGitWorkspaceFileDigest(cwd, file, protectedProjectPath = null) { return streamedWorkingTreeFileDigest(cwd, file, false, protectedProjectPath); }
|
|
691
606
|
|
|
692
|
-
function pathWithinNonGitWorkspaceEvidenceRoot(candidate, gitRootPrefixes) {
|
|
693
|
-
const topSegment = candidate.split("/")[0];
|
|
694
|
-
return Boolean(
|
|
695
|
-
topSegment
|
|
696
|
-
&& !WORKSPACE_EVIDENCE_SKIP_NAMES.has(topSegment)
|
|
697
|
-
&& !topSegment.startsWith(".")
|
|
698
|
-
&& !gitRootPrefixes.includes(topSegment)
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
607
|
export function hasGitEvidenceRoot(cwd) {
|
|
703
608
|
return gitEvidenceRoots(cwd).length > 0;
|
|
704
609
|
}
|
|
@@ -739,35 +644,36 @@ export function workingTreeFiles(cwd) {
|
|
|
739
644
|
return [];
|
|
740
645
|
}
|
|
741
646
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const limit = Number.isInteger(maxFiles) ? Math.max(1, Math.min(50_000, maxFiles)) : 10_000;
|
|
745
|
-
const files = [];
|
|
647
|
+
export function repositoryFileManifestDetails(cwd, maxFiles = 10_000) {
|
|
648
|
+
const limit = Number.isInteger(maxFiles) ? Math.max(1, Math.min(50_000, maxFiles)) : 10_000, files = new Set();
|
|
746
649
|
try {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
.
|
|
650
|
+
const rootDetails = gitEvidenceRootDetails(cwd), roots = rootDetails.roots;
|
|
651
|
+
const add = (candidate) => {
|
|
652
|
+
if (!candidate || candidate.startsWith(".pi/") || candidate.startsWith(".git/") || candidate.split("/").includes("node_modules")
|
|
653
|
+
|| /(?:^|\/)\.env(?:\.|$)/i.test(candidate) || /(?:^|\/)(?:auth|credentials?|secrets?|tokens?)\.json$/i.test(candidate)) return true;
|
|
654
|
+
files.add(candidate);
|
|
655
|
+
return files.size <= limit;
|
|
656
|
+
};
|
|
657
|
+
for (const root of roots) {
|
|
658
|
+
const listed = gitOutput(root.cwd, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]).split("\0").filter(Boolean);
|
|
751
659
|
for (const file of listed) {
|
|
752
660
|
const candidate = prefixedGitPath(root.prefix, file);
|
|
753
|
-
if (
|
|
754
|
-
!candidate
|
|
755
|
-
|| candidate.startsWith(".pi/")
|
|
756
|
-
|| candidate.startsWith(".git/")
|
|
757
|
-
|| candidate.split("/").includes("node_modules")
|
|
758
|
-
|| /(?:^|\/)\.env(?:\.|$)/i.test(candidate)
|
|
759
|
-
|| /(?:^|\/)(?:auth|credentials?|secrets?|tokens?)\.json$/i.test(candidate)
|
|
760
|
-
) continue;
|
|
761
|
-
files.push(candidate);
|
|
762
|
-
if (files.length >= limit) return [...new Set(files)].sort();
|
|
661
|
+
if (!add(candidate)) return { files: [...files].slice(0, limit).sort(), complete: false, candidateCount: files.size };
|
|
763
662
|
}
|
|
764
663
|
}
|
|
664
|
+
if (!isGitWorkingTree(cwd) && roots.length > 0) {
|
|
665
|
+
const looseDetails = nonGitWorkspaceFileDetails(cwd, roots.map((root) => root.prefix));
|
|
666
|
+
const looseFiles = looseDetails.files;
|
|
667
|
+
for (const file of looseFiles) if (!add(file)) return { files: [...files].slice(0, limit).sort(), complete: false, candidateCount: files.size };
|
|
668
|
+
if (!looseDetails.complete) return { files: [...files].sort(), complete: false, candidateCount: files.size };
|
|
669
|
+
}
|
|
670
|
+
if (!rootDetails.complete) return { files: [...files].sort(), complete: false, candidateCount: files.size };
|
|
765
671
|
} catch {
|
|
766
|
-
return [];
|
|
672
|
+
return { files: [], complete: false, candidateCount: 0 };
|
|
767
673
|
}
|
|
768
|
-
return [...
|
|
674
|
+
return { files: [...files].sort(), complete: true, candidateCount: files.size };
|
|
769
675
|
}
|
|
770
|
-
|
|
676
|
+
export function repositoryFileManifest(cwd, maxFiles = 10_000) { return repositoryFileManifestDetails(cwd, maxFiles).files; }
|
|
771
677
|
function changedPathsFromNameStatus(output, observeRename) {
|
|
772
678
|
const fields = String(output ?? "").split("\0").filter(Boolean);
|
|
773
679
|
const files = [];
|
|
@@ -295,7 +295,9 @@ export function selectVerificationPlan(profile, requestedGroup, changeMode, cwd,
|
|
|
295
295
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
296
296
|
const scripts = manifest.scripts && typeof manifest.scripts === "object" ? manifest.scripts : {};
|
|
297
297
|
const selected = ["type-check", "typecheck", "lint", "test"].filter((name) => typeof scripts[name] === "string");
|
|
298
|
-
if (selected.length > 0)
|
|
298
|
+
if (selected.length > 0) {
|
|
299
|
+
commands = [selected.map((name) => name === "test" ? "npm test" : `npm run ${name}`).join(" && ")];
|
|
300
|
+
}
|
|
299
301
|
}
|
|
300
302
|
} catch {
|
|
301
303
|
// Preserve the profile's fail-closed verifier when package metadata is absent or invalid.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const WORKSPACE_EVIDENCE_MAX_FILES = 2000;
|
|
6
|
+
const WORKSPACE_EVIDENCE_SKIP_NAMES = new Set([
|
|
7
|
+
".git", ".hg", ".svn", ".pi", "node_modules", "dist", "build", ".next", "coverage"
|
|
8
|
+
]);
|
|
9
|
+
const WORKSPACE_EVIDENCE_PRIORITY_NAMES = new Map([["plans", 0]]);
|
|
10
|
+
|
|
11
|
+
function hiddenWorkspaceEvidenceSegment(value) {
|
|
12
|
+
return String(value ?? "").startsWith(".");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function gitOutput(cwd, args, options = {}) {
|
|
16
|
+
return execFileSync("git", ["-C", cwd, ...args], {
|
|
17
|
+
encoding: options.encoding ?? "utf8",
|
|
18
|
+
maxBuffer: options.maxBuffer ?? 16 * 1024 * 1024,
|
|
19
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isGitWorkingTree(cwd) {
|
|
24
|
+
try {
|
|
25
|
+
return gitOutput(cwd, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizedGitPath(value) {
|
|
32
|
+
return String(value ?? "").replaceAll("\\", "/").replace(/^\/+/, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function prefixedGitPath(prefix, file) {
|
|
36
|
+
const normalized = normalizedGitPath(file);
|
|
37
|
+
return prefix ? `${prefix}/${normalized}` : normalized;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function directChildGitEvidenceRootDetails(cwd) {
|
|
41
|
+
const roots = [];
|
|
42
|
+
let complete = true;
|
|
43
|
+
let entries;
|
|
44
|
+
try {
|
|
45
|
+
entries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
46
|
+
} catch {
|
|
47
|
+
return { roots, complete: false };
|
|
48
|
+
}
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
51
|
+
const child = path.join(cwd, entry.name);
|
|
52
|
+
try {
|
|
53
|
+
if (fs.lstatSync(child).isSymbolicLink()) continue;
|
|
54
|
+
let gitMarker;
|
|
55
|
+
try {
|
|
56
|
+
gitMarker = fs.lstatSync(path.join(child, ".git"));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error?.code === "ENOENT") continue;
|
|
59
|
+
complete = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!gitMarker.isDirectory() && !gitMarker.isFile()) {
|
|
63
|
+
complete = false;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!isGitWorkingTree(child)) {
|
|
67
|
+
complete = false;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const topLevel = gitOutput(child, ["rev-parse", "--show-toplevel"]).trim();
|
|
71
|
+
if (fs.realpathSync(topLevel) !== fs.realpathSync(child)) {
|
|
72
|
+
complete = false;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
roots.push({ cwd: child, prefix: normalizedGitPath(entry.name) });
|
|
76
|
+
} catch {
|
|
77
|
+
complete = false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { roots: roots.sort((left, right) => left.prefix.localeCompare(right.prefix)), complete };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function directChildGitEvidenceRoots(cwd) {
|
|
84
|
+
return directChildGitEvidenceRootDetails(cwd).roots;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function gitEvidenceRootDetails(cwd) {
|
|
88
|
+
if (isGitWorkingTree(cwd)) return { roots: [{ cwd, prefix: "" }], complete: true };
|
|
89
|
+
return directChildGitEvidenceRootDetails(cwd);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function gitEvidenceRoots(cwd) {
|
|
93
|
+
return gitEvidenceRootDetails(cwd).roots;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function nonGitWorkspaceFileDetails(cwd, gitRootPrefixes) {
|
|
97
|
+
const files = [];
|
|
98
|
+
let complete = true;
|
|
99
|
+
const gitRoots = new Set(gitRootPrefixes.filter(Boolean));
|
|
100
|
+
const orderedEntries = (entries) => [...entries].sort((left, right) => {
|
|
101
|
+
const leftPriority = WORKSPACE_EVIDENCE_PRIORITY_NAMES.get(left.name.toLowerCase()) ?? 10;
|
|
102
|
+
const rightPriority = WORKSPACE_EVIDENCE_PRIORITY_NAMES.get(right.name.toLowerCase()) ?? 10;
|
|
103
|
+
return leftPriority - rightPriority || left.name.localeCompare(right.name, "en-US");
|
|
104
|
+
});
|
|
105
|
+
function visit(directory, relativeDirectory) {
|
|
106
|
+
if (files.length >= WORKSPACE_EVIDENCE_MAX_FILES) {
|
|
107
|
+
complete = false;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let entries;
|
|
111
|
+
try {
|
|
112
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
113
|
+
} catch {
|
|
114
|
+
complete = false;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const entry of orderedEntries(entries)) {
|
|
118
|
+
if (files.length >= WORKSPACE_EVIDENCE_MAX_FILES) {
|
|
119
|
+
complete = false;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Loose files beside nested repositories are not governed by either
|
|
123
|
+
// repository's ignore/trust policy. Keep hidden editor, agent, cloud,
|
|
124
|
+
// credential, and cache state out of both manifests and change evidence
|
|
125
|
+
// instead of attempting to maintain an incomplete name blacklist.
|
|
126
|
+
if (WORKSPACE_EVIDENCE_SKIP_NAMES.has(entry.name) || hiddenWorkspaceEvidenceSegment(entry.name)) continue;
|
|
127
|
+
const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
|
|
128
|
+
if (gitRoots.has(relative.split("/")[0])) continue;
|
|
129
|
+
const absolute = path.join(directory, entry.name);
|
|
130
|
+
try {
|
|
131
|
+
if (fs.lstatSync(absolute).isSymbolicLink()) continue;
|
|
132
|
+
} catch {
|
|
133
|
+
complete = false;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (entry.isDirectory()) visit(absolute, relative);
|
|
137
|
+
else if (entry.isFile()) files.push(normalizedGitPath(relative));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
visit(cwd, "");
|
|
141
|
+
return { files: files.sort(), complete };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function nonGitWorkspaceFiles(cwd, gitRootPrefixes) {
|
|
145
|
+
return nonGitWorkspaceFileDetails(cwd, gitRootPrefixes).files;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function pathWithinNonGitWorkspaceEvidenceRoot(candidate, gitRootPrefixes) {
|
|
149
|
+
const segments = candidate.split("/").filter(Boolean);
|
|
150
|
+
const topSegment = segments[0];
|
|
151
|
+
return Boolean(
|
|
152
|
+
topSegment
|
|
153
|
+
&& !WORKSPACE_EVIDENCE_SKIP_NAMES.has(topSegment)
|
|
154
|
+
&& !segments.some(hiddenWorkspaceEvidenceSegment)
|
|
155
|
+
&& !gitRootPrefixes.includes(topSegment)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
},
|
|
108
108
|
"contextBudget": {
|
|
109
109
|
"defaultMode": "enforce",
|
|
110
|
+
"contextDeltaShadow": "sample",
|
|
110
111
|
"maxContextFileChars": 50000,
|
|
111
112
|
"maxMemoryFileChars": 20000,
|
|
112
113
|
"maxManifestFiles": 80,
|
|
@@ -155,6 +156,7 @@
|
|
|
155
156
|
"ls": ["filesystem-readonly"],
|
|
156
157
|
"write": ["filesystem-write"],
|
|
157
158
|
"edit": ["filesystem-write"],
|
|
159
|
+
"apply_patch": ["filesystem-write"],
|
|
158
160
|
"browser": ["browser"],
|
|
159
161
|
"github": ["github"]
|
|
160
162
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { TaskContract } from "../../extensions/guard-types.js";
|
|
4
|
+
import type { PendingContextDelivery, RuntimeSessionState, ObservedTaskContext } from "../session/runtime-state.ts";
|
|
5
|
+
|
|
6
|
+
export type ContextDeliveryConfirmationDependencies = {
|
|
7
|
+
state: RuntimeSessionState;
|
|
8
|
+
maxManifestFiles: number;
|
|
9
|
+
activeTask: (ctx: ExtensionContext) => TaskContract | undefined;
|
|
10
|
+
flushObservedTaskContext: (
|
|
11
|
+
pi: ExtensionAPI,
|
|
12
|
+
ctx: ExtensionContext,
|
|
13
|
+
pendingContext: ObservedTaskContext[],
|
|
14
|
+
maxManifestFiles: number,
|
|
15
|
+
event: string
|
|
16
|
+
) => TaskContract | undefined;
|
|
17
|
+
telemetry: (ctx: ExtensionContext, payload: Record<string, unknown>) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function contextDeliveryIdFromDetails(details: unknown): string | undefined {
|
|
21
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) return undefined;
|
|
22
|
+
const rawDelivery = (details as { contextDelivery?: unknown }).contextDelivery;
|
|
23
|
+
if (!rawDelivery || typeof rawDelivery !== "object" || Array.isArray(rawDelivery)) return undefined;
|
|
24
|
+
const deliveryId = (rawDelivery as { deliveryId?: unknown }).deliveryId;
|
|
25
|
+
return typeof deliveryId === "string" && deliveryId.length > 0 && deliveryId.length <= 200
|
|
26
|
+
? deliveryId
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function confirmContextDelivery(
|
|
31
|
+
pi: ExtensionAPI,
|
|
32
|
+
ctx: ExtensionContext,
|
|
33
|
+
deliveryId: string,
|
|
34
|
+
dependencies: ContextDeliveryConfirmationDependencies
|
|
35
|
+
): TaskContract | undefined {
|
|
36
|
+
const delivery = dependencies.state.takeContextDelivery(ctx, deliveryId);
|
|
37
|
+
if (!delivery) return undefined;
|
|
38
|
+
const task = dependencies.activeTask(ctx);
|
|
39
|
+
if (!task || task.trace.outcome !== "pending" || task.taskRunId !== delivery.taskRunId) {
|
|
40
|
+
dependencies.telemetry(ctx, { event: "context_delivery_rejected", deliveryId, reason: "task-identity-mismatch" });
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
for (const entry of delivery.entries) {
|
|
44
|
+
dependencies.state.rememberObservedContext(ctx, entry);
|
|
45
|
+
dependencies.state.rememberQualifiedContextEvidence(ctx, delivery.taskRunId, entry);
|
|
46
|
+
}
|
|
47
|
+
const written = dependencies.flushObservedTaskContext(
|
|
48
|
+
pi,
|
|
49
|
+
ctx,
|
|
50
|
+
dependencies.state.qualifiedContextEvidence(ctx, delivery.taskRunId),
|
|
51
|
+
dependencies.maxManifestFiles,
|
|
52
|
+
"context_delivery_confirmed"
|
|
53
|
+
);
|
|
54
|
+
if (!written || written.taskRunId !== delivery.taskRunId) return undefined;
|
|
55
|
+
if (delivery.pack) {
|
|
56
|
+
const { retrievalKey, ...pack } = delivery.pack;
|
|
57
|
+
dependencies.state.rememberInjectedContextPack(ctx, retrievalKey, pack);
|
|
58
|
+
}
|
|
59
|
+
dependencies.telemetry(ctx, {
|
|
60
|
+
event: "context_delivery_confirmed",
|
|
61
|
+
deliveryId,
|
|
62
|
+
turnId: delivery.turnId,
|
|
63
|
+
taskId: written.taskId,
|
|
64
|
+
taskRunId: written.taskRunId,
|
|
65
|
+
selectedPaths: delivery.entries.map((entry) => entry.path),
|
|
66
|
+
selected: delivery.entries.length
|
|
67
|
+
});
|
|
68
|
+
if (delivery.injection) dependencies.telemetry(ctx, {
|
|
69
|
+
event: "context_pack_injected",
|
|
70
|
+
injectionId: deliveryId,
|
|
71
|
+
turnId: delivery.turnId,
|
|
72
|
+
...delivery.injection,
|
|
73
|
+
selectedPaths: delivery.injection.selectedItems.map((item) => item.path)
|
|
74
|
+
});
|
|
75
|
+
return written;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function stageContextDelivery(
|
|
79
|
+
ctx: ExtensionContext,
|
|
80
|
+
delivery: PendingContextDelivery,
|
|
81
|
+
dependencies: Pick<ContextDeliveryConfirmationDependencies, "state" | "telemetry">
|
|
82
|
+
): void {
|
|
83
|
+
dependencies.state.stageContextDelivery(ctx, delivery);
|
|
84
|
+
if (!delivery.injection) return;
|
|
85
|
+
dependencies.telemetry(ctx, {
|
|
86
|
+
event: "context_pack_offered",
|
|
87
|
+
deliveryId: delivery.deliveryId,
|
|
88
|
+
turnId: delivery.turnId,
|
|
89
|
+
...delivery.injection,
|
|
90
|
+
selectedPaths: delivery.injection.selectedItems.map((item) => item.path)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function confirmContextDeliveryFromToolResult(
|
|
95
|
+
pi: ExtensionAPI,
|
|
96
|
+
ctx: ExtensionContext,
|
|
97
|
+
event: { details?: unknown; isError?: boolean },
|
|
98
|
+
dependencies: ContextDeliveryConfirmationDependencies
|
|
99
|
+
): TaskContract | undefined {
|
|
100
|
+
if (event.isError === true) return undefined;
|
|
101
|
+
const deliveryId = contextDeliveryIdFromDetails(event.details);
|
|
102
|
+
return deliveryId ? confirmContextDelivery(pi, ctx, deliveryId, dependencies) : undefined;
|
|
103
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
import { buildContextPack, ensureContextIndexV2 } from "../../extensions/context-engine.js";
|
|
5
|
+
import { durableContextEvidenceEntries } from "../../extensions/context-evidence.js";
|
|
6
|
+
import type { TaskContract } from "../../extensions/guard-types.js";
|
|
7
|
+
|
|
8
|
+
export type ContextDeltaShadowMode = "off" | "sample" | "on";
|
|
9
|
+
|
|
10
|
+
type ShadowInput = {
|
|
11
|
+
ctx: ExtensionContext;
|
|
12
|
+
query: string;
|
|
13
|
+
turnId: string;
|
|
14
|
+
task: TaskContract;
|
|
15
|
+
mode: ContextDeltaShadowMode;
|
|
16
|
+
protectedTarget: boolean;
|
|
17
|
+
excludePatterns: string[];
|
|
18
|
+
telemetry: (ctx: ExtensionContext, payload: Record<string, unknown>) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function sampled(taskRunId: string, query: string): boolean {
|
|
22
|
+
const byte = crypto.createHash("sha256").update(`${taskRunId}\0${query}`).digest()[0];
|
|
23
|
+
return byte < 64;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function measureContextDeltaShadow(input: ShadowInput): Promise<void> {
|
|
27
|
+
if (input.mode === "off" || input.protectedTarget || (input.mode === "sample" && !sampled(input.task.taskRunId, input.query))) return;
|
|
28
|
+
const usage = input.ctx.getContextUsage();
|
|
29
|
+
if (usage && Number(usage.percent ?? 0) >= 80) return;
|
|
30
|
+
try {
|
|
31
|
+
const { status } = await ensureContextIndexV2(input.ctx.cwd, { excludePatterns: input.excludePatterns, rebuildMissing: false });
|
|
32
|
+
if (!status.exists || status.stale) return;
|
|
33
|
+
const pack = await buildContextPack(input.ctx.cwd, input.query, {
|
|
34
|
+
budgetTokens: 1_200,
|
|
35
|
+
includeCode: false,
|
|
36
|
+
limit: 10,
|
|
37
|
+
excludePatterns: input.excludePatterns
|
|
38
|
+
});
|
|
39
|
+
const manifested = new Set(durableContextEvidenceEntries(input.task).map((entry) => entry.path));
|
|
40
|
+
const duplicateItems = pack.selected.filter((item) => manifested.has(item.path));
|
|
41
|
+
input.telemetry(input.ctx, {
|
|
42
|
+
event: "context_delta_shadow",
|
|
43
|
+
turnId: input.turnId,
|
|
44
|
+
queryHash: pack.queryHash,
|
|
45
|
+
candidatePaths: pack.selected.map((item) => item.path),
|
|
46
|
+
pathsAlreadyManifested: duplicateItems.map((item) => item.path),
|
|
47
|
+
candidateTokens: pack.selected.reduce((sum, item) => sum + item.estimatedTokens, 0),
|
|
48
|
+
duplicateCandidateTokens: duplicateItems.reduce((sum, item) => sum + item.estimatedTokens, 0)
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
input.telemetry(input.ctx, { event: "context_delta_shadow", turnId: input.turnId, skipped: "error", error: error instanceof Error ? error.message : String(error) });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
type ContextEfficiencyReport = {
|
|
2
|
+
metrics: {
|
|
3
|
+
contextWasteScore: number | null;
|
|
4
|
+
contextWasteScoreEstimate: number;
|
|
5
|
+
contextWasteScoreEvidenceCoverage: number;
|
|
6
|
+
averageActiveTools: number;
|
|
7
|
+
toolSchemaPrefixShare: number;
|
|
8
|
+
toolSchemaToSystemRatio: number;
|
|
9
|
+
duplicateReads: number;
|
|
10
|
+
comparableReadCalls: number;
|
|
11
|
+
readCalls: number;
|
|
12
|
+
readEvidenceCoverage: number;
|
|
13
|
+
contextFallbackRereads: number;
|
|
14
|
+
contextSelections: number;
|
|
15
|
+
duplicateOutputRate: number;
|
|
16
|
+
lowConfidencePacks: number;
|
|
17
|
+
editRecoveryContextCount: number;
|
|
18
|
+
editRecoveryFailures: number;
|
|
19
|
+
editRecoverySuppressedFailures: number;
|
|
20
|
+
editRecoveryEstimatedTokens: number;
|
|
21
|
+
editRecoveryFailureEvidenceCoverage: number;
|
|
22
|
+
};
|
|
23
|
+
coverage: { wasteScore: { status: string } };
|
|
24
|
+
sample: { contextPacks: number };
|
|
25
|
+
recommendations: string[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function formatContextEfficiencyReport(
|
|
29
|
+
report: ContextEfficiencyReport,
|
|
30
|
+
formatPercent: (value: number) => string
|
|
31
|
+
): string {
|
|
32
|
+
const wasteScore = report.metrics.contextWasteScore === null
|
|
33
|
+
? `unavailable (diagnostic estimate ${report.metrics.contextWasteScoreEstimate}/100; evidence ${formatPercent(report.metrics.contextWasteScoreEvidenceCoverage)}, ${report.coverage.wasteScore.status})`
|
|
34
|
+
: `${report.metrics.contextWasteScore}/100 (lower is better; evidence ${formatPercent(report.metrics.contextWasteScoreEvidenceCoverage)}, ${report.coverage.wasteScore.status})`;
|
|
35
|
+
return [
|
|
36
|
+
`contextWasteScore: ${wasteScore}`,
|
|
37
|
+
`activeTools: ${report.metrics.averageActiveTools}`,
|
|
38
|
+
`toolSchemaPrefixShare: ${formatPercent(report.metrics.toolSchemaPrefixShare)}`,
|
|
39
|
+
`toolSchemaToSystemRatio: ${report.metrics.toolSchemaToSystemRatio}`,
|
|
40
|
+
`duplicateReads: ${report.metrics.duplicateReads}/${report.metrics.comparableReadCalls} comparable (${report.metrics.readCalls} observed; coverage ${formatPercent(report.metrics.readEvidenceCoverage)})`,
|
|
41
|
+
`fallbackRereads: ${report.metrics.contextFallbackRereads}/${report.metrics.contextSelections}`,
|
|
42
|
+
`duplicateOutput: ${formatPercent(report.metrics.duplicateOutputRate)}`,
|
|
43
|
+
`lowConfidencePacks: ${report.metrics.lowConfidencePacks}/${report.sample.contextPacks}`,
|
|
44
|
+
`editRecovery: ${report.metrics.editRecoveryContextCount}/${report.metrics.editRecoveryFailures} failures returned context; ${report.metrics.editRecoverySuppressedFailures} suppressed; ${report.metrics.editRecoveryEstimatedTokens} estimated tokens; classification coverage ${formatPercent(report.metrics.editRecoveryFailureEvidenceCoverage)}`,
|
|
45
|
+
...report.recommendations.map((recommendation) => `- ${recommendation}`)
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|