@piagent/platform 1.2.13 → 1.2.14
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 +26 -0
- package/README.md +11 -3
- package/README.vi.md +90 -0
- package/architecture/layers.json +117 -0
- package/docs/README.md +23 -0
- package/docs/architecture.md +7 -61
- package/docs/command-reference-vietnamese.md +6 -6
- package/docs/distribution-standard.md +7 -7
- package/docs/en/README.md +13 -0
- package/docs/en/architecture.md +81 -0
- package/docs/en/maintainer-guide.md +121 -0
- package/docs/languages.json +40 -0
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/package-architecture-notes.md +9 -0
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +1 -1
- package/docs/team-onboarding.md +2 -2
- package/docs/vi/README.md +13 -0
- package/docs/vi/architecture.md +81 -0
- package/docs/vi/maintainer-guide.md +121 -0
- package/docs-site/architecture.html +38 -14
- package/docs-site/benchmark.html +3 -1
- package/docs-site/commands.html +3 -1
- package/docs-site/content/architecture.html +35 -13
- package/docs-site/context-engine.html +3 -1
- package/docs-site/index.html +3 -1
- package/docs-site/mcp-approval.html +3 -1
- package/docs-site/mcp-auth.html +3 -1
- package/docs-site/mcp-servers.html +3 -1
- package/docs-site/mcp.html +3 -1
- package/docs-site/permissions.html +3 -1
- package/docs-site/profiles.html +3 -1
- package/docs-site/quickstart.html +3 -1
- package/docs-site/runtime.html +3 -1
- package/docs-site/scope.html +3 -1
- package/docs-site/security.html +3 -1
- package/docs-site/team.html +3 -1
- package/docs-site/workflows.html +3 -1
- package/package.json +5 -1
- package/packages/piagent-core/README.md +6 -2
- package/packages/piagent-core/capabilities/capability-core.js +3 -0
- package/packages/piagent-core/capabilities/runtime-integrity.js +18 -0
- package/packages/piagent-core/extensions/guard-types.ts +0 -37
- package/packages/piagent-core/extensions/piagent-guard.ts +170 -2099
- package/packages/piagent-core/extensions/task-state.js +22 -3
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +213 -0
- package/packages/piagent-core/runtime/hooks/completion-hook.ts +203 -0
- package/packages/piagent-core/runtime/hooks/input-hook.ts +122 -0
- package/packages/piagent-core/runtime/hooks/session-hooks.ts +104 -0
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +140 -0
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +316 -0
- package/packages/piagent-core/runtime/input/chat-images.ts +299 -0
- package/packages/piagent-core/runtime/runtime-limits.ts +21 -0
- package/packages/piagent-core/runtime/session/message-signals.ts +64 -0
- package/packages/piagent-core/runtime/session/runtime-state.ts +175 -0
- package/packages/piagent-core/runtime/session/system-prompt.ts +52 -0
- package/packages/piagent-core/runtime/session/tool-result-compaction.ts +352 -0
- package/packages/piagent-core/runtime/session/usage.ts +178 -0
- package/packages/piagent-core/runtime/tools/tool-groups.ts +86 -0
- package/packages/piagent-core/runtime/workflows/input-routing.ts +113 -0
- package/packages/piagent-core/runtime/workflows/task-intake.ts +73 -0
- package/scripts/build-docs-site.mjs +4 -1
- package/scripts/check-architecture.mjs +132 -0
- package/scripts/check-doc-languages.mjs +67 -0
- package/scripts/verify-local.sh +31 -0
|
@@ -20,12 +20,10 @@ import {
|
|
|
20
20
|
shellHasFileWriteRedirection
|
|
21
21
|
} from "./policy-core.js";
|
|
22
22
|
import {
|
|
23
|
-
appendObservedBashResult,
|
|
24
23
|
commandMatchesVerifyPlan,
|
|
25
24
|
createBashResultLedger,
|
|
26
25
|
findMatchingObservedBashResult,
|
|
27
|
-
readObservedBashResults
|
|
28
|
-
observedBashResultFromToolResultEvent
|
|
26
|
+
readObservedBashResults
|
|
29
27
|
} from "./runtime-evidence.js";
|
|
30
28
|
import {
|
|
31
29
|
findPackageRoot,
|
|
@@ -99,7 +97,6 @@ import {
|
|
|
99
97
|
createTaskRunId,
|
|
100
98
|
isGitWorkingTree,
|
|
101
99
|
listTaskContracts,
|
|
102
|
-
migrateTaskState,
|
|
103
100
|
priorTaskAttempts,
|
|
104
101
|
resolveTaskContract,
|
|
105
102
|
safeTaskId,
|
|
@@ -113,12 +110,56 @@ import {
|
|
|
113
110
|
runtimeLifecycleMode,
|
|
114
111
|
workingTreeEvidenceDigest
|
|
115
112
|
} from "./task-lifecycle.js";
|
|
116
|
-
import {
|
|
117
|
-
appendJsonlBounded,
|
|
118
|
-
pruneCaptureFiles,
|
|
119
|
-
readJsonlTail
|
|
120
|
-
} from "./state-retention.js";
|
|
113
|
+
import { appendJsonlBounded } from "./state-retention.js";
|
|
121
114
|
import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
|
|
115
|
+
import {
|
|
116
|
+
TOOL_RESULT_CAPTURE_MAX_CHARS,
|
|
117
|
+
TOOL_RESULT_COMPACT_CHAR_THRESHOLD,
|
|
118
|
+
TOOL_RESULT_COMPACT_LINE_THRESHOLD,
|
|
119
|
+
TOOL_RESULT_PREVIEW_MAX_CHARS
|
|
120
|
+
} from "../runtime/runtime-limits.ts";
|
|
121
|
+
import {
|
|
122
|
+
cleanSessionNameInput,
|
|
123
|
+
currentSessionName,
|
|
124
|
+
hasOperatorSessionName
|
|
125
|
+
} from "../runtime/session/message-signals.ts";
|
|
126
|
+
import {
|
|
127
|
+
buildContextPreflight,
|
|
128
|
+
buildUsageSnapshot,
|
|
129
|
+
formatContextPreflight,
|
|
130
|
+
formatCount,
|
|
131
|
+
formatPercent,
|
|
132
|
+
formatUsageSnapshot
|
|
133
|
+
} from "../runtime/session/usage.ts";
|
|
134
|
+
import {
|
|
135
|
+
formatToolResultCaptureStatus,
|
|
136
|
+
readRecentToolResultCaptures
|
|
137
|
+
} from "../runtime/session/tool-result-compaction.ts";
|
|
138
|
+
import { RuntimeSessionState } from "../runtime/session/runtime-state.ts";
|
|
139
|
+
import type { ObservedTaskContext } from "../runtime/session/runtime-state.ts";
|
|
140
|
+
import {
|
|
141
|
+
PIAGENT_TOOL_GROUPS,
|
|
142
|
+
PIAGENT_TOOL_NAMES,
|
|
143
|
+
PIAGENT_TOOL_ORDER,
|
|
144
|
+
activeTaskToolGroups
|
|
145
|
+
} from "../runtime/tools/tool-groups.ts";
|
|
146
|
+
import type { PiagentToolGroup } from "../runtime/tools/tool-groups.ts";
|
|
147
|
+
import { shortTaskLabel } from "../runtime/workflows/input-routing.ts";
|
|
148
|
+
import {
|
|
149
|
+
automaticReviewLenses,
|
|
150
|
+
automaticTaskIntakeEligible,
|
|
151
|
+
automaticTaskScope,
|
|
152
|
+
validTaskScopePattern
|
|
153
|
+
} from "../runtime/workflows/task-intake.ts";
|
|
154
|
+
import { readChatImage } from "../runtime/input/chat-images.ts";
|
|
155
|
+
import { registerInputHook } from "../runtime/hooks/input-hook.ts";
|
|
156
|
+
import { registerAgentStartHook } from "../runtime/hooks/agent-start-hook.ts";
|
|
157
|
+
import { registerCompletionHook } from "../runtime/hooks/completion-hook.ts";
|
|
158
|
+
import { registerSessionHooks } from "../runtime/hooks/session-hooks.ts";
|
|
159
|
+
import { registerSessionStartHook } from "../runtime/hooks/session-start-hook.ts";
|
|
160
|
+
import { registerToolResultHook } from "../runtime/hooks/tool-result-hook.ts";
|
|
161
|
+
|
|
162
|
+
export { readChatImage };
|
|
122
163
|
|
|
123
164
|
import type { ActionClassification } from "./guard-shell-analysis.ts";
|
|
124
165
|
|
|
@@ -132,7 +173,6 @@ import type {
|
|
|
132
173
|
ContextIndexNode,
|
|
133
174
|
ContextIndexNodeKind,
|
|
134
175
|
ContextIndexSettings,
|
|
135
|
-
ContextPreflight,
|
|
136
176
|
ExecPolicyConfig,
|
|
137
177
|
ExternalActionPolicyConfig,
|
|
138
178
|
FinalGateConfig,
|
|
@@ -159,43 +199,13 @@ import type {
|
|
|
159
199
|
TechStackEntry,
|
|
160
200
|
TechStackManifest,
|
|
161
201
|
ToolRegistryConfig,
|
|
162
|
-
UsageSnapshot,
|
|
163
202
|
WorkPlanStep
|
|
164
203
|
} from "./guard-types.js";
|
|
165
204
|
|
|
166
205
|
|
|
167
206
|
const PIAGENT_TRACE_STATE_TYPE = "piagent-task-trace";
|
|
168
|
-
const CONTEXT_WATCH_PERCENT = 50;
|
|
169
|
-
const CONTEXT_COMPACT_PERCENT = 70;
|
|
170
|
-
const CONTEXT_FRESH_PERCENT = 82;
|
|
171
|
-
const LONG_INPUT_CHARS = 8000;
|
|
172
207
|
const BOILERPLATE_COLLAPSE_CHARS = 300;
|
|
173
|
-
const MAX_INLINE_COLLAPSED_TASK_CHARS = 2200;
|
|
174
|
-
const MAX_CHAT_IMAGE_ATTACHMENTS = 4;
|
|
175
|
-
const MAX_CHAT_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
176
|
-
const TOOL_RESULT_COMPACT_CHAR_THRESHOLD = 12_000;
|
|
177
|
-
const TOOL_RESULT_COMPACT_LINE_THRESHOLD = 180;
|
|
178
|
-
const TOOL_RESULT_PREVIEW_HEAD_LINES = 24;
|
|
179
|
-
const TOOL_RESULT_PREVIEW_TAIL_LINES = 24;
|
|
180
|
-
const TOOL_RESULT_PREVIEW_INTERESTING_LINES = 24;
|
|
181
|
-
const TOOL_RESULT_PREVIEW_MAX_CHARS = 6_000;
|
|
182
|
-
const TOOL_RESULT_CAPTURE_MAX_CHARS = 500_000;
|
|
183
208
|
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
209
|
type TaskStartParameters = {
|
|
200
210
|
taskId?: string;
|
|
201
211
|
summary: string;
|
|
@@ -219,7 +229,6 @@ type TaskStartParameters = {
|
|
|
219
229
|
note?: string;
|
|
220
230
|
}>;
|
|
221
231
|
};
|
|
222
|
-
const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp", "bmp"] as const;
|
|
223
232
|
const ORCHESTRATION_MODES = ["solo-first", "bounded-subagents", "parallel-readonly"] as const;
|
|
224
233
|
const REVIEW_LENSES = ["correctness", "tests", "scope", "security", "docs", "release", "package"] as const;
|
|
225
234
|
const ORCHESTRATION_ROLES = ["parent", "piagent-scout", "piagent-planner", "piagent-worker", "piagent-reviewer", "piagent-oracle"] as const;
|
|
@@ -228,55 +237,6 @@ const CONTEXT_INDEX_EDGE_KINDS = ["uses_tech", "depends_on", "verified_by", "pro
|
|
|
228
237
|
const CONTEXT_INDEX_FILE = ".pi/context-index.json";
|
|
229
238
|
const TECH_STACK_MANIFEST_FILE = ".pi/tech-stack.json";
|
|
230
239
|
const TECH_CONTEXT_DIR = ".pi/tech-context";
|
|
231
|
-
const PIAGENT_TOOL_GROUPS = {
|
|
232
|
-
loader: ["piagent_tools"],
|
|
233
|
-
intake: ["piagent_task_start"],
|
|
234
|
-
governance: [
|
|
235
|
-
"piagent_context",
|
|
236
|
-
"piagent_context_preflight"
|
|
237
|
-
],
|
|
238
|
-
task: ["piagent_task_progress"],
|
|
239
|
-
recovery: [
|
|
240
|
-
"piagent_context_record",
|
|
241
|
-
"piagent_verify_record",
|
|
242
|
-
"piagent_trace_record",
|
|
243
|
-
"piagent_task_gate_check"
|
|
244
|
-
],
|
|
245
|
-
policy: [
|
|
246
|
-
"piagent_permission_status",
|
|
247
|
-
"piagent_exec_policy_check",
|
|
248
|
-
"piagent_tool_policy_check"
|
|
249
|
-
],
|
|
250
|
-
retrieval: [
|
|
251
|
-
"piagent_context_engine",
|
|
252
|
-
"piagent_context_budget",
|
|
253
|
-
"piagent_context_index_status",
|
|
254
|
-
"piagent_context_index_search"
|
|
255
|
-
],
|
|
256
|
-
knowledge: [
|
|
257
|
-
"piagent_memory_status",
|
|
258
|
-
"piagent_memory_search",
|
|
259
|
-
"piagent_memory_citation_record",
|
|
260
|
-
"piagent_document_read",
|
|
261
|
-
"piagent_source_checkout",
|
|
262
|
-
"piagent_orchestration_policy"
|
|
263
|
-
],
|
|
264
|
-
onboarding: [
|
|
265
|
-
"piagent_profile_options",
|
|
266
|
-
"piagent_profile_apply",
|
|
267
|
-
"piagent_profile_tech_options",
|
|
268
|
-
"piagent_profile_tech_apply",
|
|
269
|
-
"piagent_profile_tech_context_record",
|
|
270
|
-
"piagent_project_onboarding_record",
|
|
271
|
-
"piagent_context_index_record",
|
|
272
|
-
"piagent_memory_note"
|
|
273
|
-
],
|
|
274
|
-
usage: ["piagent_usage_snapshot"]
|
|
275
|
-
} as const;
|
|
276
|
-
type PiagentToolGroup = keyof typeof PIAGENT_TOOL_GROUPS;
|
|
277
|
-
const PIAGENT_TOOL_ORDER = Object.values(PIAGENT_TOOL_GROUPS).flat();
|
|
278
|
-
const PIAGENT_TOOL_NAMES = new Set<string>(PIAGENT_TOOL_ORDER);
|
|
279
|
-
|
|
280
240
|
const TECH_OPTIONS: TechOption[] = [
|
|
281
241
|
{ id: "nextjs", label: "Next.js", role: "frontend", description: "React framework with App Router/SSR/static rendering patterns.", context7Query: "next.js", topics: ["app-router", "routing", "data-fetching", "server-components"] },
|
|
282
242
|
{ id: "react-vite", label: "React + Vite", role: "frontend", description: "Client-side React app built with Vite.", context7Query: "react vite", topics: ["components", "hooks", "vite", "testing"] },
|
|
@@ -327,21 +287,6 @@ const PROFILE_TECH_ROLES: Record<string, TechRole[]> = {
|
|
|
327
287
|
generic: ["runtime"]
|
|
328
288
|
};
|
|
329
289
|
|
|
330
|
-
type ChatImageAttachmentResult = {
|
|
331
|
-
text: string;
|
|
332
|
-
images: Array<{ type: "image"; data: string; mimeType: string }>;
|
|
333
|
-
attached: Array<{ marker: string; path: string; mimeType: string; bytes: number }>;
|
|
334
|
-
skipped: Array<{ path: string; reason: string }>;
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
type ChatImageAccessPolicy = {
|
|
338
|
-
roots: ReturnType<typeof resolveDocumentRoots>;
|
|
339
|
-
readProtectedPaths: string[];
|
|
340
|
-
filesystemRead?: string[];
|
|
341
|
-
enforceFilesystemRead: boolean;
|
|
342
|
-
onImageInspected?: (path: string) => void;
|
|
343
|
-
};
|
|
344
|
-
|
|
345
290
|
const DEFAULT_MEMORY_SETTINGS: Required<MemorySettings> = {
|
|
346
291
|
enabled: true,
|
|
347
292
|
mode: "manual",
|
|
@@ -1357,39 +1302,12 @@ function shellGlobTargetsProtectedPath(
|
|
|
1357
1302
|
return undefined;
|
|
1358
1303
|
}
|
|
1359
1304
|
|
|
1360
|
-
function countChangedStringLeaves(before: unknown, after: unknown): number {
|
|
1361
|
-
if (typeof before === "string" && typeof after === "string") return before === after ? 0 : 1;
|
|
1362
|
-
if (Array.isArray(before) && Array.isArray(after)) {
|
|
1363
|
-
return before.reduce((total, item, index) => total + countChangedStringLeaves(item, after[index]), 0);
|
|
1364
|
-
}
|
|
1365
|
-
if (!before || !after || typeof before !== "object" || typeof after !== "object") return 0;
|
|
1366
|
-
return Object.entries(before as Record<string, unknown>).reduce(
|
|
1367
|
-
(total, [key, value]) => total + countChangedStringLeaves(value, (after as Record<string, unknown>)[key]),
|
|
1368
|
-
0
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
1305
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
1373
1306
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1374
1307
|
const prototype = Object.getPrototypeOf(value);
|
|
1375
1308
|
return prototype === Object.prototype || prototype === null;
|
|
1376
1309
|
}
|
|
1377
1310
|
|
|
1378
|
-
function redactToolResultTextContent(content: unknown): { content: unknown; redacted: number } {
|
|
1379
|
-
if (!Array.isArray(content)) return { content, redacted: 0 };
|
|
1380
|
-
let redacted = 0;
|
|
1381
|
-
const safeContent = content.map((block) => {
|
|
1382
|
-
if (!block || typeof block !== "object") return block;
|
|
1383
|
-
const typed = block as { type?: unknown; text?: unknown };
|
|
1384
|
-
if (typed.type !== "text" || typeof typed.text !== "string") return block;
|
|
1385
|
-
const safeText = redactSensitiveText(typed.text);
|
|
1386
|
-
if (!safeText.redacted) return block;
|
|
1387
|
-
redacted += 1;
|
|
1388
|
-
return { ...block, text: safeText.text };
|
|
1389
|
-
});
|
|
1390
|
-
return { content: safeContent, redacted };
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
1311
|
function evaluatePathLikeToolAccess(
|
|
1394
1312
|
cwd: string,
|
|
1395
1313
|
toolName: string,
|
|
@@ -1513,107 +1431,6 @@ function evaluatePathLikeToolAccess(
|
|
|
1513
1431
|
return { block: false };
|
|
1514
1432
|
}
|
|
1515
1433
|
|
|
1516
|
-
function grepOutputLinePath(line: string): string | undefined {
|
|
1517
|
-
const match = line.match(/^(.+?)(?::\d+:|-\d+-)/);
|
|
1518
|
-
return match?.[1];
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
function filterGrepProtectedContent(content: unknown, protectedPatterns: string[]): {
|
|
1522
|
-
changed: boolean;
|
|
1523
|
-
content?: unknown;
|
|
1524
|
-
redactedLines: number;
|
|
1525
|
-
} {
|
|
1526
|
-
if (!Array.isArray(content)) return { changed: false, redactedLines: 0 };
|
|
1527
|
-
|
|
1528
|
-
let changed = false;
|
|
1529
|
-
let redactedLines = 0;
|
|
1530
|
-
const filtered = content.map((block) => {
|
|
1531
|
-
if (!block || typeof block !== "object") return block;
|
|
1532
|
-
const text = (block as { type?: unknown; text?: unknown }).text;
|
|
1533
|
-
if ((block as { type?: unknown }).type !== "text" || typeof text !== "string") return block;
|
|
1534
|
-
|
|
1535
|
-
const kept: string[] = [];
|
|
1536
|
-
let blockRedactedLines = 0;
|
|
1537
|
-
for (const line of text.split(/\r?\n/)) {
|
|
1538
|
-
const linePath = grepOutputLinePath(line);
|
|
1539
|
-
if (linePath && matchesProtectedPath(linePath, protectedPatterns)) {
|
|
1540
|
-
changed = true;
|
|
1541
|
-
redactedLines += 1;
|
|
1542
|
-
blockRedactedLines += 1;
|
|
1543
|
-
continue;
|
|
1544
|
-
}
|
|
1545
|
-
kept.push(line);
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
if (blockRedactedLines === 0) return block;
|
|
1549
|
-
const notice = `[Piagent Pi guard redacted ${blockRedactedLines} protected grep line${blockRedactedLines === 1 ? "" : "s"}.]`;
|
|
1550
|
-
const nextText = kept.join("\n").trim().length > 0
|
|
1551
|
-
? `${kept.join("\n")}\n${notice}`
|
|
1552
|
-
: `No matches found in non-protected paths.\n${notice}`;
|
|
1553
|
-
return { ...block, text: nextText };
|
|
1554
|
-
});
|
|
1555
|
-
|
|
1556
|
-
return { changed, content: filtered, redactedLines };
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
function resultLineProtectedPathCandidates(cwd: string, basePath: string, line: string): string[] {
|
|
1560
|
-
const trimmed = line.trim();
|
|
1561
|
-
if (!trimmed || trimmed.startsWith("[") || trimmed.startsWith("No ") || trimmed === "(empty directory)") return [];
|
|
1562
|
-
const entry = trimmed.replace(/[\\/]+$/, "");
|
|
1563
|
-
const candidates = new Set<string>();
|
|
1564
|
-
const add = (value: string | undefined) => {
|
|
1565
|
-
if (value !== undefined) candidates.add(value);
|
|
1566
|
-
};
|
|
1567
|
-
|
|
1568
|
-
add(normalizeRelative(cwd, entry));
|
|
1569
|
-
add(normalizeRelative(cwd, path.posix.join(basePath || ".", entry)));
|
|
1570
|
-
return [...candidates];
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
function filterProtectedPathListContent(
|
|
1574
|
-
cwd: string,
|
|
1575
|
-
content: unknown,
|
|
1576
|
-
protectedPatterns: string[],
|
|
1577
|
-
basePath: string,
|
|
1578
|
-
toolName: string
|
|
1579
|
-
): {
|
|
1580
|
-
changed: boolean;
|
|
1581
|
-
content?: unknown;
|
|
1582
|
-
redactedLines: number;
|
|
1583
|
-
} {
|
|
1584
|
-
if (!Array.isArray(content)) return { changed: false, redactedLines: 0 };
|
|
1585
|
-
|
|
1586
|
-
let changed = false;
|
|
1587
|
-
let redactedLines = 0;
|
|
1588
|
-
const filtered = content.map((block) => {
|
|
1589
|
-
if (!block || typeof block !== "object") return block;
|
|
1590
|
-
const text = (block as { type?: unknown; text?: unknown }).text;
|
|
1591
|
-
if ((block as { type?: unknown }).type !== "text" || typeof text !== "string") return block;
|
|
1592
|
-
|
|
1593
|
-
const kept: string[] = [];
|
|
1594
|
-
let blockRedactedLines = 0;
|
|
1595
|
-
for (const line of text.split(/\r?\n/)) {
|
|
1596
|
-
const candidates = resultLineProtectedPathCandidates(cwd, basePath, line);
|
|
1597
|
-
if (candidates.some((candidate) => matchesProtectedPath(candidate, protectedPatterns))) {
|
|
1598
|
-
changed = true;
|
|
1599
|
-
redactedLines += 1;
|
|
1600
|
-
blockRedactedLines += 1;
|
|
1601
|
-
continue;
|
|
1602
|
-
}
|
|
1603
|
-
kept.push(line);
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
if (blockRedactedLines === 0) return block;
|
|
1607
|
-
const notice = `[Piagent Pi guard redacted ${blockRedactedLines} protected ${toolName} line${blockRedactedLines === 1 ? "" : "s"}.]`;
|
|
1608
|
-
const nextText = kept.join("\n").trim().length > 0
|
|
1609
|
-
? `${kept.join("\n")}\n${notice}`
|
|
1610
|
-
: `No entries found in non-protected paths.\n${notice}`;
|
|
1611
|
-
return { ...block, text: nextText };
|
|
1612
|
-
});
|
|
1613
|
-
|
|
1614
|
-
return { changed, content: filtered, redactedLines };
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
1434
|
function stateRoot(cwd: string): string {
|
|
1618
1435
|
return path.join(cwd, ".pi", "piagent-state");
|
|
1619
1436
|
}
|
|
@@ -1634,14 +1451,6 @@ function observedBashLedgerPath(cwd: string): string {
|
|
|
1634
1451
|
return path.join(stateRoot(cwd), "observed-bash.jsonl");
|
|
1635
1452
|
}
|
|
1636
1453
|
|
|
1637
|
-
function toolResultCaptureRoot(cwd: string): string {
|
|
1638
|
-
return path.join(stateRoot(cwd), "tool-results");
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
function toolResultCaptureIndexPath(cwd: string): string {
|
|
1642
|
-
return path.join(toolResultCaptureRoot(cwd), "index.jsonl");
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
1454
|
function nowIso(): string {
|
|
1646
1455
|
return new Date().toISOString();
|
|
1647
1456
|
}
|
|
@@ -2250,8 +2059,6 @@ function taskMutationTargets(cwd: string, toolName: string, input: Record<string
|
|
|
2250
2059
|
)));
|
|
2251
2060
|
}
|
|
2252
2061
|
|
|
2253
|
-
type ObservedTaskContext = { path: string; reason: string };
|
|
2254
|
-
|
|
2255
2062
|
function observedTaskContextFromToolResult(
|
|
2256
2063
|
cwd: string,
|
|
2257
2064
|
event: { toolName: string; input?: unknown; isError?: boolean },
|
|
@@ -4005,913 +3812,35 @@ function evaluateTaskGate(
|
|
|
4005
3812
|
))) {
|
|
4006
3813
|
warnings.push("Verification evidence from a different working-tree snapshot is stale and is ignored.");
|
|
4007
3814
|
}
|
|
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);
|
|
4072
|
-
}
|
|
4073
|
-
|
|
4074
|
-
function formatCount(value: number | null | undefined): string {
|
|
4075
|
-
if (value === null || value === undefined || Number.isNaN(value)) return "unknown";
|
|
4076
|
-
return Math.round(value).toLocaleString("en-US");
|
|
4077
|
-
}
|
|
4078
|
-
|
|
4079
|
-
function formatPercent(value: number | null | undefined): string {
|
|
4080
|
-
if (value === null || value === undefined || Number.isNaN(value)) return "unknown";
|
|
4081
|
-
return `${value.toFixed(1)}%`;
|
|
4082
|
-
}
|
|
4083
|
-
|
|
4084
|
-
function modelLabel(ctx: ExtensionContext): string {
|
|
4085
|
-
const model = ctx.model as { provider?: string; id?: string; name?: string } | undefined;
|
|
4086
|
-
if (!model) return "none";
|
|
4087
|
-
if (model.provider && model.id) return `${model.provider}/${model.id}`;
|
|
4088
|
-
return model.name ?? model.id ?? "unknown";
|
|
4089
|
-
}
|
|
4090
|
-
|
|
4091
|
-
function currentSessionName(ctx: ExtensionContext): string {
|
|
4092
|
-
try {
|
|
4093
|
-
return String(ctx.sessionManager.getSessionName() ?? "").trim();
|
|
4094
|
-
} catch {
|
|
4095
|
-
return "";
|
|
4096
|
-
}
|
|
4097
|
-
}
|
|
4098
|
-
|
|
4099
|
-
function hasOperatorSessionName(name: string): boolean {
|
|
4100
|
-
const normalized = name.trim().toLowerCase();
|
|
4101
|
-
return Boolean(normalized && normalized !== "session");
|
|
4102
|
-
}
|
|
4103
|
-
|
|
4104
|
-
function cleanSessionNameInput(input: string): string {
|
|
4105
|
-
let name = input.trim();
|
|
4106
|
-
if ((name.startsWith("\"") && name.endsWith("\"")) || (name.startsWith("'") && name.endsWith("'"))) {
|
|
4107
|
-
name = name.slice(1, -1).trim();
|
|
4108
|
-
}
|
|
4109
|
-
return name;
|
|
4110
|
-
}
|
|
4111
|
-
|
|
4112
|
-
type ToolResultCaptureSummary = {
|
|
4113
|
-
path?: string;
|
|
4114
|
-
error?: string;
|
|
4115
|
-
source: string;
|
|
4116
|
-
toolName: string;
|
|
4117
|
-
originalChars: number;
|
|
4118
|
-
originalLines: number;
|
|
4119
|
-
previewChars?: number;
|
|
4120
|
-
storedChars?: number;
|
|
4121
|
-
storedTruncated?: boolean;
|
|
4122
|
-
sha256: string;
|
|
4123
|
-
};
|
|
4124
|
-
|
|
4125
|
-
function normalizeToolResultText(text: string): string {
|
|
4126
|
-
return text.replace(/\r\n/g, "\n");
|
|
4127
|
-
}
|
|
4128
|
-
|
|
4129
|
-
function toolResultLineCount(text: string): number {
|
|
4130
|
-
if (!text) return 0;
|
|
4131
|
-
return normalizeToolResultText(text).split("\n").length;
|
|
4132
|
-
}
|
|
4133
|
-
|
|
4134
|
-
function shouldCompactToolResultText(text: string): boolean {
|
|
4135
|
-
return text.length > TOOL_RESULT_COMPACT_CHAR_THRESHOLD
|
|
4136
|
-
|| toolResultLineCount(text) > TOOL_RESULT_COMPACT_LINE_THRESHOLD;
|
|
4137
|
-
}
|
|
4138
|
-
|
|
4139
|
-
function clipPreviewLine(line: string, maxChars = 260): string {
|
|
4140
|
-
if (line.length <= maxChars) return line;
|
|
4141
|
-
return `${line.slice(0, maxChars).trimEnd()} ... [line clipped]`;
|
|
4142
|
-
}
|
|
4143
|
-
|
|
4144
|
-
function boundedPreview(text: string): string {
|
|
4145
|
-
if (text.length <= TOOL_RESULT_PREVIEW_MAX_CHARS) return text;
|
|
4146
|
-
const marker = "\n[Piagent preview shortened further to stay light.]\n";
|
|
4147
|
-
const edge = Math.floor((TOOL_RESULT_PREVIEW_MAX_CHARS - marker.length) / 2);
|
|
4148
|
-
return `${text.slice(0, edge).trimEnd()}${marker}${text.slice(-edge).trimStart()}`;
|
|
4149
|
-
}
|
|
4150
|
-
|
|
4151
|
-
function interestingToolResultLines(lines: string[]): string[] {
|
|
4152
|
-
const interesting = /\b(?:error|errors|err!|failed?|failure|warning|warn|exception|traceback|assert(?:ion)?|panic|fatal|timeout|timed out|denied|not found|cannot|eacces|enoent|ts\d{3,5}|err_[a-z0-9_]+)\b/i;
|
|
4153
|
-
const seen = new Set<string>();
|
|
4154
|
-
const result: string[] = [];
|
|
4155
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
4156
|
-
const line = lines[index];
|
|
4157
|
-
if (!interesting.test(line)) continue;
|
|
4158
|
-
const rendered = `${index + 1}: ${clipPreviewLine(line)}`;
|
|
4159
|
-
if (seen.has(rendered)) continue;
|
|
4160
|
-
seen.add(rendered);
|
|
4161
|
-
result.push(rendered);
|
|
4162
|
-
if (result.length >= TOOL_RESULT_PREVIEW_INTERESTING_LINES) break;
|
|
4163
|
-
}
|
|
4164
|
-
return result;
|
|
4165
|
-
}
|
|
4166
|
-
|
|
4167
|
-
function storedToolResultText(text: string): { text: string; truncated: boolean } {
|
|
4168
|
-
if (text.length <= TOOL_RESULT_CAPTURE_MAX_CHARS) return { text, truncated: false };
|
|
4169
|
-
const marker = `\n\n[Piagent capture truncated: omitted ${formatCount(text.length - TOOL_RESULT_CAPTURE_MAX_CHARS)} chars from the middle.]\n\n`;
|
|
4170
|
-
const edge = Math.floor((TOOL_RESULT_CAPTURE_MAX_CHARS - marker.length) / 2);
|
|
4171
|
-
return {
|
|
4172
|
-
text: `${text.slice(0, edge).trimEnd()}${marker}${text.slice(-edge).trimStart()}`,
|
|
4173
|
-
truncated: true
|
|
4174
|
-
};
|
|
4175
|
-
}
|
|
4176
|
-
|
|
4177
|
-
function compactToolResultPreview(toolName: string, source: string, text: string, capture: ToolResultCaptureSummary): string {
|
|
4178
|
-
const normalized = normalizeToolResultText(text);
|
|
4179
|
-
const lines = normalized.split("\n");
|
|
4180
|
-
const head = lines.slice(0, TOOL_RESULT_PREVIEW_HEAD_LINES).map((line) => clipPreviewLine(line));
|
|
4181
|
-
const tailStart = Math.max(TOOL_RESULT_PREVIEW_HEAD_LINES, lines.length - TOOL_RESULT_PREVIEW_TAIL_LINES);
|
|
4182
|
-
const tail = lines.slice(tailStart).map((line) => clipPreviewLine(line));
|
|
4183
|
-
const omitted = Math.max(0, lines.length - head.length - tail.length);
|
|
4184
|
-
const interesting = interestingToolResultLines(lines);
|
|
4185
|
-
const captureLine = capture.path
|
|
4186
|
-
? `capture: ${capture.path}${capture.storedTruncated ? " (stored head/tail sample; original too large)" : ""}`
|
|
4187
|
-
: `capture: unavailable (${capture.error ?? "write failed"})`;
|
|
4188
|
-
const sections = [
|
|
4189
|
-
`[Piagent compacted large ${toolName} output from ${source}: ${formatCount(capture.originalChars)} chars / ${formatCount(capture.originalLines)} lines.]`,
|
|
4190
|
-
`[${captureLine}]`,
|
|
4191
|
-
"[Preview keeps head, notable lines, and tail.]",
|
|
4192
|
-
"",
|
|
4193
|
-
"head:",
|
|
4194
|
-
...head
|
|
4195
|
-
];
|
|
4196
|
-
if (interesting.length > 0) {
|
|
4197
|
-
sections.push("", "notable:", ...interesting);
|
|
4198
|
-
}
|
|
4199
|
-
if (omitted > 0) {
|
|
4200
|
-
sections.push("", `... ${formatCount(omitted)} middle line${omitted === 1 ? "" : "s"} omitted ...`);
|
|
4201
|
-
}
|
|
4202
|
-
sections.push("", "tail:", ...tail);
|
|
4203
|
-
return boundedPreview(sections.join("\n"));
|
|
4204
|
-
}
|
|
4205
|
-
|
|
4206
|
-
function maybeNumber(value: unknown): number | undefined {
|
|
4207
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
4208
|
-
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number.parseInt(value, 10);
|
|
4209
|
-
return undefined;
|
|
4210
|
-
}
|
|
4211
|
-
|
|
4212
|
-
function currentSessionField(ctx: ExtensionContext, field: "getSessionFile" | "getSessionId" | "getSessionName"): string | undefined {
|
|
4213
|
-
try {
|
|
4214
|
-
const value = ctx.sessionManager[field]?.();
|
|
4215
|
-
return value === undefined || value === null ? undefined : redactText(String(value));
|
|
4216
|
-
} catch {
|
|
4217
|
-
return undefined;
|
|
4218
|
-
}
|
|
4219
|
-
}
|
|
4220
|
-
|
|
4221
|
-
function writeToolResultCapture(
|
|
4222
|
-
cwd: string,
|
|
4223
|
-
event: any,
|
|
4224
|
-
ctx: ExtensionContext,
|
|
4225
|
-
source: string,
|
|
4226
|
-
text: string,
|
|
4227
|
-
cache: Map<string, ToolResultCaptureSummary>
|
|
4228
|
-
): ToolResultCaptureSummary {
|
|
4229
|
-
const normalized = normalizeToolResultText(text);
|
|
4230
|
-
const sha256 = crypto.createHash("sha256").update(normalized).digest("hex");
|
|
4231
|
-
const originalLines = toolResultLineCount(normalized);
|
|
4232
|
-
const cached = cache.get(sha256);
|
|
4233
|
-
if (cached?.path) return { ...cached, source };
|
|
4234
|
-
|
|
4235
|
-
const toolName = String(event?.toolName ?? "tool");
|
|
4236
|
-
const recordedAt = nowIso();
|
|
4237
|
-
const date = recordedAt.slice(0, 10);
|
|
4238
|
-
const filename = `${recordedAt.replace(/[:.]/g, "-")}-${slugify(toolName)}-${sha256.slice(0, 12)}.log`;
|
|
4239
|
-
const relativePath = [".pi", "piagent-state", "tool-results", date, filename].join("/");
|
|
4240
|
-
const absolutePath = path.join(cwd, ".pi", "piagent-state", "tool-results", date, filename);
|
|
4241
|
-
const capture = storedToolResultText(normalized);
|
|
4242
|
-
const details = event?.details && typeof event.details === "object" ? event.details : {};
|
|
4243
|
-
const exitCode = maybeNumber(details.exitCode ?? details.status ?? event?.exitCode);
|
|
4244
|
-
const metadata = {
|
|
4245
|
-
schemaVersion: 1,
|
|
4246
|
-
recordedAt,
|
|
4247
|
-
cwd: redactText(cwd),
|
|
4248
|
-
sessionId: currentSessionField(ctx, "getSessionId"),
|
|
4249
|
-
sessionName: currentSessionField(ctx, "getSessionName"),
|
|
4250
|
-
sessionFile: currentSessionField(ctx, "getSessionFile"),
|
|
4251
|
-
toolName,
|
|
4252
|
-
source,
|
|
4253
|
-
isError: event?.isError === true,
|
|
4254
|
-
exitCode,
|
|
4255
|
-
input: redactForStorage(event?.input),
|
|
4256
|
-
path: relativePath,
|
|
4257
|
-
originalChars: normalized.length,
|
|
4258
|
-
originalLines,
|
|
4259
|
-
storedChars: capture.text.length,
|
|
4260
|
-
storedTruncated: capture.truncated,
|
|
4261
|
-
sha256
|
|
4262
|
-
};
|
|
4263
|
-
|
|
4264
|
-
try {
|
|
4265
|
-
ensurePrivateStateDirectory(cwd, path.dirname(absolutePath), "Tool-result capture directory");
|
|
4266
|
-
const capturePath = resolveLocalStatePath(cwd, absolutePath, { label: "Tool-result capture" });
|
|
4267
|
-
fs.writeFileSync(capturePath, [
|
|
4268
|
-
"# Piagent compacted tool result capture",
|
|
4269
|
-
JSON.stringify(metadata),
|
|
4270
|
-
"---",
|
|
4271
|
-
capture.text
|
|
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
|
-
}
|
|
4286
|
-
const summary = {
|
|
4287
|
-
path: relativePath,
|
|
4288
|
-
source,
|
|
4289
|
-
toolName,
|
|
4290
|
-
originalChars: normalized.length,
|
|
4291
|
-
originalLines,
|
|
4292
|
-
storedChars: capture.text.length,
|
|
4293
|
-
storedTruncated: capture.truncated,
|
|
4294
|
-
sha256
|
|
4295
|
-
};
|
|
4296
|
-
cache.set(sha256, summary);
|
|
4297
|
-
return summary;
|
|
4298
|
-
} catch (error) {
|
|
4299
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4300
|
-
const summary = {
|
|
4301
|
-
error: message,
|
|
4302
|
-
source,
|
|
4303
|
-
toolName,
|
|
4304
|
-
originalChars: normalized.length,
|
|
4305
|
-
originalLines,
|
|
4306
|
-
sha256
|
|
4307
|
-
};
|
|
4308
|
-
cache.set(sha256, summary);
|
|
4309
|
-
return summary;
|
|
4310
|
-
}
|
|
4311
|
-
}
|
|
4312
|
-
|
|
4313
|
-
function compactToolResultTextContent(
|
|
4314
|
-
cwd: string,
|
|
4315
|
-
event: any,
|
|
4316
|
-
ctx: ExtensionContext,
|
|
4317
|
-
content: unknown,
|
|
4318
|
-
cache: Map<string, ToolResultCaptureSummary>
|
|
4319
|
-
): { content: unknown; captures: ToolResultCaptureSummary[] } {
|
|
4320
|
-
if (!Array.isArray(content)) return { content, captures: [] };
|
|
4321
|
-
const captures: ToolResultCaptureSummary[] = [];
|
|
4322
|
-
const compacted = content.map((block, index) => {
|
|
4323
|
-
if (!block || typeof block !== "object") return block;
|
|
4324
|
-
const typed = block as { type?: unknown; text?: unknown };
|
|
4325
|
-
if (typed.type !== "text" || typeof typed.text !== "string" || !shouldCompactToolResultText(typed.text)) return block;
|
|
4326
|
-
const source = `content[${index}].text`;
|
|
4327
|
-
const capture = writeToolResultCapture(cwd, event, ctx, source, typed.text, cache);
|
|
4328
|
-
captures.push(capture);
|
|
4329
|
-
return {
|
|
4330
|
-
...block,
|
|
4331
|
-
text: compactToolResultPreview(String(event?.toolName ?? "tool"), source, typed.text, capture)
|
|
4332
|
-
};
|
|
4333
|
-
});
|
|
4334
|
-
return { content: compacted, captures };
|
|
4335
|
-
}
|
|
4336
|
-
|
|
4337
|
-
function compactToolResultDetails(
|
|
4338
|
-
cwd: string,
|
|
4339
|
-
event: any,
|
|
4340
|
-
ctx: ExtensionContext,
|
|
4341
|
-
value: unknown,
|
|
4342
|
-
cache: Map<string, ToolResultCaptureSummary>,
|
|
4343
|
-
captures: ToolResultCaptureSummary[],
|
|
4344
|
-
source = "details",
|
|
4345
|
-
depth = 0
|
|
4346
|
-
): unknown {
|
|
4347
|
-
if (typeof value === "string") {
|
|
4348
|
-
if (!shouldCompactToolResultText(value)) return value;
|
|
4349
|
-
const capture = writeToolResultCapture(cwd, event, ctx, source, value, cache);
|
|
4350
|
-
captures.push(capture);
|
|
4351
|
-
return compactToolResultPreview(String(event?.toolName ?? "tool"), source, value, capture);
|
|
4352
|
-
}
|
|
4353
|
-
if (Array.isArray(value)) {
|
|
4354
|
-
if (depth >= 6) return value;
|
|
4355
|
-
return value.map((item, index) => compactToolResultDetails(cwd, event, ctx, item, cache, captures, `${source}[${index}]`, depth + 1));
|
|
4356
|
-
}
|
|
4357
|
-
if (isPlainRecord(value)) {
|
|
4358
|
-
if (depth >= 6) return value;
|
|
4359
|
-
return Object.fromEntries(
|
|
4360
|
-
Object.entries(value).map(([key, item]) => [
|
|
4361
|
-
key,
|
|
4362
|
-
compactToolResultDetails(cwd, event, ctx, item, cache, captures, `${source}.${key}`, depth + 1)
|
|
4363
|
-
])
|
|
4364
|
-
);
|
|
4365
|
-
}
|
|
4366
|
-
return value;
|
|
4367
|
-
}
|
|
4368
|
-
|
|
4369
|
-
function attachToolResultCompactionDetails(details: unknown, captures: ToolResultCaptureSummary[]): unknown {
|
|
4370
|
-
if (captures.length === 0) return details;
|
|
4371
|
-
const compacted = captures.map((capture) => ({
|
|
4372
|
-
path: capture.path,
|
|
4373
|
-
error: capture.error,
|
|
4374
|
-
source: capture.source,
|
|
4375
|
-
toolName: capture.toolName,
|
|
4376
|
-
originalChars: capture.originalChars,
|
|
4377
|
-
originalLines: capture.originalLines,
|
|
4378
|
-
storedChars: capture.storedChars,
|
|
4379
|
-
storedTruncated: capture.storedTruncated,
|
|
4380
|
-
sha256: capture.sha256
|
|
4381
|
-
}));
|
|
4382
|
-
if (isPlainRecord(details)) return { ...details, piagentCompactedToolResults: compacted };
|
|
4383
|
-
if (details === undefined) return { piagentCompactedToolResults: compacted };
|
|
4384
|
-
return { value: details, piagentCompactedToolResults: compacted };
|
|
4385
|
-
}
|
|
4386
|
-
|
|
4387
|
-
function readRecentToolResultCaptures(cwd: string, limit = 5): Record<string, unknown>[] {
|
|
4388
|
-
const indexPath = toolResultCaptureIndexPath(cwd);
|
|
4389
|
-
const captures: Record<string, unknown>[] = [];
|
|
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>);
|
|
4392
|
-
}
|
|
4393
|
-
return captures.slice(-limit);
|
|
4394
|
-
}
|
|
4395
|
-
|
|
4396
|
-
function formatToolResultCaptureStatus(cwd: string, captures: Record<string, unknown>[]): string {
|
|
4397
|
-
const root = normalizeRelative(cwd, toolResultCaptureRoot(cwd)) ?? ".pi/piagent-state/tool-results";
|
|
4398
|
-
const lines = [
|
|
4399
|
-
`logPolicy: compact above ${formatCount(TOOL_RESULT_COMPACT_CHAR_THRESHOLD)} chars or ${formatCount(TOOL_RESULT_COMPACT_LINE_THRESHOLD)} lines`,
|
|
4400
|
-
`captureRoot: ${root}`,
|
|
4401
|
-
`recent: ${captures.length || "none"}`
|
|
4402
|
-
];
|
|
4403
|
-
for (const capture of captures) {
|
|
4404
|
-
const originalLines = typeof capture.originalLines === "number" ? formatCount(capture.originalLines) : "unknown";
|
|
4405
|
-
const originalChars = typeof capture.originalChars === "number" ? formatCount(capture.originalChars) : "unknown";
|
|
4406
|
-
const exit = capture.exitCode === undefined ? "" : ` exit=${capture.exitCode}`;
|
|
4407
|
-
lines.push(`- ${capture.recordedAt ?? "unknown"} ${capture.toolName ?? "tool"}${exit}: ${originalLines} lines / ${originalChars} chars -> ${capture.path ?? "no capture path"}`);
|
|
4408
|
-
}
|
|
4409
|
-
return lines.join("\n");
|
|
4410
|
-
}
|
|
4411
|
-
|
|
4412
|
-
function buildUsageSnapshot(ctx: ExtensionContext, thinkingLevel?: string): UsageSnapshot {
|
|
4413
|
-
const contextUsage = ctx.getContextUsage();
|
|
4414
|
-
const contextWithThinking = ctx as ExtensionContext & { getThinkingLevel?: () => string };
|
|
4415
|
-
return {
|
|
4416
|
-
sessionFile: ctx.sessionManager.getSessionFile(),
|
|
4417
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
4418
|
-
sessionName: ctx.sessionManager.getSessionName(),
|
|
4419
|
-
cwd: ctx.cwd,
|
|
4420
|
-
mode: ctx.mode,
|
|
4421
|
-
model: modelLabel(ctx),
|
|
4422
|
-
thinkingLevel: thinkingLevel ?? contextWithThinking.getThinkingLevel?.() ?? "unknown",
|
|
4423
|
-
entries: {
|
|
4424
|
-
total: ctx.sessionManager.getEntries().length,
|
|
4425
|
-
branch: ctx.sessionManager.getBranch().length
|
|
4426
|
-
},
|
|
4427
|
-
contextUsage: contextUsage
|
|
4428
|
-
? {
|
|
4429
|
-
tokens: contextUsage.tokens,
|
|
4430
|
-
contextWindow: contextUsage.contextWindow,
|
|
4431
|
-
percent: contextUsage.percent
|
|
4432
|
-
}
|
|
4433
|
-
: undefined,
|
|
4434
|
-
exactTotals: {
|
|
4435
|
-
availableInCommand: false,
|
|
4436
|
-
howToRead: [
|
|
4437
|
-
"Inside Pi TUI: run /session for exact tokens and cost.",
|
|
4438
|
-
"Outside Pi: run piagent-usage /path/to/project or scripts/pi-session-stats.sh /path/to/project.",
|
|
4439
|
-
"Historical totals: run piagent-usage --history /path/to/project --days 7."
|
|
4440
|
-
]
|
|
4441
|
-
}
|
|
4442
|
-
};
|
|
4443
|
-
}
|
|
4444
|
-
|
|
4445
|
-
function formatUsageSnapshot(snapshot: UsageSnapshot): string {
|
|
4446
|
-
const context = snapshot.contextUsage
|
|
4447
|
-
? `${formatCount(snapshot.contextUsage.tokens)} / ${formatCount(snapshot.contextUsage.contextWindow)} tokens (${formatPercent(snapshot.contextUsage.percent)})`
|
|
4448
|
-
: "unavailable";
|
|
4449
|
-
return [
|
|
4450
|
-
`usage: ${context}`,
|
|
4451
|
-
`session: ${snapshot.sessionName ?? "unnamed"} (${snapshot.sessionId ?? "unknown"})`,
|
|
4452
|
-
`model: ${snapshot.model}; thinking: ${snapshot.thinkingLevel}`,
|
|
4453
|
-
`entries: ${formatCount(snapshot.entries.branch)} active / ${formatCount(snapshot.entries.total)} total`,
|
|
4454
|
-
`file: ${snapshot.sessionFile ?? "not persisted"}`,
|
|
4455
|
-
"exact: /session | piagent-usage /path/to/project",
|
|
4456
|
-
"history: piagent-usage --history /path/to/project --days 7"
|
|
4457
|
-
].join("\n");
|
|
4458
|
-
}
|
|
4459
|
-
|
|
4460
|
-
function estimateTokensFromChars(chars: number): number {
|
|
4461
|
-
return Math.max(0, Math.ceil(chars / 4));
|
|
4462
|
-
}
|
|
4463
|
-
|
|
4464
|
-
function buildContextPreflight(snapshot: UsageSnapshot, workflow = "task", inputChars = 0): ContextPreflight {
|
|
4465
|
-
const inputTokenEstimate = estimateTokensFromChars(inputChars);
|
|
4466
|
-
const live = snapshot.contextUsage;
|
|
4467
|
-
let projectedContext: ContextPreflight["projectedContext"];
|
|
4468
|
-
let recommendation: ContextPreflight["recommendation"] = "unknown";
|
|
4469
|
-
let reason = "Context usage is unavailable; use /session or /usage if the task is large.";
|
|
4470
|
-
|
|
4471
|
-
if (live && live.tokens !== null && live.percent !== null) {
|
|
4472
|
-
const projectedTokens = live.tokens + inputTokenEstimate;
|
|
4473
|
-
const projectedPercent = live.contextWindow > 0 ? (projectedTokens / live.contextWindow) * 100 : live.percent;
|
|
4474
|
-
projectedContext = {
|
|
4475
|
-
tokens: projectedTokens,
|
|
4476
|
-
percent: projectedPercent
|
|
4477
|
-
};
|
|
4478
|
-
|
|
4479
|
-
if (live.percent >= CONTEXT_FRESH_PERCENT || projectedPercent >= CONTEXT_FRESH_PERCENT || inputChars >= LONG_INPUT_CHARS) {
|
|
4480
|
-
recommendation = "fresh-session";
|
|
4481
|
-
reason = "Use a fresh governed session before this task to avoid provider context overflow and stale task state.";
|
|
4482
|
-
} else if (live.percent >= CONTEXT_COMPACT_PERCENT || projectedPercent >= CONTEXT_COMPACT_PERCENT) {
|
|
4483
|
-
recommendation = "compact";
|
|
4484
|
-
reason = "Compact before continuing; the current session is close to the high-context zone.";
|
|
4485
|
-
} else if (live.percent >= CONTEXT_WATCH_PERCENT || projectedPercent >= CONTEXT_WATCH_PERCENT) {
|
|
4486
|
-
recommendation = "watch";
|
|
4487
|
-
reason = "Proceed, but keep context targeted and avoid broad file injection.";
|
|
4488
|
-
} else {
|
|
4489
|
-
recommendation = "ok";
|
|
4490
|
-
reason = "Context is within the normal range for a bounded task.";
|
|
4491
|
-
}
|
|
4492
|
-
} else if (inputChars >= LONG_INPUT_CHARS) {
|
|
4493
|
-
recommendation = "fresh-session";
|
|
4494
|
-
reason = "The incoming request is large; start a fresh governed session and keep the full intake in a file.";
|
|
4495
|
-
}
|
|
4496
|
-
|
|
4497
|
-
return {
|
|
4498
|
-
workflow,
|
|
4499
|
-
inputChars,
|
|
4500
|
-
inputTokenEstimate,
|
|
4501
|
-
liveContext: live,
|
|
4502
|
-
projectedContext,
|
|
4503
|
-
recommendation,
|
|
4504
|
-
reason,
|
|
4505
|
-
commands: [
|
|
4506
|
-
"/task-preflight",
|
|
4507
|
-
"/task-preflight compact",
|
|
4508
|
-
`/fresh ${workflow === "be-to-fe" ? "be-to-fe" : workflow === "scout" ? "scout" : "task"} <request>`,
|
|
4509
|
-
"/usage",
|
|
4510
|
-
"/session"
|
|
4511
|
-
]
|
|
4512
|
-
};
|
|
4513
|
-
}
|
|
4514
|
-
|
|
4515
|
-
function formatContextPreflight(preflight: ContextPreflight, snapshot: UsageSnapshot): string {
|
|
4516
|
-
const live = preflight.liveContext
|
|
4517
|
-
? `${formatCount(preflight.liveContext.tokens)} / ${formatCount(preflight.liveContext.contextWindow)} (${formatPercent(preflight.liveContext.percent)})`
|
|
4518
|
-
: "unavailable";
|
|
4519
|
-
const projected = preflight.projectedContext
|
|
4520
|
-
? `${formatCount(preflight.projectedContext.tokens)} (${formatPercent(preflight.projectedContext.percent)})`
|
|
4521
|
-
: "unavailable";
|
|
4522
|
-
return [
|
|
4523
|
-
`preflight: ${preflight.recommendation}`,
|
|
4524
|
-
`workflow: ${preflight.workflow}`,
|
|
4525
|
-
`session: ${snapshot.sessionName ?? "unnamed"} (${snapshot.sessionId ?? "unknown"})`,
|
|
4526
|
-
`model: ${snapshot.model}; thinking: ${snapshot.thinkingLevel}`,
|
|
4527
|
-
`context: ${live}; projected: ${projected}`,
|
|
4528
|
-
`input: ~${formatCount(preflight.inputTokenEstimate)} tokens from ${formatCount(preflight.inputChars)} chars`,
|
|
4529
|
-
`reason: ${preflight.reason}`,
|
|
4530
|
-
`next: ${preflight.commands.join(" | ")}`
|
|
4531
|
-
].join("\n");
|
|
4532
|
-
}
|
|
4533
|
-
|
|
4534
|
-
function looksLikeGovernedBoilerplate(text: string): boolean {
|
|
4535
|
-
const lower = text.toLowerCase();
|
|
4536
|
-
const markers = [
|
|
4537
|
-
"mandatory flow",
|
|
4538
|
-
"piagent_context",
|
|
4539
|
-
"piagent_task_start",
|
|
4540
|
-
"piagent_context_record",
|
|
4541
|
-
"piagent_verify_record",
|
|
4542
|
-
"piagent_task_gate_check",
|
|
4543
|
-
"output format"
|
|
4544
|
-
];
|
|
4545
|
-
return markers.filter((marker) => lower.includes(marker)).length >= 3;
|
|
4546
|
-
}
|
|
4547
|
-
|
|
4548
|
-
function extractFencedBlockAfter(label: RegExp, text: string): string | undefined {
|
|
4549
|
-
const labelMatch = label.exec(text);
|
|
4550
|
-
if (!labelMatch || labelMatch.index === undefined) return undefined;
|
|
4551
|
-
const rest = text.slice(labelMatch.index + labelMatch[0].length);
|
|
4552
|
-
const fenced = /```(?:text|md|markdown)?\s*([\s\S]*?)```/i.exec(rest);
|
|
4553
|
-
return fenced?.[1]?.trim();
|
|
4554
|
-
}
|
|
4555
|
-
|
|
4556
|
-
function extractTaskRequest(text: string): string {
|
|
4557
|
-
const labeled =
|
|
4558
|
-
extractFencedBlockAfter(/(?:implement|scout|review|plan)\s+(?:this\s+)?task\s*:?\s*/i, text) ??
|
|
4559
|
-
extractFencedBlockAfter(/request\s*:?\s*/i, text);
|
|
4560
|
-
if (labeled) return stripLeadingWorkflowCommand(labeled);
|
|
4561
|
-
|
|
4562
|
-
const firstFence = /```(?:text|md|markdown)?\s*([\s\S]*?)```/i.exec(text);
|
|
4563
|
-
if (firstFence?.[1]?.trim()) return stripLeadingWorkflowCommand(firstFence[1].trim());
|
|
4564
|
-
|
|
4565
|
-
return stripLeadingWorkflowCommand(text.trim());
|
|
4566
|
-
}
|
|
4567
|
-
|
|
4568
|
-
function stripLeadingWorkflowCommand(input: string): string {
|
|
4569
|
-
return input
|
|
4570
|
-
.replace(/^\/(?:piagent-workflow|workflow)\s+(?:task|scout|be-to-fe|review|plan|platform-improve|discuss|commit|pr)\b\s*/i, "")
|
|
4571
|
-
.replace(/^\/(?:task|scout|be-to-fe|review|plan|platform-improve|discuss|commit|pr)\b\s*/i, "")
|
|
4572
|
-
.trim();
|
|
4573
|
-
}
|
|
4574
|
-
|
|
4575
|
-
function trimTaskForInline(input: string): string {
|
|
4576
|
-
const normalized = stripLeadingWorkflowCommand(input).trim().replace(/\n{3,}/g, "\n\n");
|
|
4577
|
-
if (normalized.length <= MAX_INLINE_COLLAPSED_TASK_CHARS) return normalized;
|
|
4578
|
-
return `${normalized.slice(0, MAX_INLINE_COLLAPSED_TASK_CHARS).trim()}\n\n[Input truncated by piagent preflight. Put the full spec in a project file and reference that file.]`;
|
|
4579
|
-
}
|
|
4580
|
-
|
|
4581
|
-
function chooseFreshWorkflow(original: string, task: string): "task" | "scout" | "be-to-fe" {
|
|
4582
|
-
const semantic = stripLeadingWorkflowCommand(task || original).toLowerCase();
|
|
4583
|
-
const starts = original.trim().toLowerCase();
|
|
4584
|
-
const workflowStart = starts.match(/^\/(?:piagent-workflow|workflow)\s+(task|scout|be-to-fe)\b/);
|
|
4585
|
-
if (workflowStart?.[1] === "be-to-fe") return "be-to-fe";
|
|
4586
|
-
if (workflowStart?.[1] === "scout") return "scout";
|
|
4587
|
-
if (workflowStart?.[1] === "task") return "task";
|
|
4588
|
-
if (starts.startsWith("/be-to-fe")) return "be-to-fe";
|
|
4589
|
-
if (starts.startsWith("/scout")) return "scout";
|
|
4590
|
-
const asksForWrite = /\b(implement|support|surface|consume|write|change|fix)\b/.test(semantic);
|
|
4591
|
-
if (/\b(scout|read-only|read only|audit|mapping|mapping matrix|map contract)\b/.test(semantic) && !asksForWrite) {
|
|
4592
|
-
return "scout";
|
|
4593
|
-
}
|
|
4594
|
-
if (/\b(be|backend)\b/.test(semantic) && /\b(fe|frontend)\b/.test(semantic) && asksForWrite) {
|
|
4595
|
-
return "be-to-fe";
|
|
4596
|
-
}
|
|
4597
|
-
return "task";
|
|
4598
|
-
}
|
|
4599
|
-
|
|
4600
|
-
function isPiagentWorkflowInput(text: string): boolean {
|
|
4601
|
-
return /^\/(?:piagent-workflow|workflow|task|be-to-fe|scout|review|plan|platform-improve)\b/i.test(text.trim());
|
|
4602
|
-
}
|
|
4603
|
-
|
|
4604
|
-
function isFreshOrUtilityInput(text: string): boolean {
|
|
4605
|
-
return /^\/(?:usage|logs|context|commands|permission|memory|onboard|name|fresh|fresh-task|fresh-scout|fresh-be-to-fe|task-preflight|piagent-usage|piagent-logs|piagent-session|piagent-context|piagent-commands|piagent-permission|model-options|memory-policy|onboard-project|setname|session|compact)\b/i.test(text.trim());
|
|
4606
|
-
}
|
|
4607
|
-
|
|
4608
|
-
function taskInboxDir(cwd: string): string {
|
|
4609
|
-
return path.join(cwd, ".pi", "task-inbox");
|
|
4610
|
-
}
|
|
4611
|
-
|
|
4612
|
-
function writeTaskInbox(cwd: string, workflow: string, text: string): string {
|
|
4613
|
-
fs.mkdirSync(taskInboxDir(cwd), { recursive: true });
|
|
4614
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
4615
|
-
const safeWorkflow = workflow.replace(/[^a-z0-9-]+/gi, "-").toLowerCase() || "task";
|
|
4616
|
-
const fileName = `${stamp}-${safeWorkflow}.md`;
|
|
4617
|
-
const absolute = path.join(taskInboxDir(cwd), fileName);
|
|
4618
|
-
fs.writeFileSync(absolute, `${redactText(text)}\n`);
|
|
4619
|
-
return path.relative(cwd, absolute).split(path.sep).join("/");
|
|
4620
|
-
}
|
|
4621
|
-
|
|
4622
|
-
function buildFreshCommand(cwd: string, workflow: "task" | "scout" | "be-to-fe", originalText: string, reason: string): string {
|
|
4623
|
-
const task = extractTaskRequest(originalText);
|
|
4624
|
-
if (originalText.length >= LONG_INPUT_CHARS) {
|
|
4625
|
-
const intakePath = writeTaskInbox(cwd, workflow, originalText);
|
|
4626
|
-
return `/fresh ${workflow} Read task intake from ${intakePath}. ${reason}`;
|
|
4627
|
-
}
|
|
4628
|
-
return `/fresh ${workflow} ${trimTaskForInline(task)}`;
|
|
4629
|
-
}
|
|
4630
|
-
|
|
4631
|
-
function shortTaskLabel(text: string): string {
|
|
4632
|
-
const compact = text
|
|
4633
|
-
.replace(/```[\s\S]*?```/g, " ")
|
|
4634
|
-
.replace(/[^A-Za-z0-9\u00C0-\u1EF9_-]+/g, " ")
|
|
4635
|
-
.trim()
|
|
4636
|
-
.slice(0, 64);
|
|
4637
|
-
return compact || "piagent task";
|
|
4638
|
-
}
|
|
4639
|
-
|
|
4640
|
-
function escapeRegExp(input: string): string {
|
|
4641
|
-
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4642
|
-
}
|
|
4643
|
-
|
|
4644
|
-
function supportedImageMimeType(bytes: Buffer): string | undefined {
|
|
4645
|
-
if (
|
|
4646
|
-
bytes.length >= 24
|
|
4647
|
-
&& bytes.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
|
|
4648
|
-
&& bytes.readUInt32BE(8) === 13
|
|
4649
|
-
&& bytes.subarray(12, 16).toString("ascii") === "IHDR"
|
|
4650
|
-
&& bytes.readUInt32BE(16) > 0
|
|
4651
|
-
&& bytes.readUInt32BE(20) > 0
|
|
4652
|
-
) {
|
|
4653
|
-
return "image/png";
|
|
4654
|
-
}
|
|
4655
|
-
if (
|
|
4656
|
-
bytes.length >= 4
|
|
4657
|
-
&& bytes[0] === 0xff
|
|
4658
|
-
&& bytes[1] === 0xd8
|
|
4659
|
-
&& bytes[2] === 0xff
|
|
4660
|
-
&& bytes[bytes.length - 2] === 0xff
|
|
4661
|
-
&& bytes[bytes.length - 1] === 0xd9
|
|
4662
|
-
) {
|
|
4663
|
-
return "image/jpeg";
|
|
4664
|
-
}
|
|
4665
|
-
if (bytes.length >= 10) {
|
|
4666
|
-
const head = bytes.subarray(0, 6).toString("ascii");
|
|
4667
|
-
if (
|
|
4668
|
-
(head === "GIF87a" || head === "GIF89a")
|
|
4669
|
-
&& bytes.readUInt16LE(6) > 0
|
|
4670
|
-
&& bytes.readUInt16LE(8) > 0
|
|
4671
|
-
) {
|
|
4672
|
-
return "image/gif";
|
|
4673
|
-
}
|
|
4674
|
-
}
|
|
4675
|
-
if (
|
|
4676
|
-
bytes.length >= 16
|
|
4677
|
-
&& bytes.subarray(0, 4).toString("ascii") === "RIFF"
|
|
4678
|
-
&& bytes.subarray(8, 12).toString("ascii") === "WEBP"
|
|
4679
|
-
&& ["VP8 ", "VP8L", "VP8X"].includes(bytes.subarray(12, 16).toString("ascii"))
|
|
4680
|
-
&& bytes.readUInt32LE(4) + 8 <= bytes.length
|
|
4681
|
-
) {
|
|
4682
|
-
return "image/webp";
|
|
4683
|
-
}
|
|
4684
|
-
if (
|
|
4685
|
-
bytes.length >= 26
|
|
4686
|
-
&& bytes[0] === 0x42
|
|
4687
|
-
&& bytes[1] === 0x4d
|
|
4688
|
-
&& bytes.readUInt32LE(2) <= bytes.length
|
|
4689
|
-
&& bytes.readUInt32LE(14) >= 12
|
|
4690
|
-
) {
|
|
4691
|
-
return "image/bmp";
|
|
4692
|
-
}
|
|
4693
|
-
return undefined;
|
|
4694
|
-
}
|
|
4695
|
-
|
|
4696
|
-
function normalizeImagePathCandidate(candidate: string, cwd: string, options: { allowBareRelative?: boolean } = {}): string | undefined {
|
|
4697
|
-
let raw = candidate.trim().replace(/^['"`<]+|['"`>,.;:!?]+$/g, "");
|
|
4698
|
-
if (!raw) return undefined;
|
|
4699
|
-
const allowBareRelative = options.allowBareRelative !== false;
|
|
4700
|
-
const hasExplicitPathPrefix = raw.startsWith("file://") || raw.startsWith("~/") || path.isAbsolute(raw) || raw.startsWith("./") || raw.startsWith("../");
|
|
4701
|
-
if (!allowBareRelative && !hasExplicitPathPrefix) return undefined;
|
|
4702
|
-
try {
|
|
4703
|
-
if (raw.startsWith("file://")) raw = fileURLToPath(raw);
|
|
4704
|
-
} catch {
|
|
4705
|
-
return undefined;
|
|
4706
|
-
}
|
|
4707
|
-
if (raw.startsWith("~/")) {
|
|
4708
|
-
const home = process.env.HOME;
|
|
4709
|
-
if (!home) return undefined;
|
|
4710
|
-
raw = path.join(home, raw.slice(2));
|
|
4711
|
-
}
|
|
4712
|
-
const ext = path.extname(raw).toLowerCase().replace(/^\./, "");
|
|
4713
|
-
if (!IMAGE_EXTENSIONS.includes(ext as typeof IMAGE_EXTENSIONS[number])) return undefined;
|
|
4714
|
-
const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
4715
|
-
return absolute;
|
|
4716
|
-
}
|
|
4717
|
-
|
|
4718
|
-
function extractLocalImagePathCandidates(text: string, cwd: string): string[] {
|
|
4719
|
-
const candidates = new Set<string>();
|
|
4720
|
-
const imageExt = "(?:png|jpe?g|gif|webp|bmp)";
|
|
4721
|
-
const wholeTextPath = normalizeImagePathCandidate(text, cwd, { allowBareRelative: false });
|
|
4722
|
-
if (wholeTextPath) candidates.add(wholeTextPath);
|
|
4723
|
-
|
|
4724
|
-
const quoted = new RegExp(`(?:path=)?["']([^"']+\\.${imageExt})["']`, "gi");
|
|
4725
|
-
for (const match of text.matchAll(quoted)) {
|
|
4726
|
-
const normalized = normalizeImagePathCandidate(match[1], cwd);
|
|
4727
|
-
if (normalized) candidates.add(normalized);
|
|
4728
|
-
}
|
|
4729
|
-
|
|
4730
|
-
const fileUrl = new RegExp(`file://[^\\s"'<>]+\\.${imageExt}`, "gi");
|
|
4731
|
-
for (const match of text.matchAll(fileUrl)) {
|
|
4732
|
-
const normalized = normalizeImagePathCandidate(match[0], cwd);
|
|
4733
|
-
if (normalized) candidates.add(normalized);
|
|
4734
|
-
}
|
|
4735
|
-
|
|
4736
|
-
const linePathPattern = '\\s((?:/|~\\/|\\.\\.?/)[^\\n\\r"\\\'<>]*?\\.' + imageExt + ')(?=$|\\s|["\\\'`)>])';
|
|
4737
|
-
const linePath = new RegExp(linePathPattern, "gi");
|
|
4738
|
-
for (const match of text.matchAll(linePath)) {
|
|
4739
|
-
const normalized = normalizeImagePathCandidate(match[1], cwd);
|
|
4740
|
-
if (normalized) candidates.add(normalized);
|
|
4741
|
-
}
|
|
4742
|
-
|
|
4743
|
-
return [...candidates];
|
|
4744
|
-
}
|
|
4745
|
-
|
|
4746
|
-
type ResolvedChatImage =
|
|
4747
|
-
| { status: "ok"; absolutePath: string; bytes: Buffer; mimeType: string }
|
|
4748
|
-
| { status: "error"; reason: string };
|
|
4749
|
-
|
|
4750
|
-
function pathContainedBy(root: string, candidate: string): boolean {
|
|
4751
|
-
if (candidate === root) return true;
|
|
4752
|
-
const relative = path.relative(root, candidate);
|
|
4753
|
-
return relative.length > 0
|
|
4754
|
-
&& relative !== ".."
|
|
4755
|
-
&& !relative.startsWith(`..${path.sep}`)
|
|
4756
|
-
&& !path.isAbsolute(relative);
|
|
4757
|
-
}
|
|
4758
|
-
|
|
4759
|
-
export function readChatImage(
|
|
4760
|
-
requestedPath: string,
|
|
4761
|
-
cwd: string,
|
|
4762
|
-
access: ChatImageAccessPolicy
|
|
4763
|
-
): ResolvedChatImage {
|
|
4764
|
-
const lexicalRequestedPath = path.isAbsolute(requestedPath)
|
|
4765
|
-
? path.normalize(requestedPath)
|
|
4766
|
-
: path.resolve(cwd, requestedPath);
|
|
4767
|
-
const lexicalProjectRoot = path.resolve(cwd);
|
|
4768
|
-
const requestedProjectLexicalRelative = pathContainedBy(lexicalProjectRoot, lexicalRequestedPath)
|
|
4769
|
-
? path.relative(lexicalProjectRoot, lexicalRequestedPath).split(path.sep).join("/") || "."
|
|
4770
|
-
: undefined;
|
|
4771
|
-
let canonicalPath: string;
|
|
4772
|
-
let inspected: fs.Stats;
|
|
4773
|
-
try {
|
|
4774
|
-
canonicalPath = fs.realpathSync.native(requestedPath);
|
|
4775
|
-
inspected = fs.statSync(canonicalPath);
|
|
4776
|
-
} catch {
|
|
4777
|
-
return { status: "error", reason: "file does not exist or cannot be inspected" };
|
|
4778
|
-
}
|
|
4779
|
-
if (!inspected.isFile()) return { status: "error", reason: "not a regular file" };
|
|
4780
|
-
if (inspected.size <= 0) return { status: "error", reason: "empty file" };
|
|
4781
|
-
if (inspected.size > MAX_CHAT_IMAGE_BYTES) {
|
|
4782
|
-
return {
|
|
4783
|
-
status: "error",
|
|
4784
|
-
reason: `image is ${formatCount(inspected.size)} bytes > ${formatCount(MAX_CHAT_IMAGE_BYTES)} byte limit`
|
|
4785
|
-
};
|
|
4786
|
-
}
|
|
4787
|
-
|
|
4788
|
-
const root = access.roots.find((candidate) => pathContainedBy(candidate.path, canonicalPath));
|
|
4789
|
-
if (!root) return { status: "error", reason: "outside the project and every granted additionalReadRoots directory" };
|
|
4790
|
-
|
|
4791
|
-
const rootRelative = path.relative(root.path, canonicalPath).split(path.sep).join("/") || ".";
|
|
4792
|
-
const projectRoot = access.roots.find((candidate) => candidate.source === "project");
|
|
4793
|
-
let canonicalRequestedLocation = requestedPath;
|
|
4794
|
-
try {
|
|
4795
|
-
canonicalRequestedLocation = path.join(
|
|
4796
|
-
fs.realpathSync.native(path.dirname(requestedPath)),
|
|
4797
|
-
path.basename(requestedPath)
|
|
4798
|
-
);
|
|
4799
|
-
} catch {
|
|
4800
|
-
// The full target already resolved. This location form only preserves the
|
|
4801
|
-
// final symlink's policy position, so failure falls back to the raw path.
|
|
4802
|
-
}
|
|
4803
|
-
const requestedProjectRelative = projectRoot && pathContainedBy(projectRoot.path, canonicalRequestedLocation)
|
|
4804
|
-
? path.relative(projectRoot.path, canonicalRequestedLocation).split(path.sep).join("/") || "."
|
|
4805
|
-
: undefined;
|
|
4806
|
-
const protectedMatch = matchesProtectedPath(canonicalPath, access.readProtectedPaths)
|
|
4807
|
-
?? matchesProtectedPath(rootRelative, access.readProtectedPaths)
|
|
4808
|
-
?? matchesProtectedPath(canonicalRequestedLocation, access.readProtectedPaths)
|
|
4809
|
-
?? matchesProtectedPath(requestedPath, access.readProtectedPaths)
|
|
4810
|
-
?? (requestedProjectLexicalRelative
|
|
4811
|
-
? matchesProtectedPath(requestedProjectLexicalRelative, access.readProtectedPaths)
|
|
4812
|
-
: undefined)
|
|
4813
|
-
?? (requestedProjectRelative ? matchesProtectedPath(requestedProjectRelative, access.readProtectedPaths) : undefined);
|
|
4814
|
-
if (protectedMatch) return { status: "error", reason: `matches protected path ${protectedMatch}` };
|
|
4815
|
-
|
|
4816
|
-
if (
|
|
4817
|
-
root.source === "project"
|
|
4818
|
-
&& access.enforceFilesystemRead
|
|
4819
|
-
&& access.filesystemRead
|
|
4820
|
-
&& !matchesAnyPath(rootRelative, access.filesystemRead)
|
|
4821
|
-
) {
|
|
4822
|
-
return {
|
|
4823
|
-
status: "error",
|
|
4824
|
-
reason: `outside the resolved filesystem read scope (${access.filesystemRead.join(", ")})`
|
|
4825
|
-
};
|
|
4826
|
-
}
|
|
4827
|
-
|
|
4828
|
-
access.onImageInspected?.(canonicalPath);
|
|
4829
|
-
const openFlags = fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | fs.constants.O_NOFOLLOW;
|
|
4830
|
-
let fd: number;
|
|
4831
|
-
try {
|
|
4832
|
-
fd = fs.openSync(canonicalPath, openFlags);
|
|
4833
|
-
} catch {
|
|
4834
|
-
return { status: "error", reason: "file changed before it could be opened" };
|
|
3815
|
+
let missingVerifyCommands: string[] = [];
|
|
3816
|
+
if (task.changeMode === "source-change" && finalGate.requirePassingVerify && plannedVerifyCommands.length > 0) {
|
|
3817
|
+
const passingCommands = new Set(task.verifyEvidence
|
|
3818
|
+
.filter((evidence) => (
|
|
3819
|
+
evidence.exitCode === 0
|
|
3820
|
+
&& evidence.observed === true
|
|
3821
|
+
&& evidence.matchedProfileCommand === true
|
|
3822
|
+
&& evidence.workingTreeDigest === currentWorkingTreeDigest
|
|
3823
|
+
))
|
|
3824
|
+
.map((evidence) => evidence.command.trim()));
|
|
3825
|
+
missingVerifyCommands = plannedVerifyCommands.filter((command) => !passingCommands.has(command.trim()));
|
|
3826
|
+
if (missingVerifyCommands.length > 0) missing.push(`observed passing verify evidence for every configured command (${missingVerifyCommands.length} missing)`);
|
|
4835
3827
|
}
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|| opened.dev !== inspected.dev
|
|
4841
|
-
|| opened.ino !== inspected.ino
|
|
4842
|
-
|| opened.size !== inspected.size
|
|
4843
|
-
) {
|
|
4844
|
-
return { status: "error", reason: "file changed between the safety checks and the read" };
|
|
4845
|
-
}
|
|
4846
|
-
const bytes = Buffer.alloc(opened.size);
|
|
4847
|
-
let offset = 0;
|
|
4848
|
-
while (offset < bytes.length) {
|
|
4849
|
-
const read = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
4850
|
-
if (read <= 0) break;
|
|
4851
|
-
offset += read;
|
|
4852
|
-
}
|
|
4853
|
-
if (offset !== bytes.length) return { status: "error", reason: "file changed while it was being read" };
|
|
4854
|
-
const mimeType = supportedImageMimeType(bytes);
|
|
4855
|
-
if (!mimeType) return { status: "error", reason: "file bytes are not a supported image" };
|
|
4856
|
-
return { status: "ok", absolutePath: canonicalPath, bytes, mimeType };
|
|
4857
|
-
} finally {
|
|
4858
|
-
fs.closeSync(fd);
|
|
3828
|
+
if (finalGate.requireTrace && task.trace.outcome !== "completed") missing.push("completed final trace");
|
|
3829
|
+
const incompleteSteps = (task.workPlan ?? []).filter((step) => step.status === "pending" || step.status === "in-progress" || step.status === "failed");
|
|
3830
|
+
if (task.trace.outcome === "completed" && incompleteSteps.length > 0) {
|
|
3831
|
+
missing.push(`completed work plan (${incompleteSteps.map((step) => `${step.id}:${step.status}`).join(", ")})`);
|
|
4859
3832
|
}
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
resolveAccess: () => ChatImageAccessPolicy
|
|
4867
|
-
): ChatImageAttachmentResult | undefined {
|
|
4868
|
-
const imagePaths = extractLocalImagePathCandidates(text, cwd);
|
|
4869
|
-
if (imagePaths.length === 0) return undefined;
|
|
4870
|
-
const access = resolveAccess();
|
|
4871
|
-
|
|
4872
|
-
const existing = Array.isArray(existingImages) ? existingImages : [];
|
|
4873
|
-
const images: ChatImageAttachmentResult["images"] = [];
|
|
4874
|
-
const attached: ChatImageAttachmentResult["attached"] = [];
|
|
4875
|
-
const skipped: ChatImageAttachmentResult["skipped"] = [];
|
|
4876
|
-
let nextText = text;
|
|
4877
|
-
|
|
4878
|
-
for (const imagePath of imagePaths) {
|
|
4879
|
-
if (images.length + existing.length >= MAX_CHAT_IMAGE_ATTACHMENTS) {
|
|
4880
|
-
skipped.push({ path: imagePath, reason: `attachment limit ${MAX_CHAT_IMAGE_ATTACHMENTS} reached` });
|
|
4881
|
-
continue;
|
|
4882
|
-
}
|
|
4883
|
-
try {
|
|
4884
|
-
const resolved = readChatImage(imagePath, cwd, access);
|
|
4885
|
-
if (resolved.status === "error") {
|
|
4886
|
-
skipped.push({ path: imagePath, reason: resolved.reason });
|
|
4887
|
-
continue;
|
|
4888
|
-
}
|
|
4889
|
-
const marker = `[image${existing.length + images.length + 1}]`;
|
|
4890
|
-
images.push({ type: "image", mimeType: resolved.mimeType, data: resolved.bytes.toString("base64") });
|
|
4891
|
-
attached.push({ marker, path: imagePath, mimeType: resolved.mimeType, bytes: resolved.bytes.length });
|
|
4892
|
-
nextText = nextText.replace(new RegExp(escapeRegExp(imagePath), "g"), marker);
|
|
4893
|
-
nextText = nextText.replace(new RegExp(escapeRegExp(`file://${imagePath}`), "g"), marker);
|
|
4894
|
-
} catch (error) {
|
|
4895
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4896
|
-
skipped.push({ path: imagePath, reason: message });
|
|
4897
|
-
}
|
|
3833
|
+
const changedFileEvidence = taskChangedFileEvidence(cwd, task, currentDigests);
|
|
3834
|
+
if (task.changeMode === "source-change" && task.trace.outcome === "completed") {
|
|
3835
|
+
if (task.changedFiles.length === 0) missing.push("changed files");
|
|
3836
|
+
if (changedFileEvidence.undeclared.length > 0) missing.push(`declared observed changes (${changedFileEvidence.undeclared.join(", ")})`);
|
|
3837
|
+
if (changedFileEvidence.unsupportedClaims.length > 0) missing.push(`supported changed-file claims (${changedFileEvidence.unsupportedClaims.join(", ")})`);
|
|
3838
|
+
if (changedFileEvidence.outsideScope.length > 0) missing.push(`changes within task scope (${changedFileEvidence.outsideScope.join(", ")})`);
|
|
4898
3839
|
}
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
return skipped.length > 0 ? { text, images: [], attached, skipped } : undefined;
|
|
3840
|
+
if (task.changeMode === "read-only" && changedFileEvidence.expected.length > 0) {
|
|
3841
|
+
missing.push(`read-only task has observed changes (${changedFileEvidence.expected.join(", ")})`);
|
|
4902
3842
|
}
|
|
4903
|
-
|
|
4904
|
-
const attachmentLines = attached.map((item) => `- ${item.marker}: ${path.basename(item.path)} (${item.mimeType}, ${formatCount(item.bytes)} bytes)`);
|
|
4905
|
-
const skippedLines = skipped.map((item) => `- skipped ${path.basename(item.path)}: ${item.reason}`);
|
|
4906
|
-
nextText = [
|
|
4907
|
-
nextText.trim(),
|
|
4908
|
-
"",
|
|
4909
|
-
"Attached local image(s):",
|
|
4910
|
-
...attachmentLines,
|
|
4911
|
-
...(skippedLines.length > 0 ? ["", "Skipped local image path(s):", ...skippedLines] : [])
|
|
4912
|
-
].join("\n").trim();
|
|
4913
|
-
|
|
4914
|
-
return { text: nextText, images, attached, skipped };
|
|
3843
|
+
return { decision: missing.length === 0 ? "pass" : "fail", missing, missingVerifyCommands, warnings, changedFileEvidence };
|
|
4915
3844
|
}
|
|
4916
3845
|
|
|
4917
3846
|
function appendTrace(cwd: string, payload: Record<string, unknown>): void {
|
|
@@ -5092,116 +4021,6 @@ function checkoutReferenceRepo(repoRef: string, forceUpdate = false): ReferenceR
|
|
|
5092
4021
|
return { host, owner, repo, cloneUrl, checkoutPath, commit, fetched };
|
|
5093
4022
|
}
|
|
5094
4023
|
|
|
5095
|
-
function toolGroupsForPrompt(prompt: string): PiagentToolGroup[] {
|
|
5096
|
-
const signal = classifyContextTask(prompt);
|
|
5097
|
-
const lower = prompt.toLowerCase();
|
|
5098
|
-
const groups = new Set<PiagentToolGroup>();
|
|
5099
|
-
|
|
5100
|
-
if (signal.workflow === "usage") {
|
|
5101
|
-
groups.add("usage");
|
|
5102
|
-
return [...groups];
|
|
5103
|
-
}
|
|
5104
|
-
if (signal.workflow === "onboard") {
|
|
5105
|
-
groups.add("governance");
|
|
5106
|
-
groups.add("policy");
|
|
5107
|
-
groups.add("retrieval");
|
|
5108
|
-
groups.add("knowledge");
|
|
5109
|
-
groups.add("onboarding");
|
|
5110
|
-
return [...groups];
|
|
5111
|
-
}
|
|
5112
|
-
|
|
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");
|
|
5117
|
-
if (
|
|
5118
|
-
/\b(context (?:engine|index|search|diagnostic)|source checkout|vendor checkout)\b/.test(lower)
|
|
5119
|
-
) {
|
|
5120
|
-
groups.add("retrieval");
|
|
5121
|
-
}
|
|
5122
|
-
if (
|
|
5123
|
-
/\b(document intake|project memory|orchestration policy|source checkout|subagent policy|vendor checkout)\b/.test(lower)
|
|
5124
|
-
) {
|
|
5125
|
-
groups.add("knowledge");
|
|
5126
|
-
}
|
|
5127
|
-
if (/\b(permission|capability|exec policy|tool policy)\b/.test(lower)) groups.add("policy");
|
|
5128
|
-
if (/\b(profile|tech stack|onboard|project context)\b/.test(lower)) groups.add("onboarding");
|
|
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");
|
|
5131
|
-
return [...groups];
|
|
5132
|
-
}
|
|
5133
|
-
|
|
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
4024
|
function sessionTaskReference(ctx: ExtensionContext): { taskId?: string; taskRunId?: string } | undefined {
|
|
5206
4025
|
const entries = ctx.sessionManager.getBranch();
|
|
5207
4026
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
@@ -5258,48 +4077,6 @@ function environmentFeatureEnabled(name: string, fallback = true): boolean {
|
|
|
5258
4077
|
return !["0", "false", "no", "off", "disabled"].includes(value);
|
|
5259
4078
|
}
|
|
5260
4079
|
|
|
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
|
-
|
|
5303
4080
|
export default function piagentGuard(pi: ExtensionAPI) {
|
|
5304
4081
|
const extensionDir = path.dirname(fileURLToPath(import.meta.url));
|
|
5305
4082
|
const policy = loadPolicy(extensionDir);
|
|
@@ -5308,90 +4085,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5308
4085
|
const contextTelemetryEnabled = environmentFeatureEnabled("PIAGENT_CONTEXT_TELEMETRY");
|
|
5309
4086
|
const autoContextEnabled = environmentFeatureEnabled("PIAGENT_AUTO_CONTEXT");
|
|
5310
4087
|
const autoRecoveryEnabled = environmentFeatureEnabled("PIAGENT_AUTO_RECOVERY");
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
const advisedTools = new Set<string>();
|
|
5315
|
-
const seenToolResults = new Map<string, { outputHash: string; recordedAt: string }>();
|
|
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
|
-
}
|
|
4088
|
+
const runtimeState = new RuntimeSessionState({
|
|
4089
|
+
maxObservedContext: contextBudgetConfig(policy).maxManifestFiles
|
|
4090
|
+
});
|
|
5395
4091
|
|
|
5396
4092
|
function retrievalKey(ctx: ExtensionContext, query: string): string {
|
|
5397
4093
|
const signal = classifyContextTask(query);
|
|
@@ -5413,11 +4109,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5413
4109
|
if (!contextTelemetryEnabled) return;
|
|
5414
4110
|
try {
|
|
5415
4111
|
const safePayload = redactForStorage(payload) as Record<string, unknown>;
|
|
5416
|
-
let taskIdentity =
|
|
4112
|
+
let taskIdentity = runtimeState.taskIdentity(ctx);
|
|
5417
4113
|
if (!taskIdentity) {
|
|
5418
4114
|
const task = activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined;
|
|
5419
4115
|
if (task) {
|
|
5420
|
-
cacheTaskIdentity(ctx, task);
|
|
4116
|
+
runtimeState.cacheTaskIdentity(ctx, task);
|
|
5421
4117
|
taskIdentity = { taskId: task.taskId, taskRunId: task.taskRunId };
|
|
5422
4118
|
}
|
|
5423
4119
|
}
|
|
@@ -5468,159 +4164,41 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5468
4164
|
return ordered;
|
|
5469
4165
|
}
|
|
5470
4166
|
|
|
5471
|
-
pi
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
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));
|
|
5517
|
-
const profileHint = explicitProfile || (projectTrusted && fs.existsSync(projectProfilePath(ctx.cwd)))
|
|
5518
|
-
? ""
|
|
5519
|
-
: " (run /onboard to select a profile)";
|
|
5520
|
-
const snapshot = buildUsageSnapshot(ctx, String(pi.getThinkingLevel()));
|
|
5521
|
-
const preflight = buildContextPreflight(snapshot, "task", 0);
|
|
5522
|
-
const capabilityState = verifyProjectCapabilityState(extensionDir, ctx.cwd, projectTrusted);
|
|
5523
|
-
const permissionProfile = resolvePermissionProfile(profile, policy, permissionOverrideFromContext(ctx));
|
|
5524
|
-
const contextHint = preflight.recommendation === "fresh-session"
|
|
5525
|
-
? " Context is high; use /fresh task or /fresh scout for new work."
|
|
5526
|
-
: preflight.recommendation === "compact"
|
|
5527
|
-
? " Context is warm; run /task-preflight before large work."
|
|
5528
|
-
: "";
|
|
5529
|
-
const permissionHint = ` permission=${permissionProfile.mode}`;
|
|
5530
|
-
ctx.ui.notify(`Piagent Pi guard loaded: ${name}${profileHint}${permissionHint}${contextHint}`, preflight.recommendation === "fresh-session" ? "warning" : "info");
|
|
5531
|
-
if (!capabilityState.ok) ctx.ui.notify(capabilityState.reason ?? "Capability validation failed.", "warning");
|
|
5532
|
-
if (capabilityState.repinned) {
|
|
5533
|
-
ctx.ui.notify(`Capability lock re-pinned: ${capabilityState.repinned}. The capabilities this project grants are unchanged.`, "info");
|
|
5534
|
-
}
|
|
5535
|
-
const legacyWarning = legacyProjectStateWarning(ctx.cwd);
|
|
5536
|
-
if (legacyWarning) ctx.ui.notify(legacyWarning, "warning");
|
|
5537
|
-
if (permissionProfile.warning) ctx.ui.notify(permissionProfile.warning, "warning");
|
|
5538
|
-
if (permissionProfile.mode === "trusted-full-access") {
|
|
5539
|
-
ctx.ui.notify("Piagent permission profile trusted-full-access is active; protected paths, secret redaction, and destructive/external confirmations remain enforced.", "warning");
|
|
5540
|
-
}
|
|
5541
|
-
// A server this repository defines and nobody approved is a decision waiting
|
|
5542
|
-
// on the operator, so it is a warning and it comes before the release news.
|
|
5543
|
-
const mcpNotice = mcpReadinessNotice(ctx.cwd);
|
|
5544
|
-
if (mcpNotice) ctx.ui.notify(mcpNotice, "warning");
|
|
5545
|
-
// Last, so a release announcement never pushes a security warning out of
|
|
5546
|
-
// the first thing the operator reads.
|
|
5547
|
-
const updateNotice = updateAvailabilityNotice();
|
|
5548
|
-
if (updateNotice) ctx.ui.notify(updateNotice, "info");
|
|
5549
|
-
let engineStatus: unknown;
|
|
5550
|
-
try {
|
|
5551
|
-
engineStatus = await contextIndexV2Status(ctx.cwd, {
|
|
5552
|
-
excludePatterns: contextIndexExcludePatterns(policy, profile)
|
|
5553
|
-
});
|
|
5554
|
-
} catch (error) {
|
|
5555
|
-
engineStatus = { exists: false, error: error instanceof Error ? error.message : String(error) };
|
|
5556
|
-
}
|
|
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
|
-
});
|
|
4167
|
+
registerSessionStartHook(pi, {
|
|
4168
|
+
state: runtimeState,
|
|
4169
|
+
loadProfile: loadProfileFromContext,
|
|
4170
|
+
projectProfileExists: (cwd) => fs.existsSync(projectProfilePath(cwd)),
|
|
4171
|
+
activateToolGroups,
|
|
4172
|
+
taskReference: sessionTaskReference,
|
|
4173
|
+
activeTask: (cwd, sessionId) => activeSessionTask(cwd, sessionId) as TaskContract | undefined,
|
|
4174
|
+
resolveTask: (cwd, reference, sessionId) => resolveTaskContract(cwd, reference, sessionId) as TaskContract | undefined,
|
|
4175
|
+
bindTask: bindSessionTask,
|
|
4176
|
+
writeTask,
|
|
4177
|
+
capabilityState: (ctx) => verifyProjectCapabilityState(extensionDir, ctx.cwd, ctx.isProjectTrusted()),
|
|
4178
|
+
permissionProfile: (ctx, profile) => resolvePermissionProfile(profile, policy, permissionOverrideFromContext(ctx)),
|
|
4179
|
+
legacyProjectWarning: legacyProjectStateWarning,
|
|
4180
|
+
mcpReadinessNotice,
|
|
4181
|
+
updateAvailabilityNotice,
|
|
4182
|
+
contextExcludePatterns: (profile) => contextIndexExcludePatterns(policy, profile),
|
|
4183
|
+
telemetry
|
|
5566
4184
|
});
|
|
5567
4185
|
|
|
5568
|
-
pi
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
event: "task_session_renamed",
|
|
5578
|
-
taskId: written.taskId,
|
|
5579
|
-
taskRunId: written.taskRunId,
|
|
5580
|
-
sessionId,
|
|
5581
|
-
sessionName
|
|
5582
|
-
});
|
|
4186
|
+
registerSessionHooks(pi, {
|
|
4187
|
+
state: runtimeState,
|
|
4188
|
+
maxManifestFiles: contextBudgetConfig(policy).maxManifestFiles,
|
|
4189
|
+
telemetry,
|
|
4190
|
+
activeTask: (cwd, sessionId) => activeSessionTask(cwd, sessionId) as TaskContract | undefined,
|
|
4191
|
+
writeTask,
|
|
4192
|
+
bindTask: bindSessionTask,
|
|
4193
|
+
appendTrace,
|
|
4194
|
+
flushObservedTaskContext
|
|
5583
4195
|
});
|
|
5584
4196
|
|
|
5585
|
-
pi
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
}
|
|
5591
|
-
const taskSignal = classifyContextTask(text);
|
|
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);
|
|
5608
|
-
telemetry(ctx, {
|
|
5609
|
-
event: "user_input",
|
|
5610
|
-
source: event.source,
|
|
5611
|
-
promptHash: taskSignal.promptHash,
|
|
5612
|
-
promptChars: taskSignal.promptChars,
|
|
5613
|
-
workflow: taskSignal.workflow,
|
|
5614
|
-
riskLane: taskSignal.lane,
|
|
5615
|
-
intakeMode: runtimeIntake ? "runtime" : "model",
|
|
5616
|
-
protectedTarget,
|
|
5617
|
-
protectedOnlyTarget,
|
|
5618
|
-
manualIntake,
|
|
5619
|
-
explicitPathCount: taskSignal.paths.length,
|
|
5620
|
-
termCount: taskSignal.terms.length
|
|
5621
|
-
});
|
|
5622
|
-
|
|
5623
|
-
const imageAttachment = attachLocalImagesFromText(text, event.images, ctx.cwd, () => {
|
|
4197
|
+
registerInputHook(pi, {
|
|
4198
|
+
boilerplateCollapseChars: BOILERPLATE_COLLAPSE_CHARS,
|
|
4199
|
+
activeTask: (ctx) => activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined,
|
|
4200
|
+
readProtectedPaths: (ctx) => effectiveProtectedPaths(policy, loadProfileFromContext(ctx)).readProtectedPaths,
|
|
4201
|
+
imageAccess: (ctx) => {
|
|
5624
4202
|
const projectTrusted = ctx.isProjectTrusted();
|
|
5625
4203
|
const profile = loadProfileFromContext(ctx);
|
|
5626
4204
|
const permissionProfile = resolvePermissionProfile(profile, policy, permissionOverrideFromContext(ctx));
|
|
@@ -5635,557 +4213,57 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5635
4213
|
filesystemRead: capabilityState.filesystemRead,
|
|
5636
4214
|
enforceFilesystemRead: permissionProfile.mode !== "trusted-full-access"
|
|
5637
4215
|
};
|
|
5638
|
-
}
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
} else if (imageAttachment?.skipped.length) {
|
|
5642
|
-
ctx.ui.notify(`Piagent image input: skipped ${imageAttachment.skipped.length} local image path(s)`, "warning");
|
|
5643
|
-
}
|
|
5644
|
-
|
|
5645
|
-
const inputText = imageAttachment?.text ?? text;
|
|
5646
|
-
const canRewriteWorkflow = event.source !== "extension";
|
|
5647
|
-
const snapshot = buildUsageSnapshot(ctx, String(pi.getThinkingLevel()));
|
|
5648
|
-
const preflight = buildContextPreflight(snapshot, chooseFreshWorkflow(inputText, inputText), inputText.length);
|
|
5649
|
-
const hasBoilerplate = looksLikeGovernedBoilerplate(inputText);
|
|
5650
|
-
const shouldFreshen =
|
|
5651
|
-
canRewriteWorkflow &&
|
|
5652
|
-
preflight.recommendation === "fresh-session" &&
|
|
5653
|
-
(hasBoilerplate || isPiagentWorkflowInput(inputText) || inputText.length >= LONG_INPUT_CHARS);
|
|
5654
|
-
const shouldCollapseBoilerplate = canRewriteWorkflow && hasBoilerplate && inputText.length >= BOILERPLATE_COLLAPSE_CHARS;
|
|
5655
|
-
|
|
5656
|
-
const outgoingImages = [
|
|
5657
|
-
...(Array.isArray(event.images) ? event.images : []),
|
|
5658
|
-
...(imageAttachment?.images ?? [])
|
|
5659
|
-
];
|
|
5660
|
-
|
|
5661
|
-
if (!shouldFreshen && !shouldCollapseBoilerplate) {
|
|
5662
|
-
if (imageAttachment?.attached.length) return { action: "transform", text: inputText, images: outgoingImages };
|
|
5663
|
-
return { action: "continue" };
|
|
5664
|
-
}
|
|
5665
|
-
|
|
5666
|
-
const task = extractTaskRequest(inputText);
|
|
5667
|
-
const workflow = chooseFreshWorkflow(inputText, task);
|
|
5668
|
-
const reason = shouldFreshen
|
|
5669
|
-
? "Current session is near context limits; use a fresh governed session."
|
|
5670
|
-
: "Mandatory flow boilerplate is already part of the platform; collapse it to the task request.";
|
|
5671
|
-
const command = shouldFreshen
|
|
5672
|
-
? buildFreshCommand(ctx.cwd, workflow, inputText, reason)
|
|
5673
|
-
: `/${workflow} ${trimTaskForInline(task)}`;
|
|
5674
|
-
|
|
5675
|
-
ctx.ui.notify(`Piagent preflight: ${reason}`, "warning");
|
|
5676
|
-
return outgoingImages.length > 0
|
|
5677
|
-
? { action: "transform", text: command, images: outgoingImages }
|
|
5678
|
-
: { action: "transform", text: command };
|
|
5679
|
-
});
|
|
5680
|
-
|
|
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;
|
|
5703
|
-
const active = new Set<string>(pi.getActiveTools() as string[]);
|
|
5704
|
-
const toolMetadata = pi.getAllTools()
|
|
5705
|
-
.filter((tool) => active.has(tool.name))
|
|
5706
|
-
.map((tool) => ({
|
|
5707
|
-
name: tool.name,
|
|
5708
|
-
description: tool.description,
|
|
5709
|
-
parameters: tool.parameters,
|
|
5710
|
-
promptGuidelines: tool.promptGuidelines
|
|
5711
|
-
}));
|
|
5712
|
-
const toolSchemaTokens = estimateContextTokens(JSON.stringify(toolMetadata));
|
|
5713
|
-
const systemPromptTokens = estimateContextTokens(effectiveSystemPrompt);
|
|
5714
|
-
const autoPackUseful = activeTask?.trace.outcome !== "pending"
|
|
5715
|
-
&& (runtimeIntake || signal.paths.length === 0);
|
|
5716
|
-
telemetry(ctx, {
|
|
5717
|
-
event: "agent_prompt",
|
|
5718
|
-
promptHash: signal.promptHash,
|
|
5719
|
-
promptChars: signal.promptChars,
|
|
5720
|
-
workflow: signal.workflow,
|
|
5721
|
-
riskLane: signal.lane,
|
|
5722
|
-
activeTools: active.size,
|
|
5723
|
-
activePiagentTools: [...active].filter((toolName) => PIAGENT_TOOL_NAMES.has(toolName)).length,
|
|
5724
|
-
toolSchemaTokens,
|
|
5725
|
-
systemPromptTokens,
|
|
5726
|
-
systemPromptHash: crypto.createHash("sha256").update(effectiveSystemPrompt).digest("hex"),
|
|
5727
|
-
legacyProjectInstructionsRewritten: projectInstructions.rewritten,
|
|
5728
|
-
managedInstructionsCompacted: compactedInstructions.compacted,
|
|
5729
|
-
contextUsage: ctx.getContextUsage()
|
|
5730
|
-
});
|
|
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
|
-
|
|
5763
|
-
if (
|
|
5764
|
-
query.length < 20
|
|
5765
|
-
|| signal.workflow === "usage"
|
|
5766
|
-
|| !autoContextEnabled
|
|
5767
|
-
|| !autoPackUseful
|
|
5768
|
-
|| autoPackedPrompts.has(promptPackKey(ctx, signal.promptHash))
|
|
5769
|
-
) {
|
|
5770
|
-
return finishAgentStart();
|
|
5771
|
-
}
|
|
5772
|
-
|
|
5773
|
-
autoPackedPrompts.add(promptPackKey(ctx, signal.promptHash));
|
|
5774
|
-
if (autoPackedPrompts.size > 50) autoPackedPrompts.delete(autoPackedPrompts.values().next().value as string);
|
|
5775
|
-
try {
|
|
5776
|
-
const excludePatterns = contextIndexExcludePatterns(policy, loadProfileFromContext(ctx));
|
|
5777
|
-
const ensured = await ensureContextIndexV2(ctx.cwd, {
|
|
5778
|
-
excludePatterns,
|
|
5779
|
-
rebuildMissing: false
|
|
5780
|
-
});
|
|
5781
|
-
const status = ensured.status;
|
|
5782
|
-
if (!status.exists || status.stale) {
|
|
5783
|
-
telemetry(ctx, {
|
|
5784
|
-
event: "context_pack",
|
|
5785
|
-
queryHash: signal.promptHash,
|
|
5786
|
-
confidence: "none",
|
|
5787
|
-
candidates: 0,
|
|
5788
|
-
selected: 0,
|
|
5789
|
-
skipped: status.exists ? "stale-index" : "missing-index"
|
|
5790
|
-
});
|
|
5791
|
-
return finishAgentStart();
|
|
5792
|
-
}
|
|
5793
|
-
const budgetTokens = runtimeIntake
|
|
5794
|
-
? 900
|
|
5795
|
-
: signal.lane === "high-risk" ? 1_200 : signal.lane === "tiny" ? 500 : 900;
|
|
5796
|
-
const pack = await buildContextPack(ctx.cwd, query, {
|
|
5797
|
-
budgetTokens,
|
|
5798
|
-
includeCode: runtimeIntake,
|
|
5799
|
-
includePatterns: runtimeIntake ? AUTO_INTAKE_SNAPSHOT_PATTERNS : undefined,
|
|
5800
|
-
currentSnapshot: runtimeIntake,
|
|
5801
|
-
limit: runtimeIntake ? 8 : 12,
|
|
5802
|
-
excludePatterns
|
|
5803
|
-
});
|
|
5804
|
-
telemetry(ctx, {
|
|
5805
|
-
event: "context_pack",
|
|
5806
|
-
queryHash: pack.queryHash,
|
|
5807
|
-
confidence: pack.confidence,
|
|
5808
|
-
candidates: pack.candidates,
|
|
5809
|
-
selected: pack.selected.length,
|
|
5810
|
-
estimatedTokens: pack.estimatedTokens,
|
|
5811
|
-
selectedPaths: pack.selected.map((item) => item.path),
|
|
5812
|
-
finderRecommended: pack.finderRecommended,
|
|
5813
|
-
currentSnapshot: runtimeIntake
|
|
5814
|
-
});
|
|
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
|
|
5839
|
-
}
|
|
5840
|
-
});
|
|
5841
|
-
} catch (error) {
|
|
5842
|
-
telemetry(ctx, {
|
|
5843
|
-
event: "context_pack",
|
|
5844
|
-
queryHash: signal.promptHash,
|
|
5845
|
-
confidence: "none",
|
|
5846
|
-
candidates: 0,
|
|
5847
|
-
selected: 0,
|
|
5848
|
-
error: error instanceof Error ? error.message : String(error)
|
|
5849
|
-
});
|
|
5850
|
-
return finishAgentStart();
|
|
5851
|
-
}
|
|
4216
|
+
},
|
|
4217
|
+
activateToolGroups,
|
|
4218
|
+
telemetry
|
|
5852
4219
|
});
|
|
5853
4220
|
|
|
5854
|
-
pi
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
});
|
|
4221
|
+
registerAgentStartHook(pi, {
|
|
4222
|
+
state: runtimeState,
|
|
4223
|
+
autoContextEnabled,
|
|
4224
|
+
activeTask: (ctx) => activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined,
|
|
4225
|
+
readProtectedPaths: (ctx) => effectiveProtectedPaths(policy, loadProfileFromContext(ctx)).readProtectedPaths,
|
|
4226
|
+
contextExcludePatterns: (ctx) => contextIndexExcludePatterns(policy, loadProfileFromContext(ctx)),
|
|
4227
|
+
promptPackKey,
|
|
4228
|
+
retrievalKey,
|
|
4229
|
+
startAutomaticTask: maybeStartAutomaticTask,
|
|
4230
|
+
telemetry
|
|
5865
4231
|
});
|
|
5866
4232
|
|
|
5867
|
-
pi
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
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, {
|
|
4233
|
+
registerCompletionHook(pi, {
|
|
4234
|
+
state: runtimeState,
|
|
4235
|
+
maxManifestFiles: contextBudgetConfig(policy).maxManifestFiles,
|
|
4236
|
+
autoRecoveryEnabled,
|
|
4237
|
+
activeTask: (ctx) => activeSessionTask(ctx.cwd, ctx.sessionManager.getSessionId()) as TaskContract | undefined,
|
|
4238
|
+
flushObservedTaskContext,
|
|
4239
|
+
completionProjection: completionTaskProjection,
|
|
4240
|
+
evaluateGate: (cwd, task, currentDigests, currentDigest) => evaluateTaskGate(cwd, task, policy, {
|
|
5929
4241
|
currentDigests,
|
|
5930
4242
|
currentWorkingTreeDigest: currentDigest
|
|
5931
|
-
})
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
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
|
-
|
|
6021
|
-
pi.on("session_compact", async (event, ctx) => {
|
|
6022
|
-
telemetry(ctx, {
|
|
6023
|
-
event: "session_compact",
|
|
6024
|
-
reason: event.reason,
|
|
6025
|
-
willRetry: event.willRetry,
|
|
6026
|
-
fromExtension: event.fromExtension
|
|
6027
|
-
});
|
|
6028
|
-
});
|
|
6029
|
-
|
|
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
|
-
);
|
|
6038
|
-
telemetry(ctx, {
|
|
6039
|
-
event: "session_shutdown",
|
|
6040
|
-
reason: event.reason,
|
|
6041
|
-
targetSessionFile: event.targetSessionFile ? path.basename(event.targetSessionFile) : undefined
|
|
6042
|
-
});
|
|
6043
|
-
clearObservedContext(ctx);
|
|
6044
|
-
clearShellMutationSnapshots(ctx);
|
|
4243
|
+
}),
|
|
4244
|
+
writeTask,
|
|
4245
|
+
activateBaseTools: (ctx) => activateToolGroups(ctx, []),
|
|
4246
|
+
appendTrace,
|
|
4247
|
+
appendSessionTrace,
|
|
4248
|
+
telemetry,
|
|
4249
|
+
finalGateMode: (ctx) => resolveRuntimePolicy(loadProfileFromContext(ctx)).finalGate,
|
|
4250
|
+
verifierInstructions: verifierCommandInstructions
|
|
6045
4251
|
});
|
|
6046
4252
|
|
|
6047
|
-
pi
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
ctx.ui.notify(`Piagent Pi guard could not persist bash evidence ledger: ${message}`, "warn");
|
|
6062
|
-
}
|
|
6063
|
-
}
|
|
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
|
-
|
|
6075
|
-
let resultContent: unknown = event.content;
|
|
6076
|
-
let resultDetails: unknown = event.details;
|
|
6077
|
-
let resultChanged = false;
|
|
6078
|
-
|
|
6079
|
-
if (event.toolName === "grep") {
|
|
6080
|
-
const filtered = filterGrepProtectedContent(resultContent, pathPolicy.readProtectedPaths);
|
|
6081
|
-
if (filtered.changed) {
|
|
6082
|
-
resultContent = filtered.content;
|
|
6083
|
-
resultDetails = resultDetails && typeof resultDetails === "object"
|
|
6084
|
-
? { ...resultDetails, protectedMatchesRedacted: filtered.redactedLines }
|
|
6085
|
-
: { protectedMatchesRedacted: filtered.redactedLines };
|
|
6086
|
-
resultChanged = true;
|
|
6087
|
-
}
|
|
6088
|
-
}
|
|
6089
|
-
|
|
6090
|
-
if (event.toolName === "find" || event.toolName === "ls") {
|
|
6091
|
-
const input = event.input && typeof event.input === "object"
|
|
6092
|
-
? event.input as Record<string, unknown>
|
|
6093
|
-
: {};
|
|
6094
|
-
const basePath = extractLikelyPathFromInput(ctx.cwd, input) || ".";
|
|
6095
|
-
const filtered = filterProtectedPathListContent(ctx.cwd, resultContent, pathPolicy.readProtectedPaths, basePath, event.toolName);
|
|
6096
|
-
if (filtered.changed) {
|
|
6097
|
-
resultContent = filtered.content;
|
|
6098
|
-
resultDetails = resultDetails && typeof resultDetails === "object"
|
|
6099
|
-
? { ...resultDetails, protectedPathsRedacted: filtered.redactedLines }
|
|
6100
|
-
: { protectedPathsRedacted: filtered.redactedLines };
|
|
6101
|
-
resultChanged = true;
|
|
6102
|
-
}
|
|
6103
|
-
}
|
|
6104
|
-
|
|
6105
|
-
const safeContent = redactToolResultTextContent(resultContent);
|
|
6106
|
-
const safeDetails = redactForStorage(resultDetails);
|
|
6107
|
-
const detailRedactions = countChangedStringLeaves(resultDetails, safeDetails);
|
|
6108
|
-
const sensitiveValuesRedacted = safeContent.redacted + detailRedactions;
|
|
6109
|
-
if (sensitiveValuesRedacted > 0) {
|
|
6110
|
-
resultContent = safeContent.content;
|
|
6111
|
-
resultDetails = isPlainRecord(safeDetails)
|
|
6112
|
-
? { ...safeDetails, sensitiveValuesRedacted }
|
|
6113
|
-
: safeDetails;
|
|
6114
|
-
resultChanged = true;
|
|
6115
|
-
}
|
|
6116
|
-
|
|
6117
|
-
const fingerprint = toolResultFingerprint(event.toolName, event.input, resultContent);
|
|
6118
|
-
const previousFingerprint = seenToolResults.get(fingerprint.key);
|
|
6119
|
-
const repeated = previousFingerprint?.outputHash === fingerprint.outputHash;
|
|
6120
|
-
seenToolResults.set(fingerprint.key, { outputHash: fingerprint.outputHash, recordedAt: nowIso() });
|
|
6121
|
-
if (seenToolResults.size > 500) seenToolResults.delete(seenToolResults.keys().next().value as string);
|
|
6122
|
-
if (repeated && ["read", "grep", "find", "ls"].includes(event.toolName) && fingerprint.outputChars > 0) {
|
|
6123
|
-
resultContent = [{
|
|
6124
|
-
type: "text",
|
|
6125
|
-
text: `[Piagent delta: unchanged ${event.toolName} result; ${fingerprint.outputChars} chars / ${fingerprint.outputLines} lines match the previous identical call.]`
|
|
6126
|
-
}];
|
|
6127
|
-
resultDetails = isPlainRecord(resultDetails)
|
|
6128
|
-
? {
|
|
6129
|
-
...resultDetails,
|
|
6130
|
-
piagentDelta: {
|
|
6131
|
-
unchanged: true,
|
|
6132
|
-
previousRecordedAt: previousFingerprint?.recordedAt,
|
|
6133
|
-
outputHash: fingerprint.outputHash,
|
|
6134
|
-
originalChars: fingerprint.outputChars,
|
|
6135
|
-
originalLines: fingerprint.outputLines
|
|
6136
|
-
}
|
|
6137
|
-
}
|
|
6138
|
-
: {
|
|
6139
|
-
value: resultDetails,
|
|
6140
|
-
piagentDelta: {
|
|
6141
|
-
unchanged: true,
|
|
6142
|
-
previousRecordedAt: previousFingerprint?.recordedAt,
|
|
6143
|
-
outputHash: fingerprint.outputHash,
|
|
6144
|
-
originalChars: fingerprint.outputChars,
|
|
6145
|
-
originalLines: fingerprint.outputLines
|
|
6146
|
-
}
|
|
6147
|
-
};
|
|
6148
|
-
resultChanged = true;
|
|
6149
|
-
}
|
|
6150
|
-
|
|
6151
|
-
const captureCache = new Map<string, ToolResultCaptureSummary>();
|
|
6152
|
-
const compactedContent = compactToolResultTextContent(ctx.cwd, event, ctx, resultContent, captureCache);
|
|
6153
|
-
const compactionCaptures = [...compactedContent.captures];
|
|
6154
|
-
if (compactedContent.captures.length > 0) {
|
|
6155
|
-
resultContent = compactedContent.content;
|
|
6156
|
-
resultChanged = true;
|
|
6157
|
-
}
|
|
6158
|
-
if (resultDetails !== undefined) {
|
|
6159
|
-
const compactedDetails = compactToolResultDetails(ctx.cwd, event, ctx, resultDetails, captureCache, compactionCaptures);
|
|
6160
|
-
if (compactionCaptures.length > compactedContent.captures.length) {
|
|
6161
|
-
resultDetails = compactedDetails;
|
|
6162
|
-
resultChanged = true;
|
|
6163
|
-
}
|
|
6164
|
-
}
|
|
6165
|
-
if (compactionCaptures.length > 0) {
|
|
6166
|
-
resultDetails = attachToolResultCompactionDetails(resultDetails, compactionCaptures);
|
|
6167
|
-
}
|
|
6168
|
-
|
|
6169
|
-
telemetry(ctx, {
|
|
6170
|
-
event: "tool_result",
|
|
6171
|
-
toolName: event.toolName,
|
|
6172
|
-
inputHash: fingerprint.inputHash,
|
|
6173
|
-
outputHash: fingerprint.outputHash,
|
|
6174
|
-
outputChars: fingerprint.outputChars,
|
|
6175
|
-
outputLines: fingerprint.outputLines,
|
|
6176
|
-
repeated,
|
|
6177
|
-
compacted: compactionCaptures.length > 0,
|
|
6178
|
-
compactedCaptures: compactionCaptures.length,
|
|
6179
|
-
sensitiveValuesRedacted,
|
|
6180
|
-
isError: event.isError,
|
|
6181
|
-
usage: event.usage
|
|
6182
|
-
});
|
|
6183
|
-
|
|
6184
|
-
if (resultChanged) {
|
|
6185
|
-
return resultDetails === undefined
|
|
6186
|
-
? { content: resultContent }
|
|
6187
|
-
: { content: resultContent, details: resultDetails };
|
|
6188
|
-
}
|
|
4253
|
+
registerToolResultHook(pi, {
|
|
4254
|
+
state: runtimeState,
|
|
4255
|
+
maxManifestFiles: contextBudgetConfig(policy).maxManifestFiles,
|
|
4256
|
+
readProtectedPaths: (ctx) => effectiveProtectedPaths(policy, loadProfileFromContext(ctx)).readProtectedPaths,
|
|
4257
|
+
recordObservedBash: (observed) => bashResults.record(observed),
|
|
4258
|
+
observedBashLedgerPath,
|
|
4259
|
+
redactText,
|
|
4260
|
+
observedTaskContext: observedTaskContextFromToolResult,
|
|
4261
|
+
recordObservedTaskChanges,
|
|
4262
|
+
recordObservedTaskVerification,
|
|
4263
|
+
extractLikelyPath: extractLikelyPathFromInput,
|
|
4264
|
+
isShellTool: (toolName) => SHELL_TOOL_NAMES.has(toolName),
|
|
4265
|
+
telemetry,
|
|
4266
|
+
now: nowIso
|
|
6189
4267
|
});
|
|
6190
4268
|
|
|
6191
4269
|
pi.on("tool_call", async (event, ctx) => {
|
|
@@ -6218,8 +4296,8 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6218
4296
|
}
|
|
6219
4297
|
// An advisory verdict that is computed and discarded makes advisory mode
|
|
6220
4298
|
// indistinguishable from off, which is not what the mode is for.
|
|
6221
|
-
if (toolDecision.decision === "warn" && !
|
|
6222
|
-
|
|
4299
|
+
if (toolDecision.decision === "warn" && !runtimeState.hasAdvisedTool(ctx, event.toolName)) {
|
|
4300
|
+
runtimeState.rememberAdvisedTool(ctx, event.toolName);
|
|
6223
4301
|
const missing = toolDecision.requiredCapabilities.length > 0
|
|
6224
4302
|
? ` Declare ${toolDecision.requiredCapabilities.join(", ")} in profile mcpCapabilities to clear this.`
|
|
6225
4303
|
: "";
|
|
@@ -6412,7 +4490,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6412
4490
|
&& sessionTask.changeMode === "source-change"
|
|
6413
4491
|
&& shellProjectMutation
|
|
6414
4492
|
) {
|
|
6415
|
-
rememberShellMutationSnapshot(ctx, event.toolName, event.input);
|
|
4493
|
+
runtimeState.rememberShellMutationSnapshot(ctx, event.toolName, event.input);
|
|
6416
4494
|
}
|
|
6417
4495
|
});
|
|
6418
4496
|
|
|
@@ -6511,7 +4589,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6511
4589
|
].join("\n");
|
|
6512
4590
|
} else if (params.action === "pack") {
|
|
6513
4591
|
if (!params.query?.trim()) throw new Error("query is required for a context pack");
|
|
6514
|
-
const injected =
|
|
4592
|
+
const injected = runtimeState.injectedContextPack(ctx, retrievalKey(ctx, params.query));
|
|
6515
4593
|
if (injected && params.refresh !== true) {
|
|
6516
4594
|
result = { reusedInjectedPack: true, ...injected };
|
|
6517
4595
|
text = [
|
|
@@ -7565,7 +5643,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
7565
5643
|
const reviewLenses = normalizeReviewLenses(params.reviewLenses, orchestration.defaultReviewLenses);
|
|
7566
5644
|
const providedWorkPlan = normalizeWorkPlanSteps(params.workPlan);
|
|
7567
5645
|
const workPlan = providedWorkPlan.length ? providedWorkPlan : defaultWorkPlan(safeSummary, params.riskLane, changeMode);
|
|
7568
|
-
const seededContext = observedContext(ctx).slice(0, contextBudgetConfig(policy).maxManifestFiles);
|
|
5646
|
+
const seededContext = runtimeState.observedContext(ctx).slice(0, contextBudgetConfig(policy).maxManifestFiles);
|
|
7569
5647
|
const workPlanError = validateNewWorkPlan(workPlan);
|
|
7570
5648
|
if (workPlanError) {
|
|
7571
5649
|
return { content: [{ type: "text", text: `Task start refused: ${workPlanError}.` }], details: workPlan, isError: true };
|
|
@@ -7627,7 +5705,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
7627
5705
|
}
|
|
7628
5706
|
const written = writeTask(ctx.cwd, task);
|
|
7629
5707
|
bindSessionTask(ctx.cwd, sessionId, sessionName, written);
|
|
7630
|
-
cacheTaskIdentity(ctx, written);
|
|
5708
|
+
runtimeState.cacheTaskIdentity(ctx, written);
|
|
7631
5709
|
if (written.intakeMode !== "runtime") {
|
|
7632
5710
|
// Intake classification defines the cache-stable surface for this agent turn.
|
|
7633
5711
|
// A model may choose a narrower lane than the prompt classifier; never remove
|
|
@@ -7675,7 +5753,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
7675
5753
|
|
|
7676
5754
|
const summary = redactText(prompt).replace(/\s+/g, " ").trim().slice(0, 320);
|
|
7677
5755
|
const sessionName = currentSessionName(ctx);
|
|
7678
|
-
const scope = automaticTaskScope(prompt, observedContext(ctx));
|
|
5756
|
+
const scope = automaticTaskScope(prompt, runtimeState.observedContext(ctx));
|
|
7679
5757
|
const started = await taskStartTool.execute(
|
|
7680
5758
|
`runtime-intake-${ctx.sessionManager.getSessionId()}`,
|
|
7681
5759
|
{
|
|
@@ -9590,7 +7668,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
9590
7668
|
`cwd: ${redactText(snapshot.cwd)}`,
|
|
9591
7669
|
`model: ${snapshot.model}; thinking: ${snapshot.thinkingLevel}`,
|
|
9592
7670
|
`entries: ${formatCount(snapshot.entries.branch)} active / ${formatCount(snapshot.entries.total)} total`,
|
|
9593
|
-
"name: /name <task name>",
|
|
7671
|
+
"name: Pi native /name <task name>",
|
|
9594
7672
|
"resume: use Pi native /resume or /session"
|
|
9595
7673
|
].join("\n"), snapshot, { message: `Piagent session: ${snapshot.sessionName ?? "unnamed"}`, level: "info" });
|
|
9596
7674
|
}
|
|
@@ -9613,7 +7691,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
9613
7691
|
if (!action) {
|
|
9614
7692
|
const chosen = await selectRuntimeAction(ctx, "Piagent session", [
|
|
9615
7693
|
{ value: "current", label: "Current session", description: "Name, id, file, model", recommended: true },
|
|
9616
|
-
{ value: "name", label: "Set name", description: "Use /name <task name>" },
|
|
7694
|
+
{ value: "name", label: "Set name", description: "Use Pi native /name <task name>" },
|
|
9617
7695
|
{ value: "resume", label: "Resume help", description: "Commands for continuing old sessions" },
|
|
9618
7696
|
{ value: "fresh-task", label: "Fresh task", description: "Use /fresh task <request>" },
|
|
9619
7697
|
{ value: "fresh-scout", label: "Fresh scout", description: "Use /fresh scout <request>" },
|
|
@@ -9635,7 +7713,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
9635
7713
|
emitRuntimeMessage(ctx, "piagent-session-help", [
|
|
9636
7714
|
"session helpers:",
|
|
9637
7715
|
"current: /usage live or Pi native /session",
|
|
9638
|
-
"name: /name <task/session name>",
|
|
7716
|
+
"name: Pi native /name <task/session name>",
|
|
9639
7717
|
"fresh: /fresh task|scout|be-to-fe <request>",
|
|
9640
7718
|
"resume: Pi native /resume or /session",
|
|
9641
7719
|
"legacy: /piagent-session | /setname | /fresh-task | /fresh-scout | /fresh-be-to-fe"
|
|
@@ -9680,7 +7758,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
9680
7758
|
emitRuntimeMessage(ctx, "piagent-session-help", [
|
|
9681
7759
|
"session helpers:",
|
|
9682
7760
|
"/usage live",
|
|
9683
|
-
"/name ABC-123 Short task name",
|
|
7761
|
+
"Pi native: /name ABC-123 Short task name",
|
|
9684
7762
|
"/fresh task Implement <request>",
|
|
9685
7763
|
"native: /session | /resume"
|
|
9686
7764
|
].join("\n"));
|
|
@@ -9728,7 +7806,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
9728
7806
|
});
|
|
9729
7807
|
|
|
9730
7808
|
pi.registerCommand("piagent-session", {
|
|
9731
|
-
description: "Legacy session helper namespace; prefer /usage, /name, and /fresh",
|
|
7809
|
+
description: "Legacy session helper namespace; prefer /usage, Pi native /name, and /fresh",
|
|
9732
7810
|
getArgumentCompletions: (prefix: string) => {
|
|
9733
7811
|
const actions = ["current", "name", "resume", "fresh", "usage", "help"];
|
|
9734
7812
|
const typed = String(prefix ?? "").trim().toLowerCase();
|
|
@@ -9739,15 +7817,8 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
9739
7817
|
}
|
|
9740
7818
|
});
|
|
9741
7819
|
|
|
9742
|
-
pi.registerCommand("name", {
|
|
9743
|
-
description: "Set the current session name for Agent Watch/report mapping",
|
|
9744
|
-
handler: async (args, ctx) => {
|
|
9745
|
-
setSessionNameFromCommand(ctx, String(args ?? ""), "/name <task/session name>");
|
|
9746
|
-
}
|
|
9747
|
-
});
|
|
9748
|
-
|
|
9749
7820
|
pi.registerCommand("setname", {
|
|
9750
|
-
description: "
|
|
7821
|
+
description: "Compatibility alias for Pi native /name",
|
|
9751
7822
|
handler: async (args, ctx) => {
|
|
9752
7823
|
setSessionNameFromCommand(ctx, String(args ?? ""), "/setname <task/session name>");
|
|
9753
7824
|
}
|
|
@@ -9901,8 +7972,8 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
9901
7972
|
const sections: Record<string, string[]> = {
|
|
9902
7973
|
overview: [
|
|
9903
7974
|
"Piagent command surface:",
|
|
9904
|
-
"runtime: /workflow | /usage | /context | /permission | /commands | /profile | /memory | /onboard | /
|
|
9905
|
-
"native: /model | /session | /resume | /compact | /mcp",
|
|
7975
|
+
"runtime: /workflow | /usage | /context | /permission | /commands | /profile | /memory | /onboard | /fresh",
|
|
7976
|
+
"native: /model | /name | /session | /resume | /compact | /mcp",
|
|
9906
7977
|
"workflow: /workflow task|scout|be-to-fe|review|commit|pr <request>",
|
|
9907
7978
|
"mcp: /mcp is Pi native; governed MCP checks stay at /piagent-mcp to avoid collision",
|
|
9908
7979
|
"legacy: /piagent-* commands still work where they existed",
|
|
@@ -9920,7 +7991,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
9920
7991
|
"Usage/session:",
|
|
9921
7992
|
"/usage",
|
|
9922
7993
|
"/usage history",
|
|
9923
|
-
"/name <task name>",
|
|
7994
|
+
"Pi native: /name <task name>",
|
|
9924
7995
|
"/fresh task|scout|be-to-fe <request>",
|
|
9925
7996
|
"native: /session | /resume"
|
|
9926
7997
|
],
|