pi-ui-extend 1.0.39 → 1.0.41

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 (113) hide show
  1. package/README.md +1 -1
  2. package/dist/app/commands/command-registry.js +2 -2
  3. package/dist/app/commands/command-session-actions.d.ts +0 -1
  4. package/dist/app/commands/command-session-actions.js +22 -13
  5. package/dist/app/icons.d.ts +14 -0
  6. package/dist/app/icons.js +33 -0
  7. package/dist/app/rendering/conversation-tool-renderer.js +2 -2
  8. package/dist/app/rendering/dcp-stats.d.ts +6 -1
  9. package/dist/app/rendering/dcp-stats.js +214 -46
  10. package/dist/app/rendering/editor-panels.js +8 -5
  11. package/dist/app/session/lazy-session-manager.js +12 -1
  12. package/dist/app/session/tabs-controller.d.ts +2 -5
  13. package/dist/app/session/tabs-controller.js +12 -21
  14. package/dist/app/subagents/subagents-model.d.ts +14 -1
  15. package/dist/app/subagents/subagents-model.js +34 -15
  16. package/dist/app/types.d.ts +2 -0
  17. package/dist/bundled-extensions/session-title/config.js +1 -1
  18. package/dist/markdown-format.js +27 -9
  19. package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
  20. package/dist/schemas/pi-tools-suite-schema.js +46 -31
  21. package/external/pi-tools-suite/README.md +392 -52
  22. package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
  23. package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
  24. package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
  25. package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
  26. package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
  27. package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
  28. package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
  29. package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
  30. package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
  31. package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
  32. package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
  33. package/external/pi-tools-suite/docs/evals.md +684 -0
  34. package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
  35. package/external/pi-tools-suite/package.json +10 -3
  36. package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
  37. package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
  38. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
  39. package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
  40. package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
  42. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
  44. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
  46. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
  47. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
  49. package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
  50. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
  52. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
  55. package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
  56. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
  57. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  58. package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
  59. package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
  60. package/external/pi-tools-suite/src/config.ts +1 -22
  61. package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
  62. package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
  63. package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
  64. package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
  65. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
  66. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
  67. package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
  68. package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
  69. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
  70. package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
  71. package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
  72. package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
  73. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
  74. package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
  75. package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
  76. package/external/pi-tools-suite/src/dcp/config.ts +36 -61
  77. package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
  78. package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
  79. package/external/pi-tools-suite/src/dcp/index.ts +617 -203
  80. package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
  81. package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
  82. package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
  83. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
  84. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
  85. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
  86. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
  87. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
  88. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
  89. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
  90. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
  91. package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
  92. package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
  93. package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
  94. package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
  95. package/external/pi-tools-suite/src/dcp/state.ts +158 -580
  96. package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
  98. package/external/pi-tools-suite/src/index.ts +9 -0
  99. package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
  100. package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
  101. package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
  102. package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
  103. package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
  104. package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
  105. package/package.json +6 -6
  106. package/schemas/pi-tools-suite.json +159 -78
  107. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
  108. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
  109. package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
  110. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
  111. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
  112. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
  113. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
@@ -43,6 +43,11 @@ type Section = {
43
43
  label: string;
44
44
  };
45
45
 
46
+ type RecoveryCursor =
47
+ | { version: 1; kind: "overview"; scope: Scope; afterSectionId: string }
48
+ | { version: 1; kind: "search"; scope: Scope; query: string; caseSensitive: boolean; afterEntryId: string }
49
+ | { version: 1; kind: "read"; scope: Scope; sectionId?: string; entryId: string; offset: number };
50
+
46
51
  const SCOPE_SCHEMA = StringEnum(["active", "all"] as const, {
47
52
  description: "Raw session scope. Defaults to the active root-to-leaf branch; all includes abandoned branches.",
48
53
  });
@@ -61,6 +66,8 @@ const MAX_RECENT_ERRORS = 20;
61
66
  const MAX_RECOVERY_FILES = 200;
62
67
  const MAX_PENDING_TOOL_CALLS = 50;
63
68
  const MAX_SEARCH_QUERY_CHARS = 500;
69
+ const CURSOR_VERSION = 1;
70
+ const DCP_CONTROL_CUSTOM_TYPES = new Set(["dcp-journal", "dcp-nudge"]);
64
71
 
