@piagent/platform 1.2.1 → 1.2.2

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 (39) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/catalog/capabilities.json +4 -4
  4. package/docs/command-reference-vietnamese.md +9 -3
  5. package/docs/context-window-policy.md +1 -0
  6. package/docs/distribution-standard.md +7 -7
  7. package/docs/operator-manual-vietnamese.md +39 -10
  8. package/docs/publishing-for-teams.md +1 -0
  9. package/docs/quickstart-vietnamese.md +4 -2
  10. package/docs/release-install-policy.md +6 -6
  11. package/docs/security-threat-model.md +1 -1
  12. package/docs/team-onboarding.md +3 -3
  13. package/docs/usage-observability.md +20 -0
  14. package/docs-site/architecture.html +1 -1
  15. package/docs-site/commands.html +1 -1
  16. package/docs-site/content/quickstart.html +1 -1
  17. package/docs-site/index.html +1 -1
  18. package/docs-site/mcp-approval.html +1 -1
  19. package/docs-site/mcp-auth.html +1 -1
  20. package/docs-site/mcp-servers.html +1 -1
  21. package/docs-site/mcp.html +1 -1
  22. package/docs-site/permissions.html +1 -1
  23. package/docs-site/profiles.html +1 -1
  24. package/docs-site/quickstart.html +2 -2
  25. package/docs-site/runtime.html +1 -1
  26. package/docs-site/scope.html +1 -1
  27. package/docs-site/security.html +1 -1
  28. package/docs-site/team.html +1 -1
  29. package/docs-site/workflows.html +1 -1
  30. package/package.json +1 -1
  31. package/packages/piagent-core/README.md +2 -0
  32. package/packages/piagent-core/extensions/piagent-guard.ts +406 -51
  33. package/packages/piagent-core/package.json +1 -1
  34. package/packages/piagent-core/prompts/be-to-fe.md +1 -0
  35. package/packages/piagent-core/prompts/piagent-commands.md +1 -0
  36. package/packages/piagent-core/prompts/platform-improve.md +1 -0
  37. package/packages/piagent-core/prompts/review.md +1 -0
  38. package/packages/piagent-core/prompts/scout.md +1 -0
  39. package/packages/piagent-core/prompts/task.md +1 -0
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.2 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.2 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/platform",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -11,6 +11,7 @@ Shared Pi package for reusable project workflows.
11
11
  - `subagents/*.md`: piagent roles for `pi-subagents`.
12
12
  - `policies/base-policy.json`: default runtime policy, including protected path and shell protected path defaults.
13
13
  - input hook support for local screenshot/image paths pasted into chat; supported images are attached as `[image1]`, `[image2]`, ...
14
+ - compact tool-result rendering: oversized redacted output is previewed in Pi and captured under `.pi/piagent-state/tool-results/` for offline audit/reporting.
14
15
 
15
16
  ## Trusted run wrapper
16
17
 
@@ -84,6 +85,7 @@ Inside Pi, slash commands can switch the current session without writing the pro
84
85
  - `/scout`: governed read-only scout/audit workflow.
85
86
  - `/task`: governed implementation lifecycle.
86
87
  - `/task-preflight`: check whether the active session should run, compact, or start fresh before large work.
88
+ - `/piagent-logs`: show compact-log policy and recent oversized tool-output captures.
87
89
  - `/fresh-task`, `/fresh-scout`, `/fresh-be-to-fe`: start a fresh governed session and replay the compact workflow prompt.
88
90
  - `/commit`: create a guarded local commit from reviewed files only; no push.
89
91
  - `/pr`: prepare a pull request; push/PR creation still requires explicit operator confirmation.
@@ -128,6 +128,13 @@ const BOILERPLATE_COLLAPSE_CHARS = 300;
128
128
  const MAX_INLINE_COLLAPSED_TASK_CHARS = 2200;
129
129
  const MAX_CHAT_IMAGE_ATTACHMENTS = 4;
130
130
  const MAX_CHAT_IMAGE_BYTES = 8 * 1024 * 1024;
131
+ const TOOL_RESULT_COMPACT_CHAR_THRESHOLD = 12_000;
132
+ const TOOL_RESULT_COMPACT_LINE_THRESHOLD = 180;
133
+ const TOOL_RESULT_PREVIEW_HEAD_LINES = 24;
134
+ const TOOL_RESULT_PREVIEW_TAIL_LINES = 24;
135
+ const TOOL_RESULT_PREVIEW_INTERESTING_LINES = 24;
136
+ const TOOL_RESULT_PREVIEW_MAX_CHARS = 6_000;
137
+ const TOOL_RESULT_CAPTURE_MAX_CHARS = 500_000;
131
138
  const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp", "bmp"] as const;
