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
@@ -1,6 +1,15 @@
1
1
  import path from "node:path";
2
2
  import { REPO_DISCOVERY_TOOLS } from "../tool-descriptions";
3
3
  import { directoryExists, findIndexedProjectRoot, findProjectRoot } from "../lib/project.js";
4
+ import {
5
+ applyNativeCompactPolicy,
6
+ describeNativeCompactArgs,
7
+ loadRepoDiscoveryProfile,
8
+ truncateNativeCompactOutput,
9
+ type NativePolicyOutcome,
10
+ type RepoDiscoveryOutputMode,
11
+ type RepoDiscoveryProfile,
12
+ } from "./native-compact.js";
4
13
 
5
14
  const IDX_COMMANDS = ["architecture", "structure", "ast", "search", "explain", "deps"] as const;
6
15
  const TARGET_COMMANDS = new Set<string>(["ast", "search", "explain", "deps"]);
@@ -20,6 +29,7 @@ type RepoDiscoveryParams = {
20
29
  args?: string[];
21
30
  maxLines?: number;
22
31
  maxBytes?: number;
32
+ outputMode?: RepoDiscoveryOutputMode;
23
33
  };
24
34
 
25
35
  type RepoDiscoveryWrapperParams = Omit<RepoDiscoveryParams, "command">;
@@ -82,6 +92,10 @@ function numberSchema(description: string, defaultValue: number) {
82
92
  return { type: "number", description, default: defaultValue };
83
93
  }
84
94
 
95
+ function boundedIntegerSchema(description: string, defaultValue: number, maximum: number) {
96
+ return { type: "integer", minimum: 1, maximum, description, default: defaultValue };
97
+ }
98
+
85
99
  function validateCommand(command: string): command is IdxCommand {
86
100
  return IDX_COMMANDS.includes(command as IdxCommand);
87
101
  }