65
72
  const READ_TOOL_NAMES = new Set([
66
73
  "read",
@@ -88,6 +95,10 @@ function isEntry(value: unknown): value is EntryLike {
88
95
  && typeof value.type === "string" && value.type.length > 0;
89
96
  }
90
97
 
98
+ function isDcpControlEntry(entry: EntryLike): boolean {
99
+ return entry.type === "custom" && typeof entry.customType === "string" && DCP_CONTROL_CUSTOM_TYPES.has(entry.customType);
100
+ }
101
+
91
102
  function clampInteger(value: unknown, fallback: number, minimum: number, maximum: number): number {
92
103
  if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
93
104
  return Math.min(maximum, Math.max(minimum, Math.floor(value)));
@@ -119,7 +130,39 @@ function entriesFor(manager: SessionManagerLike | undefined, scope: Scope): Entr
119
130
  : () => manager.getBranch?.(),
120
131
  [],
121
132
  );
122
- return Array.isArray(raw) ? raw.filter(isEntry) : [];
133
+ return Array.isArray(raw) ? raw.filter(isEntry).filter((entry) => !isDcpControlEntry(entry)) : [];
134
+ }
135
+
136
+ function encodeCursor(cursor: RecoveryCursor): string {
137
+ return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
138
+ }
139
+
140
+ function decodeCursor(value: unknown): RecoveryCursor | undefined {
141
+ if (typeof value !== "string" || value.length === 0 || value.length > 2_000) return undefined;
142
+ try {
143
+ const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
144
+ if (!isRecord(parsed) || parsed.version !== CURSOR_VERSION || typeof parsed.kind !== "string") return undefined;
145
+ if (parsed.scope !== "active" && parsed.scope !== "all") return undefined;
146
+ if (parsed.kind === "overview" && typeof parsed.afterSectionId === "string") {
147
+ return parsed as RecoveryCursor;
148
+ }
149
+ if (
150
+ parsed.kind === "search" && typeof parsed.query === "string" && typeof parsed.caseSensitive === "boolean"
151
+ && typeof parsed.afterEntryId === "string"
152
+ ) {
153
+ return parsed as RecoveryCursor;
154
+ }
155
+ if (
156
+ parsed.kind === "read" && typeof parsed.entryId === "string" && typeof parsed.offset === "number"
157
+ && Number.isInteger(parsed.offset) && parsed.offset >= 0
158
+ && (parsed.sectionId === undefined || typeof parsed.sectionId === "string")
159
+ ) {
160
+ return parsed as RecoveryCursor;
161
+ }
162
+ } catch {
163
+ return undefined;
164
+ }
165
+ return undefined;
123
166
  }
124
167
 
125
168
  function messageFrom(entry: EntryLike): UnknownRecord | undefined {
@@ -358,16 +401,16 @@ function leafCount(entries: EntryLike[]): number {
358
401
  return entries.reduce((count, entry) => count + (parents.has(entry.id) ? 0 : 1), 0);
359
402
  }
360
403
 
361
- function renderEntry(entry: EntryLike, bodyChars: number): string {
404
+ function renderEntryFull(entry: EntryLike): string {
362
405
  const heading = [`[${entry.id}]`, entry.timestamp, entry.type, messageRole(entry)].filter(Boolean).join(" ");
363
406
  const lines = [heading];
364
407
  const message = messageFrom(entry);
365
408
 
366
409
  if (message) {
367
410
  const text = textFromContent(message.content);
368
- if (text) lines.push(truncate(text, bodyChars));
411
+ if (text) lines.push(text);
369
412
  for (const call of toolCallsFrom(entry)) {
370
- lines.push(`tool_call ${call.name}#${call.id} ${safeJson(call.arguments, bodyChars)}`);
413
+ lines.push(`tool_call ${call.name}#${call.id} ${serializeJson(call.arguments)}`);
371
414
  }
372
415
  if (message.role === "toolResult") {
373
416
  const toolName = typeof message.toolName === "string" ? message.toolName : "unknown";
@@ -375,25 +418,39 @@ function renderEntry(entry: EntryLike, bodyChars: number): string {
375
418
  lines.push(`tool_result ${toolName}#${callId}${message.isError === true ? " error" : ""}`);
376
419
  }
377
420
  } else if (entry.type === "compaction") {
378
- lines.push(truncate(summaryFrom(entry), bodyChars));
421
+ lines.push(summaryFrom(entry));
379
422
  if (typeof entry.firstKeptEntryId === "string") lines.push(`firstKeptEntryId: ${entry.firstKeptEntryId}`);
380
423
  if (typeof entry.tokensBefore === "number") lines.push(`tokensBefore: ${entry.tokensBefore}`);
381
424
  } else if (entry.type === "branch_summary") {
382
- lines.push(truncate(summaryFrom(entry), bodyChars));
425
+ lines.push(summaryFrom(entry));
383
426
  if (typeof entry.fromId === "string") lines.push(`fromId: ${entry.fromId}`);
384
427
  } else if (entry.type === "custom_message") {
385
428
  const text = textFromContent(entry.content);
386
- if (text) lines.push(truncate(text, bodyChars));
429
+ if (text) lines.push(text);
387
430
  } else {
388
431
  const fields = Object.fromEntries(
389
432
  Object.entries(entry).filter(([key]) => !["id", "parentId", "timestamp", "type"].includes(key)),
390
433
  );
391
- if (Object.keys(fields).length > 0) lines.push(safeJson(fields, bodyChars));
434
+ if (Object.keys(fields).length > 0) lines.push(safeJson(fields, MAX_BODY_CHARS));
392
435
  }
393
436
 
394
437
  return lines.join("\n");
395
438
  }
396
439
 
440
+ function renderEntry(entry: EntryLike, bodyChars: number): string {
441
+ return truncate(renderEntryFull(entry), bodyChars);
442
+ }
443
+
444
+ function readEntryChunk(entry: EntryLike, offset: number, bodyChars: number): { text: string; nextOffset?: number } {
445
+ const rendered = renderEntryFull(entry);
446
+ const start = Math.min(Math.max(0, offset), rendered.length);
447
+ const end = Math.min(rendered.length, start + bodyChars);
448
+ return {
449
+ text: rendered.slice(start, end),
450
+ ...(end < rendered.length ? { nextOffset: end } : {}),
451
+ };
452
+ }
453
+
397
454
  function contentResult(payload: unknown, details: UnknownRecord): { content: Array<{ type: "text"; text: string }>; details: UnknownRecord } {
398
455
  const serialized = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
399
456
  return {
@@ -448,14 +505,19 @@ export default function sessionRecovery(pi: ExtensionAPI): void {
448
505
  ...SESSION_RECOVERY_TOOL_DESCRIPTIONS.overview,
449
506
  parameters: Type.Object({
450
507
  scope: Type.Optional(SCOPE_SCHEMA),
508
+ cursor: Type.Optional(Type.String({ description: "Opaque continuation cursor returned by a previous session_overview call.", maxLength: 2_000 })),
451
509
  max_sections: Type.Optional(Type.Number({
452
- description: "Maximum section summaries to return, split between the head and tail.",
510
+ description: "Maximum consecutive section summaries to return.",
453
511
  minimum: 1,
454
512
  maximum: MAX_OVERVIEW_SECTIONS,
455
513
  })),
456
514
  }, { additionalProperties: false }),
457
- async execute(_toolCallId: string, params: { scope?: Scope; max_sections?: number }, _signal: AbortSignal, _onUpdate: unknown, ctx: unknown) {
515
+ async execute(_toolCallId: string, params: { scope?: Scope; cursor?: string; max_sections?: number }, _signal: AbortSignal, _onUpdate: unknown, ctx: unknown) {
458
516
  const scope = scopeFrom(params.scope);
517
+ const cursor = params.cursor ? decodeCursor(params.cursor) : undefined;
518
+ if (params.cursor && (!cursor || cursor.kind !== "overview" || cursor.scope !== scope)) {
519
+ return contentResult("Invalid or stale session_overview cursor for this scope.", { scope, cursorValid: false });
520
+ }
459
521
  const manager = sessionManagerFrom(ctx);
460
522
  const entries = entriesFor(manager, scope);
461
523
  if (entries.length === 0) return emptyResult(scope);
@@ -464,7 +526,19 @@ export default function sessionRecovery(pi: ExtensionAPI): void {
464
526
  const allEntries = entriesFor(manager, "all");
465
527
  const sections = buildSections(entries);
466
528
  const maximum = clampInteger(params.max_sections, DEFAULT_OVERVIEW_SECTIONS, 1, MAX_OVERVIEW_SECTIONS);
467
- const selected = boundedHeadAndTail(sections, maximum);
529
+ let startIndex = 0;
530
+ if (cursor?.kind === "overview") {
531
+ const index = sections.findIndex((section) => section.id === cursor.afterSectionId);
532
+ if (index < 0) {
533
+ return contentResult("The session changed and the overview cursor no longer resolves on this branch.", { scope, cursorValid: false });
534
+ }
535
+ startIndex = index + 1;
536
+ }
537
+ const selected = sections.slice(startIndex, startIndex + maximum);
538
+ const hasMore = startIndex + selected.length < sections.length;
539
+ const nextCursor = hasMore && selected.length > 0
540
+ ? encodeCursor({ version: CURSOR_VERSION, kind: "overview", scope, afterSectionId: selected[selected.length - 1]!.id })
541
+ : undefined;
468
542
  const allLeaves = leafCount(allEntries);
469
543
  const header = callSafely<unknown>(() => manager?.getHeader?.(), undefined);
470
544
  const sessionId = callSafely<unknown>(() => manager?.getSessionId?.(), undefined);
@@ -487,57 +561,101 @@ export default function sessionRecovery(pi: ExtensionAPI): void {
487
561
  leaves: allLeaves,
488
562
  otherBranches: Math.max(0, allLeaves - (activeEntries.length > 0 ? 1 : 0)),
489
563
  },
490
- sections: selected.values.map(sectionSummary),
491
- omittedSections: selected.omitted,
564
+ sections: selected.map(sectionSummary),
565
+ hasMore,
566
+ nextCursor: nextCursor ?? null,
492
567
  };
493
- return contentResult(payload, { scope, entryCount: entries.length, sectionCount: sections.length, omittedSections: selected.omitted });
568
+ return contentResult(payload, { scope, entryCount: entries.length, sectionCount: sections.length, returnedSections: selected.length, hasMore, nextCursor });
494
569
  },
495
570
  });
496
571
 
497
572
  pi.registerTool({
498
573
  ...SESSION_RECOVERY_TOOL_DESCRIPTIONS.readSection,
499
574
  parameters: Type.Object({
500
- section_id: Type.String({ description: "Stable section ID returned by session_overview or session_search.", maxLength: 200 }),
575
+ section_id: Type.Optional(Type.String({ description: "Stable section ID returned by session_overview or session_search.", maxLength: 200 })),
576
+ entry_id: Type.Optional(Type.String({ description: "Read one exact raw session entry by ID without scanning from the start of its section.", maxLength: 200 })),
577
+ cursor: Type.Optional(Type.String({ description: "Opaque continuation cursor returned by a previous session_read_section call.", maxLength: 2_000 })),
501
578
  scope: Type.Optional(SCOPE_SCHEMA),
502
579
  max_entries: Type.Optional(Type.Number({ description: "Maximum entries to render from the section.", minimum: 1, maximum: MAX_SECTION_ENTRIES })),
503
- max_body_chars: Type.Optional(Type.Number({ description: "Maximum rendered body characters per entry.", minimum: 100, maximum: MAX_BODY_CHARS })),
580
+ max_body_chars: Type.Optional(Type.Number({ description: "Maximum rendered characters per entry page. Use the returned cursor to continue long entries.", minimum: 100, maximum: MAX_BODY_CHARS })),
504
581
  }, { additionalProperties: false }),
505
- async execute(_toolCallId: string, params: { section_id: string; scope?: Scope; max_entries?: number; max_body_chars?: number }, _signal: AbortSignal, _onUpdate: unknown, ctx: unknown) {
582
+ async execute(_toolCallId: string, params: { section_id?: string; entry_id?: string; cursor?: string; scope?: Scope; max_entries?: number; max_body_chars?: number }, _signal: AbortSignal, _onUpdate: unknown, ctx: unknown) {
506
583
  const scope = scopeFrom(params.scope);
584
+ if (!params.cursor && Boolean(params.section_id) === Boolean(params.entry_id)) {
585
+ return contentResult("Pass exactly one of section_id or entry_id, or continue with cursor.", { scope, found: false });
586
+ }
587
+ const cursor = params.cursor ? decodeCursor(params.cursor) : undefined;
588
+ if (params.cursor && (!cursor || cursor.kind !== "read" || cursor.scope !== scope)) {
589
+ return contentResult("Invalid or stale session_read_section cursor for this scope.", { scope, cursorValid: false, sourceAvailable: false });
590
+ }
507
591
  const entries = entriesFor(sessionManagerFrom(ctx), scope);
508
592
  if (entries.length === 0) return emptyResult(scope);
509
- const section = buildSections(entries).find((candidate) => candidate.id === params.section_id);
510
- if (!section) {
593
+ const sections = buildSections(entries);
594
+ const requestedSectionId = cursor?.kind === "read" ? cursor.sectionId : params.section_id;
595
+ const requestedEntryId = cursor?.kind === "read" ? cursor.entryId : params.entry_id;
596
+ const offset = cursor?.kind === "read" ? cursor.offset : 0;
597
+ const section = requestedSectionId ? sections.find((candidate) => candidate.id === requestedSectionId) : undefined;
598
+ if (requestedSectionId && !section) {
511
599
  return contentResult(
512
- `Section ${params.section_id} was not found in scope ${scope}. Run session_overview with the same scope to refresh section IDs.`,
513
- { scope, sectionId: params.section_id, found: false },
600
+ `Section ${requestedSectionId} was not found in scope ${scope}. Run session_overview with the same scope to refresh section IDs.`,
601
+ { scope, sectionId: requestedSectionId, found: false, sourceAvailable: false },
514
602
  );
515
603
  }
516
604
 
517
605
  const maximum = clampInteger(params.max_entries, DEFAULT_SECTION_ENTRIES, 1, MAX_SECTION_ENTRIES);
518
606
  const bodyChars = clampInteger(params.max_body_chars, DEFAULT_BODY_CHARS, 100, MAX_BODY_CHARS);
607
+ const candidates = section ? section.entries : entries;
608
+ let startIndex = requestedEntryId ? candidates.findIndex((entry) => entry.id === requestedEntryId) : 0;
609
+ if (startIndex < 0) {
610
+ return contentResult(
611
+ `Entry ${requestedEntryId} was not found in scope ${scope}${section ? ` within ${section.id}` : ""}.`,
612
+ { scope, sectionId: section?.id, entryId: requestedEntryId, found: false, sourceAvailable: false },
613
+ );
614
+ }
519
615
  const rendered: string[] = [];
520
616
  let renderedChars = 0;
521
- let truncatedOutput = false;
522
- for (const entry of section.entries.slice(0, maximum)) {
523
- const next = renderEntry(entry, bodyChars);
524
- if (renderedChars + next.length + 2 > MAX_OUTPUT_CHARS - 500) {
525
- truncatedOutput = true;
526
- break;
527
- }
617
+ let nextCursor: string | undefined;
618
+ let renderedCount = 0;
619
+ for (let index = startIndex; index < candidates.length && renderedCount < maximum; index += 1) {
620
+ const entry = candidates[index]!;
621
+ const entryOffset = index === startIndex ? offset : 0;
622
+ const chunk = readEntryChunk(entry, entryOffset, bodyChars);
623
+ const heading = entryOffset > 0 ? `[${entry.id}] continuation @${entryOffset}` : undefined;
624
+ const next = [heading, chunk.text].filter(Boolean).join("\n");
625
+ if (renderedChars + next.length + 2 > MAX_OUTPUT_CHARS - 1_000) break;
528
626
  rendered.push(next);
529
627
  renderedChars += next.length + 2;
628
+ renderedCount += 1;
629
+ if (chunk.nextOffset !== undefined) {
630
+ nextCursor = encodeCursor({ version: CURSOR_VERSION, kind: "read", scope, sectionId: section?.id, entryId: entry.id, offset: chunk.nextOffset });
631
+ break;
632
+ }
633
+ const nextEntry = candidates[index + 1];
634
+ if (nextEntry && (renderedCount >= maximum || renderedChars >= MAX_OUTPUT_CHARS - 1_000)) {
635
+ nextCursor = encodeCursor({ version: CURSOR_VERSION, kind: "read", scope, sectionId: section?.id, entryId: nextEntry.id, offset: 0 });
636
+ break;
637
+ }
530
638
  }
531
- const omittedEntries = section.entries.length - rendered.length;
639
+ if (!nextCursor) {
640
+ const lastRendered = renderedCount > 0 ? startIndex + renderedCount - 1 : startIndex - 1;
641
+ const nextEntry = candidates[lastRendered + 1];
642
+ if (nextEntry) nextCursor = encodeCursor({ version: CURSOR_VERSION, kind: "read", scope, sectionId: section?.id, entryId: nextEntry.id, offset: 0 });
643
+ }
644
+ const hasMore = nextCursor !== undefined;
645
+ const title = section ? `Section ${section.id}: ${section.label}` : `Entry ${requestedEntryId}`;
532
646
  return contentResult(
533
- [`Section ${section.id}: ${section.label}`, ...rendered, omittedEntries > 0 ? `… ${omittedEntries} entries omitted` : ""].filter(Boolean).join("\n\n"),
647
+ [title, ...rendered, hasMore ? "… more available; continue with next_cursor" : ""].filter(Boolean).join("\n\n"),
534
648
  {
535
649
  scope,
536
- sectionId: section.id,
537
- entryCount: section.entries.length,
538
- renderedCount: rendered.length,
539
- omittedEntries,
540
- truncated: truncatedOutput || omittedEntries > 0,
650
+ sectionId: section?.id,
651
+ entryId: requestedEntryId,
652
+ entryCount: candidates.length,
653
+ renderedCount,
654
+ found: true,
655
+ sourceAvailable: true,
656
+ hasMore,
657
+ truncated: hasMore,
658
+ nextCursor,
541
659
  },
542
660
  );
543
661
  },
@@ -552,10 +670,11 @@ export default function sessionRecovery(pi: ExtensionAPI): void {
552
670
  maxLength: MAX_SEARCH_QUERY_CHARS,
553
671
  }),
554
672
  scope: Type.Optional(SCOPE_SCHEMA),
673
+ cursor: Type.Optional(Type.String({ description: "Opaque continuation cursor returned by a previous session_search call.", maxLength: 2_000 })),
555
674
  case_sensitive: Type.Optional(Type.Boolean({ description: "Use exact case matching. Defaults to false." })),
556
675
  limit: Type.Optional(Type.Number({ description: "Maximum matches to return.", minimum: 1, maximum: MAX_SEARCH_RESULTS })),
557
676
  }, { additionalProperties: false }),
558
- async execute(_toolCallId: string, params: { query: string; scope?: Scope; case_sensitive?: boolean; limit?: number }, _signal: AbortSignal, _onUpdate: unknown, ctx: unknown) {
677
+ async execute(_toolCallId: string, params: { query: string; scope?: Scope; cursor?: string; case_sensitive?: boolean; limit?: number }, _signal: AbortSignal, _onUpdate: unknown, ctx: unknown) {
559
678
  const scope = scopeFrom(params.scope);
560
679
  const entries = entriesFor(sessionManagerFrom(ctx), scope);
561
680
  if (entries.length === 0) return emptyResult(scope);
@@ -563,20 +682,24 @@ export default function sessionRecovery(pi: ExtensionAPI): void {
563
682
  if (!query) return contentResult("Search query must not be empty.", { scope, query, matchCount: 0 });
564
683
 
565
684
  const caseSensitive = params.case_sensitive === true;
685
+ const cursor = params.cursor ? decodeCursor(params.cursor) : undefined;
686
+ if (
687
+ params.cursor && (!cursor || cursor.kind !== "search" || cursor.scope !== scope
688
+ || cursor.query !== query || cursor.caseSensitive !== caseSensitive)
689
+ ) {
690
+ return contentResult("Invalid or stale session_search cursor for this scope/query.", { scope, query, cursorValid: false });
691
+ }
566
692
  const needle = caseSensitive ? query : query.toLocaleLowerCase();
567
693
  const limit = clampInteger(params.limit, DEFAULT_SEARCH_RESULTS, 1, MAX_SEARCH_RESULTS);
568
694
  const sections = buildSections(entries);
569
695
  const entrySections = sectionIdByEntry(sections);
570
- const matches: UnknownRecord[] = [];
571
- let totalMatches = 0;
696
+ const allMatches: UnknownRecord[] = [];
572
697
 
573
698
  for (const entry of entries) {
574
699
  const text = entryText(entry);
575
700
  const haystack = caseSensitive ? text : text.toLocaleLowerCase();
576
701
  if (!text || !haystack.includes(needle)) continue;
577
- totalMatches += 1;
578
- if (matches.length >= limit) continue;
579
- matches.push({
702
+ allMatches.push({
580
703
  entryId: entry.id,
581
704
  sectionId: entrySections.get(entry.id),
582
705
  type: entry.type,
@@ -586,8 +709,31 @@ export default function sessionRecovery(pi: ExtensionAPI): void {
586
709
  });
587
710
  }
588
711
 
589
- const payload = { scope, query, caseSensitive, totalMatches, returnedMatches: matches.length, matches };
590
- return contentResult(payload, { scope, query, matchCount: totalMatches, returnedCount: matches.length, truncated: totalMatches > matches.length });
712
+ let startIndex = 0;
713
+ if (cursor?.kind === "search") {
714
+ const index = allMatches.findIndex((match) => match.entryId === cursor.afterEntryId);
715
+ if (index < 0) {
716
+ return contentResult("The session changed and the search cursor no longer resolves on this branch.", { scope, query, cursorValid: false });
717
+ }
718
+ startIndex = index + 1;
719
+ }
720
+ const matches = allMatches.slice(startIndex, startIndex + limit);
721
+ const hasMore = startIndex + matches.length < allMatches.length;
722
+ const lastEntryId = matches.at(-1)?.entryId;
723
+ const nextCursor = hasMore && typeof lastEntryId === "string"
724
+ ? encodeCursor({ version: CURSOR_VERSION, kind: "search", scope, query, caseSensitive, afterEntryId: lastEntryId })
725
+ : undefined;
726
+ const payload = {
727
+ scope,
728
+ query,
729
+ caseSensitive,
730
+ totalMatches: allMatches.length,
731
+ returnedMatches: matches.length,
732
+ hasMore,
733
+ nextCursor: nextCursor ?? null,
734
+ matches,
735
+ };
736
+ return contentResult(payload, { scope, query, matchCount: allMatches.length, returnedCount: matches.length, hasMore, truncated: hasMore, nextCursor });
591
737
  },
592
738
  });
593
739
 
@@ -1,4 +1,6 @@
1
1
  import { COMPRESS_RANGE_DESCRIPTION } from "./dcp/prompts.js";
2
+ import { SUBAGENT_TYPE_SELECTION_GUIDANCE } from "./async-subagents/core/agent-catalog.js";
3
+ import { SUBAGENT_DELEGATION_GUIDANCE } from "./async-subagents/core/agent-strategy.js";
2
4
 
3
5
  export type ToolDescription = {
4
6
  name: string;
@@ -39,10 +41,11 @@ export function astGrepToolDescriptions(maxLines: number, maxBytesLabel: string)
39
41
  astGrep: {
40
42
  name: "ast_grep",
41
43
  label: "ast-grep",
42
- description: `Read-only AST structural search/scan. Use for language-aware patterns, sgconfig/rule scans, JSON matches, and rewrite previews. Use text search for plain strings and ast_apply for mutations. Output truncates at ${maxLines} lines or ${maxBytesLabel} with full output saved to a temp file.`,
43
- promptSnippet: "Use ast_grep for AST/structural code search, not plain text search. It previews rewrites only; use ast_apply to mutate files.",
44
+ description: `Read-only AST structural search/scan. MANDATORY ROUTING: for structural, syntax-aware, AST, language-aware, or code-shape matching, ast_grep must be the FIRST tool call. Never preflight with Glob, Grep, Read, or shell even when files are unknown; omit paths to scan the current project. Use for sgconfig/rule scans, JSON matches, and rewrite previews. Use Grep for exact literals/regex, Glob only for filename/path-only requests, and ast_apply for mutations. Output truncates at ${maxLines} lines or ${maxBytesLabel} with full output saved to a temp file.`,
45
+ promptSnippet: "MANDATORY: structural/syntax-aware/AST/code-shape matching => call ast_grep FIRST. Do not call Glob, Grep, Read, or shell before it; unknown files are not a reason to pre-search because ast_grep scans the project by default. Exact literal/regex only => Grep; filename/path only => Glob. Use ast_apply for mutations.",
44
46
  promptGuidelines: [
45
- "Use ast_grep when syntax/AST structure matters; use text search for exact strings/regex. Keep paths/globs narrow and set lang for ambiguous snippets.",
47
+ "The first tool for syntax relationships or code-shape matching must be ast_grep, even when the file or language is unknown. Do not make a preliminary Glob/Grep/Read/shell call; start at the current project and narrow within ast_grep when needed.",
48
+ "Use Grep/Read directly for exact literal or regex lookups and Glob only for filename/path-only discovery; those text-only tasks must not trigger ast_grep.",
46
49
  "ast_grep is read-only: use rewrite to preview only; use ast_apply for mutations or command=scan fixes.",
47
50
  ],
48
51
  },
@@ -70,9 +73,9 @@ export function asyncSubagentToolDescriptions(options: ToolDescriptionSetOptions
70
73
  "For every real-browser QA request, immediately spawn subagentType='browser-qa' before checking files, URLs, servers, or other prerequisites; the QA sub-agent owns feasibility checks and blocked reports, so the parent must not attempt browser QA itself.",
71
74
  "If browser-qa reports that credentials are required, it must identify the generated project-local template and explicitly ask the user to fill it; the parent relays that request without reading or editing the credential file.",
72
75
  "After browser testing, browser-qa must return clickable links for every available screenshot, video, and trace; the parent must preserve those links in its user-facing report.",
73
- "Otherwise, manage isolated async sub-agents for large, parallel, context-heavy work.",
74
- "Presets from async-subagents config and /subagent-preset choose role model/thinking/args; AGENTS_PRESET or /subagent-preset session <name> overrides the current session; /subagent-preset init creates a sample config.",
75
- "Omit subagentType so the router chooses a configured role unless the user or task requires a role or deterministic override.",
76
+ SUBAGENT_DELEGATION_GUIDANCE,
77
+ "Presets declare available models; each agent's ordered models selects the first usable model in that pool. AGENTS_PRESET or /subagent-preset session <name> selects the current session pool; /subagent-preset init creates a sample config. Do not override the model merely to choose a role.",
78
+ SUBAGENT_TYPE_SELECTION_GUIDANCE,
76
79
  repoDiscovery
77
80
  ? "Use for broad independent tracks, review axes, or hypotheses even though repo_* tools are available."
78
81
  : "Use first for broad codebase discovery split into tracks, review axes, or incident-triage hypotheses when repo_* tools are unavailable.",
@@ -82,25 +85,26 @@ export function asyncSubagentToolDescriptions(options: ToolDescriptionSetOptions
82
85
  promptSnippet:
83
86
  "For every browser-based visual QA, UI bug reproduction, or real-browser fix-verification request, immediately spawn subagentType='browser-qa' even for a single track and before inspecting files or checking prerequisites. The browser-qa sub-agent must discover the target and report missing prerequisites; do not preflight, perform, or substitute browser QA in the parent agent. " +
84
87
  "Give browser-qa a concise acceptance brief: the known target URL/app, user-visible flow, expected observable result, and required artifacts. Do not prescribe repository files, searches, commands, server setup, or mock/synthetic substitutes; unknown setup belongs to the QA sub-agent's discovery. " +
85
- "For other work, use subagents action='spawn' for multiple independent agents, explicit delegate/parallelize/split work requests, or one large review/debug track that should stay out of the parent context. " +
86
- "Usually omit subagentType so the router chooses; set it only for user-named roles, deterministic tests, or another concrete override. Avoid trivial reads/edits and do not call status/wait immediately after spawn just for progress. " +
88
+ "For other work, use subagents action='spawn' for economical execution or context isolation, including one bounded sequential task or explicit delegate/parallelize/split work requests. " +
89
+ SUBAGENT_TYPE_SELECTION_GUIDANCE + " Avoid trivial reads/edits and do not call status/wait immediately after spawn just for progress. " +
87
90
  (repoDiscovery
88
91
  ? "For one semantic code-discovery question, use repo_search; for independent tracks/hypotheses/review axes, delegate even when repo_* tools exist. Read result only after completion when findings are needed."
89
- : "For one focused code-discovery question, use direct read/grep. Without repo_* tools, spawn several focused scan/quick agents first for broad multi-track discovery, incident triage, release readiness, risk strategy, or parallel reviews. Read result only after completion when findings are needed."),
92
+ : "For one focused code-discovery question, use direct read/grep. Without repo_* tools, delegate bounded research tracks for broad discovery rather than flooding parent context. Read result only after completion when findings are needed."),
90
93
  promptGuidelines: [
91
94
  "Treat every real-browser QA request as a mandatory delegation trigger and an explicit exception to the large/parallel threshold: immediately spawn with `subagentType: \"browser-qa\"` before checking prerequisites. The QA sub-agent owns target discovery, feasibility checks, browser automation, evidence, and blocked reports; the parent must not inspect the project first or substitute non-browser checks.",
92
95
  "Keep the browser-qa task payload at the user-visible acceptance level: known target URL/app, actions to perform, expected observable outcome, and requested evidence. Do not turn it into a repository investigation plan, name internal files or commands, dictate server setup, or invent a mock/synthetic target. Leave unknown prerequisites to the QA sub-agent.",
93
96
  "When browser-qa reports missing credentials, relay its explicit request and generated `.pi/qa_auth.jsonc` template path; never inspect, populate, or edit that credential file in the parent.",
94
97
  "After browser-qa completes a test, preserve its clickable screenshot, video, and trace links in the final user-facing response whenever those artifacts exist.",
95
- "For non-browser-QA work, use action='spawn' only for LARGE/PARALLEL work: independent investigations, repo-wide sweeps, deep debugging, code review/audit, or explicit delegate/parallelize/split requests; these are spawn triggers unless trivial/single-file.",
98
+ SUBAGENT_DELEGATION_GUIDANCE,
96
99
  repoDiscovery
97
100
  ? "For one discovery question, use repo_search; spawn for independent tracks/hypotheses/review axes, and do not let repo_* availability suppress delegation."
98
- : "For one discovery question, use direct read/grep; when repo_* tools are unavailable, spawn several focused scan/quick agents first for broad multi-file/module/hypothesis work.",
101
+ : "For one small discovery question, use direct read/grep; when repo_* tools are unavailable, delegate scoped research to keep broad search output outside the parent context.",
99
102
  repoDiscovery
100
103
  ? "For incident triage, release readiness, or risk/test strategy with separate hypotheses/review tracks, prefer focused agents over serial parent-context work."
101
104
  : "For incident triage, release readiness, or risk/test strategy with separate hypotheses/review tracks and no repo_* tools, call action='spawn' as the first discovery step; direct read/grep can follow.",
102
- "Do not use subagents for exact-string lookups, known-file edits, typo/text replacements, obvious one-file changes, or interactive user input; use the cheapest direct path.",
103
- "Spawn multiple focused agents in one action='spawn' call for independent questions; for bounded probes set timeoutSeconds; omit subagentType unless user-named/deterministic, and use oracle sparingly for high-stakes uncertainty/final checks.",
105
+ "Do not use subagents for trivial exact-string lookups, typo replacements, or interactive user input; use the cheapest direct path. A substantive bounded edit can be delegated even in one file.",
106
+ "Spawn multiple focused agents in one action='spawn' call for independent questions; set subagentType for clear role matches, timeoutSeconds for bounded probes, and use oracle sparingly for high-stakes uncertainty/final checks.",
107
+ "If spawn reports a routing error, no agents from that batch were launched. Correct the invalid or unresolved subagentType values using the available catalog and resubmit the whole batch; do not blindly retry omitted types or substitute an unsuitable role to suppress the error.",
104
108
  "For screenshot/image inspection by blind models, use lookup; subagents only receive imagePaths when a broader delegated track genuinely needs them.",
105
109
  "If asked to start/run/launch/test parallel sub-agents, spawn and stop; do not status/wait just for progress. Use status for recovery, wait only when needed/requested, result only after completion; compact results include artifact links.",
106
110
  "Use action='stop' for stop/cancel/kill requests and action='cleanup' with delete=true only after collecting results.",
@@ -154,11 +158,10 @@ export const REPO_DISCOVERY_TOOLS: RepoDiscoveryToolDescription[] = [
154
158
  name: "repo_architecture",
155
159
  label: "Repo Architecture",
156
160
  command: "architecture",
157
- description: "Indexed repo architecture map: entrypoints, module boundaries, cycles, and unresolved dependency classes. Use before broad reads in unfamiliar codebases; skip for exact-string lookups, known-file edits, or other trivial changes.",
158
- promptSnippet: "Use repo_architecture for a compact indexed architecture overview before broad multi-file reads, not for simple literal searches or small known-scope edits.",
161
+ description: "Indexed entrypoints, module boundaries, cycles and unresolved dependencies for broad, unfamiliar code.",
162
+ promptSnippet: "Map an unfamiliar area once; skip repo_architecture for known paths or exact lookups.",
159
163
  promptGuidelines: [
160
- "Exact strings, filenames, known symbols, typo/text replacements, or obvious one-file edits: skip repo_architecture and use direct text search/read/edit.",
161
- "Broad unfamiliar codebase: make one narrow repo_architecture call first, add --path-prefix when a subsystem is known, then use repo_structure for files/symbols and repo_search for behavior.",
164
+ "Scope with --path-prefix; then use repo_structure or repo_search only for remaining gaps.",
162
165
  ],
163
166
  },
164
167
  {
@@ -166,19 +169,19 @@ export const REPO_DISCOVERY_TOOLS: RepoDiscoveryToolDescription[] = [
166
169
  label: "Repo Structure",
167
170
  command: "structure",
168
171
  description: "Indexed file tree and exported-symbol view for a directory/module. Use to choose files/ranges without dumping source.",
169
- promptSnippet: "Use repo_structure for file trees, module contents, and exported symbols; narrow with idx flags.",
172
+ promptSnippet: "List one area with --max-files 20 --max-depth 2; continue with --cursor.",
170
173
  promptGuidelines: [
171
- "Pass --path-prefix, --kind, --max-files, or --max-depth when useful; use repo_ast for one large file's syntax map and repo_search for semantic behavior discovery.",
174
+ "Narrow with --path-prefix/--kind. Add --include-internal or --include-tests-summary only for a named gap; page instead of listing hundreds of files.",
172
175
  ],
173
176
  },
174
177
  {
175
178
  name: "repo_ast",
176
179
  label: "Repo AST",
177
180
  command: "ast",
178
- description: "Indexed AST map for one file. Use before repeated reads of a large file or when parent syntax structure matters.",
179
- promptSnippet: "Use repo_ast with target=<file> to map one large file before choosing exact ranges to read.",
181
+ description: "Indexed AST outline for one known large file; locate exact ranges before reading source.",
182
+ promptSnippet: "Start --max-depth 3 --max-nodes 40 --no-include-text; read needed ranges with offset/limit.",
180
183
  promptGuidelines: [
181
- "Use for one known file, not repo-wide search; pass --max-depth or --max-nodes to keep output compact.",
184
+ "Continue with --cursor; include snippets only when the outline cannot answer the question.",
182
185
  ],
183
186
  targetDescription: "File path to map, e.g. src/api/client.ts.",
184
187
  },
@@ -186,12 +189,12 @@ export const REPO_DISCOVERY_TOOLS: RepoDiscoveryToolDescription[] = [
186
189
  name: "repo_search",
187
190
  label: "Repo Search",
188
191
  command: "search",
189
- description: "Indexed hybrid/semantic repository search for behavior questions when exact identifiers or files are unknown. Use natural-language behavior queries, not synonym dumps. Defaults to hybrid ranking; read returned ranges next.",
190
- promptSnippet: "Use repo_search for conceptual codebase questions; query for behavior, not a bag of synonyms. Leave default hybrid ranking for first-pass searches and use Grep/read when exact names or positions are known.",
192
+ description: "Indexed hybrid search for behavior when files or symbols are unknown. First pass: at most 3 results, no --include-content.",
193
+ promptSnippet: "Search behavior, not synonyms; keep hybrid unless lexical matches mislead. Read best returned ranges with offset/limit, not whole files.",
191
194
  promptGuidelines: [
192
- "Phrase target as behavior, not synonym dumps; keep exact identifiers only as anchors. Prefer default hybrid first, using --mode semantic only when lexical/symbol terms mislead.",
193
- "Make one targeted search, narrow with --path-prefix/--max-files/--dedupe-file/--exclude-tests when useful, read best ranges, then refine only for a named gap; avoid duplicate broad searches.",
194
- "For bug/cause questions, stop when a read range shows the causal assignment/write/branch/call; continue only for named gaps such as callers, persistence, tests, or requested impact.",
195
+ "Use --path-prefix/--dedupe-file when appropriate. Expand only for a named gap; use Grep for exact identifiers.",
196
+ "--include-content only for a narrow follow-up needing inline code, with --max-files 1; otherwise use read.",
197
+ "After finding the causal code, stop broad search; inspect callers, persistence or tests only for a named gap.",
195
198
  ],
196
199
  targetDescription: "Natural-language behavior query, e.g. auth session token validation.",
197
200
  },
@@ -200,9 +203,9 @@ export const REPO_DISCOVERY_TOOLS: RepoDiscoveryToolDescription[] = [
200
203
  label: "Repo Explain",
201
204
  command: "explain",
202
205
  description: "Indexed explanation for a known symbol. Prefer file::symbol when the name may be ambiguous.",
203
- promptSnippet: "Use repo_explain for a known symbol after you already know its name or file scope.",
206
+ promptSnippet: "Use file::symbol; start --signature-only when signatures suffice.",
204
207
  promptGuidelines: [
205
- "Prefer target=file::symbol for ambiguous names; use repo_search instead when the relevant symbol is still unknown.",
208
+ "Add --include-body --body-lines 20 only for implementation details; use repo_search when the symbol is unknown.",
206
209
  ],
207
210
  targetDescription: "Symbol or file-scoped symbol, e.g. createClient or src/api/client.ts::createClient.",
208
211
  },
@@ -210,10 +213,10 @@ export const REPO_DISCOVERY_TOOLS: RepoDiscoveryToolDescription[] = [
210
213
  name: "repo_deps",
211
214
  label: "Repo Deps",
212
215
  command: "deps",
213
- description: "Indexed dependency/caller tracing for a known path or symbol. Use for import impact and first-hop call/dependency analysis.",
214
- promptSnippet: "Use repo_deps with target=<path|path::symbol> to trace imports, imported-by, callers, or callees.",
216
+ description: "Indexed import/call dependencies for a known path or file::symbol.",
217
+ promptSnippet: "Start --depth 1 and choose --direction callers or callees for the question.",
215
218
  promptGuidelines: [
216
- "Use repo_search first when path/symbol is unknown. Otherwise start --depth 1; add --direction, --mode calls, or --show-edges only when needed.",
219
+ "Use --mode calls for call relationships. Add --show-edges/--tests or deeper traversal only for a named impact-analysis gap.",
217
220
  ],
218
221
  targetDescription: "Path or file-scoped symbol, e.g. src/api/client.ts or src/api/client.ts::createClient.",
219
222
  },
@@ -263,10 +266,10 @@ export const SESSION_RECOVERY_TOOL_DESCRIPTIONS = {
263
266
  readSection: {
264
267
  name: "session_read_section",
265
268
  label: "Session Read Section",
266
- description: "Read a bounded raw-history section returned by session_overview or session_search, including messages, tool calls/results, and compaction summaries.",
267
- promptSnippet: "Read one stable raw-session section by ID after session_overview or session_search.",
269
+ description: "Read bounded raw session history by stable section ID or exact entry ID, with opaque continuation cursors for long sections and long entries.",
270
+ promptSnippet: "Read one raw-session section or exact entry; continue with the returned cursor when more text is available.",
268
271
  promptGuidelines: [
269
- "Pass a section ID produced with the same active/all scope; keep entry and body limits small unless more detail is necessary.",
272
+ "Pass either section_id or entry_id with the same active/all scope; use next_cursor to continue instead of restarting from the section head.",
270
273
  ],
271
274
  },
272
275
  search: {
@@ -275,7 +278,7 @@ export const SESSION_RECOVERY_TOOL_DESCRIPTIONS = {
275
278
  description: "Search raw session messages, summaries, tool results, and serialized tool arguments with a bounded literal substring query.",
276
279
  promptSnippet: "Search raw session history lexically when a concrete phrase, path, symbol, tool, or error is known.",
277
280
  promptGuidelines: [
278
- "Use session_search after overview when a concrete query is known; it is lexical rather than semantic, and scope defaults to the active branch.",
281
+ "Use session_search when a concrete query is known; it is lexical rather than semantic, scope defaults to the active branch, and next_cursor continues large match sets.",
279
282
  ],
280
283
  },
281
284
  recoveryContext: {
@@ -314,6 +317,8 @@ export const WEB_SEARCH_TOOL_DESCRIPTIONS = {
314
317
  },
315
318
  } satisfies Record<string, ToolDescription>;
316
319
 
320
+ const SHELL_TEST_OUTPUT_GUIDANCE = "Tests: save full stdout/stderr to a unique log; emit only TEST_RESULT (passed/failed/incomplete), command, original exit code, verified counts (or unknown), and log path. Omit per-test PASS lines; show bounded exact failure diagnostics and flag omissions. Read only needed log ranges, never dump it. Preserve test exit status; timeout/abort is incomplete. Prefer supported compact reporters; do not alter tests/config just to shorten output.";
321
+
317
322
  export function claudeAliasToolDescriptions(options: ToolDescriptionSetOptions | boolean = false) {
318
323
  const repoDiscovery = hasRepoDiscovery(options);
319
324
 
@@ -338,7 +343,7 @@ export function claudeAliasToolDescriptions(options: ToolDescriptionSetOptions |
338
343
  Bash: {
339
344
  name: "Bash",
340
345
  label: "Bash",
341
- description: "Run shell commands for builds, tests, package managers, git, and project CLIs. Prefer Read/Edit/Write/Grep/Glob for file operations.",
346
+ description: `Run shell commands for builds, tests, package managers, git, and project CLIs. Prefer Read/Edit/Write/Grep/Glob for file operations. ${SHELL_TEST_OUTPUT_GUIDANCE}`,
342
347
  },
343
348
  Grep: {
344
349
  name: "Grep",
@@ -364,7 +369,7 @@ export const CODEX_ALIAS_TOOL_DESCRIPTIONS = {
364
369
  shellCommand: {
365
370
  name: "shell",
366
371
  label: "shell",
367
- description: "Run shell commands for builds, tests, package managers, git, and project CLIs. For long/verification output, redirect to a log and show only bounded tail on failure; summarize passing logs. Set workdir/cwd instead of cd; prefer read for simple file reads.",
372
+ description: `Run shell commands for builds, tests, package managers, git, and project CLIs. Set workdir/cwd instead of cd; prefer read for simple file reads. ${SHELL_TEST_OUTPUT_GUIDANCE}`,
368
373
  },
369
374
  applyPatch: {
370
375
  name: "apply_patch",
@@ -0,0 +1,17 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { normalizeRedundantTruncationMetadata } from "../context-gateway/metadata-normalization.js";
4
+
5
+ /**
6
+ * Optional non-store optimization for SDK tool results.
7
+ *
8
+ * The module removes only truncation metadata text that is proven to duplicate
9
+ * the already-delivered visible text. It is disabled by default so existing
10
+ * sessions, including Context Gateway off/observe, remain byte-equivalent.
11
+ */
12
+ export default function truncationMetadataNormalizer(pi: ExtensionAPI): void {
13
+ pi.on("tool_result", async (event) => {
14
+ const normalized = normalizeRedundantTruncationMetadata(event);
15
+ return normalized.changed ? { details: normalized.details } : undefined;
16
+ });
17
+ }