@piagent/platform 1.2.5 → 1.2.6

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +3 -2
  3. package/catalog/capabilities.json +4 -4
  4. package/docs/command-reference-vietnamese.md +9 -8
  5. package/docs/context-engine.md +177 -0
  6. package/docs/context-window-policy.md +13 -8
  7. package/docs/distribution-standard.md +7 -7
  8. package/docs/operator-manual-vietnamese.md +7 -7
  9. package/docs/publishing-for-teams.md +4 -0
  10. package/docs/quality-benchmark.md +14 -1
  11. package/docs/release-install-policy.md +6 -6
  12. package/docs/runtime-harness-standard.md +16 -7
  13. package/docs/security-threat-model.md +1 -1
  14. package/docs/task-lifecycle-vietnamese.md +4 -4
  15. package/docs/team-onboarding.md +2 -2
  16. package/docs/usage-observability.md +6 -0
  17. package/docs-site/architecture.html +4 -3
  18. package/docs-site/commands.html +2 -1
  19. package/docs-site/content/context-engine.html +47 -0
  20. package/docs-site/context-engine.html +171 -0
  21. package/docs-site/index.html +2 -1
  22. package/docs-site/mcp-approval.html +2 -1
  23. package/docs-site/mcp-auth.html +2 -1
  24. package/docs-site/mcp-servers.html +2 -1
  25. package/docs-site/mcp.html +2 -1
  26. package/docs-site/permissions.html +2 -1
  27. package/docs-site/profiles.html +2 -1
  28. package/docs-site/quickstart.html +2 -1
  29. package/docs-site/runtime.html +4 -3
  30. package/docs-site/scope.html +2 -1
  31. package/docs-site/security.html +2 -1
  32. package/docs-site/team.html +2 -1
  33. package/docs-site/workflows.html +2 -1
  34. package/package.json +3 -2
  35. package/packages/piagent-core/README.md +8 -2
  36. package/packages/piagent-core/capabilities/capability-core.js +4 -0
  37. package/packages/piagent-core/extensions/context-engine.js +1175 -0
  38. package/packages/piagent-core/extensions/piagent-guard.ts +785 -17
  39. package/packages/piagent-core/package.json +1 -1
  40. package/packages/piagent-core/policies/base-policy.json +2 -0
  41. package/packages/piagent-core/prompts/scout.md +10 -24
  42. package/packages/piagent-core/prompts/task.md +25 -45
  43. package/scripts/build-docs-site.mjs +6 -0
  44. package/scripts/context-engine.mjs +115 -0
  45. package/scripts/install-global.sh +14 -1
  46. package/scripts/piagent-cli.mjs +2 -1
  47. package/scripts/quality-benchmark.sh +71 -1
  48. package/scripts/verify-local.sh +8 -0
@@ -73,6 +73,18 @@ import {
73
73
  normalizeActionToken,
74
74
  normalizeShellCommandForPolicy
75
75
  } from "./guard-shell-analysis.ts";
76
+ import {
77
+ appendContextTelemetry,
78
+ buildContextEfficiencyReport,
79
+ buildContextIndexV2,
80
+ buildContextPack,
81
+ buildTestImpact,
82
+ classifyContextTask,
83
+ contextIndexV2Status,
84
+ estimateContextTokens,
85
+ searchContextIndexV2,
86
+ toolResultFingerprint
87
+ } from "./context-engine.js";
76
88
 
77
89
  import type { ActionClassification } from "./guard-shell-analysis.ts";
78
90
 
