@piagent/platform 1.2.10 → 1.2.12
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 +174 -0
- package/README.md +29 -4
- package/adapters/backend-api/profile.json +1 -1
- package/adapters/be-readonly-fe/profile.json +2 -2
- package/adapters/data/profile.json +1 -1
- package/adapters/devops/profile.json +1 -3
- package/adapters/docs/profile.json +1 -1
- package/adapters/fullstack/profile.json +2 -4
- package/adapters/generic/profile.json +1 -1
- package/adapters/mobile/profile.json +1 -2
- package/adapters/node-typescript/profile.json +1 -3
- package/benchmarks/core-v1/suite.json +51 -0
- package/benchmarks/core-v1/tasks/discount-order/grade.mjs +17 -0
- package/benchmarks/core-v1/tasks/discount-order/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/discount-order/project/src/discount.js +3 -0
- package/benchmarks/core-v1/tasks/discount-order/project/src/order.js +6 -0
- package/benchmarks/core-v1/tasks/discount-order/project/test/order.test.js +7 -0
- package/benchmarks/core-v1/tasks/discount-order/prompt.md +4 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/grade.mjs +17 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/project/src/invoice.js +3 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/project/test/invoice.test.js +7 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/prompt.md +3 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/grade.mjs +11 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/project/README.md +4 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/project/test/smoke.test.js +4 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/prompt.md +4 -0
- package/benchmarks/core-v1/tasks/targeted-session/grade.mjs +14 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/auth/session.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/billing/currency.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/catalog/search.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/notifications/channel.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/search/tokenize.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/test/session.test.js +7 -0
- package/benchmarks/core-v1/tasks/targeted-session/prompt.md +4 -0
- package/benchmarks/production-v1/grade.mjs +220 -0
- package/benchmarks/production-v1/project/README.md +7 -0
- package/benchmarks/production-v1/project/config/service.json +4 -0
- package/benchmarks/production-v1/project/data/audit-ledger.jsonl +1 -0
- package/benchmarks/production-v1/project/docs/ops.md +3 -0
- package/benchmarks/production-v1/project/logs/incident.log +2 -0
- package/benchmarks/production-v1/project/package.json +11 -0
- package/benchmarks/production-v1/project/scripts/check.mjs +21 -0
- package/benchmarks/production-v1/project/src/backend/auth.js +3 -0
- package/benchmarks/production-v1/project/src/backend/cache.js +15 -0
- package/benchmarks/production-v1/project/src/backend/invoice.js +4 -0
- package/benchmarks/production-v1/project/src/data/csv.js +3 -0
- package/benchmarks/production-v1/project/src/data/dedup.js +5 -0
- package/benchmarks/production-v1/project/src/data/migration.js +9 -0
- package/benchmarks/production-v1/project/src/frontend/pagination.js +8 -0
- package/benchmarks/production-v1/project/src/frontend/search-state.js +8 -0
- package/benchmarks/production-v1/project/src/frontend/unicode-search.js +7 -0
- package/benchmarks/production-v1/project/src/platform/args.js +10 -0
- package/benchmarks/production-v1/project/src/platform/config.js +7 -0
- package/benchmarks/production-v1/project/src/platform/workspace.js +13 -0
- package/benchmarks/production-v1/project/src/reliability/expiry.js +5 -0
- package/benchmarks/production-v1/project/src/reliability/retry.js +15 -0
- package/benchmarks/production-v1/project/test/smoke.test.js +12 -0
- package/benchmarks/production-v1/project/vendor/README.md +1 -0
- package/benchmarks/production-v1/prompts/bounded-retry.md +7 -0
- package/benchmarks/production-v1/prompts/cli-double-dash.md +7 -0
- package/benchmarks/production-v1/prompts/config-precedence.md +6 -0
- package/benchmarks/production-v1/prompts/destructive-history-refusal.md +3 -0
- package/benchmarks/production-v1/prompts/expiry-boundary.md +7 -0
- package/benchmarks/production-v1/prompts/incident-diagnosis.md +4 -0
- package/benchmarks/production-v1/prompts/invoice-rounding.md +9 -0
- package/benchmarks/production-v1/prompts/pagination-boundary.md +7 -0
- package/benchmarks/production-v1/prompts/protected-env-refusal.md +2 -0
- package/benchmarks/production-v1/prompts/quoted-csv.md +6 -0
- package/benchmarks/production-v1/prompts/repository-prompt-injection.md +5 -0
- package/benchmarks/production-v1/prompts/schema-migration.md +6 -0
- package/benchmarks/production-v1/prompts/stable-dedup.md +6 -0
- package/benchmarks/production-v1/prompts/stale-search-response.md +6 -0
- package/benchmarks/production-v1/prompts/tenant-cache-isolation.md +6 -0
- package/benchmarks/production-v1/prompts/tenant-role-authorization.md +6 -0
- package/benchmarks/production-v1/prompts/unicode-search.md +6 -0
- package/benchmarks/production-v1/prompts/workspace-order.md +7 -0
- package/benchmarks/production-v1/suite.json +284 -0
- package/benchmarks/production-v1/variant.mjs +112 -0
- package/catalog/capabilities.json +10 -10
- package/docs/command-reference-vietnamese.md +19 -6
- package/docs/context-engine.md +33 -11
- package/docs/distribution-standard.md +11 -8
- package/docs/mcp-and-tools.md +13 -7
- package/docs/model-options.md +7 -10
- package/docs/operator-manual-vietnamese.md +98 -33
- package/docs/package-architecture-notes.md +3 -1
- package/docs/publishing-for-teams.md +4 -0
- package/docs/quality-benchmark.md +336 -58
- package/docs/quickstart-vietnamese.md +5 -0
- package/docs/release-install-policy.md +45 -7
- package/docs/runtime-harness-standard.md +47 -29
- package/docs/runtime-policy-design.md +24 -12
- package/docs/runtime-quality-baseline.md +42 -17
- package/docs/security-threat-model.md +1 -1
- package/docs/task-implementation-contract.md +113 -177
- package/docs/task-lifecycle-vietnamese.md +129 -125
- package/docs/team-onboarding.md +12 -4
- package/docs/usage-observability.md +15 -8
- package/docs-site/architecture.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/runtime.html +16 -0
- package/docs-site/context-engine.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 +17 -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/workflows.html +1 -1
- package/evals/fixtures/task-contract.invalid.json +15 -1
- package/evals/fixtures/task-contract.valid.json +18 -2
- package/package.json +3 -2
- package/packages/piagent-core/README.md +37 -3
- package/packages/piagent-core/benchmark/benchmark-core.js +975 -0
- package/packages/piagent-core/extensions/context-engine.js +81 -57
- package/packages/piagent-core/extensions/guard-types.ts +35 -3
- package/packages/piagent-core/extensions/local-state-path.js +59 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +1904 -175
- package/packages/piagent-core/extensions/policy-core.js +11 -1
- package/packages/piagent-core/extensions/runtime-evidence.js +39 -19
- package/packages/piagent-core/extensions/state-retention.js +218 -0
- package/packages/piagent-core/extensions/task-lifecycle.js +138 -0
- package/packages/piagent-core/extensions/task-state.js +630 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +1 -0
- package/packages/piagent-core/prompts/be-to-fe.md +14 -22
- package/packages/piagent-core/prompts/platform-improve.md +12 -17
- package/packages/piagent-core/prompts/review.md +7 -11
- package/packages/piagent-core/prompts/scout.md +6 -10
- package/packages/piagent-core/prompts/task.md +11 -17
- package/packages/piagent-core/skills/piagent-ops/SKILL.md +12 -15
- package/schemas/task-contract.schema.json +267 -104
- package/scripts/benchmark-runner.mjs +1432 -0
- package/scripts/install-global.sh +1 -1
- package/scripts/migrate-project-state.mjs +94 -3
- package/scripts/piagent-cli.mjs +1 -1
- package/scripts/update-global.mjs +36 -3
- package/scripts/verify-local.sh +19 -0
- package/templates/project/.pi/task-contract.template.json +19 -2
- package/templates/project/AGENTS.md +10 -20
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
globMatchesPath,
|
|
17
17
|
matchesProtectedPath,
|
|
18
18
|
normalizePathCandidate,
|
|
19
|
-
matchesAnyPath
|
|
19
|
+
matchesAnyPath,
|
|
20
|
+
shellHasFileWriteRedirection
|
|
20
21
|
} from "./policy-core.js";
|
|
21
22
|
import {
|
|
22
23
|
appendObservedBashResult,
|
|
@@ -68,6 +69,7 @@ import {
|
|
|
68
69
|
classifyActionTokenSequence,
|
|
69
70
|
classifyExplicitActionValues,
|
|
70
71
|
classifyToolNameAction,
|
|
72
|
+
externalExecutableIndex,
|
|
71
73
|
extractShellCommandInput,
|
|
72
74
|
findShellExternalConfirmationReason,
|
|
73
75
|
normalizeActionToken,
|
|
@@ -90,6 +92,33 @@ import {
|
|
|
90
92
|
contextIndexExcludePatterns,
|
|
91
93
|
effectiveProtectedPaths
|
|
92
94
|
} from "./context-index-policy.js";
|
|
95
|
+
import {
|
|
96
|
+
DEFAULT_MAX_TASK_ATTEMPTS,
|
|
97
|
+
activeSessionTask,
|
|
98
|
+
bindSessionTask,
|
|
99
|
+
createTaskRunId,
|
|
100
|
+
isGitWorkingTree,
|
|
101
|
+
listTaskContracts,
|
|
102
|
+
migrateTaskState,
|
|
103
|
+
priorTaskAttempts,
|
|
104
|
+
resolveTaskContract,
|
|
105
|
+
safeTaskId,
|
|
106
|
+
summarizeAttempt,
|
|
107
|
+
workPlanDependencyError,
|
|
108
|
+
workingTreeSnapshot,
|
|
109
|
+
writeTaskContract
|
|
110
|
+
} from "./task-state.js";
|
|
111
|
+
import {
|
|
112
|
+
applyRuntimeLifecycleObservation,
|
|
113
|
+
runtimeLifecycleMode,
|
|
114
|
+
workingTreeEvidenceDigest
|
|
115
|
+
} from "./task-lifecycle.js";
|
|
116
|
+
import {
|
|
117
|
+
appendJsonlBounded,
|
|
118
|
+
pruneCaptureFiles,
|
|
119
|
+
readJsonlTail
|
|
120
|
+
} from "./state-retention.js";
|
|
121
|
+
import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
|
|
93
122
|
|
|
94
123
|
import type { ActionClassification } from "./guard-shell-analysis.ts";
|
|
95
124
|
|
|
@@ -151,6 +180,45 @@ const TOOL_RESULT_PREVIEW_TAIL_LINES = 24;
|
|
|
151
180
|
const TOOL_RESULT_PREVIEW_INTERESTING_LINES = 24;
|
|
152
181
|
const TOOL_RESULT_PREVIEW_MAX_CHARS = 6_000;
|
|
153
182
|
const TOOL_RESULT_CAPTURE_MAX_CHARS = 500_000;
|
|
183
|
+
const TRACE_MAX_BYTES = 8 * 1024 * 1024;
|
|
184
|
+
const CAPTURE_INDEX_MAX_BYTES = 4 * 1024 * 1024;
|
|
185
|
+
const CAPTURE_RETENTION_MAX_BYTES = 128 * 1024 * 1024;
|
|
186
|
+
const CAPTURE_RETENTION_MAX_FILES = 500;
|
|
187
|
+
const CAPTURE_RETENTION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
188
|
+
const CAPTURE_WRITES_SINCE_PRUNE = new Map<string, number>();
|
|
189
|
+
const LEGACY_PROJECT_INSTRUCTIONS_START = "Before implementation:\n\n1. Load `.pi/piagent-profile.json` with `piagent_context`.";
|
|
190
|
+
const LEGACY_PROJECT_INSTRUCTIONS_END = "18. If the bundled `pi-subagents` parent skill is available, use it for delegation patterns, review loops, native supervisor coordination, and safety boundaries.";
|
|
191
|
+
const RUNTIME_MANAGED_PROJECT_INSTRUCTIONS = [
|
|
192
|
+
"Piagent runtime-managed task flow:",
|
|
193
|
+
"1. Runtime creates the task contract automatically for a bounded source-changing request before the model starts.",
|
|
194
|
+
"2. Only when runtime intake pauses for broad, high-risk, or ambiguous scope, call `piagent_task_start` exactly once with project-relative path/glob scope; reuse an active contract.",
|
|
195
|
+
"3. Read the narrow target and nearest relevant test, then use ordinary read/edit/bash tools.",
|
|
196
|
+
"4. Run every exact runtime-provided verifier after the latest mutation. Automatic tasks need no management calls.",
|
|
197
|
+
"5. Runtime hooks enforce policy and record context, changes, current-tree verification, trace and final gate. Diagnostic groups appear only for explicit requests or manual high-risk checkpoints."
|
|
198
|
+
].join("\n");
|
|
199
|
+
type TaskStartParameters = {
|
|
200
|
+
taskId?: string;
|
|
201
|
+
summary: string;
|
|
202
|
+
riskLane: "tiny" | "normal" | "high-risk";
|
|
203
|
+
intakeMode?: "model" | "runtime";
|
|
204
|
+
changeMode?: "source-change" | "read-only";
|
|
205
|
+
verifyGroup?: string;
|
|
206
|
+
maxAttempts?: number;
|
|
207
|
+
expectedOutput: string;
|
|
208
|
+
acceptanceCriteria: string[];
|
|
209
|
+
scope: string[];
|
|
210
|
+
outOfScope?: string[];
|
|
211
|
+
reviewLenses?: ReviewLens[];
|
|
212
|
+
workPlan?: Array<{
|
|
213
|
+
id: string;
|
|
214
|
+
title: string;
|
|
215
|
+
role?: OrchestrationRole;
|
|
216
|
+
mode?: "read-only" | "single-writer" | "review";
|
|
217
|
+
status?: "pending" | "in-progress" | "done" | "skipped" | "failed";
|
|
218
|
+
dependsOn?: string[];
|
|
219
|
+
note?: string;
|
|
220
|
+
}>;
|
|
221
|
+
};
|
|
154
222
|
const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp", "bmp"] as const;
|
|
155
223
|
const ORCHESTRATION_MODES = ["solo-first", "bounded-subagents", "parallel-readonly"] as const;
|
|
156
224
|
const REVIEW_LENSES = ["correctness", "tests", "scope", "security", "docs", "release", "package"] as const;
|
|
@@ -162,10 +230,13 @@ const TECH_STACK_MANIFEST_FILE = ".pi/tech-stack.json";
|
|
|
162
230
|
const TECH_CONTEXT_DIR = ".pi/tech-context";
|
|
163
231
|
const PIAGENT_TOOL_GROUPS = {
|
|
164
232
|
loader: ["piagent_tools"],
|
|
233
|
+
intake: ["piagent_task_start"],
|
|
165
234
|
governance: [
|
|
166
235
|
"piagent_context",
|
|
167
|
-
"piagent_context_preflight"
|
|
168
|
-
|
|
236
|
+
"piagent_context_preflight"
|
|
237
|
+
],
|
|
238
|
+
task: ["piagent_task_progress"],
|
|
239
|
+
recovery: [
|
|
169
240
|
"piagent_context_record",
|
|
170
241
|
"piagent_verify_record",
|
|
171
242
|
"piagent_trace_record",
|
|
@@ -420,6 +491,7 @@ const DEFAULT_POLICY: BasePolicy = {
|
|
|
420
491
|
"piagent_profile_tech_context_record",
|
|
421
492
|
"piagent_project_onboarding_record",
|
|
422
493
|
"piagent_task_start",
|
|
494
|
+
"piagent_task_progress",
|
|
423
495
|
"piagent_source_checkout",
|
|
424
496
|
"piagent_context_record",
|
|
425
497
|
"piagent_verify_record",
|
|
@@ -1546,10 +1618,6 @@ function stateRoot(cwd: string): string {
|
|
|
1546
1618
|
return path.join(cwd, ".pi", "piagent-state");
|
|
1547
1619
|
}
|
|
1548
1620
|
|
|
1549
|
-
function taskFilePath(cwd: string, taskId: string): string {
|
|
1550
|
-
return path.join(stateRoot(cwd), "tasks", `${safeTaskId(taskId)}.json`);
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
1621
|
function projectContextFilePath(cwd: string): string {
|
|
1554
1622
|
return path.join(cwd, ".pi", "project-context.md");
|
|
1555
1623
|
}
|
|
@@ -1574,21 +1642,18 @@ function toolResultCaptureIndexPath(cwd: string): string {
|
|
|
1574
1642
|
return path.join(toolResultCaptureRoot(cwd), "index.jsonl");
|
|
1575
1643
|
}
|
|
1576
1644
|
|
|
1577
|
-
function safeTaskId(taskId: string): string {
|
|
1578
|
-
return taskId.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "task";
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
1645
|
function nowIso(): string {
|
|
1582
1646
|
return new Date().toISOString();
|
|
1583
1647
|
}
|
|
1584
1648
|
|
|
1585
1649
|
function ensureStateDirs(cwd: string): void {
|
|
1586
|
-
|
|
1650
|
+
ensurePrivateStateDirectory(cwd, stateRoot(cwd), "Piagent state directory");
|
|
1651
|
+
ensurePrivateStateDirectory(cwd, path.join(stateRoot(cwd), "tasks"), "Piagent task state directory");
|
|
1587
1652
|
}
|
|
1588
1653
|
|
|
1589
1654
|
function ensureProjectContextPlaceholder(cwd: string): void {
|
|
1590
|
-
|
|
1591
|
-
const target = projectContextFilePath(cwd);
|
|
1655
|
+
ensurePrivateStateDirectory(cwd, path.join(cwd, ".pi"), "Piagent project directory");
|
|
1656
|
+
const target = resolveLocalStatePath(cwd, projectContextFilePath(cwd), { label: "Project context file" });
|
|
1592
1657
|
if (fs.existsSync(target)) return;
|
|
1593
1658
|
fs.writeFileSync(target, [
|
|
1594
1659
|
"# Project Context",
|
|
@@ -1862,14 +1927,68 @@ function normalizeWorkPlanSteps(values: unknown): WorkPlanStep[] {
|
|
|
1862
1927
|
title,
|
|
1863
1928
|
role,
|
|
1864
1929
|
mode: normalizeStepMode(item.mode, role),
|
|
1865
|
-
status: item.status === "in-progress" || item.status === "done" || item.status === "skipped" ? item.status : "pending",
|
|
1866
|
-
dependsOn: Array.isArray(item.dependsOn) ? uniqueStrings(item.dependsOn.filter((entry): entry is string => typeof entry === "string").map((entry) => safeTaskId(entry).slice(0, 40))) : undefined
|
|
1930
|
+
status: item.status === "in-progress" || item.status === "done" || item.status === "skipped" || item.status === "failed" ? item.status : "pending",
|
|
1931
|
+
dependsOn: Array.isArray(item.dependsOn) ? uniqueStrings(item.dependsOn.filter((entry): entry is string => typeof entry === "string").map((entry) => safeTaskId(entry).slice(0, 40))) : undefined,
|
|
1932
|
+
note: typeof item.note === "string" ? redactText(item.note).slice(0, 500) : undefined,
|
|
1933
|
+
updatedAt: typeof item.updatedAt === "string" ? item.updatedAt : undefined
|
|
1867
1934
|
});
|
|
1868
1935
|
}
|
|
1869
1936
|
return steps;
|
|
1870
1937
|
}
|
|
1871
1938
|
|
|
1872
|
-
function defaultWorkPlan(
|
|
1939
|
+
function defaultWorkPlan(
|
|
1940
|
+
summary: string,
|
|
1941
|
+
riskLane: TaskContract["riskLane"],
|
|
1942
|
+
changeMode: TaskContract["changeMode"]
|
|
1943
|
+
): WorkPlanStep[] {
|
|
1944
|
+
if (changeMode === "read-only") {
|
|
1945
|
+
if (riskLane === "tiny") {
|
|
1946
|
+
return [{
|
|
1947
|
+
id: "scout",
|
|
1948
|
+
title: summary.slice(0, 160) || "Inspect the bounded target read-only.",
|
|
1949
|
+
role: "parent",
|
|
1950
|
+
mode: "read-only",
|
|
1951
|
+
status: "pending"
|
|
1952
|
+
}];
|
|
1953
|
+
}
|
|
1954
|
+
const readOnlySteps: WorkPlanStep[] = [
|
|
1955
|
+
{
|
|
1956
|
+
id: "scout",
|
|
1957
|
+
title: summary.slice(0, 160) || "Inspect the bounded target read-only.",
|
|
1958
|
+
role: "parent",
|
|
1959
|
+
mode: "read-only",
|
|
1960
|
+
status: "pending"
|
|
1961
|
+
},
|
|
1962
|
+
{
|
|
1963
|
+
id: "review",
|
|
1964
|
+
title: "Review cited evidence, risks, and unknowns before handoff.",
|
|
1965
|
+
role: "piagent-reviewer",
|
|
1966
|
+
mode: "review",
|
|
1967
|
+
status: "pending",
|
|
1968
|
+
dependsOn: ["scout"]
|
|
1969
|
+
}
|
|
1970
|
+
];
|
|
1971
|
+
if (riskLane === "high-risk") {
|
|
1972
|
+
readOnlySteps.unshift({
|
|
1973
|
+
id: "scope",
|
|
1974
|
+
title: "Confirm the high-risk read-only scope and evidence boundaries.",
|
|
1975
|
+
role: "parent",
|
|
1976
|
+
mode: "read-only",
|
|
1977
|
+
status: "pending"
|
|
1978
|
+
});
|
|
1979
|
+
readOnlySteps.splice(1, 0, {
|
|
1980
|
+
id: "challenge",
|
|
1981
|
+
title: "Challenge security, data, release, or architecture assumptions.",
|
|
1982
|
+
role: "piagent-oracle",
|
|
1983
|
+
mode: "review",
|
|
1984
|
+
status: "pending",
|
|
1985
|
+
dependsOn: ["scope"]
|
|
1986
|
+
});
|
|
1987
|
+
const scout = readOnlySteps.find((step) => step.id === "scout");
|
|
1988
|
+
if (scout) scout.dependsOn = ["scope", "challenge"];
|
|
1989
|
+
}
|
|
1990
|
+
return readOnlySteps;
|
|
1991
|
+
}
|
|
1873
1992
|
if (riskLane === "tiny") {
|
|
1874
1993
|
return [
|
|
1875
1994
|
{
|
|
@@ -1929,6 +2048,16 @@ function defaultWorkPlan(summary: string, riskLane: TaskContract["riskLane"]): W
|
|
|
1929
2048
|
return steps;
|
|
1930
2049
|
}
|
|
1931
2050
|
|
|
2051
|
+
function validateNewWorkPlan(steps: WorkPlanStep[]): string | undefined {
|
|
2052
|
+
const ids = new Set<string>();
|
|
2053
|
+
for (const step of steps) {
|
|
2054
|
+
if (ids.has(step.id)) return `duplicate work-plan step id: ${step.id}`;
|
|
2055
|
+
if (step.status !== "pending") return `new work-plan step ${step.id} must start pending`;
|
|
2056
|
+
ids.add(step.id);
|
|
2057
|
+
}
|
|
2058
|
+
return workPlanDependencyError(steps)?.replace(/^workPlan/, "work-plan");
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1932
2061
|
function uniqueStrings(values: string[]): string[] {
|
|
1933
2062
|
return [...new Set(values.filter((value) => typeof value === "string" && value.trim().length > 0))];
|
|
1934
2063
|
}
|
|
@@ -2017,6 +2146,384 @@ function collectPatchTargetPaths(value: unknown, key = "", depth = 0): string[]
|
|
|
2017
2146
|
return paths;
|
|
2018
2147
|
}
|
|
2019
2148
|
|
|
2149
|
+
function taskMutationIdentity(toolName: string, input: Record<string, unknown>): string {
|
|
2150
|
+
if (normalizeActionToken(toolName) !== "mcp") return toolName;
|
|
2151
|
+
const proxyTool = typeof input.tool === "string" ? input.tool.trim() : "";
|
|
2152
|
+
return proxyTool || toolName;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function isTaskMutationTool(toolName: string, input: Record<string, unknown>): boolean {
|
|
2156
|
+
if (isPiagentTool(toolName)) return false;
|
|
2157
|
+
if (WRITE_TOOL_NAMES.has(toolName) || SHELL_TOOL_NAMES.has(toolName)) return true;
|
|
2158
|
+
const tokens = new Set(actionTokens(taskMutationIdentity(toolName, input)));
|
|
2159
|
+
return ["write", "edit", "patch", "create", "delete", "remove", "move", "rename", "update", "apply"]
|
|
2160
|
+
.some((token) => tokens.has(token));
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function isReadOnlyTaskShellCommand(
|
|
2164
|
+
command: string,
|
|
2165
|
+
segments: Array<{ words: string[] }>
|
|
2166
|
+
): boolean {
|
|
2167
|
+
if (/[<>]|`|\$\(/.test(command)) {
|
|
2168
|
+
return false;
|
|
2169
|
+
}
|
|
2170
|
+
const safeCommands = new Set(["pwd", "ls", "find", "rg", "grep", "cat", "sed", "head", "tail", "wc", "stat", "file", "test", "[", "which"]);
|
|
2171
|
+
const safeGitSubcommands = new Set(["status", "diff", "log", "show", "ls-files", "rev-parse"]);
|
|
2172
|
+
return segments.length > 0 && segments.every((segment) => {
|
|
2173
|
+
const words = segment.words.filter(Boolean);
|
|
2174
|
+
const executable = path.basename(words[0] ?? "");
|
|
2175
|
+
if (executable === "sed" && words.some((word) => /^-.*i/.test(word))) return false;
|
|
2176
|
+
if (executable === "find" && words.some((word) => ["-delete", "-exec", "-execdir", "-ok", "-okdir"].includes(word))) return false;
|
|
2177
|
+
if (safeCommands.has(executable)) return true;
|
|
2178
|
+
if (executable === "git") return safeGitSubcommands.has(words[1] ?? "");
|
|
2179
|
+
if (executable === "command") return words[1] === "-v";
|
|
2180
|
+
return false;
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
const PROJECT_MUTATING_EXECUTABLES = new Set([
|
|
2185
|
+
"apply_patch", "bash", "chmod", "chown", "cp", "dd", "install", "ln", "make", "mkdir", "mv",
|
|
2186
|
+
"node", "patch", "perl", "php", "python", "python3", "rm", "rmdir", "rsync", "ruby", "scp", "sh",
|
|
2187
|
+
"tee", "touch", "truncate", "zsh"
|
|
2188
|
+
]);
|
|
2189
|
+
|
|
2190
|
+
function isProjectMutatingShellCommand(command: string, segments: Array<{ words: string[] }>): boolean {
|
|
2191
|
+
if (shellHasFileWriteRedirection(command)) return true;
|
|
2192
|
+
const noAliases = new Map<string, string>();
|
|
2193
|
+
for (const segment of segments) {
|
|
2194
|
+
const words = segment.words.filter(Boolean);
|
|
2195
|
+
if (words.length === 0) continue;
|
|
2196
|
+
if (externalExecutableIndex(words, PROJECT_MUTATING_EXECUTABLES, noAliases) !== undefined) return true;
|
|
2197
|
+
const executable = path.basename(words[0] ?? "").toLowerCase();
|
|
2198
|
+
if (executable === "find" && words.some((word) => ["-delete", "-exec", "-execdir", "-ok", "-okdir"].includes(word))) return true;
|
|
2199
|
+
if (executable === "sed" && words.some((word) => /^-[^-]*i/.test(word) || word === "--in-place" || word.startsWith("--in-place="))) return true;
|
|
2200
|
+
if (executable === "git") {
|
|
2201
|
+
const subcommand = words.slice(1).find((word, index, values) => {
|
|
2202
|
+
if (!word.startsWith("-")) return index === 0 || !["-C", "-c", "--git-dir", "--work-tree"].includes(values[index - 1]);
|
|
2203
|
+
return false;
|
|
2204
|
+
});
|
|
2205
|
+
if (["apply", "checkout", "clean", "mv", "reset", "restore", "rm", "switch"].includes(subcommand ?? "")) return true;
|
|
2206
|
+
}
|
|
2207
|
+
if (["npm", "pnpm", "yarn", "bun"].includes(executable)) {
|
|
2208
|
+
const subcommand = words.find((word, index) => index > 0 && !word.startsWith("-"));
|
|
2209
|
+
if (["add", "ci", "install", "link", "remove", "uninstall", "update", "upgrade"].includes(subcommand ?? "")) return true;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
return false;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
function taskMutationTargets(cwd: string, toolName: string, input: Record<string, unknown>): string[] {
|
|
2216
|
+
if (!isTaskMutationTool(toolName, input) || SHELL_TOOL_NAMES.has(toolName)) return [];
|
|
2217
|
+
const identity = taskMutationIdentity(toolName, input);
|
|
2218
|
+
const collectWritePaths = (value: Record<string, unknown>, valueIdentity: string) => {
|
|
2219
|
+
const strongFilesystemSemantics = usesFilesystemContentFields(valueIdentity, false);
|
|
2220
|
+
return inspectPathInputsFromInput(cwd, value, strongFilesystemSemantics).paths
|
|
2221
|
+
.filter((item) => isFilesystemScopeField(item.field))
|
|
2222
|
+
.filter((item) => {
|
|
2223
|
+
const field = item.field.split(".").at(-1)?.toLowerCase().replace(/[-_]/g, "") ?? "";
|
|
2224
|
+
return strongFilesystemSemantics || !COPY_SOURCE_FIELDS.has(field);
|
|
2225
|
+
})
|
|
2226
|
+
.filter((item) => filesystemFieldAccessMode(valueIdentity, item.field, true) === "write")
|
|
2227
|
+
.map((item) => item.path);
|
|
2228
|
+
};
|
|
2229
|
+
const rawTargets = [
|
|
2230
|
+
...collectWritePaths(input, identity),
|
|
2231
|
+
...collectPatchTargetPaths(input)
|
|
2232
|
+
];
|
|
2233
|
+
if (typeof input.args === "string" && input.args.length <= MAX_MCP_PROXY_ARGS_CHARS) {
|
|
2234
|
+
try {
|
|
2235
|
+
const parsed = JSON.parse(input.args);
|
|
2236
|
+
if (isPlainRecord(parsed)) rawTargets.push(...collectWritePaths(parsed, identity));
|
|
2237
|
+
rawTargets.push(...collectPatchTargetPaths(parsed));
|
|
2238
|
+
} catch {
|
|
2239
|
+
// Invalid proxy JSON is blocked before execution; it contributes no evidence.
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
return uniqueStrings(rawTargets
|
|
2243
|
+
.map((candidate) => normalizeRelative(cwd, candidate))
|
|
2244
|
+
.filter((candidate): candidate is string => Boolean(
|
|
2245
|
+
candidate
|
|
2246
|
+
&& candidate !== "."
|
|
2247
|
+
&& candidate !== ".."
|
|
2248
|
+
&& !candidate.startsWith("../")
|
|
2249
|
+
&& !candidate.startsWith(".pi/piagent-state/")
|
|
2250
|
+
)));
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
type ObservedTaskContext = { path: string; reason: string };
|
|
2254
|
+
|
|
2255
|
+
function observedTaskContextFromToolResult(
|
|
2256
|
+
cwd: string,
|
|
2257
|
+
event: { toolName: string; input?: unknown; isError?: boolean },
|
|
2258
|
+
readProtectedPaths: string[]
|
|
2259
|
+
): ObservedTaskContext | undefined {
|
|
2260
|
+
if (event.isError || !["read", "piagent_document_read"].includes(event.toolName)) return undefined;
|
|
2261
|
+
const input = isPlainRecord(event.input) ? event.input : {};
|
|
2262
|
+
const relative = extractLikelyPathFromInput(cwd, input);
|
|
2263
|
+
if (
|
|
2264
|
+
!relative
|
|
2265
|
+
|| relative === "."
|
|
2266
|
+
|| relative === ".."
|
|
2267
|
+
|| relative.startsWith("../")
|
|
2268
|
+
|| relative.startsWith(".pi/piagent-state/")
|
|
2269
|
+
|| matchesProtectedPath(relative, readProtectedPaths)
|
|
2270
|
+
) return undefined;
|
|
2271
|
+
try {
|
|
2272
|
+
if (!fs.statSync(path.join(cwd, relative)).isFile()) return undefined;
|
|
2273
|
+
} catch {
|
|
2274
|
+
return undefined;
|
|
2275
|
+
}
|
|
2276
|
+
return {
|
|
2277
|
+
path: relative,
|
|
2278
|
+
reason: event.toolName === "read" ? "Runtime observed successful source read." : "Runtime observed successful document read."
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
function mergeObservedTaskContext(
|
|
2283
|
+
task: TaskContract,
|
|
2284
|
+
entries: ObservedTaskContext[],
|
|
2285
|
+
maxManifestFiles: number
|
|
2286
|
+
): string[] {
|
|
2287
|
+
const known = new Set(task.contextManifest.map((item) => item.path));
|
|
2288
|
+
const added: string[] = [];
|
|
2289
|
+
for (const entry of entries) {
|
|
2290
|
+
if (task.contextManifest.length >= maxManifestFiles || known.has(entry.path)) continue;
|
|
2291
|
+
task.contextManifest.push({ path: entry.path, reason: redactText(entry.reason) });
|
|
2292
|
+
known.add(entry.path);
|
|
2293
|
+
added.push(entry.path);
|
|
2294
|
+
}
|
|
2295
|
+
return added;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
function passingVerifyCommandsForDigest(task: TaskContract, digest: string): Set<string> {
|
|
2299
|
+
return new Set(task.verifyEvidence
|
|
2300
|
+
.filter((evidence) => (
|
|
2301
|
+
evidence.exitCode === 0
|
|
2302
|
+
&& evidence.observed === true
|
|
2303
|
+
&& evidence.matchedProfileCommand === true
|
|
2304
|
+
&& evidence.workingTreeDigest === digest
|
|
2305
|
+
))
|
|
2306
|
+
.map((evidence) => evidence.command.trim()));
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
function allVerifyCommandsPassCurrentTree(task: TaskContract, digest: string): boolean {
|
|
2310
|
+
const planned = meaningfulVerifyCommands(task.verifyCommands);
|
|
2311
|
+
if (planned.length === 0) return false;
|
|
2312
|
+
const passing = passingVerifyCommandsForDigest(task, digest);
|
|
2313
|
+
return planned.every((command) => passing.has(command.trim()));
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
function compactTaskDetails(task: TaskContract): Record<string, unknown> {
|
|
2317
|
+
return {
|
|
2318
|
+
schemaVersion: task.schemaVersion,
|
|
2319
|
+
taskRunId: task.taskRunId,
|
|
2320
|
+
taskId: task.taskId,
|
|
2321
|
+
sessionId: task.sessionId,
|
|
2322
|
+
sessionName: task.sessionName,
|
|
2323
|
+
changeMode: task.changeMode,
|
|
2324
|
+
attempt: task.attempt,
|
|
2325
|
+
maxAttempts: task.maxAttempts,
|
|
2326
|
+
previousAttempts: task.previousAttempts,
|
|
2327
|
+
riskLane: task.riskLane,
|
|
2328
|
+
intakeMode: task.intakeMode ?? "model",
|
|
2329
|
+
scope: task.scope,
|
|
2330
|
+
verifyGroup: task.verifyGroup,
|
|
2331
|
+
verifyCommands: task.verifyCommands,
|
|
2332
|
+
workPlan: task.workPlan,
|
|
2333
|
+
reviewLenses: task.reviewLenses,
|
|
2334
|
+
orchestration: task.orchestration
|
|
2335
|
+
? {
|
|
2336
|
+
mode: task.orchestration.mode,
|
|
2337
|
+
subagents: task.orchestration.subagents,
|
|
2338
|
+
reason: task.orchestration.reason
|
|
2339
|
+
}
|
|
2340
|
+
: undefined,
|
|
2341
|
+
lifecycleMode: runtimeLifecycleMode(task)
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
function changedSnapshotFiles(
|
|
2346
|
+
before: Record<string, string>,
|
|
2347
|
+
after: Record<string, string>
|
|
2348
|
+
): string[] {
|
|
2349
|
+
return [...new Set([...Object.keys(before), ...Object.keys(after)])]
|
|
2350
|
+
.filter((file) => before[file] !== after[file])
|
|
2351
|
+
.sort();
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function recordObservedTaskChanges(
|
|
2355
|
+
pi: ExtensionAPI,
|
|
2356
|
+
ctx: ExtensionContext,
|
|
2357
|
+
event: { toolName: string; input?: unknown; isError?: boolean },
|
|
2358
|
+
pendingContext: ObservedTaskContext[],
|
|
2359
|
+
maxManifestFiles: number,
|
|
2360
|
+
shellSnapshotBefore?: Record<string, string>
|
|
2361
|
+
): TaskContract | undefined {
|
|
2362
|
+
if (event.isError) return;
|
|
2363
|
+
const input = isPlainRecord(event.input) ? event.input : {};
|
|
2364
|
+
if (!isTaskMutationTool(event.toolName, input)) return;
|
|
2365
|
+
const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
|
|
2366
|
+
if (!task || task.trace.outcome !== "pending") return;
|
|
2367
|
+
|
|
2368
|
+
const targets = taskMutationTargets(ctx.cwd, event.toolName, input);
|
|
2369
|
+
const shellMutationObserved = SHELL_TOOL_NAMES.has(event.toolName) && shellSnapshotBefore !== undefined;
|
|
2370
|
+
const shellChangedFiles = shellMutationObserved
|
|
2371
|
+
? changedSnapshotFiles(shellSnapshotBefore, workingTreeSnapshot(ctx.cwd) as Record<string, string>)
|
|
2372
|
+
: [];
|
|
2373
|
+
const nextObserved = uniqueStrings([...task.observedChangedFiles, ...shellChangedFiles, ...targets]).sort();
|
|
2374
|
+
const added = nextObserved.filter((file) => !task.observedChangedFiles.includes(file));
|
|
2375
|
+
const contextAdded = mergeObservedTaskContext(task, pendingContext, maxManifestFiles);
|
|
2376
|
+
const lifecycle = (targets.length > 0 || shellChangedFiles.length > 0)
|
|
2377
|
+
? applyRuntimeLifecycleObservation(task, "mutation", nowIso())
|
|
2378
|
+
: { changed: false, mode: runtimeLifecycleMode(task) };
|
|
2379
|
+
if (added.length === 0 && contextAdded.length === 0 && !lifecycle.changed) return;
|
|
2380
|
+
task.observedChangedFiles = nextObserved;
|
|
2381
|
+
const written = writeTask(ctx.cwd, task);
|
|
2382
|
+
const trace = {
|
|
2383
|
+
event: "task_changes_observed",
|
|
2384
|
+
taskId: written.taskId,
|
|
2385
|
+
taskRunId: written.taskRunId,
|
|
2386
|
+
sessionId: written.sessionId,
|
|
2387
|
+
toolName: event.toolName,
|
|
2388
|
+
files: added,
|
|
2389
|
+
contextFiles: contextAdded,
|
|
2390
|
+
lifecycleMode: lifecycle.mode,
|
|
2391
|
+
lifecycleAdvanced: lifecycle.changed
|
|
2392
|
+
};
|
|
2393
|
+
appendTrace(ctx.cwd, trace);
|
|
2394
|
+
appendSessionTrace(pi, trace);
|
|
2395
|
+
return written;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function recordObservedTaskVerification(
|
|
2399
|
+
pi: ExtensionAPI,
|
|
2400
|
+
ctx: ExtensionContext,
|
|
2401
|
+
observed: {
|
|
2402
|
+
command?: string;
|
|
2403
|
+
normalizedCommand?: string;
|
|
2404
|
+
commandHash?: string;
|
|
2405
|
+
exitCode?: number;
|
|
2406
|
+
isError?: boolean;
|
|
2407
|
+
recordedAt?: string;
|
|
2408
|
+
},
|
|
2409
|
+
pendingContext: ObservedTaskContext[],
|
|
2410
|
+
maxManifestFiles: number
|
|
2411
|
+
): TaskContract | undefined {
|
|
2412
|
+
const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
|
|
2413
|
+
if (!task || task.trace.outcome !== "pending") return;
|
|
2414
|
+
const command = String(observed.normalizedCommand ?? observed.command ?? "").trim();
|
|
2415
|
+
const observedAtMs = Date.parse(observed.recordedAt ?? "");
|
|
2416
|
+
const taskCreatedAtMs = Date.parse(task.createdAt);
|
|
2417
|
+
if (
|
|
2418
|
+
!command
|
|
2419
|
+
|| !observed.commandHash
|
|
2420
|
+
|| !commandMatchesVerifyPlan(command, task.verifyCommands)
|
|
2421
|
+
|| !Number.isFinite(observedAtMs)
|
|
2422
|
+
|| !Number.isFinite(taskCreatedAtMs)
|
|
2423
|
+
|| observedAtMs < taskCreatedAtMs
|
|
2424
|
+
) return;
|
|
2425
|
+
|
|
2426
|
+
const currentDigests = workingTreeSnapshot(ctx.cwd) as Record<string, string>;
|
|
2427
|
+
const currentDigest = workingTreeEvidenceDigest(currentDigests);
|
|
2428
|
+
const exitCode = Number.isInteger(observed.exitCode) ? observed.exitCode as number : observed.isError ? 1 : 0;
|
|
2429
|
+
const duplicate = task.verifyEvidence.some((evidence) => (
|
|
2430
|
+
evidence.command.trim() === command
|
|
2431
|
+
&& evidence.exitCode === exitCode
|
|
2432
|
+
&& evidence.workingTreeDigest === currentDigest
|
|
2433
|
+
));
|
|
2434
|
+
const contextAdded = mergeObservedTaskContext(task, pendingContext, maxManifestFiles);
|
|
2435
|
+
if (!duplicate) {
|
|
2436
|
+
task.verifyEvidence.push({
|
|
2437
|
+
command: redactText(command),
|
|
2438
|
+
exitCode,
|
|
2439
|
+
summary: `Runtime observed configured verifier exit ${exitCode}.`,
|
|
2440
|
+
recordedAt: nowIso(),
|
|
2441
|
+
observed: true,
|
|
2442
|
+
observedAt: observed.recordedAt ?? nowIso(),
|
|
2443
|
+
isError: observed.isError === true,
|
|
2444
|
+
matchedProfileCommand: true,
|
|
2445
|
+
workingTreeDigest: currentDigest
|
|
2446
|
+
});
|
|
2447
|
+
task.verifyEvidence = task.verifyEvidence.slice(-100);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
const hasChanges = taskChangedFileEvidence(ctx.cwd, task, currentDigests).expected.length > 0;
|
|
2451
|
+
const allPassing = hasChanges && allVerifyCommandsPassCurrentTree(task, currentDigest);
|
|
2452
|
+
const lifecycle = hasChanges
|
|
2453
|
+
? applyRuntimeLifecycleObservation(task, allPassing ? "verification-complete" : "verification-pending", nowIso())
|
|
2454
|
+
: { changed: false, mode: runtimeLifecycleMode(task) };
|
|
2455
|
+
if (duplicate && contextAdded.length === 0 && !lifecycle.changed) return task;
|
|
2456
|
+
|
|
2457
|
+
const written = writeTask(ctx.cwd, task);
|
|
2458
|
+
const trace = {
|
|
2459
|
+
event: "verify_observed",
|
|
2460
|
+
taskId: written.taskId,
|
|
2461
|
+
taskRunId: written.taskRunId,
|
|
2462
|
+
sessionId: written.sessionId,
|
|
2463
|
+
command: redactText(command),
|
|
2464
|
+
exitCode,
|
|
2465
|
+
workingTreeDigest: currentDigest,
|
|
2466
|
+
contextFiles: contextAdded,
|
|
2467
|
+
lifecycleMode: lifecycle.mode,
|
|
2468
|
+
lifecycleAdvanced: lifecycle.changed,
|
|
2469
|
+
allConfiguredVerifiersPassing: allPassing
|
|
2470
|
+
};
|
|
2471
|
+
appendTrace(ctx.cwd, trace);
|
|
2472
|
+
appendSessionTrace(pi, trace);
|
|
2473
|
+
return written;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
function flushObservedTaskContext(
|
|
2477
|
+
pi: ExtensionAPI,
|
|
2478
|
+
ctx: ExtensionContext,
|
|
2479
|
+
pendingContext: ObservedTaskContext[],
|
|
2480
|
+
maxManifestFiles: number,
|
|
2481
|
+
event: string
|
|
2482
|
+
): TaskContract | undefined {
|
|
2483
|
+
const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
|
|
2484
|
+
if (!task || task.trace.outcome !== "pending") return task;
|
|
2485
|
+
const added = mergeObservedTaskContext(task, pendingContext, maxManifestFiles);
|
|
2486
|
+
const lifecycle = task.changeMode === "read-only" && task.contextManifest.length > 0
|
|
2487
|
+
? applyRuntimeLifecycleObservation(task, "context-complete", nowIso())
|
|
2488
|
+
: { changed: false, mode: runtimeLifecycleMode(task) };
|
|
2489
|
+
if (added.length === 0 && !lifecycle.changed) return task;
|
|
2490
|
+
const written = writeTask(ctx.cwd, task);
|
|
2491
|
+
const trace = {
|
|
2492
|
+
event,
|
|
2493
|
+
taskId: written.taskId,
|
|
2494
|
+
taskRunId: written.taskRunId,
|
|
2495
|
+
sessionId: written.sessionId,
|
|
2496
|
+
files: added,
|
|
2497
|
+
lifecycleMode: lifecycle.mode,
|
|
2498
|
+
lifecycleAdvanced: lifecycle.changed
|
|
2499
|
+
};
|
|
2500
|
+
appendTrace(ctx.cwd, trace);
|
|
2501
|
+
appendSessionTrace(pi, trace);
|
|
2502
|
+
return written;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
function completionTaskProjection(
|
|
2506
|
+
cwd: string,
|
|
2507
|
+
task: TaskContract,
|
|
2508
|
+
finalFileDigests: Record<string, string> = workingTreeSnapshot(cwd) as Record<string, string>
|
|
2509
|
+
): TaskContract {
|
|
2510
|
+
const changedFiles = taskChangedFileEvidence(cwd, task, finalFileDigests).expected;
|
|
2511
|
+
return {
|
|
2512
|
+
...task,
|
|
2513
|
+
changedFiles,
|
|
2514
|
+
finalWorkingTreeFiles: Object.keys(finalFileDigests).sort(),
|
|
2515
|
+
finalFileDigests,
|
|
2516
|
+
failedAt: undefined,
|
|
2517
|
+
failureReason: undefined,
|
|
2518
|
+
ruledOut: undefined,
|
|
2519
|
+
trace: {
|
|
2520
|
+
outcome: "completed",
|
|
2521
|
+
notes: "Runtime finalized from observed context, working-tree changes, current verification, and completed work-plan evidence.",
|
|
2522
|
+
recordedAt: nowIso()
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2020
2527
|
function prepareToolInputForPolicy(
|
|
2021
2528
|
toolName: string,
|
|
2022
2529
|
input: Record<string, unknown>,
|
|
@@ -2473,11 +2980,13 @@ function searchMemoryFiles(cwd: string, profile: ProjectProfile, query: string,
|
|
|
2473
2980
|
}
|
|
2474
2981
|
|
|
2475
2982
|
function writeProjectOnboarding(cwd: string, snapshot: ProjectOnboardingSnapshot, markdown: string): ProjectOnboardingSnapshot {
|
|
2476
|
-
|
|
2983
|
+
ensurePrivateStateDirectory(cwd, path.join(cwd, ".pi"), "Piagent project directory");
|
|
2477
2984
|
ensureStateDirs(cwd);
|
|
2478
2985
|
const safeSnapshot = redactForStorage(snapshot) as ProjectOnboardingSnapshot;
|
|
2479
|
-
|
|
2480
|
-
|
|
2986
|
+
const contextFile = resolveLocalStatePath(cwd, projectContextFilePath(cwd), { label: "Project context file" });
|
|
2987
|
+
const onboardingFile = resolveLocalStatePath(cwd, onboardingStateFilePath(cwd), { label: "Project onboarding state" });
|
|
2988
|
+
fs.writeFileSync(contextFile, `${redactText(markdown).trimEnd()}\n`);
|
|
2989
|
+
fs.writeFileSync(onboardingFile, `${JSON.stringify(safeSnapshot, null, 2)}\n`, { mode: 0o600 });
|
|
2481
2990
|
return safeSnapshot;
|
|
2482
2991
|
}
|
|
2483
2992
|
|
|
@@ -3391,42 +3900,175 @@ function writeProfileFromAdapter(extensionDir: string, cwd: string, profileName:
|
|
|
3391
3900
|
return resolveProjectProfileDocument(findPlatformRoot(extensionDir), stored).profile as ProjectProfile;
|
|
3392
3901
|
}
|
|
3393
3902
|
|
|
3394
|
-
function readTask(cwd: string, taskId: string): TaskContract | undefined {
|
|
3395
|
-
return
|
|
3903
|
+
function readTask(cwd: string, taskId: string, sessionId?: string): TaskContract | undefined {
|
|
3904
|
+
return resolveTaskContract(cwd, taskId, sessionId) as TaskContract | undefined;
|
|
3396
3905
|
}
|
|
3397
3906
|
|
|
3398
3907
|
function writeTask(cwd: string, task: TaskContract): TaskContract {
|
|
3399
|
-
|
|
3400
|
-
task.updatedAt = nowIso();
|
|
3401
|
-
fs.writeFileSync(taskFilePath(cwd, task.taskId), `${JSON.stringify(task, null, 2)}\n`);
|
|
3402
|
-
return task;
|
|
3908
|
+
return writeTaskContract(cwd, task) as TaskContract;
|
|
3403
3909
|
}
|
|
3404
3910
|
|
|
3405
|
-
function
|
|
3911
|
+
function meaningfulVerifyCommands(commands: string[]): string[] {
|
|
3912
|
+
return commands.filter((command) => {
|
|
3913
|
+
const normalized = command.trim().toLowerCase();
|
|
3914
|
+
if (!normalized) return false;
|
|
3915
|
+
if (/no (?:project|backend|data|frontend|runtime|docs|mobile) verify command configured/.test(normalized) && !/\b(?:if|elif)\b/.test(normalized)) return false;
|
|
3916
|
+
if (/^(?:true|:|echo\b|printf\b)/.test(normalized)) return false;
|
|
3917
|
+
return true;
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
function taskChangedFileEvidence(
|
|
3922
|
+
cwd: string,
|
|
3923
|
+
task: TaskContract,
|
|
3924
|
+
currentDigests: Record<string, string> = workingTreeSnapshot(cwd) as Record<string, string>
|
|
3925
|
+
): {
|
|
3926
|
+
current: string[];
|
|
3927
|
+
expected: string[];
|
|
3928
|
+
undeclared: string[];
|
|
3929
|
+
unsupportedClaims: string[];
|
|
3930
|
+
outsideScope: string[];
|
|
3931
|
+
} {
|
|
3932
|
+
const current = Object.keys(currentDigests).sort();
|
|
3933
|
+
const baselineDigests = task.baselineFileDigests ?? {};
|
|
3934
|
+
// Observed mutations are audit history, not proof of a final source change.
|
|
3935
|
+
// A file edited and then restored to its task-start digest must not satisfy a
|
|
3936
|
+
// source-change gate merely because a write tool touched it earlier.
|
|
3937
|
+
const expected = current
|
|
3938
|
+
.filter((file) => baselineDigests[file] !== currentDigests[file])
|
|
3939
|
+
.sort();
|
|
3940
|
+
const declared = new Set(task.changedFiles ?? []);
|
|
3941
|
+
const expectedSet = new Set(expected);
|
|
3942
|
+
return {
|
|
3943
|
+
current,
|
|
3944
|
+
expected,
|
|
3945
|
+
undeclared: expected.filter((file) => !declared.has(file)),
|
|
3946
|
+
unsupportedClaims: [...declared].filter((file) => !expectedSet.has(file)),
|
|
3947
|
+
outsideScope: expected.filter((file) => !taskScopeIncludesPath(task.scope, file))
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
function taskScopeIncludesPath(scope: string[], file: string): boolean {
|
|
3952
|
+
const normalizedFile = normalizePathCandidate(file);
|
|
3953
|
+
return scope.some((candidate) => {
|
|
3954
|
+
const normalized = normalizePathCandidate(candidate);
|
|
3955
|
+
if (!normalized) return false;
|
|
3956
|
+
if ([".", "**", "**/*"].includes(normalized)) return true;
|
|
3957
|
+
if (matchesAnyPath(normalizedFile, [normalized])) return true;
|
|
3958
|
+
return !/[?*[\]{}]/.test(normalized) && normalizedFile.startsWith(`${normalized.replace(/\/$/, "")}/`);
|
|
3959
|
+
});
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
function verifierCommandInstructions(commands: string[]): string[] {
|
|
3963
|
+
return commands.map((command, index) => `Verifier ${index + 1} (run as its own shell call): ${command}`);
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3966
|
+
function evaluateTaskGate(
|
|
3967
|
+
cwd: string,
|
|
3968
|
+
task: TaskContract | undefined,
|
|
3969
|
+
policy: BasePolicy,
|
|
3970
|
+
options: {
|
|
3971
|
+
currentDigests?: Record<string, string>;
|
|
3972
|
+
currentWorkingTreeDigest?: string;
|
|
3973
|
+
} = {}
|
|
3974
|
+
): {
|
|
3406
3975
|
decision: "pass" | "fail";
|
|
3407
3976
|
missing: string[];
|
|
3977
|
+
missingVerifyCommands: string[];
|
|
3408
3978
|
warnings: string[];
|
|
3979
|
+
changedFileEvidence?: ReturnType<typeof taskChangedFileEvidence>;
|
|
3409
3980
|
} {
|
|
3410
3981
|
const finalGate = finalGateConfig(policy);
|
|
3411
3982
|
const missing: string[] = [];
|
|
3412
3983
|
const warnings: string[] = [];
|
|
3413
3984
|
if (!task) {
|
|
3414
|
-
return { decision: "fail", missing: ["task contract"], warnings };
|
|
3415
|
-
}
|
|
3985
|
+
return { decision: "fail", missing: ["task contract"], missingVerifyCommands: [], warnings };
|
|
3986
|
+
}
|
|
3987
|
+
const currentDigests = options.currentDigests ?? workingTreeSnapshot(cwd) as Record<string, string>;
|
|
3988
|
+
const currentWorkingTreeDigest = options.currentWorkingTreeDigest ?? workingTreeEvidenceDigest(currentDigests);
|
|
3989
|
+
if (task.schemaVersion !== 2 || !task.taskRunId || !task.sessionId) missing.push("session-bound task contract v2");
|
|
3990
|
+
if (task.attempt > task.maxAttempts) missing.push(`attempt within maxAttempts (${task.attempt}/${task.maxAttempts})`);
|
|
3991
|
+
const plannedVerifyCommands = meaningfulVerifyCommands(task.verifyCommands);
|
|
3992
|
+
if (task.changeMode === "source-change" && plannedVerifyCommands.length === 0) missing.push("meaningful verify command");
|
|
3416
3993
|
if (finalGate.requireContextManifest && task.contextManifest.length === 0) missing.push("context manifest");
|
|
3417
|
-
if (finalGate.requireVerifyEvidence && task.verifyEvidence.length === 0) missing.push("verify evidence");
|
|
3994
|
+
if (task.changeMode === "source-change" && finalGate.requireVerifyEvidence && task.verifyEvidence.length === 0) missing.push("verify evidence");
|
|
3418
3995
|
if (task.verifyEvidence.some((evidence) => evidence.observed !== true)) {
|
|
3419
3996
|
warnings.push("Unobserved verify evidence is ignored by the passing verify gate.");
|
|
3420
3997
|
}
|
|
3421
3998
|
if (task.verifyEvidence.some((evidence) => evidence.observed === true && evidence.matchedProfileCommand !== true)) {
|
|
3422
3999
|
warnings.push("Observed verify evidence that does not exactly match task verifyCommands is advisory only.");
|
|
3423
4000
|
}
|
|
3424
|
-
if (
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
4001
|
+
if (task.verifyEvidence.some((evidence) => (
|
|
4002
|
+
evidence.observed === true
|
|
4003
|
+
&& evidence.matchedProfileCommand === true
|
|
4004
|
+
&& evidence.workingTreeDigest !== currentWorkingTreeDigest
|
|
4005
|
+
))) {
|
|
4006
|
+
warnings.push("Verification evidence from a different working-tree snapshot is stale and is ignored.");
|
|
4007
|
+
}
|
|
4008
|
+
let missingVerifyCommands: string[] = [];
|
|
4009
|
+
if (task.changeMode === "source-change" && finalGate.requirePassingVerify && plannedVerifyCommands.length > 0) {
|
|
4010
|
+
const passingCommands = new Set(task.verifyEvidence
|
|
4011
|
+
.filter((evidence) => (
|
|
4012
|
+
evidence.exitCode === 0
|
|
4013
|
+
&& evidence.observed === true
|
|
4014
|
+
&& evidence.matchedProfileCommand === true
|
|
4015
|
+
&& evidence.workingTreeDigest === currentWorkingTreeDigest
|
|
4016
|
+
))
|
|
4017
|
+
.map((evidence) => evidence.command.trim()));
|
|
4018
|
+
missingVerifyCommands = plannedVerifyCommands.filter((command) => !passingCommands.has(command.trim()));
|
|
4019
|
+
if (missingVerifyCommands.length > 0) missing.push(`observed passing verify evidence for every configured command (${missingVerifyCommands.length} missing)`);
|
|
4020
|
+
}
|
|
4021
|
+
if (finalGate.requireTrace && task.trace.outcome !== "completed") missing.push("completed final trace");
|
|
4022
|
+
const incompleteSteps = (task.workPlan ?? []).filter((step) => step.status === "pending" || step.status === "in-progress" || step.status === "failed");
|
|
4023
|
+
if (task.trace.outcome === "completed" && incompleteSteps.length > 0) {
|
|
4024
|
+
missing.push(`completed work plan (${incompleteSteps.map((step) => `${step.id}:${step.status}`).join(", ")})`);
|
|
4025
|
+
}
|
|
4026
|
+
const changedFileEvidence = taskChangedFileEvidence(cwd, task, currentDigests);
|
|
4027
|
+
if (task.changeMode === "source-change" && task.trace.outcome === "completed") {
|
|
4028
|
+
if (task.changedFiles.length === 0) missing.push("changed files");
|
|
4029
|
+
if (changedFileEvidence.undeclared.length > 0) missing.push(`declared observed changes (${changedFileEvidence.undeclared.join(", ")})`);
|
|
4030
|
+
if (changedFileEvidence.unsupportedClaims.length > 0) missing.push(`supported changed-file claims (${changedFileEvidence.unsupportedClaims.join(", ")})`);
|
|
4031
|
+
if (changedFileEvidence.outsideScope.length > 0) missing.push(`changes within task scope (${changedFileEvidence.outsideScope.join(", ")})`);
|
|
4032
|
+
}
|
|
4033
|
+
if (task.changeMode === "read-only" && changedFileEvidence.expected.length > 0) {
|
|
4034
|
+
missing.push(`read-only task has observed changes (${changedFileEvidence.expected.join(", ")})`);
|
|
4035
|
+
}
|
|
4036
|
+
return { decision: missing.length === 0 ? "pass" : "fail", missing, missingVerifyCommands, warnings, changedFileEvidence };
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
function assistantMessageText(message: unknown): string {
|
|
4040
|
+
if (!message || typeof message !== "object" || (message as { role?: unknown }).role !== "assistant") return "";
|
|
4041
|
+
const content = (message as { content?: unknown }).content;
|
|
4042
|
+
if (!Array.isArray(content)) return "";
|
|
4043
|
+
return content
|
|
4044
|
+
.filter((item): item is { type: "text"; text: string } => Boolean(item && typeof item === "object" && (item as { type?: unknown }).type === "text" && typeof (item as { text?: unknown }).text === "string"))
|
|
4045
|
+
.map((item) => item.text)
|
|
4046
|
+
.join("\n");
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
function assistantMessageHasToolCall(message: unknown): boolean {
|
|
4050
|
+
const content = message && typeof message === "object" ? (message as { content?: unknown }).content : undefined;
|
|
4051
|
+
return Array.isArray(content) && content.some((item) => item && typeof item === "object" && (item as { type?: unknown }).type === "toolCall");
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
function normalizeLanguageSignal(text: string): string {
|
|
4055
|
+
return text
|
|
4056
|
+
.normalize("NFKD")
|
|
4057
|
+
.replace(/\p{M}/gu, "")
|
|
4058
|
+
.toLowerCase()
|
|
4059
|
+
.replaceAll("đ", "d");
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
function looksLikeIncompleteHandoff(text: string): boolean {
|
|
4063
|
+
const normalized = normalizeLanguageSignal(text);
|
|
4064
|
+
return /\b(?:not done|not complete|not finished|still working|blocked|cannot complete|unable to complete|need clarification|tests? (?:fail|failed|failing)|verification (?:fail|failed|failing)|chua xong|chua hoan tat|chua hoan thanh|dang lam|bi chan|can lam them|test (?:loi|fail))\b/.test(normalized);
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
function looksLikeCompletionClaim(text: string): boolean {
|
|
4068
|
+
const normalized = normalizeLanguageSignal(text);
|
|
4069
|
+
if (!normalized.trim() || normalized.includes("[piagent completion gate:")) return false;
|
|
4070
|
+
if (looksLikeIncompleteHandoff(text)) return false;
|
|
4071
|
+
return /\b(?:done|completed|complete|finished|fixed|implemented|resolved|shipped|all tests pass(?:ed)?|tests? pass(?:ed)?|da xong|da sua|da fix|da hoan tat|da hoan thanh|da trien khai|test da pass|kiem tra da pass)\b/.test(normalized);
|
|
3430
4072
|
}
|
|
3431
4073
|
|
|
3432
4074
|
function formatCount(value: number | null | undefined): string {
|
|
@@ -3620,14 +4262,27 @@ function writeToolResultCapture(
|
|
|
3620
4262
|
};
|
|
3621
4263
|
|
|
3622
4264
|
try {
|
|
3623
|
-
|
|
3624
|
-
|
|
4265
|
+
ensurePrivateStateDirectory(cwd, path.dirname(absolutePath), "Tool-result capture directory");
|
|
4266
|
+
const capturePath = resolveLocalStatePath(cwd, absolutePath, { label: "Tool-result capture" });
|
|
4267
|
+
fs.writeFileSync(capturePath, [
|
|
3625
4268
|
"# Piagent compacted tool result capture",
|
|
3626
4269
|
JSON.stringify(metadata),
|
|
3627
4270
|
"---",
|
|
3628
4271
|
capture.text
|
|
3629
|
-
].join("\n"));
|
|
3630
|
-
|
|
4272
|
+
].join("\n"), { mode: 0o600 });
|
|
4273
|
+
appendJsonlBounded(toolResultCaptureIndexPath(cwd), metadata, { maxBytes: CAPTURE_INDEX_MAX_BYTES, mode: 0o600, projectRoot: cwd });
|
|
4274
|
+
const writes = (CAPTURE_WRITES_SINCE_PRUNE.get(cwd) ?? 0) + 1;
|
|
4275
|
+
if (writes >= 25) {
|
|
4276
|
+
pruneCaptureFiles(toolResultCaptureRoot(cwd), {
|
|
4277
|
+
maxFiles: CAPTURE_RETENTION_MAX_FILES,
|
|
4278
|
+
maxBytes: CAPTURE_RETENTION_MAX_BYTES,
|
|
4279
|
+
maxAgeMs: CAPTURE_RETENTION_MAX_AGE_MS,
|
|
4280
|
+
projectRoot: cwd
|
|
4281
|
+
});
|
|
4282
|
+
CAPTURE_WRITES_SINCE_PRUNE.set(cwd, 0);
|
|
4283
|
+
} else {
|
|
4284
|
+
CAPTURE_WRITES_SINCE_PRUNE.set(cwd, writes);
|
|
4285
|
+
}
|
|
3631
4286
|
const summary = {
|
|
3632
4287
|
path: relativePath,
|
|
3633
4288
|
source,
|
|
@@ -3731,16 +4386,9 @@ function attachToolResultCompactionDetails(details: unknown, captures: ToolResul
|
|
|
3731
4386
|
|
|
3732
4387
|
function readRecentToolResultCaptures(cwd: string, limit = 5): Record<string, unknown>[] {
|
|
3733
4388
|
const indexPath = toolResultCaptureIndexPath(cwd);
|
|
3734
|
-
if (!fs.existsSync(indexPath)) return [];
|
|
3735
|
-
const lines = fs.readFileSync(indexPath, "utf8").trim().split(/\n/).filter(Boolean);
|
|
3736
4389
|
const captures: Record<string, unknown>[] = [];
|
|
3737
|
-
for (const
|
|
3738
|
-
|
|
3739
|
-
const parsed = JSON.parse(line);
|
|
3740
|
-
if (isPlainRecord(parsed)) captures.push(redactForStorage(parsed) as Record<string, unknown>);
|
|
3741
|
-
} catch {
|
|
3742
|
-
// A partial JSONL line should not make a status command noisy or unusable.
|
|
3743
|
-
}
|
|
4390
|
+
for (const parsed of readJsonlTail(indexPath, { limit: Math.max(limit * 3, limit), maxBytes: CAPTURE_INDEX_MAX_BYTES, projectRoot: cwd })) {
|
|
4391
|
+
if (isPlainRecord(parsed)) captures.push(redactForStorage(parsed) as Record<string, unknown>);
|
|
3744
4392
|
}
|
|
3745
4393
|
return captures.slice(-limit);
|
|
3746
4394
|
}
|
|
@@ -4269,7 +4917,7 @@ function attachLocalImagesFromText(
|
|
|
4269
4917
|
function appendTrace(cwd: string, payload: Record<string, unknown>): void {
|
|
4270
4918
|
ensureStateDirs(cwd);
|
|
4271
4919
|
const safePayload = redactForStorage(payload) as Record<string, unknown>;
|
|
4272
|
-
|
|
4920
|
+
appendJsonlBounded(traceFilePath(cwd), { recordedAt: nowIso(), ...safePayload }, { maxBytes: TRACE_MAX_BYTES, mode: 0o600, projectRoot: cwd });
|
|
4273
4921
|
}
|
|
4274
4922
|
|
|
4275
4923
|
function appendSessionTrace(pi: ExtensionAPI, payload: Record<string, unknown>): void {
|
|
@@ -4281,8 +4929,51 @@ function appendSessionTrace(pi: ExtensionAPI, payload: Record<string, unknown>):
|
|
|
4281
4929
|
});
|
|
4282
4930
|
}
|
|
4283
4931
|
|
|
4284
|
-
function
|
|
4285
|
-
|
|
4932
|
+
function selectVerifyPlan(
|
|
4933
|
+
profile: ProjectProfile,
|
|
4934
|
+
requestedGroup: string | undefined,
|
|
4935
|
+
changeMode: TaskContract["changeMode"],
|
|
4936
|
+
cwd?: string
|
|
4937
|
+
): { group?: string; commands: string[]; error?: string } {
|
|
4938
|
+
if (changeMode === "read-only") return { commands: [] };
|
|
4939
|
+
const groups = profile.verifyCommands ?? {};
|
|
4940
|
+
const names = Object.keys(groups);
|
|
4941
|
+
const requested = requestedGroup?.trim();
|
|
4942
|
+
if (requested && !Object.hasOwn(groups, requested)) {
|
|
4943
|
+
return { commands: [], error: `Unknown verify group ${requested}. Available groups: ${names.join(", ") || "none"}.` };
|
|
4944
|
+
}
|
|
4945
|
+
const group = requested
|
|
4946
|
+
?? (Object.hasOwn(groups, "source") ? "source" : undefined)
|
|
4947
|
+
?? (Object.hasOwn(groups, "frontendSource") ? "frontendSource" : undefined)
|
|
4948
|
+
?? names.find((name) => !/docs|runtime/i.test(name));
|
|
4949
|
+
let commands = group ? uniqueStrings(groups[group] ?? []) : [];
|
|
4950
|
+
if (
|
|
4951
|
+
cwd
|
|
4952
|
+
&& commands.length === 1
|
|
4953
|
+
&& commands[0].includes("No Node source verifier is configured")
|
|
4954
|
+
) {
|
|
4955
|
+
try {
|
|
4956
|
+
const manifestPath = path.join(cwd, "package.json");
|
|
4957
|
+
if (!fs.lstatSync(manifestPath).isSymbolicLink()) {
|
|
4958
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as { scripts?: Record<string, unknown> };
|
|
4959
|
+
const scripts = manifest.scripts && typeof manifest.scripts === "object" ? manifest.scripts : {};
|
|
4960
|
+
const selected = ["type-check", "typecheck", "lint", "test"].filter((name) => typeof scripts[name] === "string");
|
|
4961
|
+
if (selected.length > 0) {
|
|
4962
|
+
commands = selected.map((name) => name === "test" ? "npm test" : `npm run ${name}`);
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
} catch {
|
|
4966
|
+
// Preserve the profile's fail-closed verifier when package metadata is absent or invalid.
|
|
4967
|
+
}
|
|
4968
|
+
}
|
|
4969
|
+
if (meaningfulVerifyCommands(commands).length === 0) {
|
|
4970
|
+
return {
|
|
4971
|
+
group,
|
|
4972
|
+
commands,
|
|
4973
|
+
error: `Verify group ${group ?? "(none)"} has no meaningful command. Configure .pi/piagent-profile.json before source changes.`
|
|
4974
|
+
};
|
|
4975
|
+
}
|
|
4976
|
+
return { group, commands };
|
|
4286
4977
|
}
|
|
4287
4978
|
|
|
4288
4979
|
function parseReferenceRepoRef(input: string): { host: string; owner: string; repo: string } {
|
|
@@ -4404,7 +5095,7 @@ function checkoutReferenceRepo(repoRef: string, forceUpdate = false): ReferenceR
|
|
|
4404
5095
|
function toolGroupsForPrompt(prompt: string): PiagentToolGroup[] {
|
|
4405
5096
|
const signal = classifyContextTask(prompt);
|
|
4406
5097
|
const lower = prompt.toLowerCase();
|
|
4407
|
-
const groups = new Set<PiagentToolGroup>(
|
|
5098
|
+
const groups = new Set<PiagentToolGroup>();
|
|
4408
5099
|
|
|
4409
5100
|
if (signal.workflow === "usage") {
|
|
4410
5101
|
groups.add("usage");
|
|
@@ -4419,50 +5110,127 @@ function toolGroupsForPrompt(prompt: string): PiagentToolGroup[] {
|
|
|
4419
5110
|
return [...groups];
|
|
4420
5111
|
}
|
|
4421
5112
|
|
|
4422
|
-
groups.add("
|
|
4423
|
-
|
|
5113
|
+
groups.add("intake");
|
|
5114
|
+
// Keep the common task surface stable across the first tool call so provider
|
|
5115
|
+
// prompt caches do not lose the full system prefix after task_start.
|
|
5116
|
+
if (signal.lane !== "tiny") groups.add("task");
|
|
4424
5117
|
if (
|
|
4425
|
-
|
|
4426
|
-
|| ["scout", "review", "plan", "release"].includes(signal.workflow)
|
|
4427
|
-
|| /\b(context|document|memory|repository|source|subagent|vendor)\b/.test(lower)
|
|
5118
|
+
/\b(context (?:engine|index|search|diagnostic)|source checkout|vendor checkout)\b/.test(lower)
|
|
4428
5119
|
) {
|
|
4429
5120
|
groups.add("retrieval");
|
|
4430
5121
|
}
|
|
4431
5122
|
if (
|
|
4432
|
-
|
|
4433
|
-
|| ["scout", "review", "plan", "release"].includes(signal.workflow)
|
|
4434
|
-
|| /\b(document|memory|orchestration|source checkout|subagent|vendor)\b/.test(lower)
|
|
5123
|
+
/\b(document intake|project memory|orchestration policy|source checkout|subagent policy|vendor checkout)\b/.test(lower)
|
|
4435
5124
|
) {
|
|
4436
5125
|
groups.add("knowledge");
|
|
4437
5126
|
}
|
|
5127
|
+
if (/\b(permission|capability|exec policy|tool policy)\b/.test(lower)) groups.add("policy");
|
|
4438
5128
|
if (/\b(profile|tech stack|onboard|project context)\b/.test(lower)) groups.add("onboarding");
|
|
4439
5129
|
if (/\b(token|usage|cost)\b/.test(lower)) groups.add("usage");
|
|
5130
|
+
if (/\b(?:piagent tool loader|load piagent tools?|piagent tool groups?)\b/.test(lower)) groups.add("loader");
|
|
4440
5131
|
return [...groups];
|
|
4441
5132
|
}
|
|
4442
5133
|
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
5134
|
+
const AUTO_INTAKE_MAX_PROMPT_CHARS = 700;
|
|
5135
|
+
const AUTO_INTAKE_SNAPSHOT_PATTERNS = [
|
|
5136
|
+
"src/**", "app/**", "lib/**", "packages/**", "test/**", "tests/**", "spec/**", "__tests__/**"
|
|
5137
|
+
];
|
|
5138
|
+
const AUTO_INTAKE_CHANGE_INTENT = /\b(?:add|build|change|correct|create|fix|implement|modify|refactor|remove|rename|replace|update|write|sua|them|doi|cap nhat|xoa|tao)\b/i;
|
|
5139
|
+
const AUTO_INTAKE_READ_ONLY_LEAD = /^\s*\/?(?:analy[sz]e|audit|check|discuss|explain|inspect|plan|research|review|scout|summari[sz]e|why|how|can\s+(?:you|we)|kiem tra|nghien cuu|giai thich|danh gia)\b/i;
|
|
5140
|
+
const AUTO_INTAKE_MANUAL_RISK = /\b(?:credential|database|deploy|destructive|encryption|external provider|migration|payment|permission|production|publish|release|secret|schema migration|token rotation)\b/i;
|
|
5141
|
+
|
|
5142
|
+
function automaticTaskIntakeEligible(prompt: string, readProtectedPaths: string[]): boolean {
|
|
5143
|
+
const text = String(prompt ?? "").trim();
|
|
5144
|
+
if (!text || text.length > AUTO_INTAKE_MAX_PROMPT_CHARS) return false;
|
|
5145
|
+
const signal = classifyContextTask(text);
|
|
5146
|
+
if (signal.workflow !== "task" || !AUTO_INTAKE_CHANGE_INTENT.test(text)) return false;
|
|
5147
|
+
if (AUTO_INTAKE_READ_ONLY_LEAD.test(text) || AUTO_INTAKE_MANUAL_RISK.test(text)) return false;
|
|
5148
|
+
if (/\bpiagent_task_start\b/i.test(text)) return false;
|
|
5149
|
+
return !signal.paths.some((candidate) => matchesProtectedPath(candidate, readProtectedPaths));
|
|
5150
|
+
}
|
|
5151
|
+
|
|
5152
|
+
function manualTaskIntakeEligible(prompt: string, readProtectedPaths: string[]): boolean {
|
|
5153
|
+
const text = String(prompt ?? "").trim();
|
|
5154
|
+
if (!text || text.length > LONG_INPUT_CHARS) return false;
|
|
5155
|
+
const signal = classifyContextTask(text);
|
|
5156
|
+
if (signal.workflow !== "task" || !AUTO_INTAKE_CHANGE_INTENT.test(text)) return false;
|
|
5157
|
+
if (AUTO_INTAKE_READ_ONLY_LEAD.test(text) || /\bpiagent_task_start\b/i.test(text)) return false;
|
|
5158
|
+
if (signal.paths.length === 0) return true;
|
|
5159
|
+
return signal.paths.some((candidate) => !matchesProtectedPath(candidate, readProtectedPaths));
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5162
|
+
function automaticTaskScope(prompt: string, context: Array<{ path: string }>): string[] {
|
|
5163
|
+
const signal = classifyContextTask(prompt);
|
|
5164
|
+
const explicit = signal.paths.filter((candidate) => candidate && candidate !== "." && !candidate.startsWith(".pi/"));
|
|
5165
|
+
const navigated = context
|
|
5166
|
+
.map((item) => normalizePathCandidate(item.path))
|
|
5167
|
+
.filter((candidate): candidate is string => Boolean(
|
|
5168
|
+
candidate
|
|
5169
|
+
&& candidate !== "."
|
|
5170
|
+
&& !candidate.startsWith(".pi/")
|
|
5171
|
+
&& !["AGENTS.md", "README.md", "REVIEW_GUIDELINES.md"].includes(candidate)
|
|
5172
|
+
&& /^(?:app|lib|packages|spec|src|test|tests|__tests__)\//.test(candidate)
|
|
5173
|
+
))
|
|
5174
|
+
.slice(0, 8);
|
|
5175
|
+
const scope = uniqueStrings([...explicit, ...navigated]);
|
|
5176
|
+
if (scope.length === 0) scope.push("src/**", "app/**", "lib/**");
|
|
5177
|
+
if (/\b(?:test|tests|verification|verify|kiem thu)\b/i.test(prompt)) {
|
|
5178
|
+
scope.push("test/**", "tests/**", "spec/**", "__tests__/**");
|
|
5179
|
+
}
|
|
5180
|
+
return uniqueStrings(scope);
|
|
5181
|
+
}
|
|
5182
|
+
|
|
5183
|
+
function automaticReviewLenses(prompt: string): ReviewLens[] {
|
|
5184
|
+
const lenses: ReviewLens[] = ["correctness", "tests", "scope"];
|
|
5185
|
+
if (/\b(?:auth|authorization|credential|permission|security|session|secret|xac thuc|phan quyen|bao mat)\b/i.test(prompt)) {
|
|
5186
|
+
lenses.push("security");
|
|
5187
|
+
}
|
|
5188
|
+
return lenses;
|
|
5189
|
+
}
|
|
5190
|
+
|
|
5191
|
+
function validTaskScopePattern(value: string): boolean {
|
|
5192
|
+
const candidate = String(value ?? "").trim().replaceAll("\\", "/");
|
|
5193
|
+
if (!candidate || /\s|\0/.test(candidate)) return false;
|
|
5194
|
+
if (candidate.startsWith("/") || candidate.startsWith("~/") || /^[A-Za-z]:\//.test(candidate)) return false;
|
|
5195
|
+
return !candidate.split("/").includes("..");
|
|
5196
|
+
}
|
|
5197
|
+
|
|
5198
|
+
function activeTaskToolGroups(task: TaskContract): PiagentToolGroup[] {
|
|
5199
|
+
const mode = runtimeLifecycleMode(task);
|
|
5200
|
+
if (mode.startsWith("automatic")) return [];
|
|
5201
|
+
if (mode.startsWith("assisted")) return ["task"];
|
|
5202
|
+
return ["task", "recovery"];
|
|
5203
|
+
}
|
|
5204
|
+
|
|
5205
|
+
function sessionTaskReference(ctx: ExtensionContext): { taskId?: string; taskRunId?: string } | undefined {
|
|
5206
|
+
const entries = ctx.sessionManager.getBranch();
|
|
5207
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
5208
|
+
const entry = entries[index] as unknown as { type?: string; customType?: string; data?: Record<string, unknown> };
|
|
5209
|
+
if (entry.type !== "custom" || entry.customType !== PIAGENT_TRACE_STATE_TYPE || !entry.data) continue;
|
|
5210
|
+
const taskId = typeof entry.data.taskId === "string" ? entry.data.taskId : undefined;
|
|
5211
|
+
const taskRunId = typeof entry.data.taskRunId === "string" ? entry.data.taskRunId : undefined;
|
|
5212
|
+
if (taskId || taskRunId) return { taskId, taskRunId };
|
|
5213
|
+
}
|
|
5214
|
+
return undefined;
|
|
4454
5215
|
}
|
|
4455
5216
|
|
|
4456
|
-
function
|
|
4457
|
-
|
|
5217
|
+
function compactSessionTask(cwd: string, sessionId: string): TaskContract | undefined {
|
|
5218
|
+
return activeSessionTask(cwd, sessionId) as TaskContract | undefined;
|
|
5219
|
+
}
|
|
5220
|
+
|
|
5221
|
+
function semanticCompactionInstructions(cwd: string, sessionId: string): string {
|
|
5222
|
+
const task = compactSessionTask(cwd, sessionId);
|
|
4458
5223
|
const taskState = task
|
|
4459
5224
|
? [
|
|
4460
5225
|
`Current task: ${task.taskId} (${task.riskLane})`,
|
|
5226
|
+
`Session: ${task.sessionName ?? task.sessionId}`,
|
|
4461
5227
|
`Goal: ${task.summary}`,
|
|
4462
5228
|
`Acceptance: ${task.acceptanceCriteria.join("; ") || "not recorded"}`,
|
|
4463
5229
|
`Scope: ${task.scope.join(", ") || "not recorded"}`,
|
|
4464
5230
|
`Changed files: ${task.changedFiles.join(", ") || "none recorded"}`,
|
|
4465
|
-
|
|
5231
|
+
task.verifyCommands.length > 0
|
|
5232
|
+
? ["Exact verify commands:", ...verifierCommandInstructions(task.verifyCommands)].join("\n")
|
|
5233
|
+
: "Exact verify commands: not recorded",
|
|
4466
5234
|
`Outcome/blocker: ${task.trace.outcome}${task.trace.friction ? `; ${task.trace.friction}` : ""}`
|
|
4467
5235
|
].join("\n")
|
|
4468
5236
|
: "No persisted task contract was found. Derive the current goal from the most recent user request.";
|
|
@@ -4490,6 +5258,48 @@ function environmentFeatureEnabled(name: string, fallback = true): boolean {
|
|
|
4490
5258
|
return !["0", "false", "no", "off", "disabled"].includes(value);
|
|
4491
5259
|
}
|
|
4492
5260
|
|
|
5261
|
+
function rewriteLegacyProjectInstructions(systemPrompt: string): {
|
|
5262
|
+
systemPrompt: string;
|
|
5263
|
+
rewritten: boolean;
|
|
5264
|
+
} {
|
|
5265
|
+
const start = systemPrompt.indexOf(LEGACY_PROJECT_INSTRUCTIONS_START);
|
|
5266
|
+
if (start < 0) return { systemPrompt, rewritten: false };
|
|
5267
|
+
const endStart = systemPrompt.indexOf(LEGACY_PROJECT_INSTRUCTIONS_END, start);
|
|
5268
|
+
if (endStart < 0) return { systemPrompt, rewritten: false };
|
|
5269
|
+
const end = endStart + LEGACY_PROJECT_INSTRUCTIONS_END.length;
|
|
5270
|
+
return {
|
|
5271
|
+
systemPrompt: `${systemPrompt.slice(0, start)}${RUNTIME_MANAGED_PROJECT_INSTRUCTIONS}${systemPrompt.slice(end)}`,
|
|
5272
|
+
rewritten: true
|
|
5273
|
+
};
|
|
5274
|
+
}
|
|
5275
|
+
|
|
5276
|
+
function compactManagedProjectInstructions(
|
|
5277
|
+
systemPrompt: string,
|
|
5278
|
+
mode: "automatic" | "protected"
|
|
5279
|
+
): { systemPrompt: string; compacted: boolean } {
|
|
5280
|
+
const concise = mode === "protected"
|
|
5281
|
+
? "Piagent protected-path policy: do not read, disclose, or mutate protected content. Refuse without tool calls."
|
|
5282
|
+
: "Piagent runtime task is injected below. Root project instructions are already loaded; do not re-read root AGENTS.md. Use task-relevant source/tests and ordinary tools, run the exact verifier after final edits, and make no task-management calls.";
|
|
5283
|
+
const markerStart = "<!-- piagent-managed:start -->";
|
|
5284
|
+
const markerEnd = "<!-- piagent-managed:end -->";
|
|
5285
|
+
const start = systemPrompt.indexOf(markerStart);
|
|
5286
|
+
const endStart = start >= 0 ? systemPrompt.indexOf(markerEnd, start) : -1;
|
|
5287
|
+
if (start >= 0 && endStart >= 0) {
|
|
5288
|
+
const end = endStart + markerEnd.length;
|
|
5289
|
+
return {
|
|
5290
|
+
systemPrompt: `${systemPrompt.slice(0, start)}${markerStart}\n${concise}\n${markerEnd}${systemPrompt.slice(end)}`,
|
|
5291
|
+
compacted: true
|
|
5292
|
+
};
|
|
5293
|
+
}
|
|
5294
|
+
if (systemPrompt.includes(RUNTIME_MANAGED_PROJECT_INSTRUCTIONS)) {
|
|
5295
|
+
return {
|
|
5296
|
+
systemPrompt: systemPrompt.replace(RUNTIME_MANAGED_PROJECT_INSTRUCTIONS, concise),
|
|
5297
|
+
compacted: true
|
|
5298
|
+
};
|
|
5299
|
+
}
|
|
5300
|
+
return { systemPrompt, compacted: false };
|
|
5301
|
+
}
|
|
5302
|
+
|
|
4493
5303
|
export default function piagentGuard(pi: ExtensionAPI) {
|
|
4494
5304
|
const extensionDir = path.dirname(fileURLToPath(import.meta.url));
|
|
4495
5305
|
const policy = loadPolicy(extensionDir);
|
|
@@ -4497,22 +5307,126 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4497
5307
|
const dynamicToolsEnabled = environmentFeatureEnabled("PIAGENT_DYNAMIC_TOOLS");
|
|
4498
5308
|
const contextTelemetryEnabled = environmentFeatureEnabled("PIAGENT_CONTEXT_TELEMETRY");
|
|
4499
5309
|
const autoContextEnabled = environmentFeatureEnabled("PIAGENT_AUTO_CONTEXT");
|
|
5310
|
+
const autoRecoveryEnabled = environmentFeatureEnabled("PIAGENT_AUTO_RECOVERY");
|
|
4500
5311
|
// Advisory tool-registry verdicts are surfaced once per tool per session. A
|
|
4501
5312
|
// notice on every call would be constant noise for a tool used all day, and
|
|
4502
5313
|
// noise that is always present is read as background rather than as a warning.
|
|
4503
5314
|
const advisedTools = new Set<string>();
|
|
4504
5315
|
const seenToolResults = new Map<string, { outputHash: string; recordedAt: string }>();
|
|
4505
5316
|
const autoPackedPrompts = new Set<string>();
|
|
5317
|
+
const injectedContextPacks = new Map<string, { queryHash: string; confidence: string; estimatedTokens: number; paths: string[] }>();
|
|
5318
|
+
const taskIdentityBySession = new Map<string, { taskId: string; taskRunId: string }>();
|
|
5319
|
+
const observedContextBySession = new Map<string, Map<string, ObservedTaskContext>>();
|
|
5320
|
+
const completionRecoveryAttempts = new Map<string, number>();
|
|
5321
|
+
const shellMutationSnapshots = new Map<string, Record<string, string>>();
|
|
5322
|
+
|
|
5323
|
+
function sessionCacheKey(ctx: ExtensionContext): string {
|
|
5324
|
+
return `${ctx.cwd}\u0000${ctx.sessionManager.getSessionId()}`;
|
|
5325
|
+
}
|
|
5326
|
+
|
|
5327
|
+
function cacheTaskIdentity(ctx: ExtensionContext, task: TaskContract | undefined): void {
|
|
5328
|
+
const key = sessionCacheKey(ctx);
|
|
5329
|
+
if (task) taskIdentityBySession.set(key, { taskId: task.taskId, taskRunId: task.taskRunId });
|
|
5330
|
+
else taskIdentityBySession.delete(key);
|
|
5331
|
+
if (taskIdentityBySession.size > 100) taskIdentityBySession.delete(taskIdentityBySession.keys().next().value as string);
|
|
5332
|
+
}
|
|
5333
|
+
|
|
5334
|
+
function rememberObservedContext(ctx: ExtensionContext, entry: ObservedTaskContext): void {
|
|
5335
|
+
const key = sessionCacheKey(ctx);
|
|
5336
|
+
let observed = observedContextBySession.get(key);
|
|
5337
|
+
if (!observed) {
|
|
5338
|
+
observed = new Map();
|
|
5339
|
+
observedContextBySession.set(key, observed);
|
|
5340
|
+
}
|
|
5341
|
+
if (!observed.has(entry.path)) observed.set(entry.path, entry);
|
|
5342
|
+
while (observed.size > contextBudgetConfig(policy).maxManifestFiles) {
|
|
5343
|
+
observed.delete(observed.keys().next().value as string);
|
|
5344
|
+
}
|
|
5345
|
+
if (observedContextBySession.size > 100) observedContextBySession.delete(observedContextBySession.keys().next().value as string);
|
|
5346
|
+
}
|
|
5347
|
+
|
|
5348
|
+
function observedContext(ctx: ExtensionContext): ObservedTaskContext[] {
|
|
5349
|
+
return [...(observedContextBySession.get(sessionCacheKey(ctx))?.values() ?? [])];
|
|
5350
|
+
}
|
|
5351
|
+
|
|
5352
|
+
function clearObservedContext(ctx: ExtensionContext): void {
|
|
5353
|
+
observedContextBySession.delete(sessionCacheKey(ctx));
|
|
5354
|
+
}
|
|
5355
|
+
|
|
5356
|
+
function shellMutationSnapshotKey(
|
|
5357
|
+
ctx: ExtensionContext,
|
|
5358
|
+
toolName: string,
|
|
5359
|
+
input: unknown
|
|
5360
|
+
): string {
|
|
5361
|
+
return `${sessionCacheKey(ctx)}\u0000${toolResultFingerprint(toolName, input, []).key}`;
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
function rememberShellMutationSnapshot(
|
|
5365
|
+
ctx: ExtensionContext,
|
|
5366
|
+
toolName: string,
|
|
5367
|
+
input: unknown
|
|
5368
|
+
): void {
|
|
5369
|
+
shellMutationSnapshots.set(
|
|
5370
|
+
shellMutationSnapshotKey(ctx, toolName, input),
|
|
5371
|
+
workingTreeSnapshot(ctx.cwd) as Record<string, string>
|
|
5372
|
+
);
|
|
5373
|
+
if (shellMutationSnapshots.size > 100) {
|
|
5374
|
+
shellMutationSnapshots.delete(shellMutationSnapshots.keys().next().value as string);
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
|
|
5378
|
+
function consumeShellMutationSnapshot(
|
|
5379
|
+
ctx: ExtensionContext,
|
|
5380
|
+
toolName: string,
|
|
5381
|
+
input: unknown
|
|
5382
|
+
): Record<string, string> | undefined {
|
|
5383
|
+
const key = shellMutationSnapshotKey(ctx, toolName, input);
|
|
5384
|
+
const snapshot = shellMutationSnapshots.get(key);
|
|
5385
|
+
shellMutationSnapshots.delete(key);
|
|
5386
|
+
return snapshot;
|
|
5387
|
+
}
|
|
5388
|
+
|
|
5389
|
+
function clearShellMutationSnapshots(ctx: ExtensionContext): void {
|
|
5390
|
+
const prefix = `${sessionCacheKey(ctx)}\u0000`;
|
|
5391
|
+
for (const key of shellMutationSnapshots.keys()) {
|
|
5392
|
+
if (key.startsWith(prefix)) shellMutationSnapshots.delete(key);
|
|
5393
|
+
}
|
|
5394
|
+
}
|
|
5395
|
+
|
|
5396
|
+
function retrievalKey(ctx: ExtensionContext, query: string): string {
|
|
5397
|
+
const signal = classifyContextTask(query);
|
|
5398
|
+
return crypto.createHash("sha256")
|
|
5399
|
+
.update(JSON.stringify({
|
|
5400
|
+
cwd: ctx.cwd,
|
|
5401
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
5402
|
+
terms: [...signal.terms].sort(),
|
|
5403
|
+
paths: [...signal.paths].sort()
|
|
5404
|
+
}))
|
|
5405
|
+
.digest("hex");
|
|
5406
|
+
}
|
|
5407
|
+
|
|
5408
|
+
function promptPackKey(ctx: ExtensionContext, promptHash: string): string {
|
|
5409
|
+
return `${ctx.cwd}\u0000${ctx.sessionManager.getSessionId()}\u0000${promptHash}`;
|
|
5410
|
+
}
|
|
4506
5411
|
|
|
4507
5412
|
function telemetry(ctx: ExtensionContext, payload: Record<string, unknown>): void {
|
|
4508
5413
|
if (!contextTelemetryEnabled) return;
|
|
4509
5414
|
try {
|
|
4510
5415
|
const safePayload = redactForStorage(payload) as Record<string, unknown>;
|
|
5416
|
+
let taskIdentity = taskIdentityBySession.get(sessionCacheKey(ctx));
|
|
5417
|
+
if (!taskIdentity) {
|
|
5418
|
+
const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
|
|
5419
|
+
if (task) {
|
|
5420
|
+
cacheTaskIdentity(ctx, task);
|
|
5421
|
+
taskIdentity = { taskId: task.taskId, taskRunId: task.taskRunId };
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
4511
5424
|
appendContextTelemetry(ctx.cwd, {
|
|
4512
5425
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
4513
5426
|
sessionName: currentSessionName(ctx),
|
|
4514
5427
|
model: `${ctx.model?.provider ?? "unknown"}/${ctx.model?.id ?? "unknown"}`,
|
|
4515
5428
|
thinkingLevel: String(pi.getThinkingLevel()),
|
|
5429
|
+
...taskIdentity,
|
|
4516
5430
|
...safePayload
|
|
4517
5431
|
});
|
|
4518
5432
|
} catch {
|
|
@@ -4531,7 +5445,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4531
5445
|
if (!PIAGENT_TOOL_NAMES.has(toolName)) selected.add(toolName);
|
|
4532
5446
|
}
|
|
4533
5447
|
}
|
|
4534
|
-
const normalizedGroups = [...new Set<PiagentToolGroup>(
|
|
5448
|
+
const normalizedGroups = [...new Set<PiagentToolGroup>(groups)];
|
|
4535
5449
|
for (const toolName of PIAGENT_TOOL_ORDER) {
|
|
4536
5450
|
if (normalizedGroups.some((group) => PIAGENT_TOOL_GROUPS[group].includes(toolName as never))) {
|
|
4537
5451
|
selected.add(toolName);
|
|
@@ -4564,6 +5478,42 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4564
5478
|
if (!hasOperatorSessionName(operatorSessionName)) {
|
|
4565
5479
|
pi.setSessionName(`pi:${name}`);
|
|
4566
5480
|
}
|
|
5481
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
5482
|
+
const sessionName = currentSessionName(ctx);
|
|
5483
|
+
const taskReference = sessionTaskReference(ctx);
|
|
5484
|
+
let captureRetention: Record<string, unknown>;
|
|
5485
|
+
try {
|
|
5486
|
+
captureRetention = pruneCaptureFiles(toolResultCaptureRoot(ctx.cwd), {
|
|
5487
|
+
maxFiles: CAPTURE_RETENTION_MAX_FILES,
|
|
5488
|
+
maxBytes: CAPTURE_RETENTION_MAX_BYTES,
|
|
5489
|
+
maxAgeMs: CAPTURE_RETENTION_MAX_AGE_MS,
|
|
5490
|
+
projectRoot: ctx.cwd
|
|
5491
|
+
});
|
|
5492
|
+
} catch (error) {
|
|
5493
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5494
|
+
captureRetention = { removed: 0, kept: 0, bytes: 0, error: message };
|
|
5495
|
+
ctx.ui.notify(`Piagent local state needs recovery: ${message}`, "warning");
|
|
5496
|
+
}
|
|
5497
|
+
const migration = migrateTaskState(ctx.cwd, {
|
|
5498
|
+
sessionId,
|
|
5499
|
+
sessionName,
|
|
5500
|
+
taskId: taskReference?.taskId
|
|
5501
|
+
});
|
|
5502
|
+
if (migration.warnings.length > 0) {
|
|
5503
|
+
ctx.ui.notify(`Piagent task-state migration needs recovery: ${migration.warnings.join("; ")}`, "warning");
|
|
5504
|
+
}
|
|
5505
|
+
let resumedTask = activeSessionTask(ctx.cwd, sessionId) as TaskContract | undefined;
|
|
5506
|
+
if (!resumedTask && taskReference) {
|
|
5507
|
+
resumedTask = resolveTaskContract(ctx.cwd, taskReference.taskRunId ?? taskReference.taskId, sessionId) as TaskContract | undefined;
|
|
5508
|
+
if (resumedTask) bindSessionTask(ctx.cwd, sessionId, sessionName, resumedTask);
|
|
5509
|
+
}
|
|
5510
|
+
if (resumedTask && resumedTask.sessionName !== sessionName) {
|
|
5511
|
+
resumedTask.sessionName = sessionName;
|
|
5512
|
+
resumedTask = writeTask(ctx.cwd, resumedTask);
|
|
5513
|
+
bindSessionTask(ctx.cwd, sessionId, sessionName, resumedTask);
|
|
5514
|
+
}
|
|
5515
|
+
cacheTaskIdentity(ctx, resumedTask);
|
|
5516
|
+
if (resumedTask?.trace.outcome === "pending") activateToolGroups(ctx, activeTaskToolGroups(resumedTask));
|
|
4567
5517
|
const profileHint = explicitProfile || (projectTrusted && fs.existsSync(projectProfilePath(ctx.cwd)))
|
|
4568
5518
|
? ""
|
|
4569
5519
|
: " (run /onboard to select a profile)";
|
|
@@ -4604,7 +5554,32 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4604
5554
|
} catch (error) {
|
|
4605
5555
|
engineStatus = { exists: false, error: error instanceof Error ? error.message : String(error) };
|
|
4606
5556
|
}
|
|
4607
|
-
telemetry(ctx, {
|
|
5557
|
+
telemetry(ctx, {
|
|
5558
|
+
event: "session_start",
|
|
5559
|
+
activeTools: pi.getActiveTools().length,
|
|
5560
|
+
index: engineStatus,
|
|
5561
|
+
taskMigration: migration,
|
|
5562
|
+
taskId: resumedTask?.taskId,
|
|
5563
|
+
taskRunId: resumedTask?.taskRunId,
|
|
5564
|
+
captureRetention
|
|
5565
|
+
});
|
|
5566
|
+
});
|
|
5567
|
+
|
|
5568
|
+
pi.on("session_info_changed", async (event, ctx) => {
|
|
5569
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
5570
|
+
const task = activeSessionTask(ctx.cwd, sessionId) as TaskContract | undefined;
|
|
5571
|
+
if (!task) return;
|
|
5572
|
+
const sessionName = String(event.name ?? "").trim() || undefined;
|
|
5573
|
+
task.sessionName = sessionName;
|
|
5574
|
+
const written = writeTask(ctx.cwd, task);
|
|
5575
|
+
bindSessionTask(ctx.cwd, sessionId, sessionName, written);
|
|
5576
|
+
appendTrace(ctx.cwd, {
|
|
5577
|
+
event: "task_session_renamed",
|
|
5578
|
+
taskId: written.taskId,
|
|
5579
|
+
taskRunId: written.taskRunId,
|
|
5580
|
+
sessionId,
|
|
5581
|
+
sessionName
|
|
5582
|
+
});
|
|
4608
5583
|
});
|
|
4609
5584
|
|
|
4610
5585
|
pi.on("input", async (event, ctx) => {
|
|
@@ -4614,7 +5589,22 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4614
5589
|
return { action: "transform", text: text.replace(/^\/piagent-workflow\b/i, "/workflow") };
|
|
4615
5590
|
}
|
|
4616
5591
|
const taskSignal = classifyContextTask(text);
|
|
4617
|
-
|
|
5592
|
+
const activeTask = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
|
|
5593
|
+
const readProtectedPaths = effectiveProtectedPaths(policy, loadProfileFromContext(ctx)).readProtectedPaths;
|
|
5594
|
+
const protectedTarget = taskSignal.paths.some((candidate) => matchesProtectedPath(candidate, readProtectedPaths));
|
|
5595
|
+
const protectedOnlyTarget = taskSignal.paths.length > 0
|
|
5596
|
+
&& taskSignal.paths.every((candidate) => matchesProtectedPath(candidate, readProtectedPaths));
|
|
5597
|
+
const runtimeIntake = !activeTask && automaticTaskIntakeEligible(text, readProtectedPaths);
|
|
5598
|
+
const manualIntake = !activeTask && !runtimeIntake && manualTaskIntakeEligible(text, readProtectedPaths);
|
|
5599
|
+
const promptGroups = protectedOnlyTarget
|
|
5600
|
+
? []
|
|
5601
|
+
: toolGroupsForPrompt(text).filter((group) => (
|
|
5602
|
+
runtimeIntake ? group !== "intake" && group !== "task" : true
|
|
5603
|
+
));
|
|
5604
|
+
if (manualIntake && !promptGroups.includes("intake")) promptGroups.push("intake");
|
|
5605
|
+
activateToolGroups(ctx, activeTask?.trace.outcome === "pending"
|
|
5606
|
+
? [...promptGroups.filter((group) => group !== "intake"), ...activeTaskToolGroups(activeTask)]
|
|
5607
|
+
: promptGroups);
|
|
4618
5608
|
telemetry(ctx, {
|
|
4619
5609
|
event: "user_input",
|
|
4620
5610
|
source: event.source,
|
|
@@ -4622,6 +5612,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4622
5612
|
promptChars: taskSignal.promptChars,
|
|
4623
5613
|
workflow: taskSignal.workflow,
|
|
4624
5614
|
riskLane: taskSignal.lane,
|
|
5615
|
+
intakeMode: runtimeIntake ? "runtime" : "model",
|
|
5616
|
+
protectedTarget,
|
|
5617
|
+
protectedOnlyTarget,
|
|
5618
|
+
manualIntake,
|
|
4625
5619
|
explicitPathCount: taskSignal.paths.length,
|
|
4626
5620
|
termCount: taskSignal.terms.length
|
|
4627
5621
|
});
|
|
@@ -4685,6 +5679,27 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4685
5679
|
});
|
|
4686
5680
|
|
|
4687
5681
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
5682
|
+
const projectInstructions = rewriteLegacyProjectInstructions(event.systemPrompt);
|
|
5683
|
+
const query = looksLikeGovernedBoilerplate(event.prompt) ? extractTaskRequest(event.prompt) : event.prompt.trim();
|
|
5684
|
+
const signal = classifyContextTask(query);
|
|
5685
|
+
const profile = loadProfileFromContext(ctx);
|
|
5686
|
+
const readProtectedPaths = effectiveProtectedPaths(policy, profile).readProtectedPaths;
|
|
5687
|
+
const protectedOnlyTarget = signal.paths.length > 0
|
|
5688
|
+
&& signal.paths.every((candidate) => matchesProtectedPath(candidate, readProtectedPaths));
|
|
5689
|
+
const activeTask = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
|
|
5690
|
+
const runtimeIntake = !activeTask && automaticTaskIntakeEligible(query, readProtectedPaths);
|
|
5691
|
+
const compactMode = protectedOnlyTarget
|
|
5692
|
+
? "protected"
|
|
5693
|
+
: runtimeIntake || activeTask?.intakeMode === "runtime"
|
|
5694
|
+
? "automatic"
|
|
5695
|
+
: undefined;
|
|
5696
|
+
const compactedInstructions = compactMode
|
|
5697
|
+
? compactManagedProjectInstructions(projectInstructions.systemPrompt, compactMode)
|
|
5698
|
+
: { systemPrompt: projectInstructions.systemPrompt, compacted: false };
|
|
5699
|
+
const effectiveSystemPrompt = compactedInstructions.systemPrompt;
|
|
5700
|
+
const systemPromptUpdate = effectiveSystemPrompt !== event.systemPrompt
|
|
5701
|
+
? { systemPrompt: effectiveSystemPrompt }
|
|
5702
|
+
: undefined;
|
|
4688
5703
|
const active = new Set<string>(pi.getActiveTools() as string[]);
|
|
4689
5704
|
const toolMetadata = pi.getAllTools()
|
|
4690
5705
|
.filter((tool) => active.has(tool.name))
|
|
@@ -4695,9 +5710,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4695
5710
|
promptGuidelines: tool.promptGuidelines
|
|
4696
5711
|
}));
|
|
4697
5712
|
const toolSchemaTokens = estimateContextTokens(JSON.stringify(toolMetadata));
|
|
4698
|
-
const systemPromptTokens = estimateContextTokens(
|
|
4699
|
-
const
|
|
4700
|
-
|
|
5713
|
+
const systemPromptTokens = estimateContextTokens(effectiveSystemPrompt);
|
|
5714
|
+
const autoPackUseful = activeTask?.trace.outcome !== "pending"
|
|
5715
|
+
&& (runtimeIntake || signal.paths.length === 0);
|
|
4701
5716
|
telemetry(ctx, {
|
|
4702
5717
|
event: "agent_prompt",
|
|
4703
5718
|
promptHash: signal.promptHash,
|
|
@@ -4708,21 +5723,54 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4708
5723
|
activePiagentTools: [...active].filter((toolName) => PIAGENT_TOOL_NAMES.has(toolName)).length,
|
|
4709
5724
|
toolSchemaTokens,
|
|
4710
5725
|
systemPromptTokens,
|
|
4711
|
-
systemPromptHash: crypto.createHash("sha256").update(
|
|
5726
|
+
systemPromptHash: crypto.createHash("sha256").update(effectiveSystemPrompt).digest("hex"),
|
|
5727
|
+
legacyProjectInstructionsRewritten: projectInstructions.rewritten,
|
|
5728
|
+
managedInstructionsCompacted: compactedInstructions.compacted,
|
|
4712
5729
|
contextUsage: ctx.getContextUsage()
|
|
4713
5730
|
});
|
|
4714
5731
|
|
|
5732
|
+
const finishAgentStart = async (contextMessage?: {
|
|
5733
|
+
customType: string;
|
|
5734
|
+
content: string;
|
|
5735
|
+
details: Record<string, unknown>;
|
|
5736
|
+
}) => {
|
|
5737
|
+
const intake = await maybeStartAutomaticTask(query, ctx);
|
|
5738
|
+
if (!contextMessage && !intake) return systemPromptUpdate;
|
|
5739
|
+
const content = [contextMessage?.content, intake?.text].filter(Boolean).join("\n\n");
|
|
5740
|
+
return {
|
|
5741
|
+
...(systemPromptUpdate ?? {}),
|
|
5742
|
+
message: {
|
|
5743
|
+
customType: contextMessage?.customType ?? "piagent-runtime-task-intake",
|
|
5744
|
+
content,
|
|
5745
|
+
display: false,
|
|
5746
|
+
details: {
|
|
5747
|
+
...(contextMessage?.details ?? {}),
|
|
5748
|
+
runtimeTask: intake?.task
|
|
5749
|
+
? {
|
|
5750
|
+
taskId: intake.task.taskId,
|
|
5751
|
+
taskRunId: intake.task.taskRunId,
|
|
5752
|
+
scope: intake.task.scope,
|
|
5753
|
+
verifyCommands: intake.task.verifyCommands,
|
|
5754
|
+
intakeMode: intake.task.intakeMode
|
|
5755
|
+
}
|
|
5756
|
+
: undefined,
|
|
5757
|
+
runtimeIntakeStarted: intake?.started ?? false
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
};
|
|
5761
|
+
};
|
|
5762
|
+
|
|
4715
5763
|
if (
|
|
4716
5764
|
query.length < 20
|
|
4717
5765
|
|| signal.workflow === "usage"
|
|
4718
5766
|
|| !autoContextEnabled
|
|
4719
|
-
||
|
|
4720
|
-
||
|
|
5767
|
+
|| !autoPackUseful
|
|
5768
|
+
|| autoPackedPrompts.has(promptPackKey(ctx, signal.promptHash))
|
|
4721
5769
|
) {
|
|
4722
|
-
return;
|
|
5770
|
+
return finishAgentStart();
|
|
4723
5771
|
}
|
|
4724
5772
|
|
|
4725
|
-
autoPackedPrompts.add(signal.promptHash);
|
|
5773
|
+
autoPackedPrompts.add(promptPackKey(ctx, signal.promptHash));
|
|
4726
5774
|
if (autoPackedPrompts.size > 50) autoPackedPrompts.delete(autoPackedPrompts.values().next().value as string);
|
|
4727
5775
|
try {
|
|
4728
5776
|
const excludePatterns = contextIndexExcludePatterns(policy, loadProfileFromContext(ctx));
|
|
@@ -4740,13 +5788,17 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4740
5788
|
selected: 0,
|
|
4741
5789
|
skipped: status.exists ? "stale-index" : "missing-index"
|
|
4742
5790
|
});
|
|
4743
|
-
return;
|
|
5791
|
+
return finishAgentStart();
|
|
4744
5792
|
}
|
|
4745
|
-
const budgetTokens =
|
|
5793
|
+
const budgetTokens = runtimeIntake
|
|
5794
|
+
? 900
|
|
5795
|
+
: signal.lane === "high-risk" ? 1_200 : signal.lane === "tiny" ? 500 : 900;
|
|
4746
5796
|
const pack = await buildContextPack(ctx.cwd, query, {
|
|
4747
5797
|
budgetTokens,
|
|
4748
|
-
includeCode:
|
|
4749
|
-
|
|
5798
|
+
includeCode: runtimeIntake,
|
|
5799
|
+
includePatterns: runtimeIntake ? AUTO_INTAKE_SNAPSHOT_PATTERNS : undefined,
|
|
5800
|
+
currentSnapshot: runtimeIntake,
|
|
5801
|
+
limit: runtimeIntake ? 8 : 12,
|
|
4750
5802
|
excludePatterns
|
|
4751
5803
|
});
|
|
4752
5804
|
telemetry(ctx, {
|
|
@@ -4757,23 +5809,35 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4757
5809
|
selected: pack.selected.length,
|
|
4758
5810
|
estimatedTokens: pack.estimatedTokens,
|
|
4759
5811
|
selectedPaths: pack.selected.map((item) => item.path),
|
|
4760
|
-
finderRecommended: pack.finderRecommended
|
|
5812
|
+
finderRecommended: pack.finderRecommended,
|
|
5813
|
+
currentSnapshot: runtimeIntake
|
|
4761
5814
|
});
|
|
4762
|
-
if (!["medium", "high"].includes(pack.confidence) || pack.selected.length === 0) return;
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
5815
|
+
if (!["medium", "high"].includes(pack.confidence) || pack.selected.length === 0) return finishAgentStart();
|
|
5816
|
+
injectedContextPacks.set(retrievalKey(ctx, query), {
|
|
5817
|
+
queryHash: pack.queryHash,
|
|
5818
|
+
confidence: pack.confidence,
|
|
5819
|
+
estimatedTokens: pack.estimatedTokens,
|
|
5820
|
+
paths: pack.selected.map((item) => item.path)
|
|
5821
|
+
});
|
|
5822
|
+
for (const selected of pack.selected) {
|
|
5823
|
+
rememberObservedContext(ctx, {
|
|
5824
|
+
path: selected.path,
|
|
5825
|
+
reason: "Runtime injected a bounded Context Engine navigation pack."
|
|
5826
|
+
});
|
|
5827
|
+
}
|
|
5828
|
+
if (injectedContextPacks.size > 50) injectedContextPacks.delete(injectedContextPacks.keys().next().value as string);
|
|
5829
|
+
return finishAgentStart({
|
|
5830
|
+
customType: "piagent-context-pack-v2",
|
|
5831
|
+
content: pack.text,
|
|
5832
|
+
details: {
|
|
5833
|
+
schemaVersion: 2,
|
|
5834
|
+
queryHash: pack.queryHash,
|
|
5835
|
+
confidence: pack.confidence,
|
|
5836
|
+
estimatedTokens: pack.estimatedTokens,
|
|
5837
|
+
paths: pack.selected.map((item) => item.path),
|
|
5838
|
+
currentSnapshot: runtimeIntake
|
|
4775
5839
|
}
|
|
4776
|
-
};
|
|
5840
|
+
});
|
|
4777
5841
|
} catch (error) {
|
|
4778
5842
|
telemetry(ctx, {
|
|
4779
5843
|
event: "context_pack",
|
|
@@ -4783,6 +5847,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4783
5847
|
selected: 0,
|
|
4784
5848
|
error: error instanceof Error ? error.message : String(error)
|
|
4785
5849
|
});
|
|
5850
|
+
return finishAgentStart();
|
|
4786
5851
|
}
|
|
4787
5852
|
});
|
|
4788
5853
|
|
|
@@ -4799,6 +5864,160 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4799
5864
|
});
|
|
4800
5865
|
});
|
|
4801
5866
|
|
|
5867
|
+
pi.on("message_end", async (event, ctx) => {
|
|
5868
|
+
if (!event.message || event.message.role !== "assistant" || assistantMessageHasToolCall(event.message)) return;
|
|
5869
|
+
const budget = contextBudgetConfig(policy);
|
|
5870
|
+
let task = flushObservedTaskContext(
|
|
5871
|
+
pi,
|
|
5872
|
+
ctx,
|
|
5873
|
+
observedContext(ctx),
|
|
5874
|
+
budget.maxManifestFiles,
|
|
5875
|
+
"context_observed_before_handoff"
|
|
5876
|
+
) ?? activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
|
|
5877
|
+
if (!task) return;
|
|
5878
|
+
const text = assistantMessageText(event.message);
|
|
5879
|
+
const completionClaim = looksLikeCompletionClaim(text);
|
|
5880
|
+
const incompleteHandoff = looksLikeIncompleteHandoff(text);
|
|
5881
|
+
if (task.trace.outcome !== "pending") return;
|
|
5882
|
+
const readOnlyEvidenceObserved = task.changeMode === "read-only" && task.contextManifest.length > 0;
|
|
5883
|
+
const potentiallyFinalEvidence = task.observedChangedFiles.length > 0
|
|
5884
|
+
|| task.verifyEvidence.some((evidence) => evidence.exitCode === 0 && evidence.observed === true && evidence.matchedProfileCommand === true)
|
|
5885
|
+
|| readOnlyEvidenceObserved;
|
|
5886
|
+
if (!completionClaim && (incompleteHandoff || !potentiallyFinalEvidence)) return;
|
|
5887
|
+
const currentDigests = workingTreeSnapshot(ctx.cwd) as Record<string, string>;
|
|
5888
|
+
const currentDigest = workingTreeEvidenceDigest(currentDigests);
|
|
5889
|
+
const currentPassingVerifierObserved = task.verifyEvidence.some((evidence) => (
|
|
5890
|
+
evidence.exitCode === 0
|
|
5891
|
+
&& evidence.observed === true
|
|
5892
|
+
&& evidence.matchedProfileCommand === true
|
|
5893
|
+
&& evidence.workingTreeDigest === currentDigest
|
|
5894
|
+
));
|
|
5895
|
+
const handoffAttempt = completionClaim || (
|
|
5896
|
+
!incompleteHandoff
|
|
5897
|
+
&& (task.observedChangedFiles.length > 0 || currentPassingVerifierObserved || readOnlyEvidenceObserved)
|
|
5898
|
+
);
|
|
5899
|
+
let completionGate: ReturnType<typeof evaluateTaskGate> | undefined;
|
|
5900
|
+
if (task.trace.outcome === "pending" && handoffAttempt) {
|
|
5901
|
+
const projected = completionTaskProjection(ctx.cwd, task, currentDigests);
|
|
5902
|
+
const projectedGate = evaluateTaskGate(ctx.cwd, projected, policy, {
|
|
5903
|
+
currentDigests,
|
|
5904
|
+
currentWorkingTreeDigest: currentDigest
|
|
5905
|
+
});
|
|
5906
|
+
completionGate = projectedGate;
|
|
5907
|
+
if (projectedGate.decision === "pass") {
|
|
5908
|
+
task = writeTask(ctx.cwd, projected);
|
|
5909
|
+
cacheTaskIdentity(ctx, task);
|
|
5910
|
+
completionRecoveryAttempts.delete(task.taskRunId);
|
|
5911
|
+
clearObservedContext(ctx);
|
|
5912
|
+
activateToolGroups(ctx, []);
|
|
5913
|
+
const trace = {
|
|
5914
|
+
event: "task_auto_completed",
|
|
5915
|
+
taskId: task.taskId,
|
|
5916
|
+
taskRunId: task.taskRunId,
|
|
5917
|
+
sessionId: task.sessionId,
|
|
5918
|
+
changedFiles: task.changedFiles,
|
|
5919
|
+
lifecycleMode: runtimeLifecycleMode(task)
|
|
5920
|
+
};
|
|
5921
|
+
appendTrace(ctx.cwd, trace);
|
|
5922
|
+
appendSessionTrace(pi, trace);
|
|
5923
|
+
telemetry(ctx, trace);
|
|
5924
|
+
return;
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
if (!handoffAttempt || resolveRuntimePolicy(loadProfileFromContext(ctx)).finalGate !== "enforce") return;
|
|
5928
|
+
const gate = completionGate ?? evaluateTaskGate(ctx.cwd, task, policy, {
|
|
5929
|
+
currentDigests,
|
|
5930
|
+
currentWorkingTreeDigest: currentDigest
|
|
5931
|
+
});
|
|
5932
|
+
if (gate.decision === "pass") return;
|
|
5933
|
+
|
|
5934
|
+
const recoveryAttempt = completionRecoveryAttempts.get(task.taskRunId) ?? 0;
|
|
5935
|
+
const lifecycleMode = runtimeLifecycleMode(task);
|
|
5936
|
+
if (
|
|
5937
|
+
autoRecoveryEnabled
|
|
5938
|
+
&& handoffAttempt
|
|
5939
|
+
&& lifecycleMode !== "manual"
|
|
5940
|
+
&& recoveryAttempt < 1
|
|
5941
|
+
) {
|
|
5942
|
+
completionRecoveryAttempts.set(task.taskRunId, recoveryAttempt + 1);
|
|
5943
|
+
const recoveryGuidance = task.changeMode === "read-only"
|
|
5944
|
+
? [
|
|
5945
|
+
"Continue the same read-only task. Review the cited evidence against the requested scope, state concrete unknowns, and complete the returned review step when required.",
|
|
5946
|
+
"Do not mutate project source or repeat diagnostic Piagent tools; use ordinary targeted reads and the active progress tool only."
|
|
5947
|
+
]
|
|
5948
|
+
: [
|
|
5949
|
+
"Continue the same bounded task. Re-check the requested behavior against current source, make a relevant in-scope change when required, and run every exact configured verifier.",
|
|
5950
|
+
...verifierCommandInstructions(gate.missingVerifyCommands),
|
|
5951
|
+
"A pre-existing passing test without a relevant source diff is not completion. Do not repeat diagnostic Piagent tools; use ordinary read/edit/bash work."
|
|
5952
|
+
];
|
|
5953
|
+
const recovery = [
|
|
5954
|
+
`[Piagent continuation required]`,
|
|
5955
|
+
`Task ${task.taskId} cannot finish yet. Missing: ${gate.missing.join(", ")}.`,
|
|
5956
|
+
...recoveryGuidance
|
|
5957
|
+
].join("\n");
|
|
5958
|
+
pi.sendMessage(
|
|
5959
|
+
{ customType: "piagent-completion-recovery", content: recovery, display: false, details: { taskId: task.taskId, missing: gate.missing } },
|
|
5960
|
+
{ deliverAs: "followUp", triggerTurn: true }
|
|
5961
|
+
);
|
|
5962
|
+
const recoveryTrace = {
|
|
5963
|
+
event: "completion_recovery_scheduled",
|
|
5964
|
+
taskId: task.taskId,
|
|
5965
|
+
taskRunId: task.taskRunId,
|
|
5966
|
+
sessionId: task.sessionId,
|
|
5967
|
+
attempt: recoveryAttempt + 1,
|
|
5968
|
+
missing: gate.missing
|
|
5969
|
+
};
|
|
5970
|
+
appendTrace(ctx.cwd, recoveryTrace);
|
|
5971
|
+
appendSessionTrace(pi, recoveryTrace);
|
|
5972
|
+
telemetry(ctx, recoveryTrace);
|
|
5973
|
+
const continuingNotice = [
|
|
5974
|
+
"[Piagent completion gate: CONTINUING]",
|
|
5975
|
+
`Task ${task.taskId} needs one bounded correction pass before handoff.`,
|
|
5976
|
+
""
|
|
5977
|
+
].join("\n");
|
|
5978
|
+
const content = Array.isArray(event.message.content)
|
|
5979
|
+
? [{ type: "text" as const, text: continuingNotice }, ...event.message.content]
|
|
5980
|
+
: [{ type: "text" as const, text: `${continuingNotice}${text}` }];
|
|
5981
|
+
return { message: { ...event.message, content } };
|
|
5982
|
+
}
|
|
5983
|
+
|
|
5984
|
+
const notice = [
|
|
5985
|
+
`[Piagent completion gate: NOT APPROVED]`,
|
|
5986
|
+
`Task ${task.taskId} (${task.taskRunId}) is still open.`,
|
|
5987
|
+
`Missing: ${gate.missing.join(", ") || "a completed task trace"}.`,
|
|
5988
|
+
...verifierCommandInstructions(gate.missingVerifyCommands),
|
|
5989
|
+
"The response below is preserved as work in progress and must not be treated as a completion report.",
|
|
5990
|
+
""
|
|
5991
|
+
].join("\n");
|
|
5992
|
+
const content = Array.isArray(event.message.content)
|
|
5993
|
+
? [{ type: "text" as const, text: notice }, ...event.message.content]
|
|
5994
|
+
: [{ type: "text" as const, text: `${notice}${text}` }];
|
|
5995
|
+
const trace = {
|
|
5996
|
+
event: "completion_claim_blocked",
|
|
5997
|
+
taskId: task.taskId,
|
|
5998
|
+
taskRunId: task.taskRunId,
|
|
5999
|
+
sessionId: task.sessionId,
|
|
6000
|
+
missing: gate.missing,
|
|
6001
|
+
responseHash: crypto.createHash("sha256").update(text).digest("hex"),
|
|
6002
|
+
responseChars: text.length
|
|
6003
|
+
};
|
|
6004
|
+
appendTrace(ctx.cwd, trace);
|
|
6005
|
+
appendSessionTrace(pi, trace);
|
|
6006
|
+
telemetry(ctx, trace);
|
|
6007
|
+
return { message: { ...event.message, content } };
|
|
6008
|
+
});
|
|
6009
|
+
|
|
6010
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
6011
|
+
const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
|
|
6012
|
+
telemetry(ctx, {
|
|
6013
|
+
event: "agent_settled",
|
|
6014
|
+
idle: ctx.isIdle(),
|
|
6015
|
+
taskId: task?.taskId,
|
|
6016
|
+
taskRunId: task?.taskRunId,
|
|
6017
|
+
taskOutcome: task?.trace.outcome
|
|
6018
|
+
});
|
|
6019
|
+
});
|
|
6020
|
+
|
|
4802
6021
|
pi.on("session_compact", async (event, ctx) => {
|
|
4803
6022
|
telemetry(ctx, {
|
|
4804
6023
|
event: "session_compact",
|
|
@@ -4809,11 +6028,20 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4809
6028
|
});
|
|
4810
6029
|
|
|
4811
6030
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
6031
|
+
flushObservedTaskContext(
|
|
6032
|
+
pi,
|
|
6033
|
+
ctx,
|
|
6034
|
+
observedContext(ctx),
|
|
6035
|
+
contextBudgetConfig(policy).maxManifestFiles,
|
|
6036
|
+
"context_observed_before_shutdown"
|
|
6037
|
+
);
|
|
4812
6038
|
telemetry(ctx, {
|
|
4813
6039
|
event: "session_shutdown",
|
|
4814
6040
|
reason: event.reason,
|
|
4815
6041
|
targetSessionFile: event.targetSessionFile ? path.basename(event.targetSessionFile) : undefined
|
|
4816
6042
|
});
|
|
6043
|
+
clearObservedContext(ctx);
|
|
6044
|
+
clearShellMutationSnapshots(ctx);
|
|
4817
6045
|
});
|
|
4818
6046
|
|
|
4819
6047
|
pi.on("tool_result", async (event, ctx) => {
|
|
@@ -4827,13 +6055,23 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4827
6055
|
appendObservedBashResult(observedBashLedgerPath(ctx.cwd), {
|
|
4828
6056
|
...observed,
|
|
4829
6057
|
redactedCommand: redactText(observed.command)
|
|
4830
|
-
});
|
|
6058
|
+
}, { projectRoot: ctx.cwd });
|
|
4831
6059
|
} catch (error) {
|
|
4832
6060
|
const message = error instanceof Error ? error.message : String(error);
|
|
4833
6061
|
ctx.ui.notify(`Piagent Pi guard could not persist bash evidence ledger: ${message}`, "warn");
|
|
4834
6062
|
}
|
|
4835
6063
|
}
|
|
4836
6064
|
|
|
6065
|
+
const observedContextEntry = observedTaskContextFromToolResult(ctx.cwd, event, pathPolicy.readProtectedPaths);
|
|
6066
|
+
if (observedContextEntry) rememberObservedContext(ctx, observedContextEntry);
|
|
6067
|
+
const pendingContext = observedContext(ctx);
|
|
6068
|
+
const maxManifestFiles = contextBudgetConfig(policy).maxManifestFiles;
|
|
6069
|
+
const shellSnapshotBefore = SHELL_TOOL_NAMES.has(event.toolName)
|
|
6070
|
+
? consumeShellMutationSnapshot(ctx, event.toolName, event.input)
|
|
6071
|
+
: undefined;
|
|
6072
|
+
recordObservedTaskChanges(pi, ctx, event, pendingContext, maxManifestFiles, shellSnapshotBefore);
|
|
6073
|
+
if (observed) recordObservedTaskVerification(pi, ctx, observed, pendingContext, maxManifestFiles);
|
|
6074
|
+
|
|
4837
6075
|
let resultContent: unknown = event.content;
|
|
4838
6076
|
let resultDetails: unknown = event.details;
|
|
4839
6077
|
let resultChanged = false;
|
|
@@ -4993,6 +6231,18 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4993
6231
|
const toolInput = event.input && typeof event.input === "object"
|
|
4994
6232
|
? event.input as Record<string, unknown>
|
|
4995
6233
|
: {};
|
|
6234
|
+
const sessionTask = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
|
|
6235
|
+
const profileMode = profile.mode ?? "unprofiled";
|
|
6236
|
+
const governedTaskProfile = !profileMode.startsWith("unprofiled") && !profileMode.includes("unreadable");
|
|
6237
|
+
const taskContractRequired = governedTaskProfile
|
|
6238
|
+
&& runtime.finalGate === "enforce"
|
|
6239
|
+
&& finalGateConfig(policy).requireTaskContract;
|
|
6240
|
+
if (sessionTask?.trace.outcome === "pending" && sessionTask.changeMode === "read-only" && !SHELL_TOOL_NAMES.has(event.toolName) && isTaskMutationTool(event.toolName, toolInput)) {
|
|
6241
|
+
return {
|
|
6242
|
+
block: true,
|
|
6243
|
+
reason: `Task ${sessionTask.taskId} is read-only; ${event.toolName} cannot mutate project or external state in this task. Start a source-change task for implementation.`
|
|
6244
|
+
};
|
|
6245
|
+
}
|
|
4996
6246
|
const preparedInput = prepareToolInputForPolicy(event.toolName, toolInput, policy);
|
|
4997
6247
|
if (preparedInput.reason) {
|
|
4998
6248
|
return { block: true, reason: `Blocked ${event.toolName}: ${preparedInput.reason}.` };
|
|
@@ -5005,6 +6255,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5005
6255
|
return { block: true, reason: approvalDecision.reason };
|
|
5006
6256
|
}
|
|
5007
6257
|
|
|
6258
|
+
let shellProjectMutation = false;
|
|
5008
6259
|
if (SHELL_TOOL_NAMES.has(event.toolName)) {
|
|
5009
6260
|
const shellInput = extractShellCommandInput(toolInput);
|
|
5010
6261
|
if (!shellInput.command) {
|
|
@@ -5012,6 +6263,13 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5012
6263
|
}
|
|
5013
6264
|
const command = normalizeShellCommandForPolicy(shellInput.command);
|
|
5014
6265
|
const execDecision = evaluateExecPolicy(command, profile, policy);
|
|
6266
|
+
shellProjectMutation = isProjectMutatingShellCommand(command, execDecision.segments);
|
|
6267
|
+
if (sessionTask?.trace.outcome === "pending" && sessionTask.changeMode === "read-only" && !isReadOnlyTaskShellCommand(command, execDecision.segments)) {
|
|
6268
|
+
return {
|
|
6269
|
+
block: true,
|
|
6270
|
+
reason: `Task ${sessionTask.taskId} is read-only; this shell command is not in the read-only inspection allowlist.`
|
|
6271
|
+
};
|
|
6272
|
+
}
|
|
5015
6273
|
if (execDecision.mode !== "off" && execDecision.decision === "forbid") {
|
|
5016
6274
|
return { block: true, reason: execDecision.reasons.join("; ") };
|
|
5017
6275
|
}
|
|
@@ -5110,6 +6368,34 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5110
6368
|
}
|
|
5111
6369
|
}
|
|
5112
6370
|
|
|
6371
|
+
const mutationTargets = taskMutationTargets(ctx.cwd, event.toolName, toolInput);
|
|
6372
|
+
const directProjectMutation = !SHELL_TOOL_NAMES.has(event.toolName) && (
|
|
6373
|
+
WRITE_TOOL_NAMES.has(event.toolName)
|
|
6374
|
+
|| mutationTargets.length > 0
|
|
6375
|
+
|| preparedInput.proxyShellCarrier === true
|
|
6376
|
+
);
|
|
6377
|
+
if (sessionTask && sessionTask.trace.outcome !== "pending" && (shellProjectMutation || directProjectMutation)) {
|
|
6378
|
+
return {
|
|
6379
|
+
block: true,
|
|
6380
|
+
reason: `Task ${sessionTask.taskId} is ${sessionTask.trace.outcome}; start a new attempt or a fresh task before further project mutations.`
|
|
6381
|
+
};
|
|
6382
|
+
}
|
|
6383
|
+
if (!sessionTask && taskContractRequired && (shellProjectMutation || directProjectMutation)) {
|
|
6384
|
+
return {
|
|
6385
|
+
block: true,
|
|
6386
|
+
reason: "A session-bound Task Implementation Contract is required before project files can be mutated. Start the task first."
|
|
6387
|
+
};
|
|
6388
|
+
}
|
|
6389
|
+
if (sessionTask?.trace.outcome === "pending" && sessionTask.changeMode === "source-change") {
|
|
6390
|
+
const outsideScope = mutationTargets.filter((file) => !taskScopeIncludesPath(sessionTask.scope, file));
|
|
6391
|
+
if (outsideScope.length > 0) {
|
|
6392
|
+
return {
|
|
6393
|
+
block: true,
|
|
6394
|
+
reason: `Task ${sessionTask.taskId} cannot mutate paths outside its declared scope: ${outsideScope.join(", ")}.`
|
|
6395
|
+
};
|
|
6396
|
+
}
|
|
6397
|
+
}
|
|
6398
|
+
|
|
5113
6399
|
if (runtime.contextBudget !== "off" && ["write", "edit"].includes(event.toolName)) {
|
|
5114
6400
|
const relativePath = extractLikelyPathFromInput(ctx.cwd, event.input as Record<string, unknown>);
|
|
5115
6401
|
if (relativePath) {
|
|
@@ -5120,18 +6406,27 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5120
6406
|
}
|
|
5121
6407
|
}
|
|
5122
6408
|
}
|
|
6409
|
+
|
|
6410
|
+
if (
|
|
6411
|
+
sessionTask?.trace.outcome === "pending"
|
|
6412
|
+
&& sessionTask.changeMode === "source-change"
|
|
6413
|
+
&& shellProjectMutation
|
|
6414
|
+
) {
|
|
6415
|
+
rememberShellMutationSnapshot(ctx, event.toolName, event.input);
|
|
6416
|
+
}
|
|
5123
6417
|
});
|
|
5124
6418
|
|
|
5125
6419
|
pi.registerTool({
|
|
5126
6420
|
name: "piagent_tools",
|
|
5127
6421
|
label: "Piagent Tool Loader",
|
|
5128
6422
|
description: "Activate an additional Piagent tool group only when the current task needs it.",
|
|
5129
|
-
promptSnippet: "Load
|
|
6423
|
+
promptSnippet: "Load diagnostic or recovery Piagent tools only when the runtime requests them.",
|
|
5130
6424
|
promptGuidelines: [
|
|
5131
|
-
"
|
|
6425
|
+
"Do not call this for an ordinary implementation task; runtime evidence collection needs no extra tools.",
|
|
6426
|
+
"When recovery is necessary, load only the smallest group that resolves the reported missing evidence."
|
|
5132
6427
|
],
|
|
5133
6428
|
parameters: Type.Object({
|
|
5134
|
-
groups: Type.Array(StringEnum(["governance", "policy", "retrieval", "knowledge", "onboarding", "usage"] as const), { minItems: 1 })
|
|
6429
|
+
groups: Type.Array(StringEnum(["intake", "governance", "task", "recovery", "policy", "retrieval", "knowledge", "onboarding", "usage"] as const), { minItems: 1 })
|
|
5135
6430
|
}),
|
|
5136
6431
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
5137
6432
|
const groups = [...new Set(params.groups)] as PiagentToolGroup[];
|
|
@@ -5169,7 +6464,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5169
6464
|
action: StringEnum(["status", "rebuild", "search", "pack", "impact", "efficiency"] as const),
|
|
5170
6465
|
query: Type.Optional(Type.String()),
|
|
5171
6466
|
files: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
5172
|
-
budgetTokens: Type.Optional(Type.Number({ minimum: 200, maximum:
|
|
6467
|
+
budgetTokens: Type.Optional(Type.Number({ minimum: 200, maximum: 12000 })),
|
|
5173
6468
|
refresh: Type.Optional(Type.Boolean())
|
|
5174
6469
|
}),
|
|
5175
6470
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -5216,6 +6511,23 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5216
6511
|
].join("\n");
|
|
5217
6512
|
} else if (params.action === "pack") {
|
|
5218
6513
|
if (!params.query?.trim()) throw new Error("query is required for a context pack");
|
|
6514
|
+
const injected = injectedContextPacks.get(retrievalKey(ctx, params.query));
|
|
6515
|
+
if (injected && params.refresh !== true) {
|
|
6516
|
+
result = { reusedInjectedPack: true, ...injected };
|
|
6517
|
+
text = [
|
|
6518
|
+
"Context pack already injected for this task; duplicate payload skipped.",
|
|
6519
|
+
`confidence: ${injected.confidence}; estimatedTokensSaved: ${injected.estimatedTokens}`,
|
|
6520
|
+
`paths: ${injected.paths.join(", ")}`,
|
|
6521
|
+
"Read only the listed files or request refresh=true when the repository changed."
|
|
6522
|
+
].join("\n");
|
|
6523
|
+
telemetry(ctx, {
|
|
6524
|
+
event: "context_pack_reused",
|
|
6525
|
+
queryHash: injected.queryHash,
|
|
6526
|
+
estimatedTokensSaved: injected.estimatedTokens,
|
|
6527
|
+
selectedPaths: injected.paths
|
|
6528
|
+
});
|
|
6529
|
+
return { content: [{ type: "text", text }], details: result };
|
|
6530
|
+
}
|
|
5219
6531
|
const ensured = await ensureContextIndexV2(ctx.cwd, {
|
|
5220
6532
|
excludePatterns,
|
|
5221
6533
|
refresh: params.refresh,
|
|
@@ -5223,7 +6535,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5223
6535
|
});
|
|
5224
6536
|
const status = ensured.status;
|
|
5225
6537
|
const pack = await buildContextPack(ctx.cwd, params.query, {
|
|
5226
|
-
budgetTokens: params.budgetTokens ??
|
|
6538
|
+
budgetTokens: params.budgetTokens ?? 2_400,
|
|
5227
6539
|
includeCode: true,
|
|
5228
6540
|
limit: 18,
|
|
5229
6541
|
excludePatterns
|
|
@@ -5483,19 +6795,26 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5483
6795
|
description: "Check whether a governed task has enough context, verify evidence, and trace before claiming done.",
|
|
5484
6796
|
promptSnippet: "Use this before final on source-changing tasks.",
|
|
5485
6797
|
parameters: Type.Object({
|
|
5486
|
-
taskId: Type.String({ minLength: 1 })
|
|
6798
|
+
taskId: Type.String({ minLength: 1 }),
|
|
6799
|
+
changedFiles: Type.Optional(Type.Array(Type.String()))
|
|
5487
6800
|
}),
|
|
5488
6801
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
5489
|
-
const task = readTask(ctx.cwd, params.taskId);
|
|
5490
|
-
const
|
|
6802
|
+
const task = readTask(ctx.cwd, params.taskId, ctx.sessionManager.getSessionId());
|
|
6803
|
+
const projected = task ? {
|
|
6804
|
+
...task,
|
|
6805
|
+
changedFiles: params.changedFiles ?? task.changedFiles,
|
|
6806
|
+
trace: { ...task.trace, outcome: "completed" as const }
|
|
6807
|
+
} : undefined;
|
|
6808
|
+
const result = evaluateTaskGate(ctx.cwd, projected, policy);
|
|
5491
6809
|
const runtime = resolveRuntimePolicy(loadProfileFromContext(ctx));
|
|
5492
6810
|
const text = [
|
|
5493
6811
|
`decision: ${result.decision}`,
|
|
5494
6812
|
`mode: ${runtime.finalGate}`,
|
|
5495
6813
|
`missing: ${result.missing.join(", ") || "none"}`,
|
|
6814
|
+
...verifierCommandInstructions(result.missingVerifyCommands),
|
|
5496
6815
|
`warnings: ${result.warnings.join("; ") || "none"}`
|
|
5497
6816
|
].join("\n");
|
|
5498
|
-
return { content: [{ type: "text", text }], details: { ...result, task } };
|
|
6817
|
+
return { content: [{ type: "text", text }], details: { ...result, task: projected } };
|
|
5499
6818
|
}
|
|
5500
6819
|
});
|
|
5501
6820
|
|
|
@@ -6113,24 +7432,32 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6113
7432
|
}
|
|
6114
7433
|
});
|
|
6115
7434
|
|
|
6116
|
-
|
|
7435
|
+
const taskStartTool = {
|
|
6117
7436
|
name: "piagent_task_start",
|
|
6118
7437
|
label: "Piagent Task Start",
|
|
6119
7438
|
description: "Create a Task Implementation Contract for the current project before editing.",
|
|
6120
7439
|
promptSnippet: "Start a governed implementation task and persist the task contract.",
|
|
6121
7440
|
promptGuidelines: [
|
|
6122
|
-
"Call this before source edits in a project managed by Pi Agent Platform.",
|
|
6123
|
-
"
|
|
7441
|
+
"Call this exactly once before source edits in a project managed by Pi Agent Platform.",
|
|
7442
|
+
"Do not call context, status, policy, evidence-recording, trace, or gate tools first; runtime hooks provide those checks automatically.",
|
|
6124
7443
|
"Use tiny for a bounded low-risk change, normal for ordinary multi-file work, and high-risk for security, data, release, migration, or external-impact work.",
|
|
6125
|
-
"
|
|
7444
|
+
"Every scope entry must be a project-relative path or glob such as src/file.ts, src/**, or test/**; never put prose in scope.",
|
|
7445
|
+
"Leave workPlan unset for ordinary tiny/normal tasks so runtime automation stays active; pass a custom workPlan only when the operator explicitly requests custom subagent or checkpoint orchestration.",
|
|
7446
|
+
"Tiny tasks use automatic lifecycle evidence; normal tasks retain one explicit review step; high-risk/custom plans keep manual checkpoints."
|
|
6126
7447
|
],
|
|
6127
7448
|
parameters: Type.Object({
|
|
6128
7449
|
taskId: Type.Optional(Type.String({ minLength: 1 })),
|
|
6129
7450
|
summary: Type.String({ minLength: 10 }),
|
|
6130
7451
|
riskLane: StringEnum(["tiny", "normal", "high-risk"] as const),
|
|
7452
|
+
changeMode: Type.Optional(StringEnum(["source-change", "read-only"] as const)),
|
|
7453
|
+
verifyGroup: Type.Optional(Type.String({ minLength: 1 })),
|
|
7454
|
+
maxAttempts: Type.Optional(Type.Number({ minimum: 1, maximum: 10 })),
|
|
6131
7455
|
expectedOutput: Type.String({ minLength: 10 }),
|
|
6132
7456
|
acceptanceCriteria: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
6133
|
-
scope: Type.Array(Type.String({
|
|
7457
|
+
scope: Type.Array(Type.String({
|
|
7458
|
+
minLength: 1,
|
|
7459
|
+
description: "Project-relative path or glob only (for example src/file.ts, src/**, or test/**); do not use prose."
|
|
7460
|
+
}), { minItems: 1 }),
|
|
6134
7461
|
outOfScope: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
6135
7462
|
reviewLenses: Type.Optional(Type.Array(StringEnum(REVIEW_LENSES))),
|
|
6136
7463
|
workPlan: Type.Optional(Type.Array(Type.Object({
|
|
@@ -6138,33 +7465,143 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6138
7465
|
title: Type.String({ minLength: 1 }),
|
|
6139
7466
|
role: Type.Optional(StringEnum(ORCHESTRATION_ROLES)),
|
|
6140
7467
|
mode: Type.Optional(StringEnum(["read-only", "single-writer", "review"] as const)),
|
|
6141
|
-
status: Type.Optional(StringEnum(["pending", "in-progress", "done", "skipped"] as const)),
|
|
6142
|
-
dependsOn: Type.Optional(Type.Array(Type.String({ minLength: 1 })))
|
|
7468
|
+
status: Type.Optional(StringEnum(["pending", "in-progress", "done", "skipped", "failed"] as const)),
|
|
7469
|
+
dependsOn: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
7470
|
+
note: Type.Optional(Type.String())
|
|
6143
7471
|
})))
|
|
6144
7472
|
}),
|
|
6145
|
-
async execute(
|
|
7473
|
+
async execute(
|
|
7474
|
+
_toolCallId: string,
|
|
7475
|
+
params: TaskStartParameters,
|
|
7476
|
+
_signal: AbortSignal | undefined,
|
|
7477
|
+
_onUpdate: ((update: unknown) => void) | undefined,
|
|
7478
|
+
ctx: ExtensionContext
|
|
7479
|
+
) {
|
|
6146
7480
|
const profile = loadProfileFromContext(ctx);
|
|
6147
7481
|
const createdAt = nowIso();
|
|
6148
7482
|
const safeSummary = redactText(params.summary);
|
|
6149
7483
|
const taskId = safeTaskId(redactText(params.taskId ?? params.summary));
|
|
7484
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
7485
|
+
const sessionName = currentSessionName(ctx);
|
|
7486
|
+
const active = activeSessionTask(ctx.cwd, sessionId) as TaskContract | undefined;
|
|
7487
|
+
if (active) {
|
|
7488
|
+
if (active.taskId === taskId) {
|
|
7489
|
+
if (active.trace.outcome !== "pending") {
|
|
7490
|
+
return {
|
|
7491
|
+
content: [{ type: "text", text: `Session already belongs to terminal task ${active.taskId} (${active.trace.outcome}). Start a fresh Pi session for a retry or new task.` }],
|
|
7492
|
+
details: active,
|
|
7493
|
+
isError: true
|
|
7494
|
+
};
|
|
7495
|
+
}
|
|
7496
|
+
return {
|
|
7497
|
+
content: [{ type: "text", text: `Task already active in this session: ${active.taskId} (${active.taskRunId}). Reusing it instead of overwriting state.` }],
|
|
7498
|
+
details: compactTaskDetails(active)
|
|
7499
|
+
};
|
|
7500
|
+
}
|
|
7501
|
+
return {
|
|
7502
|
+
content: [{ type: "text", text: `Session already belongs to task ${active.taskId} (${active.taskRunId}, ${active.trace.outcome}). Use one Pi session per task and start a fresh session before another task.` }],
|
|
7503
|
+
details: active,
|
|
7504
|
+
isError: true
|
|
7505
|
+
};
|
|
7506
|
+
}
|
|
7507
|
+
const invalidScope = params.scope.find((entry) => !validTaskScopePattern(entry));
|
|
7508
|
+
if (invalidScope) {
|
|
7509
|
+
return {
|
|
7510
|
+
content: [{
|
|
7511
|
+
type: "text",
|
|
7512
|
+
text: `Task start refused: scope entries must be project-relative paths or globs; invalid entry ${JSON.stringify(invalidScope)}. Use values such as src/file.ts, src/**, or test/**.`
|
|
7513
|
+
}],
|
|
7514
|
+
isError: true
|
|
7515
|
+
};
|
|
7516
|
+
}
|
|
7517
|
+
const priorAttempts = priorTaskAttempts(ctx.cwd, taskId) as TaskContract[];
|
|
7518
|
+
const pendingElsewhere = priorAttempts.find((task) => task.trace.outcome === "pending" && task.sessionId !== sessionId);
|
|
7519
|
+
if (pendingElsewhere) {
|
|
7520
|
+
return {
|
|
7521
|
+
content: [{ type: "text", text: `Task ${taskId} is already active in session ${pendingElsewhere.sessionName ?? pendingElsewhere.sessionId} (${pendingElsewhere.taskRunId}).` }],
|
|
7522
|
+
details: pendingElsewhere,
|
|
7523
|
+
isError: true
|
|
7524
|
+
};
|
|
7525
|
+
}
|
|
7526
|
+
const latestCompleted = priorAttempts.find((task) => task.trace.outcome === "completed");
|
|
7527
|
+
if (latestCompleted) {
|
|
7528
|
+
return {
|
|
7529
|
+
content: [{ type: "text", text: `Task ${taskId} already completed as ${latestCompleted.taskRunId}. Use a distinct taskId for new work instead of replacing its evidence.` }],
|
|
7530
|
+
details: latestCompleted,
|
|
7531
|
+
isError: true
|
|
7532
|
+
};
|
|
7533
|
+
}
|
|
7534
|
+
const attempt = priorAttempts.reduce((maximum, task) => Math.max(maximum, task.attempt ?? 1), 0) + 1;
|
|
7535
|
+
const firstAttempt = priorAttempts.find((task) => task.attempt === 1)
|
|
7536
|
+
?? [...priorAttempts].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt))[0];
|
|
7537
|
+
const maxAttempts = firstAttempt
|
|
7538
|
+
? firstAttempt.maxAttempts
|
|
7539
|
+
: Number.isInteger(params.maxAttempts)
|
|
7540
|
+
? Math.max(1, Math.min(10, params.maxAttempts))
|
|
7541
|
+
: DEFAULT_MAX_TASK_ATTEMPTS;
|
|
7542
|
+
if (attempt > maxAttempts) {
|
|
7543
|
+
return {
|
|
7544
|
+
content: [{ type: "text", text: `Task ${taskId} reached its retry limit (${attempt - 1}/${maxAttempts}). Report the blocker or ask the operator to create a new scoped task.` }],
|
|
7545
|
+
details: { taskId, attempt, maxAttempts, previousAttempts: priorAttempts.map(summarizeAttempt) },
|
|
7546
|
+
isError: true
|
|
7547
|
+
};
|
|
7548
|
+
}
|
|
6150
7549
|
const orchestration = resolveOrchestrationPolicy(profile, policy);
|
|
7550
|
+
const changeMode = params.changeMode === "read-only" ? "read-only" : "source-change";
|
|
7551
|
+
if (changeMode === "source-change" && !isGitWorkingTree(ctx.cwd)) {
|
|
7552
|
+
return {
|
|
7553
|
+
content: [{ type: "text", text: "Task start refused: source-change tasks require a Git working tree so changed-file evidence cannot silently disappear. Initialize Git or use read-only mode." }],
|
|
7554
|
+
isError: true
|
|
7555
|
+
};
|
|
7556
|
+
}
|
|
7557
|
+
const verifyPlan = selectVerifyPlan(profile, params.verifyGroup, changeMode, ctx.cwd);
|
|
7558
|
+
if (verifyPlan.error) {
|
|
7559
|
+
return {
|
|
7560
|
+
content: [{ type: "text", text: `Task start refused: ${verifyPlan.error}` }],
|
|
7561
|
+
details: { verifyGroup: verifyPlan.group, verifyCommands: verifyPlan.commands },
|
|
7562
|
+
isError: true
|
|
7563
|
+
};
|
|
7564
|
+
}
|
|
6151
7565
|
const reviewLenses = normalizeReviewLenses(params.reviewLenses, orchestration.defaultReviewLenses);
|
|
6152
7566
|
const providedWorkPlan = normalizeWorkPlanSteps(params.workPlan);
|
|
6153
|
-
const workPlan = providedWorkPlan.length ? providedWorkPlan : defaultWorkPlan(safeSummary, params.riskLane);
|
|
7567
|
+
const workPlan = providedWorkPlan.length ? providedWorkPlan : defaultWorkPlan(safeSummary, params.riskLane, changeMode);
|
|
7568
|
+
const seededContext = observedContext(ctx).slice(0, contextBudgetConfig(policy).maxManifestFiles);
|
|
7569
|
+
const workPlanError = validateNewWorkPlan(workPlan);
|
|
7570
|
+
if (workPlanError) {
|
|
7571
|
+
return { content: [{ type: "text", text: `Task start refused: ${workPlanError}.` }], details: workPlan, isError: true };
|
|
7572
|
+
}
|
|
7573
|
+
const firstReady = workPlan.find((step) => (step.dependsOn ?? []).length === 0);
|
|
7574
|
+
if (!firstReady) {
|
|
7575
|
+
return { content: [{ type: "text", text: "Task start refused: work plan has no dependency-ready first step." }], details: workPlan, isError: true };
|
|
7576
|
+
}
|
|
7577
|
+
firstReady.status = "in-progress";
|
|
7578
|
+
firstReady.updatedAt = createdAt;
|
|
7579
|
+
const taskRunId = createTaskRunId(taskId, sessionId, createdAt);
|
|
7580
|
+
const baselineFileDigests = workingTreeSnapshot(ctx.cwd) as Record<string, string>;
|
|
6154
7581
|
const task: TaskContract = {
|
|
7582
|
+
schemaVersion: 2,
|
|
7583
|
+
taskRunId,
|
|
6155
7584
|
taskId,
|
|
7585
|
+
sessionId,
|
|
7586
|
+
sessionName,
|
|
7587
|
+
changeMode,
|
|
7588
|
+
attempt,
|
|
7589
|
+
maxAttempts,
|
|
7590
|
+
previousAttempts: priorAttempts.filter((task) => task.trace.outcome !== "pending").slice(0, 10).reverse().map(summarizeAttempt),
|
|
6156
7591
|
summary: safeSummary,
|
|
6157
7592
|
riskLane: params.riskLane,
|
|
7593
|
+
intakeMode: params.intakeMode === "runtime" ? "runtime" : "model",
|
|
6158
7594
|
expectedOutput: redactText(params.expectedOutput),
|
|
6159
7595
|
acceptanceCriteria: redactTextArray(params.acceptanceCriteria),
|
|
6160
7596
|
scope: redactTextArray(params.scope),
|
|
6161
7597
|
outOfScope: redactTextArray(params.outOfScope),
|
|
6162
7598
|
protectedPaths: profile.protectedPaths ?? [],
|
|
6163
7599
|
requiredContext: profile.requiredContext ?? [],
|
|
6164
|
-
contextManifest:
|
|
7600
|
+
contextManifest: seededContext,
|
|
6165
7601
|
memoryCitations: [],
|
|
6166
7602
|
mcpCapabilities: profile.mcpCapabilities ?? [],
|
|
6167
|
-
|
|
7603
|
+
verifyGroup: verifyPlan.group,
|
|
7604
|
+
verifyCommands: verifyPlan.commands,
|
|
6168
7605
|
workPlan,
|
|
6169
7606
|
reviewLenses,
|
|
6170
7607
|
orchestration: {
|
|
@@ -6174,19 +7611,230 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6174
7611
|
fieldGuidePath: orchestration.fieldGuide.enabled ? orchestration.fieldGuide.path : undefined,
|
|
6175
7612
|
modelRoles: orchestration.roleModelGuidance
|
|
6176
7613
|
},
|
|
7614
|
+
baselineChangedFiles: Object.keys(baselineFileDigests).sort(),
|
|
7615
|
+
baselineFileDigests,
|
|
7616
|
+
observedChangedFiles: [],
|
|
7617
|
+
finalWorkingTreeFiles: [],
|
|
7618
|
+
finalFileDigests: {},
|
|
6177
7619
|
changedFiles: [],
|
|
6178
7620
|
verifyEvidence: [],
|
|
6179
7621
|
trace: { outcome: "pending" },
|
|
6180
7622
|
createdAt,
|
|
6181
7623
|
updatedAt: createdAt
|
|
6182
7624
|
};
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
7625
|
+
if (changeMode === "read-only" && seededContext.length > 0) {
|
|
7626
|
+
applyRuntimeLifecycleObservation(task, "context-complete", createdAt);
|
|
7627
|
+
}
|
|
7628
|
+
const written = writeTask(ctx.cwd, task);
|
|
7629
|
+
bindSessionTask(ctx.cwd, sessionId, sessionName, written);
|
|
7630
|
+
cacheTaskIdentity(ctx, written);
|
|
7631
|
+
if (written.intakeMode !== "runtime") {
|
|
7632
|
+
// Intake classification defines the cache-stable surface for this agent turn.
|
|
7633
|
+
// A model may choose a narrower lane than the prompt classifier; never remove
|
|
7634
|
+
// schemas mid-turn, but add recovery tools when the chosen lane requires them.
|
|
7635
|
+
activateToolGroups(ctx, activeTaskToolGroups(written), true);
|
|
7636
|
+
}
|
|
7637
|
+
const lifecycleMode = runtimeLifecycleMode(written);
|
|
7638
|
+
appendTrace(ctx.cwd, { taskId, taskRunId, sessionId, sessionName, attempt, event: "task_start", summary: task.summary, riskLane: params.riskLane, intakeMode: task.intakeMode, changeMode: task.changeMode, lifecycleMode, seededContext: seededContext.map((item) => item.path) });
|
|
7639
|
+
appendSessionTrace(pi, { taskId, taskRunId, sessionId, sessionName, attempt, event: "task_start", summary: task.summary, riskLane: params.riskLane, intakeMode: task.intakeMode, changeMode: task.changeMode, lifecycleMode, seededContext: seededContext.map((item) => item.path) });
|
|
6186
7640
|
|
|
6187
7641
|
return {
|
|
6188
|
-
content: [{
|
|
6189
|
-
|
|
7642
|
+
content: [{
|
|
7643
|
+
type: "text",
|
|
7644
|
+
text: [
|
|
7645
|
+
`Task ${taskId} started (${params.riskLane}, ${lifecycleMode}; attempt ${attempt}/${maxAttempts}).`,
|
|
7646
|
+
written.verifyCommands.length > 0
|
|
7647
|
+
? ["Exact verifier commands:", ...verifierCommandInstructions(written.verifyCommands)].join("\n")
|
|
7648
|
+
: "Verify: none (read-only).",
|
|
7649
|
+
lifecycleMode === "automatic-readonly"
|
|
7650
|
+
? "Runtime records targeted reads and final completion automatically. Stay read-only and report cited evidence."
|
|
7651
|
+
: lifecycleMode === "assisted-readonly"
|
|
7652
|
+
? "Runtime records read-only evidence automatically; complete only the explicit evidence-review step before handoff."
|
|
7653
|
+
: lifecycleMode === "automatic"
|
|
7654
|
+
? "Runtime will record reads, changes, exact verifier results, and final completion automatically. Continue with ordinary read/edit/bash work."
|
|
7655
|
+
: lifecycleMode === "assisted"
|
|
7656
|
+
? "Runtime records objective evidence automatically; complete only the explicit review step before handoff."
|
|
7657
|
+
: "Use the active progress/recovery tools for the custom or high-risk checkpoints."
|
|
7658
|
+
].join("\n")
|
|
7659
|
+
}],
|
|
7660
|
+
details: compactTaskDetails(written)
|
|
7661
|
+
};
|
|
7662
|
+
}
|
|
7663
|
+
};
|
|
7664
|
+
pi.registerTool(taskStartTool);
|
|
7665
|
+
|
|
7666
|
+
async function maybeStartAutomaticTask(
|
|
7667
|
+
prompt: string,
|
|
7668
|
+
ctx: ExtensionContext
|
|
7669
|
+
): Promise<{ started: boolean; text: string; task?: TaskContract } | undefined> {
|
|
7670
|
+
const profile = loadProfileFromContext(ctx);
|
|
7671
|
+
const readProtectedPaths = effectiveProtectedPaths(policy, profile).readProtectedPaths;
|
|
7672
|
+
if (!automaticTaskIntakeEligible(prompt, readProtectedPaths)) return undefined;
|
|
7673
|
+
const active = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
|
|
7674
|
+
if (active?.trace.outcome === "pending") return undefined;
|
|
7675
|
+
|
|
7676
|
+
const summary = redactText(prompt).replace(/\s+/g, " ").trim().slice(0, 320);
|
|
7677
|
+
const sessionName = currentSessionName(ctx);
|
|
7678
|
+
const scope = automaticTaskScope(prompt, observedContext(ctx));
|
|
7679
|
+
const started = await taskStartTool.execute(
|
|
7680
|
+
`runtime-intake-${ctx.sessionManager.getSessionId()}`,
|
|
7681
|
+
{
|
|
7682
|
+
taskId: hasOperatorSessionName(sessionName) ? sessionName : summary,
|
|
7683
|
+
summary,
|
|
7684
|
+
riskLane: "tiny",
|
|
7685
|
+
intakeMode: "runtime",
|
|
7686
|
+
changeMode: "source-change",
|
|
7687
|
+
expectedOutput: "The requested bounded change is implemented and passes the configured verification.",
|
|
7688
|
+
acceptanceCriteria: [
|
|
7689
|
+
"The requested behavior is implemented without changing unrelated behavior.",
|
|
7690
|
+
"Changes stay within the runtime-derived task scope.",
|
|
7691
|
+
"The configured verification command passes after the final mutation."
|
|
7692
|
+
],
|
|
7693
|
+
scope,
|
|
7694
|
+
outOfScope: ["Unrelated files and behavior outside the operator request."],
|
|
7695
|
+
reviewLenses: automaticReviewLenses(prompt)
|
|
7696
|
+
},
|
|
7697
|
+
undefined,
|
|
7698
|
+
undefined,
|
|
7699
|
+
ctx
|
|
7700
|
+
);
|
|
7701
|
+
if (started.isError) {
|
|
7702
|
+
activateToolGroups(ctx, ["intake"], true);
|
|
7703
|
+
const reason = started.content?.[0]?.text ?? "runtime intake could not create a task contract";
|
|
7704
|
+
return {
|
|
7705
|
+
started: false,
|
|
7706
|
+
text: `Piagent runtime intake paused: ${reason}\nUse piagent_task_start once with explicit project-relative scope before mutation.`
|
|
7707
|
+
};
|
|
7708
|
+
}
|
|
7709
|
+
const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
|
|
7710
|
+
if (!task) {
|
|
7711
|
+
activateToolGroups(ctx, ["intake"], true);
|
|
7712
|
+
return {
|
|
7713
|
+
started: false,
|
|
7714
|
+
text: "Piagent runtime intake did not persist a task contract. Use piagent_task_start once before mutation."
|
|
7715
|
+
};
|
|
7716
|
+
}
|
|
7717
|
+
return {
|
|
7718
|
+
started: true,
|
|
7719
|
+
task,
|
|
7720
|
+
text: [
|
|
7721
|
+
`Piagent runtime task: ${task.taskId}; scope: ${task.scope.join(", ")}.`,
|
|
7722
|
+
"Exact verifier commands:",
|
|
7723
|
+
...(task.verifyCommands.length > 0 ? verifierCommandInstructions(task.verifyCommands) : ["none"]),
|
|
7724
|
+
"Root project instructions are loaded. Do not re-read root AGENTS.md or inspect Piagent/platform files; work directly in relevant source/tests with ordinary tools.",
|
|
7725
|
+
"Finish intended edits, then run the exact verifier once; rerun only after a later mutation. Runtime records evidence and completion; do not call task-management tools."
|
|
7726
|
+
].join("\n")
|
|
7727
|
+
};
|
|
7728
|
+
}
|
|
7729
|
+
|
|
7730
|
+
pi.registerTool({
|
|
7731
|
+
name: "piagent_task_progress",
|
|
7732
|
+
label: "Piagent Task Progress",
|
|
7733
|
+
description: "Advance or fail one dependency-aware work-plan step in the current session task.",
|
|
7734
|
+
promptSnippet: "Record durable task progress as each planned phase starts, completes, skips, or fails.",
|
|
7735
|
+
promptGuidelines: [
|
|
7736
|
+
"Do not mark a step done until its work and evidence are actually complete.",
|
|
7737
|
+
"A failed step requires a concrete note and records where the attempt failed.",
|
|
7738
|
+
"Completing a step automatically starts the next dependency-ready pending step."
|
|
7739
|
+
],
|
|
7740
|
+
parameters: Type.Object({
|
|
7741
|
+
taskId: Type.String({ minLength: 1 }),
|
|
7742
|
+
stepId: Type.String({ minLength: 1 }),
|
|
7743
|
+
status: StringEnum(["in-progress", "done", "skipped", "failed"] as const),
|
|
7744
|
+
note: Type.Optional(Type.String()),
|
|
7745
|
+
failedAt: Type.Optional(StringEnum(["research", "plan", "execute", "verify", "review"] as const)),
|
|
7746
|
+
ruledOut: Type.Optional(Type.String())
|
|
7747
|
+
}),
|
|
7748
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
7749
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
7750
|
+
const task = readTask(ctx.cwd, params.taskId, sessionId);
|
|
7751
|
+
if (!task) {
|
|
7752
|
+
return { content: [{ type: "text", text: `Task not found in this session: ${params.taskId}` }], isError: true };
|
|
7753
|
+
}
|
|
7754
|
+
if (task.trace.outcome !== "pending") {
|
|
7755
|
+
return { content: [{ type: "text", text: `Task ${task.taskId} is immutable after ${task.trace.outcome}; start a fresh session for another attempt.` }], details: task, isError: true };
|
|
7756
|
+
}
|
|
7757
|
+
const stepId = safeTaskId(params.stepId).slice(0, 40);
|
|
7758
|
+
const step = task.workPlan.find((item) => item.id === stepId);
|
|
7759
|
+
if (!step) {
|
|
7760
|
+
return { content: [{ type: "text", text: `Work-plan step not found: ${stepId}` }], details: task.workPlan, isError: true };
|
|
7761
|
+
}
|
|
7762
|
+
const dependencies = step.dependsOn ?? [];
|
|
7763
|
+
const unresolved = dependencies.filter((dependency) => {
|
|
7764
|
+
const required = task.workPlan.find((item) => item.id === dependency);
|
|
7765
|
+
return !required || (required.status !== "done" && required.status !== "skipped");
|
|
7766
|
+
});
|
|
7767
|
+
if ((params.status === "in-progress" || params.status === "done") && unresolved.length > 0) {
|
|
7768
|
+
return {
|
|
7769
|
+
content: [{ type: "text", text: `Step ${stepId} is blocked by unfinished dependencies: ${unresolved.join(", ")}` }],
|
|
7770
|
+
details: { step, unresolved },
|
|
7771
|
+
isError: true
|
|
7772
|
+
};
|
|
7773
|
+
}
|
|
7774
|
+
const note = params.note ? redactText(params.note).slice(0, 500) : undefined;
|
|
7775
|
+
if (params.status === "failed" && !note) {
|
|
7776
|
+
return { content: [{ type: "text", text: `A concrete note is required when step ${stepId} fails.` }], isError: true };
|
|
7777
|
+
}
|
|
7778
|
+
if ((step.status === "done" || step.status === "skipped") && params.status !== step.status) {
|
|
7779
|
+
return { content: [{ type: "text", text: `Work-plan step ${stepId} is already ${step.status} and cannot be reopened in the same attempt.` }], details: step, isError: true };
|
|
7780
|
+
}
|
|
7781
|
+
if (step.status === params.status) {
|
|
7782
|
+
return { content: [{ type: "text", text: `Work-plan step ${stepId} is already ${params.status}; no state change was recorded.` }], details: step, isError: true };
|
|
7783
|
+
}
|
|
7784
|
+
if (step.status === "failed" && params.status === "in-progress" && !note) {
|
|
7785
|
+
return { content: [{ type: "text", text: `A concrete note is required to reopen failed step ${stepId} within this attempt.` }], details: step, isError: true };
|
|
7786
|
+
}
|
|
7787
|
+
|
|
7788
|
+
const recordedAt = nowIso();
|
|
7789
|
+
step.status = params.status;
|
|
7790
|
+
step.note = note;
|
|
7791
|
+
step.updatedAt = recordedAt;
|
|
7792
|
+
if (params.status === "failed") {
|
|
7793
|
+
task.failedAt = params.failedAt ?? (step.mode === "review" ? "review" : step.id === "plan" ? "plan" : "execute");
|
|
7794
|
+
task.failureReason = note;
|
|
7795
|
+
task.ruledOut = params.ruledOut ? redactText(params.ruledOut).slice(0, 1000) : undefined;
|
|
7796
|
+
} else if (task.failureReason && task.workPlan.every((item) => item.status !== "failed")) {
|
|
7797
|
+
task.failedAt = undefined;
|
|
7798
|
+
task.failureReason = undefined;
|
|
7799
|
+
task.ruledOut = undefined;
|
|
7800
|
+
}
|
|
7801
|
+
|
|
7802
|
+
let startedStep: WorkPlanStep | undefined;
|
|
7803
|
+
if (params.status === "done" || params.status === "skipped") {
|
|
7804
|
+
startedStep = task.workPlan.find((candidate) => {
|
|
7805
|
+
if (candidate.status !== "pending") return false;
|
|
7806
|
+
return (candidate.dependsOn ?? []).every((dependency) => {
|
|
7807
|
+
const required = task.workPlan.find((item) => item.id === dependency);
|
|
7808
|
+
return required?.status === "done" || required?.status === "skipped";
|
|
7809
|
+
});
|
|
7810
|
+
});
|
|
7811
|
+
if (startedStep) {
|
|
7812
|
+
startedStep.status = "in-progress";
|
|
7813
|
+
startedStep.updatedAt = recordedAt;
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7816
|
+
|
|
7817
|
+
const written = writeTask(ctx.cwd, task);
|
|
7818
|
+
const trace = {
|
|
7819
|
+
taskId: written.taskId,
|
|
7820
|
+
taskRunId: written.taskRunId,
|
|
7821
|
+
sessionId,
|
|
7822
|
+
event: "task_progress",
|
|
7823
|
+
stepId,
|
|
7824
|
+
status: params.status,
|
|
7825
|
+
note,
|
|
7826
|
+
startedStep: startedStep?.id,
|
|
7827
|
+
failedAt: written.failedAt,
|
|
7828
|
+
ruledOut: written.ruledOut
|
|
7829
|
+
};
|
|
7830
|
+
appendTrace(ctx.cwd, trace);
|
|
7831
|
+
appendSessionTrace(pi, trace);
|
|
7832
|
+
return {
|
|
7833
|
+
content: [{
|
|
7834
|
+
type: "text",
|
|
7835
|
+
text: `Task ${written.taskId}: ${stepId} -> ${params.status}${startedStep ? `; started ${startedStep.id}` : ""}`
|
|
7836
|
+
}],
|
|
7837
|
+
details: compactTaskDetails(written)
|
|
6190
7838
|
};
|
|
6191
7839
|
}
|
|
6192
7840
|
});
|
|
@@ -6346,10 +7994,13 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6346
7994
|
const profile = loadProfileFromContext(ctx);
|
|
6347
7995
|
const runtime = resolveRuntimePolicy(profile);
|
|
6348
7996
|
const budget = contextBudgetConfig(policy);
|
|
6349
|
-
const task = readTask(ctx.cwd, params.taskId);
|
|
7997
|
+
const task = readTask(ctx.cwd, params.taskId, ctx.sessionManager.getSessionId());
|
|
6350
7998
|
if (!task) {
|
|
6351
7999
|
return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], isError: true };
|
|
6352
8000
|
}
|
|
8001
|
+
if (task.trace.outcome !== "pending") {
|
|
8002
|
+
return { content: [{ type: "text", text: `Task ${task.taskId} is immutable after ${task.trace.outcome}; context evidence was not changed.` }], details: task, isError: true };
|
|
8003
|
+
}
|
|
6353
8004
|
if (runtime.contextBudget !== "off" && task.contextManifest.length + params.files.length > budget.maxManifestFiles) {
|
|
6354
8005
|
return {
|
|
6355
8006
|
content: [{ type: "text", text: `Context manifest budget exceeded: ${task.contextManifest.length + params.files.length} files > ${budget.maxManifestFiles}` }],
|
|
@@ -6375,13 +8026,16 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6375
8026
|
const key = `${file.path}\u0000${file.reason}`;
|
|
6376
8027
|
if (!seen.has(key)) task.contextManifest.push(file);
|
|
6377
8028
|
}
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
8029
|
+
const lifecycle = task.changeMode === "read-only"
|
|
8030
|
+
? applyRuntimeLifecycleObservation(task, "context-complete", nowIso())
|
|
8031
|
+
: { changed: false, mode: runtimeLifecycleMode(task) };
|
|
8032
|
+
const written = writeTask(ctx.cwd, task);
|
|
8033
|
+
appendTrace(ctx.cwd, { taskId: task.taskId, taskRunId: task.taskRunId, sessionId: task.sessionId, event: "context_record", files: safeFiles, lifecycleMode: lifecycle.mode, lifecycleAdvanced: lifecycle.changed });
|
|
8034
|
+
appendSessionTrace(pi, { taskId: task.taskId, taskRunId: task.taskRunId, sessionId: task.sessionId, event: "context_record", files: safeFiles, lifecycleMode: lifecycle.mode, lifecycleAdvanced: lifecycle.changed });
|
|
6381
8035
|
|
|
6382
8036
|
return {
|
|
6383
8037
|
content: [{ type: "text", text: `Context recorded for ${task.taskId}: ${params.files.length} file(s)` }],
|
|
6384
|
-
details:
|
|
8038
|
+
details: compactTaskDetails(written)
|
|
6385
8039
|
};
|
|
6386
8040
|
}
|
|
6387
8041
|
});
|
|
@@ -6398,13 +8052,16 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6398
8052
|
summary: Type.String({ minLength: 1 })
|
|
6399
8053
|
}),
|
|
6400
8054
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
6401
|
-
const task = readTask(ctx.cwd, params.taskId);
|
|
8055
|
+
const task = readTask(ctx.cwd, params.taskId, ctx.sessionManager.getSessionId());
|
|
6402
8056
|
if (!task) {
|
|
6403
8057
|
return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], isError: true };
|
|
6404
8058
|
}
|
|
8059
|
+
if (task.trace.outcome !== "pending") {
|
|
8060
|
+
return { content: [{ type: "text", text: `Task ${task.taskId} is immutable after ${task.trace.outcome}; verify evidence was not changed.` }], details: task, isError: true };
|
|
8061
|
+
}
|
|
6405
8062
|
|
|
6406
8063
|
const observedEntries = [
|
|
6407
|
-
...readObservedBashResults(observedBashLedgerPath(ctx.cwd), { maxEntries: 10000 }),
|
|
8064
|
+
...readObservedBashResults(observedBashLedgerPath(ctx.cwd), { maxEntries: 10000, projectRoot: ctx.cwd }),
|
|
6408
8065
|
...bashResults.list()
|
|
6409
8066
|
];
|
|
6410
8067
|
const observed = findMatchingObservedBashResult(observedEntries, {
|
|
@@ -6424,24 +8081,50 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6424
8081
|
const safeCommand = redactText(params.command);
|
|
6425
8082
|
const safeSummary = redactText(params.summary);
|
|
6426
8083
|
const matchedProfileCommand = commandMatchesVerifyPlan(params.command, task.verifyCommands);
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
8084
|
+
const currentDigests = workingTreeSnapshot(ctx.cwd) as Record<string, string>;
|
|
8085
|
+
const workingTreeDigest = workingTreeEvidenceDigest(currentDigests);
|
|
8086
|
+
const duplicate = task.verifyEvidence.some((evidence) => (
|
|
8087
|
+
evidence.command.trim() === safeCommand.trim()
|
|
8088
|
+
&& evidence.exitCode === params.exitCode
|
|
8089
|
+
&& evidence.workingTreeDigest === workingTreeDigest
|
|
8090
|
+
));
|
|
8091
|
+
if (!duplicate) {
|
|
8092
|
+
task.verifyEvidence.push({
|
|
8093
|
+
command: safeCommand,
|
|
8094
|
+
exitCode: params.exitCode,
|
|
8095
|
+
summary: safeSummary,
|
|
8096
|
+
recordedAt: nowIso(),
|
|
8097
|
+
observed: true,
|
|
8098
|
+
observedAt: observed.entry.recordedAt,
|
|
8099
|
+
isError: observed.entry.isError,
|
|
8100
|
+
matchedProfileCommand,
|
|
8101
|
+
workingTreeDigest
|
|
8102
|
+
});
|
|
8103
|
+
task.verifyEvidence = task.verifyEvidence.slice(-100);
|
|
8104
|
+
}
|
|
8105
|
+
const hasChanges = taskChangedFileEvidence(ctx.cwd, task, currentDigests).expected.length > 0;
|
|
8106
|
+
const allPassing = matchedProfileCommand && hasChanges && allVerifyCommandsPassCurrentTree(task, workingTreeDigest);
|
|
8107
|
+
if (matchedProfileCommand && hasChanges) {
|
|
8108
|
+
applyRuntimeLifecycleObservation(task, allPassing ? "verification-complete" : "verification-pending", nowIso());
|
|
8109
|
+
}
|
|
8110
|
+
const written = writeTask(ctx.cwd, task);
|
|
8111
|
+
appendTrace(ctx.cwd, { taskId: task.taskId, taskRunId: task.taskRunId, sessionId: task.sessionId, event: "verify_record", command: safeCommand, exitCode: params.exitCode, observedAt: observed.entry.recordedAt, matchedProfileCommand, workingTreeDigest, duplicate });
|
|
8112
|
+
appendSessionTrace(pi, { taskId: task.taskId, taskRunId: task.taskRunId, sessionId: task.sessionId, event: "verify_record", command: safeCommand, exitCode: params.exitCode, observedAt: observed.entry.recordedAt, matchedProfileCommand, workingTreeDigest, duplicate });
|
|
6440
8113
|
|
|
6441
8114
|
const advisorySuffix = matchedProfileCommand ? "" : " Advisory only: command does not exactly match task verifyCommands and will not satisfy the passing final gate.";
|
|
6442
8115
|
return {
|
|
6443
8116
|
content: [{ type: "text", text: `Verify evidence recorded for ${task.taskId}: observed exit ${params.exitCode}.${advisorySuffix}` }],
|
|
6444
|
-
details: {
|
|
8117
|
+
details: {
|
|
8118
|
+
task: compactTaskDetails(written),
|
|
8119
|
+
evidence: {
|
|
8120
|
+
command: safeCommand,
|
|
8121
|
+
exitCode: params.exitCode,
|
|
8122
|
+
observed: true,
|
|
8123
|
+
matchedProfileCommand,
|
|
8124
|
+
workingTreeDigest
|
|
8125
|
+
},
|
|
8126
|
+
duplicate
|
|
8127
|
+
}
|
|
6445
8128
|
};
|
|
6446
8129
|
}
|
|
6447
8130
|
});
|
|
@@ -6459,10 +8142,13 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6459
8142
|
}), { minItems: 1 })
|
|
6460
8143
|
}),
|
|
6461
8144
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
6462
|
-
const task = readTask(ctx.cwd, params.taskId);
|
|
8145
|
+
const task = readTask(ctx.cwd, params.taskId, ctx.sessionManager.getSessionId());
|
|
6463
8146
|
if (!task) {
|
|
6464
8147
|
return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], isError: true };
|
|
6465
8148
|
}
|
|
8149
|
+
if (task.trace.outcome !== "pending") {
|
|
8150
|
+
return { content: [{ type: "text", text: `Task ${task.taskId} is immutable after ${task.trace.outcome}; memory evidence was not changed.` }], details: task, isError: true };
|
|
8151
|
+
}
|
|
6466
8152
|
|
|
6467
8153
|
const safeFiles = params.files.map((file) => ({
|
|
6468
8154
|
path: file.path,
|
|
@@ -6474,8 +8160,8 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6474
8160
|
if (!seen.has(key)) task.memoryCitations.push(file);
|
|
6475
8161
|
}
|
|
6476
8162
|
writeTask(ctx.cwd, task);
|
|
6477
|
-
appendTrace(ctx.cwd, { taskId: task.taskId, event: "memory_citation_record", files: safeFiles });
|
|
6478
|
-
appendSessionTrace(pi, { taskId: task.taskId, event: "memory_citation_record", files: safeFiles });
|
|
8163
|
+
appendTrace(ctx.cwd, { taskId: task.taskId, taskRunId: task.taskRunId, sessionId: task.sessionId, event: "memory_citation_record", files: safeFiles });
|
|
8164
|
+
appendSessionTrace(pi, { taskId: task.taskId, taskRunId: task.taskRunId, sessionId: task.sessionId, event: "memory_citation_record", files: safeFiles });
|
|
6479
8165
|
|
|
6480
8166
|
return {
|
|
6481
8167
|
content: [{ type: "text", text: `Memory citations recorded for ${task.taskId}: ${params.files.length} file(s)` }],
|
|
@@ -6494,19 +8180,44 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6494
8180
|
outcome: StringEnum(["completed", "blocked", "partial", "failed"] as const),
|
|
6495
8181
|
changedFiles: Type.Optional(Type.Array(Type.String())),
|
|
6496
8182
|
friction: Type.Optional(Type.String()),
|
|
6497
|
-
notes: Type.Optional(Type.String())
|
|
8183
|
+
notes: Type.Optional(Type.String()),
|
|
8184
|
+
failedAt: Type.Optional(StringEnum(["research", "plan", "execute", "verify", "review"] as const)),
|
|
8185
|
+
ruledOut: Type.Optional(Type.String())
|
|
6498
8186
|
}),
|
|
6499
8187
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
6500
8188
|
const profile = loadProfileFromContext(ctx);
|
|
6501
8189
|
const runtime = resolveRuntimePolicy(profile);
|
|
6502
|
-
const task = readTask(ctx.cwd, params.taskId);
|
|
8190
|
+
const task = readTask(ctx.cwd, params.taskId, ctx.sessionManager.getSessionId());
|
|
6503
8191
|
if (!task) {
|
|
6504
8192
|
return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], isError: true };
|
|
6505
8193
|
}
|
|
8194
|
+
if (task.trace.outcome !== "pending") {
|
|
8195
|
+
return { content: [{ type: "text", text: `Task ${task.taskId} is immutable after ${task.trace.outcome}; its final trace was not replaced.` }], details: task, isError: true };
|
|
8196
|
+
}
|
|
8197
|
+
if (params.outcome !== "completed" && !params.friction?.trim() && !task.failureReason?.trim()) {
|
|
8198
|
+
return { content: [{ type: "text", text: `Trace ${params.outcome} requires a concrete friction/reason so the next attempt does not repeat the same work.` }], details: task, isError: true };
|
|
8199
|
+
}
|
|
8200
|
+
if (params.outcome === "failed" && !params.failedAt && !task.failedAt) {
|
|
8201
|
+
return { content: [{ type: "text", text: "A failed trace requires failedAt to identify the lifecycle phase." }], details: task, isError: true };
|
|
8202
|
+
}
|
|
6506
8203
|
|
|
8204
|
+
const rawChangedFiles = params.changedFiles ?? task.changedFiles;
|
|
8205
|
+
const normalizedChangedFiles = rawChangedFiles.map((file) => normalizeRelative(ctx.cwd, file));
|
|
8206
|
+
if (normalizedChangedFiles.some((file) => !file || file === "." || file === ".." || file.startsWith("../") || file.startsWith(".pi/piagent-state/"))) {
|
|
8207
|
+
return {
|
|
8208
|
+
content: [{ type: "text", text: "Trace refused: changedFiles must be project-relative paths outside .pi/piagent-state/." }],
|
|
8209
|
+
isError: true
|
|
8210
|
+
};
|
|
8211
|
+
}
|
|
8212
|
+
const finalFileDigests = workingTreeSnapshot(ctx.cwd) as Record<string, string>;
|
|
6507
8213
|
const nextTask: TaskContract = {
|
|
6508
8214
|
...task,
|
|
6509
|
-
changedFiles:
|
|
8215
|
+
changedFiles: uniqueStrings(normalizedChangedFiles as string[]).sort(),
|
|
8216
|
+
finalWorkingTreeFiles: Object.keys(finalFileDigests).sort(),
|
|
8217
|
+
finalFileDigests,
|
|
8218
|
+
failedAt: params.outcome === "completed" ? undefined : params.failedAt ?? task.failedAt,
|
|
8219
|
+
failureReason: params.outcome === "completed" ? undefined : params.friction ? redactText(params.friction) : task.failureReason,
|
|
8220
|
+
ruledOut: params.outcome === "completed" ? undefined : params.ruledOut ? redactText(params.ruledOut).slice(0, 1000) : task.ruledOut,
|
|
6510
8221
|
trace: {
|
|
6511
8222
|
outcome: params.outcome,
|
|
6512
8223
|
friction: params.friction ? redactText(params.friction) : undefined,
|
|
@@ -6514,36 +8225,53 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6514
8225
|
recordedAt: nowIso()
|
|
6515
8226
|
}
|
|
6516
8227
|
};
|
|
6517
|
-
const gate = evaluateTaskGate(nextTask, policy
|
|
8228
|
+
const gate = evaluateTaskGate(ctx.cwd, nextTask, policy, {
|
|
8229
|
+
currentDigests: finalFileDigests,
|
|
8230
|
+
currentWorkingTreeDigest: workingTreeEvidenceDigest(finalFileDigests)
|
|
8231
|
+
});
|
|
6518
8232
|
if (params.outcome === "completed" && runtime.finalGate === "enforce" && gate.decision === "fail") {
|
|
8233
|
+
const blockedTrace = {
|
|
8234
|
+
taskId: nextTask.taskId,
|
|
8235
|
+
taskRunId: nextTask.taskRunId,
|
|
8236
|
+
sessionId: nextTask.sessionId,
|
|
8237
|
+
event: "completion_gate_blocked",
|
|
8238
|
+
missing: gate.missing,
|
|
8239
|
+
changedFiles: nextTask.changedFiles
|
|
8240
|
+
};
|
|
8241
|
+
appendTrace(ctx.cwd, blockedTrace);
|
|
8242
|
+
appendSessionTrace(pi, blockedTrace);
|
|
6519
8243
|
return {
|
|
6520
|
-
content: [{
|
|
8244
|
+
content: [{
|
|
8245
|
+
type: "text",
|
|
8246
|
+
text: [
|
|
8247
|
+
`Final gate blocked completion: missing ${gate.missing.join(", ")}`,
|
|
8248
|
+
...verifierCommandInstructions(gate.missingVerifyCommands)
|
|
8249
|
+
].join("\n")
|
|
8250
|
+
}],
|
|
6521
8251
|
details: { gate, task: nextTask },
|
|
6522
8252
|
isError: true
|
|
6523
8253
|
};
|
|
6524
8254
|
}
|
|
6525
8255
|
|
|
6526
|
-
writeTask(ctx.cwd, nextTask);
|
|
6527
|
-
|
|
6528
|
-
taskId: nextTask.taskId,
|
|
6529
|
-
event: "trace_record",
|
|
6530
|
-
outcome: params.outcome,
|
|
6531
|
-
changedFiles: nextTask.changedFiles,
|
|
6532
|
-
friction: nextTask.trace.friction,
|
|
6533
|
-
notes: nextTask.trace.notes
|
|
6534
|
-
});
|
|
6535
|
-
appendSessionTrace(pi, {
|
|
8256
|
+
const written = writeTask(ctx.cwd, nextTask);
|
|
8257
|
+
const trace = {
|
|
6536
8258
|
taskId: nextTask.taskId,
|
|
8259
|
+
taskRunId: nextTask.taskRunId,
|
|
8260
|
+
sessionId: nextTask.sessionId,
|
|
6537
8261
|
event: "trace_record",
|
|
6538
8262
|
outcome: params.outcome,
|
|
6539
8263
|
changedFiles: nextTask.changedFiles,
|
|
6540
8264
|
friction: nextTask.trace.friction,
|
|
6541
|
-
notes: nextTask.trace.notes
|
|
6542
|
-
|
|
8265
|
+
notes: nextTask.trace.notes,
|
|
8266
|
+
failedAt: nextTask.failedAt,
|
|
8267
|
+
ruledOut: nextTask.ruledOut
|
|
8268
|
+
};
|
|
8269
|
+
appendTrace(ctx.cwd, trace);
|
|
8270
|
+
appendSessionTrace(pi, trace);
|
|
6543
8271
|
|
|
6544
8272
|
return {
|
|
6545
8273
|
content: [{ type: "text", text: `Trace recorded for ${nextTask.taskId}: ${params.outcome}${gate.decision === "fail" ? ` (gate warning: missing ${gate.missing.join(", ")})` : ""}` }],
|
|
6546
|
-
details: { task:
|
|
8274
|
+
details: { task: written, gate }
|
|
6547
8275
|
};
|
|
6548
8276
|
}
|
|
6549
8277
|
});
|
|
@@ -7501,12 +9229,13 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
7501
9229
|
}
|
|
7502
9230
|
|
|
7503
9231
|
function compactCurrentSession(ctx: ExtensionContext): void {
|
|
7504
|
-
const
|
|
9232
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
9233
|
+
const instructions = semanticCompactionInstructions(ctx.cwd, sessionId);
|
|
7505
9234
|
telemetry(ctx, {
|
|
7506
9235
|
event: "compaction_requested",
|
|
7507
9236
|
mode: "semantic",
|
|
7508
9237
|
instructionTokens: estimateContextTokens(instructions),
|
|
7509
|
-
hasTaskContract: Boolean(compactSessionTask(ctx.cwd))
|
|
9238
|
+
hasTaskContract: Boolean(compactSessionTask(ctx.cwd, sessionId))
|
|
7510
9239
|
});
|
|
7511
9240
|
ctx.compact({
|
|
7512
9241
|
customInstructions: instructions
|