@@ -263,10 +277,26 @@ async function executeRepoDiscovery(
263
277
  signal: AbortSignal | undefined,
264
278
  ctx: ToolContext,
265
279
  toolName: string,
280
+ profile: RepoDiscoveryProfile,
266
281
  ) {
267
282
  if (signal?.aborted) return textResult(`${toolName} cancelled`);
268
283
 
269
- const idxArgs = buildIdxArgs(params, toolName);
284
+ let policyOutcome: NativePolicyOutcome | undefined;
285
+ let effectiveParams = params;
286
+ if (profile === "native-compact") {
287
+ const policy = applyNativeCompactPolicy({
288
+ command: params.command,
289
+ args: params.args,
290
+ maxLines: params.maxLines,
291
+ maxBytes: params.maxBytes,
292
+ outputMode: params.outputMode,
293
+ });
294
+ policyOutcome = policy.outcome;
295
+ if (policy.ok === false) return textResult(`Native Compact refused ${toolName}: ${policy.message}`, true, { nativePolicy: policy.outcome });
296
+ effectiveParams = { ...params, args: policy.args, maxLines: policy.maxLines, maxBytes: policy.maxBytes };
297
+ }
298
+
299
+ const idxArgs = buildIdxArgs(effectiveParams, toolName);
270
300
  if (typeof idxArgs === "string") return textResult(idxArgs, true);
271
301
 
272
302
  const indexedProject = ensureIndexedProject(ctx.cwd, toolName);
@@ -276,9 +306,11 @@ async function executeRepoDiscovery(
276
306
  const exitCode = result.code ?? 0;
277
307
  const combined = [result.stdout, result.stderr].filter(Boolean).join(result.stdout && result.stderr ? "\n" : "");
278
308
  const output = combined.trim() ? combined : "No output";
279
- const maxLines = positiveInteger(params.maxLines, DEFAULT_MAX_LINES);
280
- const maxBytes = positiveInteger(params.maxBytes, DEFAULT_MAX_BYTES);
281
- const truncated = truncateOutput(output, maxLines, maxBytes);
309
+ const maxLines = positiveInteger(effectiveParams.maxLines, DEFAULT_MAX_LINES);
310
+ const maxBytes = positiveInteger(effectiveParams.maxBytes, DEFAULT_MAX_BYTES);
311
+ const truncated = profile === "native-compact"
312
+ ? truncateNativeCompactOutput(output, maxLines, maxBytes, effectiveParams.outputMode ?? "compact")
313
+ : truncateOutput(output, maxLines, maxBytes);
282
314
 
283
315
  return textResult(truncated.text, exitCode !== 0, {
284
316
  command: ["idx", ...idxArgs],
@@ -286,12 +318,18 @@ async function executeRepoDiscovery(
286
318
  initializedProject: indexedProject.initialized,
287
319
  exitCode,
288
320
  truncation: truncated.truncation,
321
+ ...(policyOutcome ? { nativePolicy: policyOutcome } : {}),
289
322
  });
290
323
  }
291
324
 
292
- const COMMON_REPO_TOOL_PROPERTIES = {
293
- maxLines: numberSchema("Maximum output lines to return before truncating from the bottom (keeps the first/top lines; default 2000).", DEFAULT_MAX_LINES),
294
- maxBytes: numberSchema("Maximum output bytes to return before truncating from the bottom (keeps the first/top lines; default 50000).", DEFAULT_MAX_BYTES),
325
+ const BASELINE_REPO_TOOL_PROPERTIES = {
326
+ maxLines: numberSchema("Returned line cap, keeps top lines (default 2000). Prefer native limits/cursors before raising.", DEFAULT_MAX_LINES),
327
+ maxBytes: numberSchema("Returned byte cap, keeps top bytes (default 50000). Narrow the query before raising.", DEFAULT_MAX_BYTES),
328
+ };
329
+
330
+ const NATIVE_COMPACT_REPO_TOOL_PROPERTIES = {
331
+ maxLines: boundedIntegerSchema("Final delivered line cap. compact default/max 400; outputMode=full permits up to 2000.", 400, 2_000),
332
+ maxBytes: boundedIntegerSchema("Final delivered byte cap. compact default/max 12000; outputMode=full permits up to 50000.", 12_000, 50_000),
295
333
  };
296
334
 
297
335
  const IDX_ARG_DESCRIPTIONS: Record<IdxCommand, string> = {
@@ -300,22 +338,41 @@ const IDX_ARG_DESCRIPTIONS: Record<IdxCommand, string> = {
300
338
  "idx structure flags: [--path-prefix <area>] [--kind <kind>] [--max-depth <n>] [--max-files <n>] [--cursor <n>] [--include-internal] [--no-tests] [--include-tests-summary].",
301
339
  ast: "idx ast flags: [--max-depth <n>] [--max-nodes <n>] [--cursor <n>] [--no-include-text].",
302
340
  search:
303
- "idx search flags: [--max-files <n>] [--path-prefix <area>] [--chunk-types <types|api|impl|tests|imports>] [--mode hybrid|semantic|lexical|symbol] [--min-score <score>] [--include-content] [--include-imports] [--dedupe-file] [--dedupe-symbol] [--cluster] [--exclude-tests] [--include-tests].",
341
+ "idx search: default 3 results without code; --include-content only for narrow follow-up. Flags: [--max-files <n>] [--path-prefix <area>] [--chunk-types <types|api|impl|tests|imports>] [--mode hybrid|semantic|lexical|symbol] [--min-score <score>] [--include-content] [--include-imports] [--dedupe-file] [--dedupe-symbol] [--cluster] [--exclude-tests] [--include-tests].",
304
342
  explain:
305
343
  "idx explain flags: [--path-prefix <area>] [--include-body] [--body-lines <n>] [--signature-only].",
306
344
  deps: "idx deps flags: [--mode modules|module-imports|calls|call-graph] [--direction callers|callees|both] [--depth <n>] [--show-edges] [--tests].",
307
345
  };
308
346
 
309
- function argsSchema(command: IdxCommand) {
347
+ function argsSchema(command: IdxCommand, profile: RepoDiscoveryProfile) {
348
+ const baseDescription = IDX_ARG_DESCRIPTIONS[command];
310
349
  return {
311
350
  type: "array",
312
- items: stringSchema("idx argv token; pass flags and values as separate items"),
313
- description: IDX_ARG_DESCRIPTIONS[command],
351
+ items: profile === "native-compact"
352
+ ? { type: "string", minLength: 1, description: "idx argv token; --flag=value is normalized and revalidated by Native Compact" }
353
+ : stringSchema("idx argv token; pass flags and values as separate items"),
354
+ description: profile === "native-compact"
355
+ ? `${baseDescription} ${describeNativeCompactArgs(command)}`
356
+ : baseDescription,
314
357
  };
315
358
  }
316
359
 
317
- function repoToolParameters(command: IdxCommand, targetDescription?: string) {
318
- const properties = { args: argsSchema(command), ...COMMON_REPO_TOOL_PROPERTIES };
360
+ function repoToolParameters(command: IdxCommand, targetDescription: string | undefined, profile: RepoDiscoveryProfile) {
361
+ const outputProperties = profile === "native-compact"
362
+ ? NATIVE_COMPACT_REPO_TOOL_PROPERTIES
363
+ : BASELINE_REPO_TOOL_PROPERTIES;
364
+ const properties = {
365
+ args: argsSchema(command, profile),
366
+ ...outputProperties,
367
+ ...(profile === "native-compact" ? {
368
+ outputMode: {
369
+ type: "string",
370
+ enum: ["compact", "full"],
371
+ default: "compact",
372
+ description: "Native Compact delivery: compact by default. Use full only on this same tool call when broader output is intentional or its compact result was actually truncated; do not switch an unrelated repo tool to full after a policy refusal.",
373
+ },
374
+ } : {}),
375
+ };
319
376
 
320
377
  return {
321
378
  type: "object",
@@ -336,6 +393,7 @@ function registerRepoCommandTool(
336
393
  promptGuidelines: string[];
337
394
  targetDescription?: string;
338
395
  },
396
+ profile: RepoDiscoveryProfile,
339
397
  ) {
340
398
  pi.registerTool({
341
399
  name: options.name,
@@ -343,15 +401,23 @@ function registerRepoCommandTool(
343
401
  description: options.description,
344
402
  promptSnippet: options.promptSnippet,
345
403
  promptGuidelines: options.promptGuidelines,
346
- parameters: repoToolParameters(options.command, options.targetDescription),
404
+ parameters: repoToolParameters(options.command, options.targetDescription, profile),
347
405
 
348
406
  async execute(_toolCallId: string, params: RepoDiscoveryWrapperParams, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ToolContext) {
349
- return executeRepoDiscovery(pi, { ...params, command: options.command }, signal, ctx, options.name);
407
+ return executeRepoDiscovery(pi, { ...params, command: options.command }, signal, ctx, options.name, profile);
350
408
  },
351
409
  });
352
410
  }
353
411
 
354
- export default function repoDiscoveryExtension(pi: ExtensionAPI) {
412
+ export type RepoDiscoveryExtensionOptions = {
413
+ profile?: RepoDiscoveryProfile;
414
+ cwd?: string;
415
+ };
416
+
417
+ export default function repoDiscoveryExtension(pi: ExtensionAPI, options: RepoDiscoveryExtensionOptions = {}) {
418
+ const registrationCwd = options.cwd ?? process.cwd();
419
+ const profileConfig = options.profile ? { profile: options.profile, issues: [] } : loadRepoDiscoveryProfile(registrationCwd);
420
+ const profile = profileConfig.profile;
355
421
  pi.registerCommand(INIT_COMMAND_NAME, {
356
422
  description: "Initialize idx repository discovery for this project, then reload Pi to expose repo_* tools",
357
423
  handler: async (_args: string, ctx: CommandContext) => {
@@ -424,8 +490,8 @@ export default function repoDiscoveryExtension(pi: ExtensionAPI) {
424
490
  },
425
491
  });
426
492
 
427
- if (!findIndexedProjectRoot(process.cwd())) return;
493
+ if (!findIndexedProjectRoot(registrationCwd)) return;
428
494
 
429
- for (const tool of REPO_DISCOVERY_TOOLS) registerRepoCommandTool(pi, tool);
495
+ for (const tool of REPO_DISCOVERY_TOOLS) registerRepoCommandTool(pi, tool, profile);
430
496
 
431
497
  }
@@ -0,0 +1,458 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, parse, resolve } from "node:path";
4
+ import { parse as parseJsonc } from "jsonc-parser";
5
+
6
+ import { getPiToolsSuiteUserConfigPath } from "../config.js";
7
+
8
+ export const REPO_DISCOVERY_PROFILES = ["baseline", "native-compact"] as const;
9
+ export type RepoDiscoveryProfile = (typeof REPO_DISCOVERY_PROFILES)[number];
10
+ export type RepoDiscoveryOutputMode = "compact" | "full";
11
+ export type RepoIdxCommand = "architecture" | "structure" | "ast" | "search" | "explain" | "deps";
12
+
13
+ export const NATIVE_COMPACT_OUTPUT_LIMITS = Object.freeze({
14
+ compact: { maxLines: 400, maxBytes: 12_000 },
15
+ full: { maxLines: 2_000, maxBytes: 50_000 },
16
+ });
17
+
18
+ export interface RepoDiscoveryProfileConfig {
19
+ profile: RepoDiscoveryProfile;
20
+ issues: string[];
21
+ }
22
+
23
+ export interface NativePolicyOutcome {
24
+ version: 1;
25
+ profile: "native-compact";
26
+ outputMode: RepoDiscoveryOutputMode;
27
+ maxLines: number;
28
+ maxBytes: number;
29
+ refused: boolean;
30
+ reason?: NativePolicyRefusalReason;
31
+ }
32
+
33
+ export type NativePolicyRefusalReason =
34
+ | "invalid-wrapper-budget"
35
+ | "unknown-flag"
36
+ | "missing-flag-value"
37
+ | "duplicate-flag"
38
+ | "invalid-flag-value"
39
+ | "conflicting-flags"
40
+ | "compact-limit-exceeded"
41
+ | "full-limit-exceeded";
42
+
43
+ export type NativePolicyResult =
44
+ | { ok: true; args: string[]; maxLines: number; maxBytes: number; outcome: NativePolicyOutcome }
45
+ | { ok: false; message: string; outcome: NativePolicyOutcome };
46
+
47
+ export interface NativePolicyParams {
48
+ command: RepoIdxCommand;
49
+ args?: string[];
50
+ maxLines?: number;
51
+ maxBytes?: number;
52
+ outputMode?: RepoDiscoveryOutputMode;
53
+ }
54
+
55
+ export interface StrictTruncation {
56
+ truncated: boolean;
57
+ totalLines: number;
58
+ outputLines: number;
59
+ totalBytes: number;
60
+ outputBytes: number;
61
+ }
62
+
63
+ type FlagKind = "boolean" | "string" | "positive-int" | "nonnegative-int" | "number";
64
+ type FlagSpec = { kind: FlagKind; values?: readonly string[] };
65
+
66
+ const FLAG_SPECS: Record<RepoIdxCommand, Record<string, FlagSpec>> = {
67
+ architecture: {
68
+ "--path-prefix": { kind: "string" },
69
+ },
70
+ structure: {
71
+ "--path-prefix": { kind: "string" },
72
+ "--kind": { kind: "string" },
73
+ "--max-depth": { kind: "positive-int" },
74
+ "--max-files": { kind: "positive-int" },
75
+ "--cursor": { kind: "nonnegative-int" },
76
+ "--include-internal": { kind: "boolean" },
77
+ "--no-tests": { kind: "boolean" },
78
+ "--include-tests-summary": { kind: "boolean" },
79
+ },
80
+ ast: {
81
+ "--max-depth": { kind: "positive-int" },
82
+ "--max-nodes": { kind: "positive-int" },
83
+ "--cursor": { kind: "nonnegative-int" },
84
+ "--no-include-text": { kind: "boolean" },
85
+ },
86
+ search: {
87
+ "--max-files": { kind: "positive-int" },
88
+ "--path-prefix": { kind: "string" },
89
+ "--chunk-types": { kind: "string" },
90
+ "--mode": { kind: "string", values: ["hybrid", "semantic", "lexical", "symbol"] },
91
+ "--include-imports": { kind: "boolean" },
92
+ "--min-score": { kind: "number" },
93
+ "--include-content": { kind: "boolean" },
94
+ "--dedupe-file": { kind: "boolean" },
95
+ "--dedupe-symbol": { kind: "boolean" },
96
+ "--cluster": { kind: "boolean" },
97
+ "--exclude-tests": { kind: "boolean" },
98
+ "--include-tests": { kind: "boolean" },
99
+ },
100
+ explain: {
101
+ "--path-prefix": { kind: "string" },
102
+ "--include-body": { kind: "boolean" },
103
+ "--body-lines": { kind: "positive-int" },
104
+ "--signature-only": { kind: "boolean" },
105
+ },
106
+ deps: {
107
+ "--mode": { kind: "string", values: ["modules", "module-imports", "calls", "call-graph"] },
108
+ "--direction": { kind: "string", values: ["callers", "imported-by", "callees", "imports", "both"] },
109
+ "--depth": { kind: "positive-int" },
110
+ "--show-edges": { kind: "boolean" },
111
+ "--tests": { kind: "boolean" },
112
+ },
113
+ };
114
+
115
+ const COMPACT_NATIVE_LIMITS: Partial<Record<RepoIdxCommand, Record<string, number>>> = {
116
+ structure: { "--max-depth": 2, "--max-files": 20 },
117
+ ast: { "--max-depth": 3, "--max-nodes": 40 },
118
+ search: { "--max-files": 3 },
119
+ explain: { "--body-lines": 20 },
120
+ deps: { "--depth": 1 },
121
+ };
122
+
123
+ const FULL_NATIVE_LIMITS: Partial<Record<RepoIdxCommand, Record<string, number>>> = {
124
+ structure: { "--max-depth": 8, "--max-files": 300 },
125
+ ast: { "--max-depth": 8, "--max-nodes": 500 },
126
+ search: { "--max-files": 50 },
127
+ explain: { "--body-lines": 200 },
128
+ deps: { "--depth": 6 },
129
+ };
130
+
131
+ const NATIVE_COMPACT_ARG_GUIDANCE: Record<RepoIdxCommand, string> = {
132
+ architecture:
133
+ "Native Compact: keep outputMode=compact unless this same tool result is actually truncated; scope with --path-prefix before using full.",
134
+ structure:
135
+ "Native Compact: compact allows --max-files<=20 and --max-depth<=2; continue with --cursor. For an intentional broader structure call, set outputMode=full on that same call (full limits: --max-files<=300, --max-depth<=8). Do not retry a rejected compact value unchanged.",
136
+ ast:
137
+ "Native Compact: compact allows --max-nodes<=40 and --max-depth<=3 and defaults to --no-include-text; continue with --cursor. For an intentional broader AST call, set outputMode=full on that same call (full limits: --max-nodes<=500, --max-depth<=8). Do not retry a rejected compact value unchanged.",
138
+ search:
139
+ "Native Compact: compact allows --max-files<=3; with --include-content use exactly --max-files 1. For an intentional broader search, set outputMode=full on that same call (full limit: --max-files<=50). Do not retry a rejected compact value unchanged.",
140
+ explain:
141
+ "Native Compact: compact defaults to --signature-only; with --include-body use --body-lines<=20. For an intentional broader explanation, set outputMode=full on that same call (full limit: --body-lines<=200). Do not retry a rejected compact value unchanged.",
142
+ deps:
143
+ "Native Compact: compact allows --depth<=1. For an intentional deeper dependency traversal, set outputMode=full on that same call (full limit: --depth<=6). Do not retry a rejected compact value unchanged.",
144
+ };
145
+
146
+ export function describeNativeCompactArgs(command: RepoIdxCommand): string {
147
+ return NATIVE_COMPACT_ARG_GUIDANCE[command];
148
+ }
149
+
150
+ function isRecord(value: unknown): value is Record<string, unknown> {
151
+ return value !== null && typeof value === "object" && !Array.isArray(value);
152
+ }
153
+
154
+ function readJsonc(filePath: string): Record<string, unknown> {
155
+ if (!existsSync(filePath)) return {};
156
+ try {
157
+ const parsed = parseJsonc(readFileSync(filePath, "utf8"));
158
+ return isRecord(parsed) ? parsed : {};
159
+ } catch {
160
+ return {};
161
+ }
162
+ }
163
+
164
+ function findProjectConfig(startDir: string): string | undefined {
165
+ let dir = resolve(startDir);
166
+ const root = parse(dir).root;
167
+ while (true) {
168
+ const candidate = join(dir, ".pi", "pi-tools-suite.jsonc");
169
+ if (existsSync(candidate)) return candidate;
170
+ if (dir === root) return undefined;
171
+ const parent = dirname(dir);
172
+ if (parent === dir) return undefined;
173
+ dir = parent;
174
+ }
175
+ }
176
+
177
+ function parseProfile(value: unknown): RepoDiscoveryProfile | undefined {
178
+ if (value === "baseline" || value === "native-compact") return value;
179
+ return undefined;
180
+ }
181
+
182
+ export function loadRepoDiscoveryProfile(
183
+ cwd: string = process.cwd(),
184
+ env: NodeJS.ProcessEnv = process.env,
185
+ homeDir: string = env.HOME ?? process.env.HOME ?? homedir(),
186
+ ): RepoDiscoveryProfileConfig {
187
+ let profile: RepoDiscoveryProfile = "baseline";
188
+ const issues: string[] = [];
189
+ const layers = [getPiToolsSuiteUserConfigPath(homeDir)];
190
+ if (env.PI_CONFIG_DIR) layers.push(join(env.PI_CONFIG_DIR, "pi-tools-suite.jsonc"));
191
+ const projectConfig = findProjectConfig(cwd);
192
+ if (projectConfig) layers.push(projectConfig);
193
+
194
+ for (const filePath of layers) {
195
+ const section = readJsonc(filePath).repoDiscovery;
196
+ if (!isRecord(section) || section.profile === undefined) continue;
197
+ const next = parseProfile(section.profile);
198
+ if (next) profile = next;
199
+ else issues.push(`repoDiscovery.profile must be baseline or native-compact; keeping ${profile}.`);
200
+ }
201
+
202
+ if (env.PI_REPO_DISCOVERY_PROFILE !== undefined) {
203
+ const next = parseProfile(env.PI_REPO_DISCOVERY_PROFILE);
204
+ if (next) profile = next;
205
+ else issues.push("PI_REPO_DISCOVERY_PROFILE must be baseline or native-compact; ignoring it.");
206
+ }
207
+
208
+ return { profile, issues };
209
+ }
210
+
211
+ function validateNumeric(kind: FlagKind, value: string): boolean {
212
+ const numeric = Number(value);
213
+ if (!Number.isFinite(numeric)) return false;
214
+ if (kind === "number") return true;
215
+ if (!Number.isSafeInteger(numeric)) return false;
216
+ if (kind === "positive-int") return numeric > 0;
217
+ if (kind === "nonnegative-int") return numeric >= 0;
218
+ return true;
219
+ }
220
+
221
+ function parseArgs(command: RepoIdxCommand, args: string[]):
222
+ | { ok: true; normalized: string[]; values: Map<string, string | true> }
223
+ | { ok: false; reason: NativePolicyRefusalReason; message: string } {
224
+ const specs = FLAG_SPECS[command];
225
+ const normalized: string[] = [];
226
+ const values = new Map<string, string | true>();
227
+
228
+ for (let index = 0; index < args.length; index++) {
229
+ const token = args[index]!;
230
+ if (!token.startsWith("--")) {
231
+ return { ok: false, reason: "unknown-flag", message: `Unexpected native idx argument: ${token}. Pass only documented flags in args.` };
232
+ }
233
+
234
+ const equalsIndex = token.indexOf("=");
235
+ const flag = equalsIndex >= 0 ? token.slice(0, equalsIndex) : token;
236
+ const inlineValue = equalsIndex >= 0 ? token.slice(equalsIndex + 1) : undefined;
237
+ const spec = specs[flag];
238
+ if (!spec) return { ok: false, reason: "unknown-flag", message: `Unsupported ${command} flag in Native Compact: ${flag}.` };
239
+ if (values.has(flag)) return { ok: false, reason: "duplicate-flag", message: `Duplicate ${command} flag is not allowed: ${flag}.` };
240
+
241
+ if (spec.kind === "boolean") {
242
+ if (inlineValue !== undefined) {
243
+ return { ok: false, reason: "invalid-flag-value", message: `${flag} is a boolean flag and does not accept =value.` };
244
+ }
245
+ values.set(flag, true);
246
+ normalized.push(flag);
247
+ continue;
248
+ }
249
+
250
+ const value = inlineValue ?? args[++index];
251
+ if (value === undefined || value.length === 0) {
252
+ return { ok: false, reason: "missing-flag-value", message: `${flag} requires a value.` };
253
+ }
254
+ if (inlineValue === undefined && value.startsWith("--")) {
255
+ return { ok: false, reason: "missing-flag-value", message: `${flag} requires a value before the next flag.` };
256
+ }
257
+ if (spec.values && !spec.values.includes(value)) {
258
+ return { ok: false, reason: "invalid-flag-value", message: `${flag} must be one of: ${spec.values.join(", ")}.` };
259
+ }
260
+ if (["positive-int", "nonnegative-int", "number"].includes(spec.kind) && !validateNumeric(spec.kind, value)) {
261
+ return { ok: false, reason: "invalid-flag-value", message: `${flag} has an invalid numeric value.` };
262
+ }
263
+ values.set(flag, value);
264
+ normalized.push(flag, value);
265
+ }
266
+
267
+ return { ok: true, normalized, values };
268
+ }
269
+
270
+ function numericValue(values: Map<string, string | true>, flag: string): number | undefined {
271
+ const raw = values.get(flag);
272
+ return typeof raw === "string" ? Number(raw) : undefined;
273
+ }
274
+
275
+ function appendValue(args: string[], values: Map<string, string | true>, flag: string, value: string): void {
276
+ if (values.has(flag)) return;
277
+ values.set(flag, value);
278
+ args.push(flag, value);
279
+ }
280
+
281
+ function appendBoolean(args: string[], values: Map<string, string | true>, flag: string): void {
282
+ if (values.has(flag)) return;
283
+ values.set(flag, true);
284
+ args.push(flag);
285
+ }
286
+
287
+ function refusal(
288
+ outputMode: RepoDiscoveryOutputMode,
289
+ maxLines: number,
290
+ maxBytes: number,
291
+ reason: NativePolicyRefusalReason,
292
+ message: string,
293
+ ): NativePolicyResult {
294
+ return {
295
+ ok: false,
296
+ message,
297
+ outcome: { version: 1, profile: "native-compact", outputMode, maxLines, maxBytes, refused: true, reason },
298
+ };
299
+ }
300
+
301
+ function validWrapperBudget(value: number | undefined): value is number {
302
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
303
+ }
304
+
305
+ export function applyNativeCompactPolicy(params: NativePolicyParams): NativePolicyResult {
306
+ const outputMode = params.outputMode ?? "compact";
307
+ if (outputMode !== "compact" && outputMode !== "full") {
308
+ return refusal("compact", NATIVE_COMPACT_OUTPUT_LIMITS.compact.maxLines, NATIVE_COMPACT_OUTPUT_LIMITS.compact.maxBytes, "invalid-flag-value", "Native Compact outputMode must be compact or full.");
309
+ }
310
+ const outputLimits = NATIVE_COMPACT_OUTPUT_LIMITS[outputMode];
311
+ const maxLines = params.maxLines === undefined ? outputLimits.maxLines : params.maxLines;
312
+ const maxBytes = params.maxBytes === undefined ? outputLimits.maxBytes : params.maxBytes;
313
+ if (!validWrapperBudget(maxLines) || !validWrapperBudget(maxBytes)) {
314
+ return refusal(outputMode, outputLimits.maxLines, outputLimits.maxBytes, "invalid-wrapper-budget", "Native Compact maxLines/maxBytes must be positive integers.");
315
+ }
316
+ if (maxLines > outputLimits.maxLines || maxBytes > outputLimits.maxBytes) {
317
+ return refusal(
318
+ outputMode,
319
+ maxLines,
320
+ maxBytes,
321
+ outputMode === "compact" ? "compact-limit-exceeded" : "full-limit-exceeded",
322
+ `Native Compact ${outputMode} output budget exceeded. Allowed maxLines=${outputLimits.maxLines}, maxBytes=${outputLimits.maxBytes}.`,
323
+ );
324
+ }
325
+
326
+ const parsed = parseArgs(params.command, params.args ?? []);
327
+ if (parsed.ok === false) return refusal(outputMode, maxLines, maxBytes, parsed.reason, parsed.message);
328
+ const args = [...parsed.normalized];
329
+ const values = parsed.values;
330
+
331
+ if (params.command === "search" && values.has("--include-tests") && values.has("--exclude-tests")) {
332
+ return refusal(outputMode, maxLines, maxBytes, "conflicting-flags", "repo_search cannot combine --include-tests with --exclude-tests.");
333
+ }
334
+ if (params.command === "structure" && values.has("--no-tests") && values.has("--include-tests-summary")) {
335
+ return refusal(outputMode, maxLines, maxBytes, "conflicting-flags", "repo_structure cannot combine --no-tests with --include-tests-summary.");
336
+ }
337
+ if (params.command === "explain" && values.has("--include-body") && values.has("--signature-only")) {
338
+ return refusal(outputMode, maxLines, maxBytes, "conflicting-flags", "repo_explain cannot combine --include-body with --signature-only.");
339
+ }
340
+
341
+ if (outputMode === "compact") {
342
+ if (params.command === "structure") {
343
+ appendValue(args, values, "--max-files", "20");
344
+ appendValue(args, values, "--max-depth", "2");
345
+ }
346
+ if (params.command === "ast") {
347
+ appendValue(args, values, "--max-depth", "3");
348
+ appendValue(args, values, "--max-nodes", "40");
349
+ appendBoolean(args, values, "--no-include-text");
350
+ }
351
+ if (params.command === "search" && values.has("--include-content")) {
352
+ appendValue(args, values, "--max-files", "1");
353
+ }
354
+ if (params.command === "explain") {
355
+ if (values.has("--include-body")) appendValue(args, values, "--body-lines", "20");
356
+ else appendBoolean(args, values, "--signature-only");
357
+ }
358
+ if (params.command === "deps") appendValue(args, values, "--depth", "1");
359
+ }
360
+
361
+ const nativeLimits = outputMode === "compact" ? COMPACT_NATIVE_LIMITS[params.command] : FULL_NATIVE_LIMITS[params.command];
362
+ for (const [flag, maximum] of Object.entries(nativeLimits ?? {})) {
363
+ const value = numericValue(values, flag);
364
+ if (value !== undefined && value > maximum) {
365
+ const recovery = outputMode === "compact"
366
+ ? `Retry with ${flag}<=${maximum} (prefer a native cursor/narrower scope), or set outputMode=full on this same tool call when the broader request is intentional. Do not repeat the rejected compact value unchanged.`
367
+ : `Retry with ${flag}<=${maximum} and narrow/page the request. Do not repeat the rejected full value unchanged.`;
368
+ return refusal(
369
+ outputMode,
370
+ maxLines,
371
+ maxBytes,
372
+ outputMode === "compact" ? "compact-limit-exceeded" : "full-limit-exceeded",
373
+ `${flag}=${value} exceeds Native Compact ${outputMode} limit ${maximum}. ${recovery}`,
374
+ );
375
+ }
376
+ }
377
+
378
+ if (params.command === "search" && outputMode === "compact" && values.has("--include-content")) {
379
+ const maxFiles = numericValue(values, "--max-files");
380
+ if (maxFiles !== 1) {
381
+ return refusal(
382
+ outputMode,
383
+ maxLines,
384
+ maxBytes,
385
+ "compact-limit-exceeded",
386
+ "--include-content in compact mode requires --max-files 1. Retry with --max-files 1, or set outputMode=full on this same search call when broad inline content is intentional; do not repeat the rejected compact value unchanged.",
387
+ );
388
+ }
389
+ }
390
+
391
+ return {
392
+ ok: true,
393
+ args,
394
+ maxLines,
395
+ maxBytes,
396
+ outcome: { version: 1, profile: "native-compact", outputMode, maxLines, maxBytes, refused: false },
397
+ };
398
+ }
399
+
400
+ function truncateUtf8Prefix(text: string, maxBytes: number): string {
401
+ let output = "";
402
+ let bytes = 0;
403
+ for (const char of text) {
404
+ const charBytes = Buffer.byteLength(char, "utf8");
405
+ if (bytes + charBytes > maxBytes) break;
406
+ output += char;
407
+ bytes += charBytes;
408
+ }
409
+ return output;
410
+ }
411
+
412
+ export function truncateNativeCompactOutput(
413
+ text: string,
414
+ maxLines: number,
415
+ maxBytes: number,
416
+ outputMode: RepoDiscoveryOutputMode,
417
+ ): { text: string; truncation: StrictTruncation } {
418
+ const totalBytes = Buffer.byteLength(text, "utf8");
419
+ const lines = text.split("\n");
420
+ if (lines.length <= maxLines && totalBytes <= maxBytes) {
421
+ return {
422
+ text,
423
+ truncation: { truncated: false, totalLines: lines.length, outputLines: lines.length, totalBytes, outputBytes: totalBytes },
424
+ };
425
+ }
426
+
427
+ const maxBodyLines = Math.max(0, maxLines - 1);
428
+ let body = lines.slice(0, maxBodyLines).join("\n");
429
+ let bodyBytes = Buffer.byteLength(body, "utf8");
430
+ let bodyLines = body.length === 0 ? 0 : body.split("\n").length;
431
+ const continuation = outputMode === "compact"
432
+ ? "Use a native cursor/narrower scope or outputMode=full."
433
+ : "Use a native cursor or narrower scope.";
434
+
435
+ for (let attempt = 0; attempt < 4; attempt++) {
436
+ const marker = `[Output truncated: first ${bodyLines}/${lines.length} lines, ${bodyBytes}/${totalBytes} bytes. ${continuation}]`;
437
+ const markerBytes = Buffer.byteLength(marker, "utf8");
438
+ const separatorBytes = body.length > 0 ? 1 : 0;
439
+ const availableBodyBytes = Math.max(0, maxBytes - markerBytes - separatorBytes);
440
+ if (bodyBytes > availableBodyBytes) {
441
+ body = truncateUtf8Prefix(body, availableBodyBytes);
442
+ bodyBytes = Buffer.byteLength(body, "utf8");
443
+ bodyLines = body.length === 0 ? 0 : body.split("\n").length;
444
+ continue;
445
+ }
446
+ const finalText = body.length > 0 ? `${body}\n${marker}` : truncateUtf8Prefix(marker, maxBytes);
447
+ return {
448
+ text: finalText,
449
+ truncation: { truncated: true, totalLines: lines.length, outputLines: bodyLines, totalBytes, outputBytes: bodyBytes },
450
+ };
451
+ }
452
+
453
+ const fallback = truncateUtf8Prefix("[Output truncated; narrow the idx query.]", maxBytes);
454
+ return {
455
+ text: fallback,
456
+ truncation: { truncated: true, totalLines: lines.length, outputLines: 0, totalBytes, outputBytes: 0 },
457
+ };
458
+ }