132
139
  const ORCHESTRATION_MODES = ["solo-first", "bounded-subagents", "parallel-readonly"] as const;
133
140
  const REVIEW_LENSES = ["correctness", "tests", "scope", "security", "docs", "release", "package"] as const;
@@ -1488,6 +1495,14 @@ function observedBashLedgerPath(cwd: string): string {
1488
1495
  return path.join(stateRoot(cwd), "observed-bash.jsonl");
1489
1496
  }
1490
1497
 
1498
+ function toolResultCaptureRoot(cwd: string): string {
1499
+ return path.join(stateRoot(cwd), "tool-results");
1500
+ }
1501
+
1502
+ function toolResultCaptureIndexPath(cwd: string): string {
1503
+ return path.join(toolResultCaptureRoot(cwd), "index.jsonl");
1504
+ }
1505
+
1491
1506
  function safeTaskId(taskId: string): string {
1492
1507
  return taskId.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "task";
1493
1508
  }
@@ -3382,6 +3397,308 @@ function hasOperatorSessionName(name: string): boolean {
3382
3397
  return Boolean(normalized && normalized !== "session");
3383
3398
  }
3384
3399
 
3400
+ function cleanSessionNameInput(input: string): string {
3401
+ let name = input.trim();
3402
+ if ((name.startsWith("\"") && name.endsWith("\"")) || (name.startsWith("'") && name.endsWith("'"))) {
3403
+ name = name.slice(1, -1).trim();
3404
+ }
3405
+ return name;
3406
+ }
3407
+
3408
+ type ToolResultCaptureSummary = {
3409
+ path?: string;
3410
+ error?: string;
3411
+ source: string;
3412
+ toolName: string;
3413
+ originalChars: number;
3414
+ originalLines: number;
3415
+ previewChars?: number;
3416
+ storedChars?: number;
3417
+ storedTruncated?: boolean;
3418
+ sha256: string;
3419
+ };
3420
+
3421
+ function normalizeToolResultText(text: string): string {
3422
+ return text.replace(/\r\n/g, "\n");
3423
+ }
3424
+
3425
+ function toolResultLineCount(text: string): number {
3426
+ if (!text) return 0;
3427
+ return normalizeToolResultText(text).split("\n").length;
3428
+ }
3429
+
3430
+ function shouldCompactToolResultText(text: string): boolean {
3431
+ return text.length > TOOL_RESULT_COMPACT_CHAR_THRESHOLD
3432
+ || toolResultLineCount(text) > TOOL_RESULT_COMPACT_LINE_THRESHOLD;
3433
+ }
3434
+
3435
+ function clipPreviewLine(line: string, maxChars = 260): string {
3436
+ if (line.length <= maxChars) return line;
3437
+ return `${line.slice(0, maxChars).trimEnd()} ... [line clipped]`;
3438
+ }
3439
+
3440
+ function boundedPreview(text: string): string {
3441
+ if (text.length <= TOOL_RESULT_PREVIEW_MAX_CHARS) return text;
3442
+ const marker = "\n[Piagent preview shortened further to stay light.]\n";
3443
+ const edge = Math.floor((TOOL_RESULT_PREVIEW_MAX_CHARS - marker.length) / 2);
3444
+ return `${text.slice(0, edge).trimEnd()}${marker}${text.slice(-edge).trimStart()}`;
3445
+ }
3446
+
3447
+ function interestingToolResultLines(lines: string[]): string[] {
3448
+ 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;
3449
+ const seen = new Set<string>();
3450
+ const result: string[] = [];
3451
+ for (let index = 0; index < lines.length; index += 1) {
3452
+ const line = lines[index];
3453
+ if (!interesting.test(line)) continue;
3454
+ const rendered = `${index + 1}: ${clipPreviewLine(line)}`;
3455
+ if (seen.has(rendered)) continue;
3456
+ seen.add(rendered);
3457
+ result.push(rendered);
3458
+ if (result.length >= TOOL_RESULT_PREVIEW_INTERESTING_LINES) break;
3459
+ }
3460
+ return result;
3461
+ }
3462
+
3463
+ function storedToolResultText(text: string): { text: string; truncated: boolean } {
3464
+ if (text.length <= TOOL_RESULT_CAPTURE_MAX_CHARS) return { text, truncated: false };
3465
+ const marker = `\n\n[Piagent capture truncated: omitted ${formatCount(text.length - TOOL_RESULT_CAPTURE_MAX_CHARS)} chars from the middle.]\n\n`;
3466
+ const edge = Math.floor((TOOL_RESULT_CAPTURE_MAX_CHARS - marker.length) / 2);
3467
+ return {
3468
+ text: `${text.slice(0, edge).trimEnd()}${marker}${text.slice(-edge).trimStart()}`,
3469
+ truncated: true
3470
+ };
3471
+ }
3472
+
3473
+ function compactToolResultPreview(toolName: string, source: string, text: string, capture: ToolResultCaptureSummary): string {
3474
+ const normalized = normalizeToolResultText(text);
3475
+ const lines = normalized.split("\n");
3476
+ const head = lines.slice(0, TOOL_RESULT_PREVIEW_HEAD_LINES).map((line) => clipPreviewLine(line));
3477
+ const tailStart = Math.max(TOOL_RESULT_PREVIEW_HEAD_LINES, lines.length - TOOL_RESULT_PREVIEW_TAIL_LINES);
3478
+ const tail = lines.slice(tailStart).map((line) => clipPreviewLine(line));
3479
+ const omitted = Math.max(0, lines.length - head.length - tail.length);
3480
+ const interesting = interestingToolResultLines(lines);
3481
+ const captureLine = capture.path
3482
+ ? `capture: ${capture.path}${capture.storedTruncated ? " (stored head/tail sample; original too large)" : ""}`
3483
+ : `capture: unavailable (${capture.error ?? "write failed"})`;
3484
+ const sections = [
3485
+ `[Piagent compacted large ${toolName} output from ${source}: ${formatCount(capture.originalChars)} chars / ${formatCount(capture.originalLines)} lines.]`,
3486
+ `[${captureLine}]`,
3487
+ "[Preview keeps head, notable lines, and tail.]",
3488
+ "",
3489
+ "head:",
3490
+ ...head
3491
+ ];
3492
+ if (interesting.length > 0) {
3493
+ sections.push("", "notable:", ...interesting);
3494
+ }
3495
+ if (omitted > 0) {
3496
+ sections.push("", `... ${formatCount(omitted)} middle line${omitted === 1 ? "" : "s"} omitted ...`);
3497
+ }
3498
+ sections.push("", "tail:", ...tail);
3499
+ return boundedPreview(sections.join("\n"));
3500
+ }
3501
+
3502
+ function maybeNumber(value: unknown): number | undefined {
3503
+ if (typeof value === "number" && Number.isFinite(value)) return value;
3504
+ if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number.parseInt(value, 10);
3505
+ return undefined;
3506
+ }
3507
+
3508
+ function currentSessionField(ctx: ExtensionContext, field: "getSessionFile" | "getSessionId" | "getSessionName"): string | undefined {
3509
+ try {
3510
+ const value = ctx.sessionManager[field]?.();
3511
+ return value === undefined || value === null ? undefined : redactText(String(value));
3512
+ } catch {
3513
+ return undefined;
3514
+ }
3515
+ }
3516
+
3517
+ function writeToolResultCapture(
3518
+ cwd: string,
3519
+ event: any,
3520
+ ctx: ExtensionContext,
3521
+ source: string,
3522
+ text: string,
3523
+ cache: Map<string, ToolResultCaptureSummary>
3524
+ ): ToolResultCaptureSummary {
3525
+ const normalized = normalizeToolResultText(text);
3526
+ const sha256 = crypto.createHash("sha256").update(normalized).digest("hex");
3527
+ const originalLines = toolResultLineCount(normalized);
3528
+ const cached = cache.get(sha256);
3529
+ if (cached?.path) return { ...cached, source };
3530
+
3531
+ const toolName = String(event?.toolName ?? "tool");
3532
+ const recordedAt = nowIso();
3533
+ const date = recordedAt.slice(0, 10);
3534
+ const filename = `${recordedAt.replace(/[:.]/g, "-")}-${slugify(toolName)}-${sha256.slice(0, 12)}.log`;
3535
+ const relativePath = [".pi", "piagent-state", "tool-results", date, filename].join("/");
3536
+ const absolutePath = path.join(cwd, ".pi", "piagent-state", "tool-results", date, filename);
3537
+ const capture = storedToolResultText(normalized);
3538
+ const details = event?.details && typeof event.details === "object" ? event.details : {};
3539
+ const exitCode = maybeNumber(details.exitCode ?? details.status ?? event?.exitCode);
3540
+ const metadata = {
3541
+ schemaVersion: 1,
3542
+ recordedAt,
3543
+ cwd: redactText(cwd),
3544
+ sessionId: currentSessionField(ctx, "getSessionId"),
3545
+ sessionName: currentSessionField(ctx, "getSessionName"),
3546
+ sessionFile: currentSessionField(ctx, "getSessionFile"),
3547
+ toolName,
3548
+ source,
3549
+ isError: event?.isError === true,
3550
+ exitCode,
3551
+ input: redactForStorage(event?.input),
3552
+ path: relativePath,
3553
+ originalChars: normalized.length,
3554
+ originalLines,
3555
+ storedChars: capture.text.length,
3556
+ storedTruncated: capture.truncated,
3557
+ sha256
3558
+ };
3559
+
3560
+ try {
3561
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
3562
+ fs.writeFileSync(absolutePath, [
3563
+ "# Piagent compacted tool result capture",
3564
+ JSON.stringify(metadata),
3565
+ "---",
3566
+ capture.text
3567
+ ].join("\n"));
3568
+ fs.appendFileSync(toolResultCaptureIndexPath(cwd), `${JSON.stringify(metadata)}\n`);
3569
+ const summary = {
3570
+ path: relativePath,
3571
+ source,
3572
+ toolName,
3573
+ originalChars: normalized.length,
3574
+ originalLines,
3575
+ storedChars: capture.text.length,
3576
+ storedTruncated: capture.truncated,
3577
+ sha256
3578
+ };
3579
+ cache.set(sha256, summary);
3580
+ return summary;
3581
+ } catch (error) {
3582
+ const message = error instanceof Error ? error.message : String(error);
3583
+ const summary = {
3584
+ error: message,
3585
+ source,
3586
+ toolName,
3587
+ originalChars: normalized.length,
3588
+ originalLines,
3589
+ sha256
3590
+ };
3591
+ cache.set(sha256, summary);
3592
+ return summary;
3593
+ }
3594
+ }
3595
+
3596
+ function compactToolResultTextContent(
3597
+ cwd: string,
3598
+ event: any,
3599
+ ctx: ExtensionContext,
3600
+ content: unknown,
3601
+ cache: Map<string, ToolResultCaptureSummary>
3602
+ ): { content: unknown; captures: ToolResultCaptureSummary[] } {
3603
+ if (!Array.isArray(content)) return { content, captures: [] };
3604
+ const captures: ToolResultCaptureSummary[] = [];
3605
+ const compacted = content.map((block, index) => {
3606
+ if (!block || typeof block !== "object") return block;
3607
+ const typed = block as { type?: unknown; text?: unknown };
3608
+ if (typed.type !== "text" || typeof typed.text !== "string" || !shouldCompactToolResultText(typed.text)) return block;
3609
+ const source = `content[${index}].text`;
3610
+ const capture = writeToolResultCapture(cwd, event, ctx, source, typed.text, cache);
3611
+ captures.push(capture);
3612
+ return {
3613
+ ...block,
3614
+ text: compactToolResultPreview(String(event?.toolName ?? "tool"), source, typed.text, capture)
3615
+ };
3616
+ });
3617
+ return { content: compacted, captures };
3618
+ }
3619
+
3620
+ function compactToolResultDetails(
3621
+ cwd: string,
3622
+ event: any,
3623
+ ctx: ExtensionContext,
3624
+ value: unknown,
3625
+ cache: Map<string, ToolResultCaptureSummary>,
3626
+ captures: ToolResultCaptureSummary[],
3627
+ source = "details",
3628
+ depth = 0
3629
+ ): unknown {
3630
+ if (typeof value === "string") {
3631
+ if (!shouldCompactToolResultText(value)) return value;
3632
+ const capture = writeToolResultCapture(cwd, event, ctx, source, value, cache);
3633
+ captures.push(capture);
3634
+ return compactToolResultPreview(String(event?.toolName ?? "tool"), source, value, capture);
3635
+ }
3636
+ if (Array.isArray(value)) {
3637
+ if (depth >= 6) return value;
3638
+ return value.map((item, index) => compactToolResultDetails(cwd, event, ctx, item, cache, captures, `${source}[${index}]`, depth + 1));
3639
+ }
3640
+ if (isPlainRecord(value)) {
3641
+ if (depth >= 6) return value;
3642
+ return Object.fromEntries(
3643
+ Object.entries(value).map(([key, item]) => [
3644
+ key,
3645
+ compactToolResultDetails(cwd, event, ctx, item, cache, captures, `${source}.${key}`, depth + 1)
3646
+ ])
3647
+ );
3648
+ }
3649
+ return value;
3650
+ }
3651
+
3652
+ function attachToolResultCompactionDetails(details: unknown, captures: ToolResultCaptureSummary[]): unknown {
3653
+ if (captures.length === 0) return details;
3654
+ const compacted = captures.map((capture) => ({
3655
+ path: capture.path,
3656
+ error: capture.error,
3657
+ source: capture.source,
3658
+ toolName: capture.toolName,
3659
+ originalChars: capture.originalChars,
3660
+ originalLines: capture.originalLines,
3661
+ storedChars: capture.storedChars,
3662
+ storedTruncated: capture.storedTruncated,
3663
+ sha256: capture.sha256
3664
+ }));
3665
+ if (isPlainRecord(details)) return { ...details, piagentCompactedToolResults: compacted };
3666
+ if (details === undefined) return { piagentCompactedToolResults: compacted };
3667
+ return { value: details, piagentCompactedToolResults: compacted };
3668
+ }
3669
+
3670
+ function readRecentToolResultCaptures(cwd: string, limit = 5): Record<string, unknown>[] {
3671
+ const indexPath = toolResultCaptureIndexPath(cwd);
3672
+ if (!fs.existsSync(indexPath)) return [];
3673
+ const lines = fs.readFileSync(indexPath, "utf8").trim().split(/\n/).filter(Boolean);
3674
+ const captures: Record<string, unknown>[] = [];
3675
+ for (const line of lines.slice(-Math.max(limit * 3, limit))) {
3676
+ try {
3677
+ const parsed = JSON.parse(line);
3678
+ if (isPlainRecord(parsed)) captures.push(redactForStorage(parsed) as Record<string, unknown>);
3679
+ } catch {
3680
+ // A partial JSONL line should not make a status command noisy or unusable.
3681
+ }
3682
+ }
3683
+ return captures.slice(-limit);
3684
+ }
3685
+
3686
+ function formatToolResultCaptureStatus(cwd: string, captures: Record<string, unknown>[]): string {
3687
+ const root = normalizeRelative(cwd, toolResultCaptureRoot(cwd)) ?? ".pi/piagent-state/tool-results";
3688
+ const lines = [
3689
+ `logPolicy: compact above ${formatCount(TOOL_RESULT_COMPACT_CHAR_THRESHOLD)} chars or ${formatCount(TOOL_RESULT_COMPACT_LINE_THRESHOLD)} lines`,
3690
+ `captureRoot: ${root}`,
3691
+ `recent: ${captures.length || "none"}`
3692
+ ];
3693
+ for (const capture of captures) {
3694
+ const originalLines = typeof capture.originalLines === "number" ? formatCount(capture.originalLines) : "unknown";
3695
+ const originalChars = typeof capture.originalChars === "number" ? formatCount(capture.originalChars) : "unknown";
3696
+ const exit = capture.exitCode === undefined ? "" : ` exit=${capture.exitCode}`;
3697
+ lines.push(`- ${capture.recordedAt ?? "unknown"} ${capture.toolName ?? "tool"}${exit}: ${originalLines} lines / ${originalChars} chars -> ${capture.path ?? "no capture path"}`);
3698
+ }
3699
+ return lines.join("\n");
3700
+ }
3701
+
3385
3702
  function buildUsageSnapshot(ctx: ExtensionContext, thinkingLevel?: string): UsageSnapshot {
3386
3703
  const contextUsage = ctx.getContextUsage();
3387
3704
  const contextWithThinking = ctx as ExtensionContext & { getThinkingLevel?: () => string };
@@ -3420,30 +3737,13 @@ function formatUsageSnapshot(snapshot: UsageSnapshot): string {
3420
3737
  ? `${formatCount(snapshot.contextUsage.tokens)} / ${formatCount(snapshot.contextUsage.contextWindow)} tokens (${formatPercent(snapshot.contextUsage.percent)})`
3421
3738
  : "unavailable";
3422
3739
  return [
3423
- "# Piagent usage snapshot",
3424
- "",
3425
- `- Session: ${snapshot.sessionName ?? "unnamed"} (${snapshot.sessionId ?? "unknown"})`,
3426
- `- Session file: ${snapshot.sessionFile ?? "not persisted"}`,
3427
- `- CWD: ${snapshot.cwd}`,
3428
- `- Mode: ${snapshot.mode}`,
3429
- `- Model: ${snapshot.model}`,
3430
- `- Thinking: ${snapshot.thinkingLevel}`,
3431
- `- Entries: ${formatCount(snapshot.entries.branch)} on active branch / ${formatCount(snapshot.entries.total)} total`,
3432
- `- Live context: ${context}`,
3433
- "",
3434
- "Exact billed input/output/cache/cost totals are exposed by Pi via `/session` or RPC `get_session_stats`, not directly inside this command context.",
3435
- "",
3436
- "From another terminal:",
3437
- "",
3438
- "```bash",
3439
- "piagent-usage /path/to/project",
3440
- "```",
3441
- "",
3442
- "Historical totals / weekly report:",
3443
- "",
3444
- "```bash",
3445
- "piagent-usage --history /path/to/project --days 7",
3446
- "```"
3740
+ `usage: ${context}`,
3741
+ `session: ${snapshot.sessionName ?? "unnamed"} (${snapshot.sessionId ?? "unknown"})`,
3742
+ `model: ${snapshot.model}; thinking: ${snapshot.thinkingLevel}`,
3743
+ `entries: ${formatCount(snapshot.entries.branch)} active / ${formatCount(snapshot.entries.total)} total`,
3744
+ `file: ${snapshot.sessionFile ?? "not persisted"}`,
3745
+ "exact: /session | piagent-usage /path/to/project",
3746
+ "history: piagent-usage --history /path/to/project --days 7"
3447
3747
  ].join("\n");
3448
3748
  }
3449
3749
 
@@ -3510,30 +3810,14 @@ function formatContextPreflight(preflight: ContextPreflight, snapshot: UsageSnap
3510
3810
  ? `${formatCount(preflight.projectedContext.tokens)} (${formatPercent(preflight.projectedContext.percent)})`
3511
3811
  : "unavailable";
3512
3812
  return [
3513
- "# Piagent task preflight",
3514
- "",
3515
- `- Workflow: ${preflight.workflow}`,
3516
- `- Session: ${snapshot.sessionName ?? "unnamed"} (${snapshot.sessionId ?? "unknown"})`,
3517
- `- Model: ${snapshot.model}`,
3518
- `- Thinking: ${snapshot.thinkingLevel}`,
3519
- `- Entries: ${formatCount(snapshot.entries.branch)} active / ${formatCount(snapshot.entries.total)} total`,
3520
- `- Live context: ${live}`,
3521
- `- Incoming input estimate: ${formatCount(preflight.inputTokenEstimate)} tokens from ${formatCount(preflight.inputChars)} chars`,
3522
- `- Projected context: ${projected}`,
3523
- `- Recommendation: ${preflight.recommendation}`,
3524
- `- Reason: ${preflight.reason}`,
3525
- "",
3526
- "Commands:",
3527
- "",
3528
- "```text",
3529
- ...preflight.commands,
3530
- "```",
3531
- "",
3532
- "Notes:",
3533
- "",
3534
- "- Do not paste the full mandatory flow into every task. Platform prompts and piagent tools already carry it.",
3535
- "- Use `/scout` for read-only risk mapping. Use `/fresh-scout` when the current session is already heavy.",
3536
- "- If exact billed token/cost totals are needed, run `/session` in Pi or `piagent-usage <project-path>` from another terminal."
3813
+ `preflight: ${preflight.recommendation}`,
3814
+ `workflow: ${preflight.workflow}`,
3815
+ `session: ${snapshot.sessionName ?? "unnamed"} (${snapshot.sessionId ?? "unknown"})`,
3816
+ `model: ${snapshot.model}; thinking: ${snapshot.thinkingLevel}`,
3817
+ `context: ${live}; projected: ${projected}`,
3818
+ `input: ~${formatCount(preflight.inputTokenEstimate)} tokens from ${formatCount(preflight.inputChars)} chars`,
3819
+ `reason: ${preflight.reason}`,
3820
+ `next: ${preflight.commands.join(" | ")}`
3537
3821
  ].join("\n");
3538
3822
  }
3539
3823
 
@@ -3601,7 +3885,7 @@ function isPiagentWorkflowInput(text: string): boolean {
3601
3885
  }
3602
3886
 
3603
3887
  function isFreshOrUtilityInput(text: string): boolean {
3604
- return /^\/(?:fresh-task|fresh-scout|fresh-be-to-fe|task-preflight|piagent-usage|session|compact)\b/i.test(text.trim());
3888
+ return /^\/(?:fresh-task|fresh-scout|fresh-be-to-fe|task-preflight|piagent-usage|piagent-logs|setname|session|compact)\b/i.test(text.trim());
3605
3889
  }
3606
3890
 
3607
3891
  function taskInboxDir(cwd: string): string {
@@ -4073,6 +4357,24 @@ export default function piagentGuard(pi: ExtensionAPI) {
4073
4357
  resultChanged = true;
4074
4358
  }
4075
4359
 
4360
+ const captureCache = new Map<string, ToolResultCaptureSummary>();
4361
+ const compactedContent = compactToolResultTextContent(ctx.cwd, event, ctx, resultContent, captureCache);
4362
+ const compactionCaptures = [...compactedContent.captures];
4363
+ if (compactedContent.captures.length > 0) {
4364
+ resultContent = compactedContent.content;
4365
+ resultChanged = true;
4366
+ }
4367
+ if (resultDetails !== undefined) {
4368
+ const compactedDetails = compactToolResultDetails(ctx.cwd, event, ctx, resultDetails, captureCache, compactionCaptures);
4369
+ if (compactionCaptures.length > compactedContent.captures.length) {
4370
+ resultDetails = compactedDetails;
4371
+ resultChanged = true;
4372
+ }
4373
+ }
4374
+ if (compactionCaptures.length > 0) {
4375
+ resultDetails = attachToolResultCompactionDetails(resultDetails, compactionCaptures);
4376
+ }
4377
+
4076
4378
  if (resultChanged) {
4077
4379
  return resultDetails === undefined
4078
4380
  ? { content: resultContent }
@@ -4377,8 +4679,8 @@ export default function piagentGuard(pi: ExtensionAPI) {
4377
4679
  }
4378
4680
  });
4379
4681
 
4380
- pi.registerTool({
4381
- name: "piagent_exec_policy_check",
4682
+ pi.registerTool({
4683
+ name: "piagent_exec_policy_check",
4382
4684
  label: "Piagent Exec Policy Check",
4383
4685
  description: "Evaluate a shell command against piagent exec policy before running it.",
4384
4686
  promptSnippet: "Use this before high-impact, complex, generated, or unfamiliar shell commands.",
@@ -6261,6 +6563,59 @@ export default function piagentGuard(pi: ExtensionAPI) {
6261
6563
  }
6262
6564
  });
6263
6565
 
6566
+ pi.registerCommand("piagent-logs", {
6567
+ description: "Show compact log policy and recent captured large tool outputs",
6568
+ handler: async (_args, ctx) => {
6569
+ const captures = readRecentToolResultCaptures(ctx.cwd, 5);
6570
+ ctx.ui.notify(`Piagent logs: ${captures.length ? `${captures.length} recent capture(s)` : "no compacted captures yet"}`, "info");
6571
+ pi.sendMessage(
6572
+ {
6573
+ customType: "piagent-log-captures",
6574
+ content: formatToolResultCaptureStatus(ctx.cwd, captures),
6575
+ display: true,
6576
+ details: {
6577
+ policy: {
6578
+ compactAboveChars: TOOL_RESULT_COMPACT_CHAR_THRESHOLD,
6579
+ compactAboveLines: TOOL_RESULT_COMPACT_LINE_THRESHOLD,
6580
+ previewMaxChars: TOOL_RESULT_PREVIEW_MAX_CHARS,
6581
+ captureMaxChars: TOOL_RESULT_CAPTURE_MAX_CHARS
6582
+ },
6583
+ captures
6584
+ }
6585
+ },
6586
+ { triggerTurn: false }
6587
+ );
6588
+ }
6589
+ });
6590
+
6591
+ pi.registerCommand("setname", {
6592
+ description: "Set the current Pi session name for reports and resume lists",
6593
+ handler: async (args, ctx) => {
6594
+ const name = cleanSessionNameInput(String(args ?? ""));
6595
+ if (!name) {
6596
+ ctx.ui.notify("Usage: /setname <task/session name>", "warning");
6597
+ return;
6598
+ }
6599
+ const previousName = currentSessionName(ctx);
6600
+ pi.setSessionName(name);
6601
+ appendSessionTrace(pi, {
6602
+ event: "session_name_set",
6603
+ previousName: previousName || undefined,
6604
+ sessionName: name
6605
+ });
6606
+ ctx.ui.notify(`Session name set: ${name}`, "info");
6607
+ pi.sendMessage(
6608
+ {
6609
+ customType: "piagent-session-name-set",
6610
+ content: `sessionName: ${name}`,
6611
+ display: true,
6612
+ details: { sessionName: name, previousName: previousName || undefined }
6613
+ },
6614
+ { triggerTurn: false }
6615
+ );
6616
+ }
6617
+ });
6618
+
6264
6619
  pi.registerCommand("task-preflight", {
6265
6620
  description: "Check context health before a large task; optionally compact",
6266
6621
  handler: async (args, ctx) => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -63,6 +63,7 @@ Mandatory flow:
63
63
  16. Run the exact FE verify command from the profile/task through Pi bash and record the observed result with `piagent_verify_record`. Ad-hoc commands are advisory only and will not satisfy the passing gate.
64
64
  17. Record handoff with `piagent_trace_record`.
65
65
  18. Call `piagent_task_gate_check`; if it fails, report blocked/partial instead of done.
66
+ 19. Do not paste full test/build/tool logs into chat or final output. Summarize the signal, quote only relevant failing lines, and use `/piagent-logs` or a narrower command when oversized output has been compacted.
66
67
 
67
68
  For generic projects, use profile `be-readonly-fe` when the repo policy is “BE scout only, FE write allowed”.
68
69
 
@@ -34,6 +34,7 @@ Mandatory flow:
34
34
  9. Explain context overflow prevention when relevant:
35
35
  - `/task-preflight` checks whether to proceed, compact, or fresh-session.
36
36
  - `/fresh-task`, `/fresh-scout`, and `/fresh-be-to-fe` open a new governed session and replay the compact workflow prompt.
37
+ - `/piagent-logs` shows compact-log policy and recent oversized tool-output captures without a model follow-up.
37
38
  - `/scout` is the read-only audit/scout workflow.
38
39
  - `/context-index` shows the compact advisory project context graph without a model follow-up.
39
40
  - `/context-index search <keyword>` searches profile/project/tech/task pointers before broad re-scouting.
@@ -42,6 +42,7 @@ Mandatory flow:
42
42
  12. Implement only the bounded target behavior.
43
43
  13. If runtime behavior changes, update README/docs and add/adjust a decision note when appropriate.
44
44
  14. For source-changing tasks, run the exact verify command from `task.verifyCommands` through Pi bash, record observed verify evidence, trace, then call `piagent_task_gate_check` before final.
45
+ 15. Do not paste full test/build/tool logs into chat or final output. Summarize the signal, quote only relevant failing lines, and use `/piagent-logs` or a narrower command when oversized output has been compacted.
45
46
 
46
47
  Default verification:
47
48
 
@@ -26,6 +26,7 @@ Rules:
26
26
  - security/high-risk only when relevant.
27
27
  If the user asks for a loop, use `/review-loop` semantics or equivalent parent-controlled max-round loop. Continue single-agent for tiny diffs or unavailable subagent tooling.
28
28
  9. Report findings by severity.
29
+ 10. Do not paste full diff/test/tool logs into chat or final output. Summarize the signal, quote only relevant lines, and use `/piagent-logs` or narrower commands when oversized output has been compacted.
29
30
 
30
31
  Output:
31
32
 
@@ -34,6 +34,7 @@ Preflight first:
34
34
  12. Run the exact verify command from the task contract and record with `piagent_verify_record`.
35
35
  13. Record handoff with `piagent_trace_record`.
36
36
  14. Call `piagent_task_gate_check`. If the gate fails, report partial/blocked.
37
+ 15. Do not paste full tool/test logs into chat or final output. Summarize the signal, quote only relevant lines, and use `/piagent-logs` or a narrower command when oversized output has been compacted.
37
38
 
38
39
  Output:
39
40
 
@@ -52,6 +52,7 @@ Mandatory flow:
52
52
  16. Call `piagent_task_gate_check`. If gate fails, final outcome is blocked/partial, not done.
53
53
  17. If the user asks about token/context/cost usage, call `piagent_usage_snapshot`; for exact token/cost totals, tell the user to run `/session` or `piagent-usage <project-path>`.
54
54
  18. If verify cannot run, stop and report the exact blocker. Do not call it done.
55
+ 19. Do not paste full test/build/tool logs into chat or final output. Summarize the signal, quote only the relevant failing lines, and use `/piagent-logs` or a narrower command when oversized output has been compacted.
55
56
 
56
57
  Do not ask the user to paste this mandatory flow. The platform prompt already contains it. If the user pasted the full flow, treat it as boilerplate and extract only the task request.
57
58