@@ -144,6 +156,51 @@ const CONTEXT_INDEX_EDGE_KINDS = ["uses_tech", "depends_on", "verified_by", "pro
144
156
  const CONTEXT_INDEX_FILE = ".pi/context-index.json";
145
157
  const TECH_STACK_MANIFEST_FILE = ".pi/tech-stack.json";
146
158
  const TECH_CONTEXT_DIR = ".pi/tech-context";
159
+ const PIAGENT_TOOL_GROUPS = {
160
+ loader: ["piagent_tools"],
161
+ governance: [
162
+ "piagent_context",
163
+ "piagent_context_preflight",
164
+ "piagent_task_start",
165
+ "piagent_context_record",
166
+ "piagent_verify_record",
167
+ "piagent_trace_record",
168
+ "piagent_task_gate_check"
169
+ ],
170
+ policy: [
171
+ "piagent_permission_status",
172
+ "piagent_exec_policy_check",
173
+ "piagent_tool_policy_check"
174
+ ],
175
+ retrieval: [
176
+ "piagent_context_engine",
177
+ "piagent_context_budget",
178
+ "piagent_context_index_status",
179
+ "piagent_context_index_search"
180
+ ],
181
+ knowledge: [
182
+ "piagent_memory_status",
183
+ "piagent_memory_search",
184
+ "piagent_memory_citation_record",
185
+ "piagent_document_read",
186
+ "piagent_source_checkout",
187
+ "piagent_orchestration_policy"
188
+ ],
189
+ onboarding: [
190
+ "piagent_profile_options",
191
+ "piagent_profile_apply",
192
+ "piagent_profile_tech_options",
193
+ "piagent_profile_tech_apply",
194
+ "piagent_profile_tech_context_record",
195
+ "piagent_project_onboarding_record",
196
+ "piagent_context_index_record",
197
+ "piagent_memory_note"
198
+ ],
199
+ usage: ["piagent_usage_snapshot"]
200
+ } as const;
201
+ type PiagentToolGroup = keyof typeof PIAGENT_TOOL_GROUPS;
202
+ const PIAGENT_TOOL_ORDER = Object.values(PIAGENT_TOOL_GROUPS).flat();
203
+ const PIAGENT_TOOL_NAMES = new Set<string>(PIAGENT_TOOL_ORDER);
147
204
 
148
205
  const TECH_OPTIONS: TechOption[] = [
149
206
  { 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"] },
@@ -327,6 +384,8 @@ const DEFAULT_POLICY: BasePolicy = {
327
384
  toolRegistry: {
328
385
  defaultMode: "advisory",
329
386
  alwaysAllowedTools: [
387
+ "piagent_tools",
388
+ "piagent_context_engine",
330
389
  "piagent_context",
331
390
  "piagent_permission_status",
332
391
  "piagent_exec_policy_check",
@@ -1799,6 +1858,25 @@ function normalizeWorkPlanSteps(values: unknown): WorkPlanStep[] {
1799
1858
  }
1800
1859
 
1801
1860
  function defaultWorkPlan(summary: string, riskLane: TaskContract["riskLane"]): WorkPlanStep[] {
1861
+ if (riskLane === "tiny") {
1862
+ return [
1863
+ {
1864
+ id: "implement",
1865
+ title: summary.slice(0, 160) || "Implement the approved bounded change.",
1866
+ role: "parent",
1867
+ mode: "single-writer",
1868
+ status: "pending"
1869
+ },
1870
+ {
1871
+ id: "verify",
1872
+ title: "Review the changed files and record exact verification evidence.",
1873
+ role: "parent",
1874
+ mode: "review",
1875
+ status: "pending",
1876
+ dependsOn: ["implement"]
1877
+ }
1878
+ ];
1879
+ }
1802
1880
  const steps: WorkPlanStep[] = [
1803
1881
  {
1804
1882
  id: "plan",
@@ -1817,7 +1895,7 @@ function defaultWorkPlan(summary: string, riskLane: TaskContract["riskLane"]): W
1817
1895
  },
1818
1896
  {
1819
1897
  id: "review",
1820
- title: riskLane === "tiny" ? "Review changed files and verify evidence." : "Run explicit review lenses against the diff and verify evidence.",
1898
+ title: "Run explicit review lenses against the diff and verify evidence.",
1821
1899
  role: "piagent-reviewer",
1822
1900
  mode: "review",
1823
1901
  status: "pending",
@@ -4205,16 +4283,161 @@ function checkoutReferenceRepo(repoRef: string, forceUpdate = false): ReferenceR
4205
4283
  return { host, owner, repo, cloneUrl, checkoutPath, commit, fetched };
4206
4284
  }
4207
4285
 
4286
+ function toolGroupsForPrompt(prompt: string): PiagentToolGroup[] {
4287
+ const signal = classifyContextTask(prompt);
4288
+ const lower = prompt.toLowerCase();
4289
+ const groups = new Set<PiagentToolGroup>(["loader"]);
4290
+
4291
+ if (signal.workflow === "usage") {
4292
+ groups.add("usage");
4293
+ return [...groups];
4294
+ }
4295
+ if (signal.workflow === "onboard") {
4296
+ groups.add("governance");
4297
+ groups.add("policy");
4298
+ groups.add("retrieval");
4299
+ groups.add("knowledge");
4300
+ groups.add("onboarding");
4301
+ return [...groups];
4302
+ }
4303
+
4304
+ groups.add("governance");
4305
+ groups.add("policy");
4306
+ if (
4307
+ signal.lane !== "tiny"
4308
+ || ["scout", "review", "plan", "release"].includes(signal.workflow)
4309
+ || /\b(context|document|memory|repository|source|subagent|vendor)\b/.test(lower)
4310
+ ) {
4311
+ groups.add("retrieval");
4312
+ }
4313
+ if (
4314
+ signal.lane === "high-risk"
4315
+ || ["scout", "review", "plan", "release"].includes(signal.workflow)
4316
+ || /\b(document|memory|orchestration|source checkout|subagent|vendor)\b/.test(lower)
4317
+ ) {
4318
+ groups.add("knowledge");
4319
+ }
4320
+ if (/\b(profile|tech stack|onboard|project context)\b/.test(lower)) groups.add("onboarding");
4321
+ if (/\b(token|usage|cost)\b/.test(lower)) groups.add("usage");
4322
+ return [...groups];
4323
+ }
4324
+
4325
+ function compactSessionTask(cwd: string): TaskContract | undefined {
4326
+ const directory = path.join(stateRoot(cwd), "tasks");
4327
+ if (!fs.existsSync(directory)) return undefined;
4328
+ const candidates = fs.readdirSync(directory, { withFileTypes: true })
4329
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
4330
+ .map((entry) => {
4331
+ const absolute = path.join(directory, entry.name);
4332
+ return { absolute, mtimeMs: fs.statSync(absolute).mtimeMs };
4333
+ })
4334
+ .sort((left, right) => right.mtimeMs - left.mtimeMs);
4335
+ return candidates[0] ? readJsonFile<TaskContract>(candidates[0].absolute) : undefined;
4336
+ }
4337
+
4338
+ function semanticCompactionInstructions(cwd: string): string {
4339
+ const task = compactSessionTask(cwd);
4340
+ const taskState = task
4341
+ ? [
4342
+ `Current task: ${task.taskId} (${task.riskLane})`,
4343
+ `Goal: ${task.summary}`,
4344
+ `Acceptance: ${task.acceptanceCriteria.join("; ") || "not recorded"}`,
4345
+ `Scope: ${task.scope.join(", ") || "not recorded"}`,
4346
+ `Changed files: ${task.changedFiles.join(", ") || "none recorded"}`,
4347
+ `Verify commands: ${task.verifyCommands.join(" | ") || "not recorded"}`,
4348
+ `Outcome/blocker: ${task.trace.outcome}${task.trace.friction ? `; ${task.trace.friction}` : ""}`
4349
+ ].join("\n")
4350
+ : "No persisted task contract was found. Derive the current goal from the most recent user request.";
4351
+ return [
4352
+ "Create a structured Piagent carry-over summary.",
4353
+ taskState,
4354
+ "",
4355
+ "Preserve:",
4356
+ "- current goal, acceptance criteria, explicit user decisions, and non-negotiable constraints",
4357
+ "- architecture facts and invariants verified from repository files",
4358
+ "- files changed, exact verification evidence, unresolved failures, blockers, and next action",
4359
+ "- citations or paths needed to re-read advisory context",
4360
+ "",
4361
+ "Discard:",
4362
+ "- superseded plans, repeated reads, raw tool logs, successful intermediate output, and speculative reasoning",
4363
+ "- full source excerpts that can be re-read from the repository",
4364
+ "",
4365
+ "Do not convert assumptions into facts. Mark unknowns explicitly. After compaction, re-read current files before editing."
4366
+ ].join("\n");
4367
+ }
4368
+
4369
+ function environmentFeatureEnabled(name: string, fallback = true): boolean {
4370
+ const value = process.env[name]?.trim().toLowerCase();
4371
+ if (!value) return fallback;
4372
+ return !["0", "false", "no", "off", "disabled"].includes(value);
4373
+ }
4374
+
4208
4375
  export default function piagentGuard(pi: ExtensionAPI) {
4209
4376
  const extensionDir = path.dirname(fileURLToPath(import.meta.url));
4210
4377
  const policy = loadPolicy(extensionDir);
4211
4378
  const bashResults = createBashResultLedger({ maxEntries: 300 });
4379
+ const dynamicToolsEnabled = environmentFeatureEnabled("PIAGENT_DYNAMIC_TOOLS");
4380
+ const contextTelemetryEnabled = environmentFeatureEnabled("PIAGENT_CONTEXT_TELEMETRY");
4381
+ const autoContextEnabled = environmentFeatureEnabled("PIAGENT_AUTO_CONTEXT");
4212
4382
  // Advisory tool-registry verdicts are surfaced once per tool per session. A
4213
4383
  // notice on every call would be constant noise for a tool used all day, and
4214
4384
  // noise that is always present is read as background rather than as a warning.
4215
4385
  const advisedTools = new Set<string>();
4386
+ const seenToolResults = new Map<string, { outputHash: string; recordedAt: string }>();
4387
+ const autoPackedPrompts = new Set<string>();
4388
+
4389
+ function telemetry(ctx: ExtensionContext, payload: Record<string, unknown>): void {
4390
+ if (!contextTelemetryEnabled) return;
4391
+ try {
4392
+ const safePayload = redactForStorage(payload) as Record<string, unknown>;
4393
+ appendContextTelemetry(ctx.cwd, {
4394
+ sessionId: ctx.sessionManager.getSessionId(),
4395
+ sessionName: currentSessionName(ctx),
4396
+ model: `${ctx.model?.provider ?? "unknown"}/${ctx.model?.id ?? "unknown"}`,
4397
+ thinkingLevel: String(pi.getThinkingLevel()),
4398
+ ...safePayload
4399
+ });
4400
+ } catch {
4401
+ // Telemetry is local observability. It must never block the agent loop.
4402
+ }
4403
+ }
4404
+
4405
+ function activateToolGroups(ctx: ExtensionContext, groups: PiagentToolGroup[], additive = false): string[] {
4406
+ const current = pi.getActiveTools();
4407
+ if (!dynamicToolsEnabled) return current;
4408
+ const selected = new Set<string>();
4409
+ if (additive) {
4410
+ for (const toolName of current) selected.add(toolName);
4411
+ } else {
4412
+ for (const toolName of current) {
4413
+ if (!PIAGENT_TOOL_NAMES.has(toolName)) selected.add(toolName);
4414
+ }
4415
+ }
4416
+ const normalizedGroups = [...new Set<PiagentToolGroup>(["loader", ...groups])];
4417
+ for (const toolName of PIAGENT_TOOL_ORDER) {
4418
+ if (normalizedGroups.some((group) => PIAGENT_TOOL_GROUPS[group].includes(toolName as never))) {
4419
+ selected.add(toolName);
4420
+ }
4421
+ }
4422
+ const ordered = [
4423
+ ...current.filter((toolName) => selected.has(toolName) && !PIAGENT_TOOL_NAMES.has(toolName)),
4424
+ ...PIAGENT_TOOL_ORDER.filter((toolName) => selected.has(toolName))
4425
+ ];
4426
+ const unchanged = ordered.length === current.length && ordered.every((toolName, index) => toolName === current[index]);
4427
+ if (!unchanged) pi.setActiveTools(ordered);
4428
+ telemetry(ctx, {
4429
+ event: "tool_activation",
4430
+ groups: normalizedGroups,
4431
+ additive,
4432
+ previousCount: current.length,
4433
+ activeCount: ordered.length,
4434
+ piagentTools: ordered.filter((toolName) => PIAGENT_TOOL_NAMES.has(toolName))
4435
+ });
4436
+ return ordered;
4437
+ }
4216
4438
 
4217
4439
  pi.on("session_start", async (_event, ctx) => {
4440
+ activateToolGroups(ctx, []);
4218
4441
  const projectTrusted = ctx.isProjectTrusted();
4219
4442
  const explicitProfile = Boolean(process.env.PIAGENT_PROFILE?.trim());
4220
4443
  const profile = loadProfile(ctx.cwd, projectTrusted);
@@ -4255,6 +4478,13 @@ export default function piagentGuard(pi: ExtensionAPI) {
4255
4478
  // the first thing the operator reads.
4256
4479
  const updateNotice = updateAvailabilityNotice();
4257
4480
  if (updateNotice) ctx.ui.notify(updateNotice, "info");
4481
+ let engineStatus: unknown;
4482
+ try {
4483
+ engineStatus = await contextIndexV2Status(ctx.cwd);
4484
+ } catch (error) {
4485
+ engineStatus = { exists: false, error: error instanceof Error ? error.message : String(error) };
4486
+ }
4487
+ telemetry(ctx, { event: "session_start", activeTools: pi.getActiveTools().length, index: engineStatus });
4258
4488
  });
4259
4489
 
4260
4490
  pi.on("input", async (event, ctx) => {
@@ -4263,6 +4493,18 @@ export default function piagentGuard(pi: ExtensionAPI) {
4263
4493
  if (/^\/piagent-workflow\b/i.test(text)) {
4264
4494
  return { action: "transform", text: text.replace(/^\/piagent-workflow\b/i, "/workflow") };
4265
4495
  }
4496
+ const taskSignal = classifyContextTask(text);
4497
+ activateToolGroups(ctx, toolGroupsForPrompt(text));
4498
+ telemetry(ctx, {
4499
+ event: "user_input",
4500
+ source: event.source,
4501
+ promptHash: taskSignal.promptHash,
4502
+ promptChars: taskSignal.promptChars,
4503
+ workflow: taskSignal.workflow,
4504
+ riskLane: taskSignal.lane,
4505
+ explicitPathCount: taskSignal.paths.length,
4506
+ termCount: taskSignal.terms.length
4507
+ });
4266
4508
 
4267
4509
  const imageAttachment = attachLocalImagesFromText(text, event.images, ctx.cwd);
4268
4510
  if (imageAttachment?.attached.length) {
@@ -4307,6 +4549,128 @@ export default function piagentGuard(pi: ExtensionAPI) {
4307
4549
  : { action: "transform", text: command };
4308
4550
  });
4309
4551
 
4552
+ pi.on("before_agent_start", async (event, ctx) => {
4553
+ const active = new Set<string>(pi.getActiveTools() as string[]);
4554
+ const toolMetadata = pi.getAllTools()
4555
+ .filter((tool) => active.has(tool.name))
4556
+ .map((tool) => ({
4557
+ name: tool.name,
4558
+ description: tool.description,
4559
+ parameters: tool.parameters,
4560
+ promptGuidelines: tool.promptGuidelines
4561
+ }));
4562
+ const toolSchemaTokens = estimateContextTokens(JSON.stringify(toolMetadata));
4563
+ const systemPromptTokens = estimateContextTokens(event.systemPrompt);
4564
+ const query = looksLikeGovernedBoilerplate(event.prompt) ? extractTaskRequest(event.prompt) : event.prompt.trim();
4565
+ const signal = classifyContextTask(query);
4566
+ telemetry(ctx, {
4567
+ event: "agent_prompt",
4568
+ promptHash: signal.promptHash,
4569
+ promptChars: signal.promptChars,
4570
+ workflow: signal.workflow,
4571
+ riskLane: signal.lane,
4572
+ activeTools: active.size,
4573
+ activePiagentTools: [...active].filter((toolName) => PIAGENT_TOOL_NAMES.has(toolName)).length,
4574
+ toolSchemaTokens,
4575
+ systemPromptTokens,
4576
+ systemPromptHash: crypto.createHash("sha256").update(event.systemPrompt).digest("hex"),
4577
+ contextUsage: ctx.getContextUsage()
4578
+ });
4579
+
4580
+ if (
4581
+ query.length < 20
4582
+ || signal.workflow === "usage"
4583
+ || !autoContextEnabled
4584
+ || autoPackedPrompts.has(signal.promptHash)
4585
+ || !active.has("piagent_context_engine")
4586
+ ) {
4587
+ return;
4588
+ }
4589
+
4590
+ autoPackedPrompts.add(signal.promptHash);
4591
+ if (autoPackedPrompts.size > 50) autoPackedPrompts.delete(autoPackedPrompts.values().next().value as string);
4592
+ try {
4593
+ const status = await contextIndexV2Status(ctx.cwd);
4594
+ if (!status.exists || status.stale) {
4595
+ telemetry(ctx, {
4596
+ event: "context_pack",
4597
+ queryHash: signal.promptHash,
4598
+ confidence: "none",
4599
+ candidates: 0,
4600
+ selected: 0,
4601
+ skipped: status.exists ? "stale-index" : "missing-index"
4602
+ });
4603
+ return;
4604
+ }
4605
+ const budgetTokens = signal.lane === "high-risk" ? 1_200 : signal.lane === "tiny" ? 500 : 900;
4606
+ const pack = await buildContextPack(ctx.cwd, query, { budgetTokens, includeCode: false, limit: 12 });
4607
+ telemetry(ctx, {
4608
+ event: "context_pack",
4609
+ queryHash: pack.queryHash,
4610
+ confidence: pack.confidence,
4611
+ candidates: pack.candidates,
4612
+ selected: pack.selected.length,
4613
+ estimatedTokens: pack.estimatedTokens,
4614
+ selectedPaths: pack.selected.map((item) => item.path),
4615
+ finderRecommended: pack.finderRecommended
4616
+ });
4617
+ if (!["medium", "high"].includes(pack.confidence) || pack.selected.length === 0) return;
4618
+ return {
4619
+ message: {
4620
+ customType: "piagent-context-pack-v2",
4621
+ content: pack.text,
4622
+ display: false,
4623
+ details: {
4624
+ schemaVersion: 2,
4625
+ queryHash: pack.queryHash,
4626
+ confidence: pack.confidence,
4627
+ estimatedTokens: pack.estimatedTokens,
4628
+ paths: pack.selected.map((item) => item.path)
4629
+ }
4630
+ }
4631
+ };
4632
+ } catch (error) {
4633
+ telemetry(ctx, {
4634
+ event: "context_pack",
4635
+ queryHash: signal.promptHash,
4636
+ confidence: "none",
4637
+ candidates: 0,
4638
+ selected: 0,
4639
+ error: error instanceof Error ? error.message : String(error)
4640
+ });
4641
+ }
4642
+ });
4643
+
4644
+ pi.on("turn_end", async (event, ctx) => {
4645
+ const message = event.message as unknown as { usage?: unknown; stopReason?: unknown; role?: unknown };
4646
+ telemetry(ctx, {
4647
+ event: "turn_end",
4648
+ turnIndex: event.turnIndex,
4649
+ toolResults: event.toolResults.length,
4650
+ role: message.role,
4651
+ stopReason: message.stopReason,
4652
+ usage: message.usage,
4653
+ contextUsage: ctx.getContextUsage()
4654
+ });
4655
+ });
4656
+
4657
+ pi.on("session_compact", async (event, ctx) => {
4658
+ telemetry(ctx, {
4659
+ event: "session_compact",
4660
+ reason: event.reason,
4661
+ willRetry: event.willRetry,
4662
+ fromExtension: event.fromExtension
4663
+ });
4664
+ });
4665
+
4666
+ pi.on("session_shutdown", async (event, ctx) => {
4667
+ telemetry(ctx, {
4668
+ event: "session_shutdown",
4669
+ reason: event.reason,
4670
+ targetSessionFile: event.targetSessionFile ? path.basename(event.targetSessionFile) : undefined
4671
+ });
4672
+ });
4673
+
4310
4674
  pi.on("tool_result", async (event, ctx) => {
4311
4675
  const profile = loadProfileFromContext(ctx);
4312
4676
  const pathPolicy = effectiveProtectedPaths(policy, profile);
@@ -4367,6 +4731,40 @@ export default function piagentGuard(pi: ExtensionAPI) {
4367
4731
  resultChanged = true;
4368
4732
  }
4369
4733
 
4734
+ const fingerprint = toolResultFingerprint(event.toolName, event.input, resultContent);
4735
+ const previousFingerprint = seenToolResults.get(fingerprint.key);
4736
+ const repeated = previousFingerprint?.outputHash === fingerprint.outputHash;
4737
+ seenToolResults.set(fingerprint.key, { outputHash: fingerprint.outputHash, recordedAt: nowIso() });
4738
+ if (seenToolResults.size > 500) seenToolResults.delete(seenToolResults.keys().next().value as string);
4739
+ if (repeated && ["read", "grep", "find", "ls"].includes(event.toolName) && fingerprint.outputChars > 0) {
4740
+ resultContent = [{
4741
+ type: "text",
4742
+ text: `[Piagent delta: unchanged ${event.toolName} result; ${fingerprint.outputChars} chars / ${fingerprint.outputLines} lines match the previous identical call.]`
4743
+ }];
4744
+ resultDetails = isPlainRecord(resultDetails)
4745
+ ? {
4746
+ ...resultDetails,
4747
+ piagentDelta: {
4748
+ unchanged: true,
4749
+ previousRecordedAt: previousFingerprint?.recordedAt,
4750
+ outputHash: fingerprint.outputHash,
4751
+ originalChars: fingerprint.outputChars,
4752
+ originalLines: fingerprint.outputLines
4753
+ }
4754
+ }
4755
+ : {
4756
+ value: resultDetails,
4757
+ piagentDelta: {
4758
+ unchanged: true,
4759
+ previousRecordedAt: previousFingerprint?.recordedAt,
4760
+ outputHash: fingerprint.outputHash,
4761
+ originalChars: fingerprint.outputChars,
4762
+ originalLines: fingerprint.outputLines
4763
+ }
4764
+ };
4765
+ resultChanged = true;
4766
+ }
4767
+
4370
4768
  const captureCache = new Map<string, ToolResultCaptureSummary>();
4371
4769
  const compactedContent = compactToolResultTextContent(ctx.cwd, event, ctx, resultContent, captureCache);
4372
4770
  const compactionCaptures = [...compactedContent.captures];
@@ -4385,6 +4783,21 @@ export default function piagentGuard(pi: ExtensionAPI) {
4385
4783
  resultDetails = attachToolResultCompactionDetails(resultDetails, compactionCaptures);
4386
4784
  }
4387
4785
 
4786
+ telemetry(ctx, {
4787
+ event: "tool_result",
4788
+ toolName: event.toolName,
4789
+ inputHash: fingerprint.inputHash,
4790
+ outputHash: fingerprint.outputHash,
4791
+ outputChars: fingerprint.outputChars,
4792
+ outputLines: fingerprint.outputLines,
4793
+ repeated,
4794
+ compacted: compactionCaptures.length > 0,
4795
+ compactedCaptures: compactionCaptures.length,
4796
+ sensitiveValuesRedacted,
4797
+ isError: event.isError,
4798
+ usage: event.usage
4799
+ });
4800
+
4388
4801
  if (resultChanged) {
4389
4802
  return resultDetails === undefined
4390
4803
  ? { content: resultContent }
@@ -4393,6 +4806,15 @@ export default function piagentGuard(pi: ExtensionAPI) {
4393
4806
  });
4394
4807
 
4395
4808
  pi.on("tool_call", async (event, ctx) => {
4809
+ const callFingerprint = toolResultFingerprint(event.toolName, event.input, []);
4810
+ const target = extractLikelyPathFromInput(ctx.cwd, event.input as Record<string, unknown>);
4811
+ telemetry(ctx, {
4812
+ event: "tool_call",
4813
+ toolName: event.toolName,
4814
+ inputHash: callFingerprint.inputHash,
4815
+ targetHash: target ? crypto.createHash("sha256").update(target).digest("hex") : undefined,
4816
+ targetPath: target ? redactText(target) : undefined
4817
+ });
4396
4818
  const projectTrusted = ctx.isProjectTrusted();
4397
4819
  const capabilityState = verifyProjectCapabilityState(extensionDir, ctx.cwd, projectTrusted);
4398
4820
  const recoveryTools = new Set(["piagent_profile_options", "piagent_profile_apply", "piagent_context", "read", "grep", "find", "ls"]);
@@ -4555,6 +4977,148 @@ export default function piagentGuard(pi: ExtensionAPI) {
4555
4977
  }
4556
4978
  });
4557
4979
 
4980
+ pi.registerTool({
4981
+ name: "piagent_tools",
4982
+ label: "Piagent Tool Loader",
4983
+ description: "Activate an additional Piagent tool group only when the current task needs it.",
4984
+ promptSnippet: "Load governance, policy, retrieval, knowledge, onboarding, or usage tools on demand.",
4985
+ promptGuidelines: [
4986
+ "Load the smallest group that resolves the current need; do not activate every group."
4987
+ ],
4988
+ parameters: Type.Object({
4989
+ groups: Type.Array(StringEnum(["governance", "policy", "retrieval", "knowledge", "onboarding", "usage"] as const), { minItems: 1 })
4990
+ }),
4991
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
4992
+ const groups = [...new Set(params.groups)] as PiagentToolGroup[];
4993
+ if (!dynamicToolsEnabled) {
4994
+ return {
4995
+ content: [{ type: "text", text: "Dynamic Piagent tool loading is disabled by PIAGENT_DYNAMIC_TOOLS." }],
4996
+ details: { groups, disabled: true, activePiagentTools: pi.getActiveTools().filter((toolName) => PIAGENT_TOOL_NAMES.has(toolName)) }
4997
+ };
4998
+ }
4999
+ const activeTools = activateToolGroups(ctx, groups, true);
5000
+ return {
5001
+ content: [{
5002
+ type: "text",
5003
+ text: `Piagent tools activated: ${groups.join(", ")}. Active Piagent tools: ${activeTools.filter((toolName) => PIAGENT_TOOL_NAMES.has(toolName)).length}.`
5004
+ }],
5005
+ details: {
5006
+ groups,
5007
+ activePiagentTools: activeTools.filter((toolName) => PIAGENT_TOOL_NAMES.has(toolName))
5008
+ }
5009
+ };
5010
+ }
5011
+ });
5012
+
5013
+ pi.registerTool({
5014
+ name: "piagent_context_engine",
5015
+ label: "Pi Context Engine",
5016
+ description: "Build or query the local code index, return a token-budgeted context pack, map impacted tests, or report context efficiency.",
5017
+ promptSnippet: "Use this instead of broad repository scouting when the task needs code navigation.",
5018
+ promptGuidelines: [
5019
+ "Prefer pack for an unfamiliar task, search for a named symbol/path, impact before targeted verification, and efficiency only for usage analysis.",
5020
+ "Index evidence is advisory; re-read selected files before editing.",
5021
+ "Run one bounded finder pass only when pack confidence is low."
5022
+ ],
5023
+ parameters: Type.Object({
5024
+ action: StringEnum(["status", "rebuild", "search", "pack", "impact", "efficiency"] as const),
5025
+ query: Type.Optional(Type.String()),
5026
+ files: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
5027
+ budgetTokens: Type.Optional(Type.Number({ minimum: 200, maximum: 50000 })),
5028
+ refresh: Type.Optional(Type.Boolean())
5029
+ }),
5030
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
5031
+ const profile = loadProfileFromContext(ctx);
5032
+ const pathPolicy = effectiveProtectedPaths(policy, profile);
5033
+ const excludePatterns = Array.from(new Set([
5034
+ ...pathPolicy.readProtectedPaths,
5035
+ ...pathPolicy.writeProtectedPaths,
5036
+ ".pi/context-index.json",
5037
+ ".pi/piagent-state/**"
5038
+ ]));
5039
+ let result: unknown;
5040
+ let text: string;
5041
+ if (params.action === "status") {
5042
+ result = await contextIndexV2Status(ctx.cwd);
5043
+ const status = result as Awaited<ReturnType<typeof contextIndexV2Status>>;
5044
+ text = [
5045
+ `indexV2: ${status.exists ? "ready" : "missing"}`,
5046
+ `path: ${status.path}`,
5047
+ `files: ${status.files ?? 0}`,
5048
+ `symbols: ${status.symbols ?? 0}`,
5049
+ `imports: ${status.imports ?? 0}`,
5050
+ `stale: ${status.stale ? "yes" : "no"}`,
5051
+ `warnings: ${status.warnings.join("; ") || "none"}`
5052
+ ].join("\n");
5053
+ } else if (params.action === "rebuild") {
5054
+ result = await buildContextIndexV2(ctx.cwd, { excludePatterns });
5055
+ const built = result as Awaited<ReturnType<typeof buildContextIndexV2>>;
5056
+ text = [
5057
+ "indexV2: rebuilt",
5058
+ `files: ${built.files}; symbols: ${built.symbols}; imports: ${built.imports}`,
5059
+ `changed: ${built.changed}; removed: ${built.removed}`,
5060
+ `skipped: ${built.skippedLarge} large, ${built.skippedBinary} binary`,
5061
+ `duration: ${built.durationMs}ms`
5062
+ ].join("\n");
5063
+ } else if (params.action === "search") {
5064
+ if (!params.query?.trim()) throw new Error("query is required for context search");
5065
+ let status = await contextIndexV2Status(ctx.cwd);
5066
+ if (params.refresh || !status.exists) {
5067
+ await buildContextIndexV2(ctx.cwd, { excludePatterns });
5068
+ status = await contextIndexV2Status(ctx.cwd);
5069
+ }
5070
+ result = await searchContextIndexV2(ctx.cwd, params.query, { limit: 15 });
5071
+ const search = result as Awaited<ReturnType<typeof searchContextIndexV2>>;
5072
+ text = [
5073
+ `confidence: ${search.confidence}`,
5074
+ `indexStale: ${status.stale ? "yes" : "no"}`,
5075
+ ...search.results.map((item) => `- ${item.path}: ${item.sources.join("+")}; ${item.symbols.slice(0, 4).map((symbol) => `${symbol.name}@${symbol.line}`).join(", ") || "no symbols"}`)
5076
+ ].join("\n");
5077
+ } else if (params.action === "pack") {
5078
+ if (!params.query?.trim()) throw new Error("query is required for a context pack");
5079
+ let status = await contextIndexV2Status(ctx.cwd);
5080
+ if (params.refresh || !status.exists) {
5081
+ await buildContextIndexV2(ctx.cwd, { excludePatterns });
5082
+ status = await contextIndexV2Status(ctx.cwd);
5083
+ }
5084
+ const pack = await buildContextPack(ctx.cwd, params.query, {
5085
+ budgetTokens: params.budgetTokens ?? 6_000,
5086
+ includeCode: true,
5087
+ limit: 18
5088
+ });
5089
+ result = { ...pack, text: undefined, status };
5090
+ text = pack.text;
5091
+ } else if (params.action === "impact") {
5092
+ result = await buildTestImpact(ctx.cwd, params.files ?? []);
5093
+ const impact = result as Awaited<ReturnType<typeof buildTestImpact>>;
5094
+ text = [
5095
+ `changed: ${impact.changedFiles.join(", ") || "none"}`,
5096
+ `impacted: ${impact.impactedFiles.map((item) => `${item.path} via ${item.via}`).join(", ") || "none"}`,
5097
+ `tests: ${impact.tests.join(", ") || "none"}`
5098
+ ].join("\n");
5099
+ } else {
5100
+ result = buildContextEfficiencyReport(ctx.cwd);
5101
+ const report = result as ReturnType<typeof buildContextEfficiencyReport>;
5102
+ text = [
5103
+ `contextWasteScore: ${report.metrics.contextWasteScore}/100 (lower is better)`,
5104
+ `activeTools: ${report.metrics.averageActiveTools}`,
5105
+ `toolSchemaShare: ${formatPercent(report.metrics.toolSchemaShare)}`,
5106
+ `duplicateReads: ${report.metrics.duplicateReads}/${report.metrics.readCalls}`,
5107
+ `duplicateOutput: ${formatPercent(report.metrics.duplicateOutputRate)}`,
5108
+ `lowConfidencePacks: ${report.metrics.lowConfidencePacks}/${report.sample.contextPacks}`,
5109
+ ...report.recommendations.map((recommendation) => `- ${recommendation}`)
5110
+ ].join("\n");
5111
+ }
5112
+ telemetry(ctx, {
5113
+ event: "context_engine_action",
5114
+ action: params.action,
5115
+ queryHash: params.query ? crypto.createHash("sha256").update(params.query).digest("hex") : undefined,
5116
+ fileCount: params.files?.length ?? 0
5117
+ });
5118
+ return { content: [{ type: "text", text }], details: result };
5119
+ }
5120
+ });
5121
+
4558
5122
  pi.registerTool({
4559
5123
  name: "piagent_context",
4560
5124
  label: "Piagent Context",
@@ -4573,6 +5137,12 @@ export default function piagentGuard(pi: ExtensionAPI) {
4573
5137
  const pathPolicy = effectiveProtectedPaths(policy, profile);
4574
5138
  const orchestrationPolicy = resolveOrchestrationPolicy(profile, policy);
4575
5139
  const contextIndex = buildContextIndexStatus(ctx.cwd, profile);
5140
+ let contextEngine: Awaited<ReturnType<typeof contextIndexV2Status>> | { exists: false; warnings: string[] };
5141
+ try {
5142
+ contextEngine = await contextIndexV2Status(ctx.cwd);
5143
+ } catch (error) {
5144
+ contextEngine = { exists: false, warnings: [error instanceof Error ? error.message : String(error)] };
5145
+ }
4576
5146
  const requiredContext = [
4577
5147
  ...policy.defaultRequiredContext,
4578
5148
  ...(profile.requiredContext ?? [])
@@ -4596,6 +5166,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4596
5166
  exists: fs.existsSync(projectContextFilePath(ctx.cwd))
4597
5167
  },
4598
5168
  contextIndex,
5169
+ contextEngine,
4599
5170
  protectedPaths: profile.protectedPaths ?? [],
4600
5171
  shellProtectedPaths: profile.shellProtectedPaths ?? profile.protectedPaths ?? [],
4601
5172
  readOnlyPaths: profile.readOnlyPaths ?? [],
@@ -4631,6 +5202,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
4631
5202
  `profile: ${payload.profile.path} (${payload.profile.exists ? "exists" : "missing"})`,
4632
5203
  `projectContext: ${payload.projectContext.path} (${payload.projectContext.exists ? "exists" : "missing"})`,
4633
5204
  `contextIndex: ${payload.contextIndex.path} (${payload.contextIndex.exists ? `${payload.contextIndex.nodes} nodes` : "missing"})`,
5205
+ `contextEngine: ${payload.contextEngine.exists ? `${payload.contextEngine.files ?? 0} files / ${payload.contextEngine.symbols ?? 0} symbols${payload.contextEngine.stale ? " / stale" : ""}` : "missing"}`,
4634
5206
  `requiredContext: ${payload.requiredContext.join(", ") || "none"}`,
4635
5207
  `verifyCommands: ${Object.keys(payload.verifyCommands).join(", ") || "none"}`,
4636
5208
  `mcpCapabilities: ${payload.mcpCapabilities.join(", ") || "none"}`,
@@ -5368,12 +5940,30 @@ export default function piagentGuard(pi: ExtensionAPI) {
5368
5940
  } catch (error) {
5369
5941
  contextIndexError = error instanceof Error ? error.message : String(error);
5370
5942
  }
5943
+ let contextEngine: Awaited<ReturnType<typeof buildContextIndexV2>> | undefined;
5944
+ let contextEngineError: string | undefined;
5945
+ try {
5946
+ const pathPolicy = effectiveProtectedPaths(policy, profile);
5947
+ contextEngine = await buildContextIndexV2(ctx.cwd, {
5948
+ excludePatterns: Array.from(new Set([
5949
+ ...pathPolicy.readProtectedPaths,
5950
+ ...pathPolicy.writeProtectedPaths,
5951
+ ".pi/context-index.json",
5952
+ ".pi/piagent-state/**"
5953
+ ]))
5954
+ });
5955
+ } catch (error) {
5956
+ contextEngineError = error instanceof Error ? error.message : String(error);
5957
+ }
5371
5958
  appendTrace(ctx.cwd, { event: "project_onboarding_record", contextFile: snapshot.contextFile, sourceFiles: params.sourceFiles, contextIndex: contextIndex?.policy.path, contextIndexError });
5372
5959
  appendSessionTrace(pi, { event: "project_onboarding_record", contextFile: snapshot.contextFile, sourceFiles: params.sourceFiles, contextIndex: contextIndex?.policy.path, contextIndexError });
5373
5960
 
5374
5961
  return {
5375
- content: [{ type: "text", text: `Project onboarding snapshot recorded: .pi/project-context.md${contextIndex ? ` and ${contextIndex.policy.path}` : contextIndexError ? ` (context index skipped: ${contextIndexError})` : ""}` }],
5376
- details: { ...snapshot, contextIndex, contextIndexError }
5962
+ content: [{
5963
+ type: "text",
5964
+ text: `Project onboarding snapshot recorded: .pi/project-context.md${contextIndex ? ` and ${contextIndex.policy.path}` : contextIndexError ? ` (context index skipped: ${contextIndexError})` : ""}${contextEngine ? `; Context Engine indexed ${contextEngine.files} files / ${contextEngine.symbols} symbols` : contextEngineError ? `; Context Engine skipped: ${contextEngineError}` : ""}`
5965
+ }],
5966
+ details: { ...snapshot, contextIndex, contextIndexError, contextEngine, contextEngineError }
5377
5967
  };
5378
5968
  }
5379
5969
  });
@@ -5385,7 +5975,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
5385
5975
  promptSnippet: "Start a governed implementation task and persist the task contract.",
5386
5976
  promptGuidelines: [
5387
5977
  "Call this before source edits in a project managed by Pi Agent Platform.",
5388
- "Use piagent_context first when possible."
5978
+ "Use piagent_context first when possible.",
5979
+ "Use tiny for a bounded low-risk change, normal for ordinary multi-file work, and high-risk for security, data, release, migration, or external-impact work.",
5980
+ "Tiny tasks should stay on the compact parent-only work plan unless the caller supplies a justified custom plan."
5389
5981
  ],
5390
5982
  parameters: Type.Object({
5391
5983
  taskId: Type.Optional(Type.String({ minLength: 1 })),
@@ -6638,17 +7230,129 @@ export default function piagentGuard(pi: ExtensionAPI) {
6638
7230
  ].join("\n"), status);
6639
7231
  }
6640
7232
 
7233
+ async function emitContextEngineStatus(ctx: ExtensionContext): Promise<void> {
7234
+ try {
7235
+ const status = await contextIndexV2Status(ctx.cwd);
7236
+ emitRuntimeMessage(ctx, "piagent-context-engine-status", [
7237
+ `contextEngine: ${status.exists ? "ready" : "missing"}`,
7238
+ `path: ${status.path}`,
7239
+ `files: ${status.files ?? 0}`,
7240
+ `symbols: ${status.symbols ?? 0}`,
7241
+ `imports: ${status.imports ?? 0}`,
7242
+ `builtAt: ${status.builtAt ?? "never"}`,
7243
+ `stale: ${status.stale ? "yes" : "no"}`,
7244
+ `warnings: ${status.warnings.join("; ") || "none"}`,
7245
+ "rebuild: /context rebuild"
7246
+ ].join("\n"), status);
7247
+ } catch (error) {
7248
+ emitRuntimeMessage(ctx, "piagent-context-engine-status", `Context Engine status failed: ${error instanceof Error ? error.message : String(error)}`);
7249
+ }
7250
+ }
7251
+
7252
+ async function rebuildContextEngine(ctx: ExtensionContext): Promise<void> {
7253
+ const profile = loadProfileFromContext(ctx);
7254
+ const pathPolicy = effectiveProtectedPaths(policy, profile);
7255
+ try {
7256
+ const result = await buildContextIndexV2(ctx.cwd, {
7257
+ excludePatterns: Array.from(new Set([
7258
+ ...pathPolicy.readProtectedPaths,
7259
+ ...pathPolicy.writeProtectedPaths,
7260
+ ".pi/context-index.json",
7261
+ ".pi/piagent-state/**"
7262
+ ]))
7263
+ });
7264
+ telemetry(ctx, { event: "context_engine_action", action: "rebuild", ...result });
7265
+ emitRuntimeMessage(ctx, "piagent-context-engine-rebuild", [
7266
+ "contextEngine: rebuilt",
7267
+ `files: ${result.files}; symbols: ${result.symbols}; imports: ${result.imports}`,
7268
+ `changed: ${result.changed}; removed: ${result.removed}`,
7269
+ `skipped: ${result.skippedLarge} large, ${result.skippedBinary} binary`,
7270
+ `duration: ${result.durationMs}ms`
7271
+ ].join("\n"), result);
7272
+ } catch (error) {
7273
+ emitRuntimeMessage(ctx, "piagent-context-engine-rebuild", `Context Engine rebuild failed: ${error instanceof Error ? error.message : String(error)}`);
7274
+ }
7275
+ }
7276
+
7277
+ async function emitContextEngineSearch(ctx: ExtensionContext, query: string): Promise<boolean> {
7278
+ const status = await contextIndexV2Status(ctx.cwd);
7279
+ if (!status.exists || !query.trim()) return false;
7280
+ const search = await searchContextIndexV2(ctx.cwd, query, { limit: 12 });
7281
+ emitRuntimeMessage(ctx, "piagent-context-engine-search", [
7282
+ `confidence: ${search.confidence}`,
7283
+ `stale: ${status.stale ? "yes" : "no"}`,
7284
+ ...search.results.map((item) => `- ${item.path}: ${item.sources.join("+")}; ${item.symbols.slice(0, 4).map((symbol) => `${symbol.name}@${symbol.line}`).join(", ") || "no symbols"}`)
7285
+ ].join("\n"), { queryHash: crypto.createHash("sha256").update(query).digest("hex"), search });
7286
+ return true;
7287
+ }
7288
+
7289
+ async function emitContextPack(ctx: ExtensionContext, query: string): Promise<void> {
7290
+ if (!query.trim()) {
7291
+ emitRuntimeMessage(ctx, "piagent-context-pack-help", "Usage: /context pack <task or symbol>");
7292
+ return;
7293
+ }
7294
+ const status = await contextIndexV2Status(ctx.cwd);
7295
+ if (!status.exists) {
7296
+ emitRuntimeMessage(ctx, "piagent-context-pack-help", "Context Engine index is missing. Run /context rebuild first.");
7297
+ return;
7298
+ }
7299
+ const pack = await buildContextPack(ctx.cwd, query, { budgetTokens: 1_500, includeCode: false, limit: 15 });
7300
+ telemetry(ctx, {
7301
+ event: "context_pack",
7302
+ queryHash: pack.queryHash,
7303
+ confidence: pack.confidence,
7304
+ candidates: pack.candidates,
7305
+ selected: pack.selected.length,
7306
+ estimatedTokens: pack.estimatedTokens,
7307
+ selectedPaths: pack.selected.map((item) => item.path),
7308
+ source: "command"
7309
+ });
7310
+ emitRuntimeMessage(ctx, "piagent-context-pack-v2", pack.text, {
7311
+ queryHash: pack.queryHash,
7312
+ confidence: pack.confidence,
7313
+ estimatedTokens: pack.estimatedTokens,
7314
+ paths: pack.selected.map((item) => item.path),
7315
+ finderRecommended: pack.finderRecommended
7316
+ });
7317
+ }
7318
+
7319
+ async function emitTestImpact(ctx: ExtensionContext, raw: string): Promise<void> {
7320
+ const files = raw.split(/\s+/).map((file) => file.trim()).filter(Boolean);
7321
+ const impact = await buildTestImpact(ctx.cwd, files);
7322
+ emitRuntimeMessage(ctx, "piagent-context-impact", [
7323
+ `changed: ${impact.changedFiles.join(", ") || "none"}`,
7324
+ `impacted: ${impact.impactedFiles.map((item) => `${item.path} via ${item.via}`).join(", ") || "none"}`,
7325
+ `tests: ${impact.tests.join(", ") || "none"}`
7326
+ ].join("\n"), impact);
7327
+ }
7328
+
7329
+ function emitContextEfficiency(ctx: ExtensionContext): void {
7330
+ const report = buildContextEfficiencyReport(ctx.cwd);
7331
+ emitRuntimeMessage(ctx, "piagent-context-efficiency", [
7332
+ `contextWasteScore: ${report.metrics.contextWasteScore}/100 (lower is better)`,
7333
+ `activeTools: ${report.metrics.averageActiveTools}`,
7334
+ `toolSchemaShare: ${formatPercent(report.metrics.toolSchemaShare)}`,
7335
+ `duplicateReads: ${report.metrics.duplicateReads}/${report.metrics.readCalls}`,
7336
+ `duplicateOutput: ${formatPercent(report.metrics.duplicateOutputRate)}`,
7337
+ `lowConfidencePacks: ${report.metrics.lowConfidencePacks}/${report.sample.contextPacks}`,
7338
+ ...report.recommendations.map((recommendation) => `- ${recommendation}`)
7339
+ ].join("\n"), report);
7340
+ }
7341
+
6641
7342
  function parsePreflightWorkflow(raw: string): string {
6642
7343
  return raw.match(/\b(?:scout|be-to-fe|review|plan|platform-improve|task)\b/i)?.[0]?.toLowerCase() ?? "task";
6643
7344
  }
6644
7345
 
6645
7346
  function compactCurrentSession(ctx: ExtensionContext): void {
7347
+ const instructions = semanticCompactionInstructions(ctx.cwd);
7348
+ telemetry(ctx, {
7349
+ event: "compaction_requested",
7350
+ mode: "semantic",
7351
+ instructionTokens: estimateContextTokens(instructions),
7352
+ hasTaskContract: Boolean(compactSessionTask(ctx.cwd))
7353
+ });
6646
7354
  ctx.compact({
6647
- customInstructions: [
6648
- "Preserve current task decisions, project constraints, changed files, verify commands, open blockers, and next action.",
6649
- "Drop repeated mandatory-flow boilerplate and stale exploration details.",
6650
- "After compaction, required project context must be re-read from current repository files before implementation."
6651
- ].join("\n")
7355
+ customInstructions: instructions
6652
7356
  });
6653
7357
  }
6654
7358
 
@@ -6668,7 +7372,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
6668
7372
  const { action, rest } = commandArgs(raw);
6669
7373
  if (!action) {
6670
7374
  const chosen = await selectRuntimeAction(ctx, "Piagent context", [
6671
- { value: "index", label: "Context index", description: "Show compact project map", recommended: true },
7375
+ { value: "index", label: "Index status", description: "Architecture map plus local code index", recommended: true },
7376
+ { value: "pack", label: "Context pack", description: "Rank paths and symbols for a task" },
7377
+ { value: "impact", label: "Test impact", description: "Map changed files to dependents and tests" },
7378
+ { value: "efficiency", label: "Efficiency", description: "Show transparent context waste metrics" },
7379
+ { value: "rebuild", label: "Rebuild index", description: "Incrementally refresh changed files" },
6672
7380
  { value: "preflight", label: "Preflight", description: "Check whether to run, compact, or fresh-session" },
6673
7381
  { value: "compact", label: "Compact", description: "Compact current session with Piagent carry-over rules" },
6674
7382
  { value: "search", label: "Search index", description: "Use /context search <keyword>" },
@@ -6676,6 +7384,27 @@ export default function piagentGuard(pi: ExtensionAPI) {
6676
7384
  ], "index");
6677
7385
  if (chosen === "index") {
6678
7386
  emitContextIndexStatus(ctx);
7387
+ await emitContextEngineStatus(ctx);
7388
+ return;
7389
+ }
7390
+ if (chosen === "rebuild") {
7391
+ await rebuildContextEngine(ctx);
7392
+ return;
7393
+ }
7394
+ if (chosen === "efficiency") {
7395
+ emitContextEfficiency(ctx);
7396
+ return;
7397
+ }
7398
+ if (chosen === "pack") {
7399
+ await emitContextPack(ctx, "");
7400
+ return;
7401
+ }
7402
+ if (chosen === "impact") {
7403
+ await emitTestImpact(ctx, "");
7404
+ return;
7405
+ }
7406
+ if (chosen === "search") {
7407
+ emitRuntimeMessage(ctx, "piagent-context-search-help", "Usage: /context search <keyword or symbol>");
6679
7408
  return;
6680
7409
  }
6681
7410
  if (chosen === "preflight") {
@@ -6689,7 +7418,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
6689
7418
  emitRuntimeMessage(ctx, "piagent-context-help", [
6690
7419
  "namespace: /context",
6691
7420
  "index: /context index",
7421
+ "rebuild: /context rebuild",
6692
7422
  "search: /context search <keyword>",
7423
+ "pack: /context pack <task or symbol>",
7424
+ "impact: /context impact [changed files]",
7425
+ "efficiency: /context efficiency",
6693
7426
  "preflight: /context preflight [task|scout|be-to-fe|review|plan]",
6694
7427
  "compact: /context compact [task|scout|be-to-fe]",
6695
7428
  "legacy: /piagent-context | /context-index | /task-preflight"
@@ -6698,10 +7431,31 @@ export default function piagentGuard(pi: ExtensionAPI) {
6698
7431
  }
6699
7432
  if (["index", "status", "show", "current"].includes(action)) {
6700
7433
  emitContextIndexStatus(ctx);
7434
+ await emitContextEngineStatus(ctx);
7435
+ return;
7436
+ }
7437
+ if (["rebuild", "build", "refresh"].includes(action)) {
7438
+ await rebuildContextEngine(ctx);
6701
7439
  return;
6702
7440
  }
6703
7441
  if (action === "search") {
6704
- emitContextIndexSearch(ctx, rest);
7442
+ try {
7443
+ if (!await emitContextEngineSearch(ctx, rest)) emitContextIndexSearch(ctx, rest);
7444
+ } catch {
7445
+ emitContextIndexSearch(ctx, rest);
7446
+ }
7447
+ return;
7448
+ }
7449
+ if (action === "pack") {
7450
+ await emitContextPack(ctx, rest);
7451
+ return;
7452
+ }
7453
+ if (["impact", "tests"].includes(action)) {
7454
+ await emitTestImpact(ctx, rest);
7455
+ return;
7456
+ }
7457
+ if (["efficiency", "stats", "waste"].includes(action)) {
7458
+ emitContextEfficiency(ctx);
6705
7459
  return;
6706
7460
  }
6707
7461
  if (["preflight", "check"].includes(action)) {
@@ -6716,7 +7470,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
6716
7470
  emitRuntimeMessage(ctx, "piagent-context-help", [
6717
7471
  "namespace: /context",
6718
7472
  "index: /context index",
7473
+ "rebuild: /context rebuild",
6719
7474
  "search: /context search <keyword>",
7475
+ "pack: /context pack <task or symbol>",
7476
+ "impact: /context impact [changed files]",
7477
+ "efficiency: /context efficiency",
6720
7478
  "preflight: /context preflight [task|scout|be-to-fe|review|plan]",
6721
7479
  "compact: /context compact [task|scout|be-to-fe]",
6722
7480
  "legacy: /piagent-context | /context-index | /task-preflight"
@@ -6729,7 +7487,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
6729
7487
  pi.registerCommand("piagent-context", {
6730
7488
  description: "Legacy alias for /context",
6731
7489
  getArgumentCompletions: (prefix: string) => {
6732
- const actions = ["index", "search", "preflight", "compact", "help"];
7490
+ const actions = ["index", "rebuild", "search", "pack", "impact", "efficiency", "preflight", "compact", "help"];
6733
7491
  const typed = String(prefix ?? "").trim().toLowerCase();
6734
7492
  return actions.filter((action) => action.startsWith(typed)).map((action) => ({ value: action, label: action }));
6735
7493
  },
@@ -6739,9 +7497,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
6739
7497
  });
6740
7498
 
6741
7499
  pi.registerCommand("context", {
6742
- description: "Context index, search, preflight, and compact controls without a model follow-up",
7500
+ description: "Context index, retrieval pack, test impact, efficiency, preflight, and compact controls without a model follow-up",
6743
7501
  getArgumentCompletions: (prefix: string) => {
6744
- const actions = ["index", "search", "preflight", "compact", "help"];
7502
+ const actions = ["index", "rebuild", "search", "pack", "impact", "efficiency", "preflight", "compact", "help"];
6745
7503
  const typed = String(prefix ?? "").trim().toLowerCase();
6746
7504
  return actions.filter((action) => action.startsWith(typed)).map((action) => ({ value: action, label: action }));
6747
7505
  },
@@ -6843,6 +7601,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
6843
7601
  { value: "preflight", label: "Preflight", description: "Check task/context health" },
6844
7602
  { value: "compact", label: "Compact", description: "Compact current session with Piagent carry-over rules" },
6845
7603
  { value: "logs", label: "Tool logs", description: "Recent compacted large tool outputs" },
7604
+ { value: "efficiency", label: "Efficiency", description: "Context waste score and causes" },
6846
7605
  { value: "help", label: "Help", description: "Show typed forms" }
6847
7606
  ], "live");
6848
7607
  if (chosen === "live") {
@@ -6865,6 +7624,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
6865
7624
  emitToolLogCaptures(ctx);
6866
7625
  return;
6867
7626
  }
7627
+ if (chosen === "efficiency") {
7628
+ emitContextEfficiency(ctx);
7629
+ return;
7630
+ }
6868
7631
  emitRuntimeMessage(ctx, "piagent-usage-help", [
6869
7632
  "namespace: /usage",
6870
7633
  "live: /usage live",
@@ -6872,6 +7635,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
6872
7635
  "preflight: /usage preflight [task|scout|be-to-fe]",
6873
7636
  "compact: /usage compact [task|scout|be-to-fe]",
6874
7637
  "logs: /usage logs",
7638
+ "efficiency: /usage efficiency",
6875
7639
  "native exact session: /session",
6876
7640
  "legacy: /piagent-usage | /task-preflight | /logs"
6877
7641
  ].join("\n"));
@@ -6897,10 +7661,14 @@ export default function piagentGuard(pi: ExtensionAPI) {
6897
7661
  emitToolLogCaptures(ctx);
6898
7662
  return;
6899
7663
  }
7664
+ if (["efficiency", "stats", "waste"].includes(action)) {
7665
+ emitContextEfficiency(ctx);
7666
+ return;
7667
+ }
6900
7668
  if (action === "help") {
6901
7669
  emitRuntimeMessage(ctx, "piagent-usage-help", [
6902
7670
  "namespace: /usage",
6903
- "live | history | preflight | compact | logs",
7671
+ "live | history | preflight | compact | logs | efficiency",
6904
7672
  "examples:",
6905
7673
  "/usage live",
6906
7674
  "/usage history",
@@ -7038,7 +7806,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
7038
7806
  pi.registerCommand("piagent-usage", {
7039
7807
  description: "Legacy alias for /usage",
7040
7808
  getArgumentCompletions: (prefix: string) => {
7041
- const actions = ["live", "history", "preflight", "compact", "logs", "help"];
7809
+ const actions = ["live", "history", "preflight", "compact", "logs", "efficiency", "help"];
7042
7810
  const typed = String(prefix ?? "").trim().toLowerCase();
7043
7811
  return actions.filter((action) => action.startsWith(typed)).map((action) => ({ value: action, label: action }));
7044
7812
  },
@@ -7050,7 +7818,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
7050
7818
  pi.registerCommand("usage", {
7051
7819
  description: "Usage namespace: live, history, preflight, compact, and logs without a model follow-up",
7052
7820
  getArgumentCompletions: (prefix: string) => {
7053
- const actions = ["live", "history", "preflight", "compact", "logs", "help"];
7821
+ const actions = ["live", "history", "preflight", "compact", "logs", "efficiency", "help"];
7054
7822
  const typed = String(prefix ?? "").trim().toLowerCase();
7055
7823
  return actions.filter((action) => action.startsWith(typed)).map((action) => ({ value: action, label: action }));
7056
7824
  },