pi-orchestrator-config 4.2.0 → 4.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +76 -67
  2. package/agents/code-reviewer-docs.md +11 -1
  3. package/agents/code-reviewer-guidelines.md +11 -1
  4. package/agents/code-reviewer-quality.md +11 -1
  5. package/agents/code-reviewer-security.md +11 -1
  6. package/agents/code-reviewer-spec.md +11 -1
  7. package/agents/git-expert.md +42 -3
  8. package/agents/github-expert.md +11 -0
  9. package/extensions/acpx-provider/runtime-models.ts +18 -6
  10. package/extensions/cli-provider/agents/claude.ts +3 -2
  11. package/extensions/cli-provider/agents/cursor.ts +3 -5
  12. package/extensions/cli-provider/agents/gemini.ts +3 -2
  13. package/extensions/cli-provider/discover.ts +7 -7
  14. package/extensions/cli-provider/index.ts +5 -0
  15. package/extensions/cli-provider/runtime-models.ts +18 -6
  16. package/extensions/cli-provider/session-reaper.ts +5 -7
  17. package/extensions/cli-provider/sessions.ts +4 -0
  18. package/extensions/cli-provider/shared/discover-cache.ts +17 -176
  19. package/extensions/coms/coms-p2p.ts +1719 -423
  20. package/extensions/coms/coms-shared.ts +261 -39
  21. package/extensions/coms/coms-wrapper.ts +100 -62
  22. package/extensions/coms/index.ts +8 -4
  23. package/extensions/coms/mixed-turn.ts +26 -0
  24. package/extensions/coms/probe-socket.ts +42 -0
  25. package/extensions/coms/themeMap.ts +0 -1
  26. package/extensions/image-gen/image-gen.ts +19 -1
  27. package/extensions/orchestrator/async-agents.ts +563 -102
  28. package/extensions/orchestrator/async-capability.ts +8 -8
  29. package/extensions/orchestrator/cron.ts +15 -9
  30. package/extensions/orchestrator/dreaming.ts +32 -25
  31. package/extensions/orchestrator/enforcement-helpers.ts +459 -17
  32. package/extensions/orchestrator/enforcement-rules.ts +5 -4
  33. package/extensions/orchestrator/enforcement.ts +80 -27
  34. package/extensions/orchestrator/extended-autocomplete.ts +7 -0
  35. package/extensions/orchestrator/git-helpers.ts +2 -2
  36. package/extensions/orchestrator/index.ts +78 -10
  37. package/extensions/orchestrator/markdown-transformer.ts +313 -0
  38. package/extensions/orchestrator/memory-embeddings.ts +36 -34
  39. package/extensions/orchestrator/memory-provenance.ts +29 -25
  40. package/extensions/orchestrator/memory-scoring.ts +23 -18
  41. package/extensions/orchestrator/memory-tools.ts +10 -3
  42. package/extensions/orchestrator/overlay-dashboard.ts +17 -5
  43. package/extensions/orchestrator/parse-model-override.ts +29 -0
  44. package/extensions/orchestrator/pi-config-review-state.ts +57 -33
  45. package/extensions/orchestrator/project-settings.ts +333 -127
  46. package/extensions/orchestrator/resolve-agent-model.ts +24 -13
  47. package/extensions/orchestrator/review-porcelain.ts +4 -0
  48. package/extensions/orchestrator/review-ui.ts +63 -15
  49. package/extensions/orchestrator/rule-placeholders.ts +411 -2
  50. package/extensions/orchestrator/rules.ts +80 -132
  51. package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
  52. package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
  53. package/extensions/orchestrator/settings-tui.ts +636 -0
  54. package/extensions/orchestrator/state-jsonl.ts +431 -0
  55. package/extensions/orchestrator/subagent-tool.ts +157 -59
  56. package/extensions/orchestrator/sync-limit.ts +23 -0
  57. package/extensions/orchestrator/task-lifecycle.ts +77 -67
  58. package/extensions/orchestrator/utils.ts +8 -5
  59. package/extensions/pidash/pidash.ts +193 -156
  60. package/extensions/pidiff/pidiff.ts +96 -38
  61. package/extensions/pidiff/spawn-lock.ts +62 -0
  62. package/extensions/pitasks/index.ts +430 -0
  63. package/extensions/pitasks/reminders.ts +37 -0
  64. package/extensions/pitasks/task-store.ts +467 -0
  65. package/extensions/pitasks/task-tools.ts +375 -0
  66. package/extensions/pitasks/task-widget.ts +181 -0
  67. package/extensions/pitasks/types.ts +38 -0
  68. package/extensions/providers/acpx-driver.ts +12 -2
  69. package/extensions/providers/built-in-drivers.ts +6 -6
  70. package/extensions/providers/claude-driver.ts +1 -1
  71. package/extensions/providers/cursor-cli-driver.ts +1 -1
  72. package/extensions/providers/gemini-driver.ts +1 -1
  73. package/extensions/providers/index.ts +144 -26
  74. package/extensions/providers/initialized-guard.ts +27 -0
  75. package/extensions/providers/restore-default-model.ts +596 -0
  76. package/extensions/providers/session-shutdown.ts +39 -0
  77. package/extensions/shared/coms-active.ts +23 -0
  78. package/extensions/shared/create-runtime-provider.ts +2 -0
  79. package/extensions/shared/daemon-manager.ts +0 -8
  80. package/extensions/shared/file-logger.ts +132 -25
  81. package/extensions/shared/logger.ts +50 -0
  82. package/extensions/shared/models-dev.ts +421 -0
  83. package/extensions/shared/oneshot.ts +116 -0
  84. package/extensions/shared/resolve-binary.ts +191 -0
  85. package/extensions/shared/types.ts +3 -0
  86. package/package.json +9 -3
  87. package/prompts/coderabbit-rate-limit.md +2 -0
  88. package/rules/00-orchestrator-core.md +3 -0
  89. package/rules/10-agent-routing.md +12 -1
  90. package/rules/20-code-review-loop.md +52 -25
  91. package/rules/30-prompt-templates.md +4 -0
  92. package/rules/35-memory.md +3 -3
  93. package/rules/40-critical-rules.md +3 -0
  94. package/rules/55-coms-protocol.md +84 -18
  95. package/rules/60-task-tracking.md +2 -0
  96. package/settings-keys.json +563 -0
  97. package/templates/coms-feature-manager-prompt.md +4 -6
  98. package/extensions/coms/coms-net-server.ts +0 -1642
  99. package/extensions/coms/coms-net-wrapper.ts +0 -521
  100. package/extensions/coms/coms-net.ts +0 -1589
@@ -5,6 +5,7 @@
5
5
  * review state tracking and gitignore checks.
6
6
  */
7
7
 
8
+ import { createLogger } from "../shared/logger.js";
8
9
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
10
  import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
10
11
  import { Type } from "typebox";
@@ -27,13 +28,16 @@ import {
27
28
  runGit,
28
29
  } from "./git-helpers.js";
29
30
  import { spawnSync } from "node:child_process";
30
- import { markNeedsReview, isReviewClean, readReviewState, statePath, markTestsPassed, markTestsFailed } from "./pi-config-review-state.js";
31
+ import { markNeedsReview, isCommitAllowed, readReviewState, statePath, markTestsPassed, markTestsFailed } from "./pi-config-review-state.js";
31
32
  import {
32
33
  checkPythonPipBlock,
33
34
  checkRemoteExecBlock,
34
35
  checkTempFileEnforcement,
35
36
  hasGitAddBulk,
36
37
  hasGitAddForce,
38
+ isRealGitCommitOrPush,
39
+ injectSignoff,
40
+ injectGhBodySignature,
37
41
  isReadOnlyStatement,
38
42
  isRmInProjectTmp,
39
43
  normalizeForRepeatCheck,
@@ -41,9 +45,16 @@ import {
41
45
  stripHeredocBodies,
42
46
  escapeForDoubleQuote,
43
47
  escapeForSingleQuote,
48
+ commandHasTrailerByName,
44
49
  isTestRunnerCommand,
50
+ isBumpVersionBranch,
51
+ getCachedBranch,
45
52
  } from "./enforcement-helpers.js";
46
53
 
54
+ const log = createLogger("enforcement");
55
+
56
+ export { isBumpVersionBranch, getCachedBranch };
57
+
47
58
  type EnforcementResult = { block: true; reason: string } | { autofix: true; modifiedCommand: string; reason: string } | undefined;
48
59
 
49
60
  // Repeat detection via temp file — immune to module reload / closure issues
@@ -72,7 +83,7 @@ function writeRepeatState(state: { lastCmd: string; count: number }): void {
72
83
  if (!REPEAT_FILE) return;
73
84
  try {
74
85
  writeFileSync(REPEAT_FILE, JSON.stringify(state));
75
- } catch (e: any) { console.debug("[enforcement] write repeat state failed:", e?.message || e); }
86
+ } catch (e: any) { log.debug("enforcement: write repeat state failed:", e?.message || e); }
76
87
  }
77
88
 
78
89
 
@@ -82,8 +93,7 @@ async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd
82
93
  const trailerSetting = getSetting(gitCwd, "commit_trailer");
83
94
  if (typeof trailerSetting !== "string") return undefined;
84
95
 
85
- const modelId = (ctx as any).model?.id;
86
- if (!modelId) return undefined;
96
+ const modelId = (process.env.PI_PRIMARY_MODEL || process.env.PI_MODEL || (ctx as any).model?.id || "unknown").replace(/[^a-zA-Z0-9._:@/ -]/g, "").trim() || "unknown";
87
97
 
88
98
  const piIdentity = `PI (${modelId}) <noreply@pi.dev>`;
89
99
  let trailerName: string;
@@ -123,10 +133,6 @@ async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd
123
133
  trailerName = trailerSetting;
124
134
  }
125
135
 
126
- // Raw trailer line for duplicate detection (what the commit message should contain)
127
- const rawTrailerLine = `${trailerName}: ${piIdentity}`;
128
- if (command.includes(rawTrailerLine)) return undefined;
129
-
130
136
  // Pattern A: echo "..." | git commit -F -
131
137
  const pipeIdx = command.lastIndexOf("|");
132
138
  if (pipeIdx !== -1 && /git\s+commit\s+.*-F\s*-/.test(command.slice(pipeIdx))) {
@@ -138,6 +144,14 @@ async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd
138
144
  if (lastQuoteIdx > 0) {
139
145
  // Determine quote context for correct escaping
140
146
  const quoteChar = echoPart[lastQuoteIdx];
147
+ // Scope dedup to the echo/message-construction portion (before the pipe), which
148
+ // excludes the git args. Do NOT attempt to slice out the exact quoted substring
149
+ // by backward-scanning for a quote — escaped quotes (\") inside the message would
150
+ // misparse the boundary and cause a missed trailer -> duplicate injection.
151
+ if (commandHasTrailerByName(echoPart, trailerName)) {
152
+ log.debug("commit_trailer already present in echo message (by name) — skipping injection", trailerName);
153
+ return undefined;
154
+ }
141
155
  const escaped = quoteChar === "'"
142
156
  ? `${escapeForSingleQuote(trailerName)}: ${piIdentity}`
143
157
  : `${escapeForDoubleQuote(trailerName)}: ${piIdentity}`;
@@ -150,6 +164,14 @@ async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd
150
164
  const mFlagMatch = command.match(/git\s+commit\s+.*-m\s+(["'])([\s\S]*?)\1/);
151
165
  if (mFlagMatch) {
152
166
  const quoteChar = mFlagMatch[1];
167
+ // Scope duplicate-by-name detection to the -m message payload only
168
+ // (mFlagMatch[2]) — not the whole command — so an unrelated arg/env var
169
+ // containing the trailer token does not wrongly skip injection.
170
+ const payload = mFlagMatch[2];
171
+ if (commandHasTrailerByName(payload, trailerName)) {
172
+ log.debug("commit_trailer already present in message (by name) — skipping injection", trailerName);
173
+ return undefined;
174
+ }
153
175
  const escaped = quoteChar === "'"
154
176
  ? `${escapeForSingleQuote(trailerName)}: ${piIdentity}`
155
177
  : `${escapeForDoubleQuote(trailerName)}: ${piIdentity}`;
@@ -269,12 +291,17 @@ async function checkGitProtection(command: string, event: any, ctx: any, gitCwd:
269
291
  const trailerResult = await handleCommitTrailer(command, event, ctx, gitCwd);
270
292
  if (trailerResult) return trailerResult;
271
293
 
272
- // DCO enforcement — inject --signoff when dco setting is enabled
273
- if (getSetting(gitCwd, "dco") && !/(?:^|\s)--signoff(?:\s|$)/.test(command) && !/(?:^|\s)-s(?:\s|$)/.test(command)) {
274
- event.input.command = event.input.command.replace(
275
- /\bgit\b((?:\s+(?:-[a-zA-Z]\s+\S+|-\S+))*\s+)commit\b/,
276
- "git$1commit --signoff",
277
- );
294
+ // DCO enforcement — inject --signoff into the REAL git commit invocation only.
295
+ // injectSignoff scopes to the actual invocation (after the last pipe for
296
+ // `echo ... | git commit -F -`, or the command-position-anchored `git commit`),
297
+ // so a `git commit` mention inside the echo/message body is never corrupted.
298
+ // The already-signed (--signoff/-s) guards live inside injectSignoff.
299
+ if (getSetting(gitCwd, "dco")) {
300
+ const signed = injectSignoff(event.input.command);
301
+ if (signed !== event.input.command) {
302
+ log.debug("DCO: injected --signoff into real git commit invocation");
303
+ event.input.command = signed;
304
+ }
278
305
  }
279
306
  }
280
307
 
@@ -362,7 +389,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
362
389
  cachedTrailerSelection = null;
363
390
  // Set comment signature env var for CLI tools to read
364
391
  if (getSetting(ctx.cwd, "comment_signature")) {
365
- const modelId = (ctx as any).model?.id || "unknown";
392
+ const modelId = (process.env.PI_PRIMARY_MODEL || process.env.PI_MODEL || (ctx as any).model?.id || "unknown").replace(/[^a-zA-Z0-9._:@/ -]/g, "").trim() || "unknown";
366
393
  process.env.PI_COMMENT_SIGNATURE = `Assisted-by: PI (${modelId})`;
367
394
  } else {
368
395
  delete process.env.PI_COMMENT_SIGNATURE;
@@ -384,6 +411,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
384
411
  const matches = matchToolCall(blockEntries, toolName, input);
385
412
  if (matches.length > 0) {
386
413
  const rule = matches[0].rule;
414
+ log.info("enforcement_block", toolName, rule.text);
387
415
  return { block: true, reason: `⛔ ENFORCEMENT [${rule.entry.class}]: ${rule.text}` };
388
416
  }
389
417
  }
@@ -396,7 +424,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
396
424
  const toolUsed = isToolCallEventType("edit", event) ? "edit" : "write";
397
425
  return {
398
426
  block: true,
399
- reason: `⛔ Orchestrator cannot use ${toolUsed} directly. Delegate to a subagent (e.g. worker, python-expert, ts-expert) via the subagent tool.`,
427
+ reason: `⛔ Orchestrator cannot use ${toolUsed} directly. Delegate to the appropriate specialist subagent via the subagent tool.`,
400
428
  };
401
429
  }
402
430
  }
@@ -407,7 +435,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
407
435
  if (filePath && resolve(ctx.cwd, filePath) === statePath(ctx.cwd)) {
408
436
  return {
409
437
  block: true,
410
- reason: "\u26d4 Direct modification of pi-config-review-state.json blocked. The review state is managed by the enforcement system.",
438
+ reason: "\u26d4 Direct modification of pi-config-review-state.jsonl blocked. The review state is managed by the enforcement system.",
411
439
  };
412
440
  }
413
441
  }
@@ -422,7 +450,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
422
450
  if (getSetting(ctx.cwd, "review_loop_enforcement") && cmdLower.includes("pi-config-review-state")) {
423
451
  return {
424
452
  block: true,
425
- reason: "\u26d4 Bash command targeting pi-config-review-state blocked. The review state is managed by the enforcement system.",
453
+ reason: "\u26d4 Bash command targeting pi-config-review-state blocked. The review state is managed exclusively by the enforcement system.",
426
454
  };
427
455
  }
428
456
 
@@ -487,14 +515,14 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
487
515
  // On acpx, do not push async (unsupported); foreground waits are allowed.
488
516
  const hasLoop = /\b(while|for|until)\b/.test(command);
489
517
  const sleepMatch = command.match(/\bsleep\s+(\d+)/);
490
- if (parentSupportsAsyncLlm && hasLoop && sleepMatch && parseInt(sleepMatch[1], 10) > 5) {
518
+ if (parentSupportsAsyncLlm && hasLoop && sleepMatch && parseInt(sleepMatch[1], 10) > getSetting(ctx.cwd, "enforcement_loop_sleep_threshold_s")) {
491
519
  return {
492
520
  block: true,
493
521
  reason: `⚠️ Polling loop with sleep ${sleepMatch[1]}s blocked. Use subagent with async: true for polling/monitoring tasks instead of blocking the session.`,
494
522
  };
495
523
  }
496
524
 
497
- if (parentSupportsAsyncLlm && !hasLoop && sleepMatch && parseInt(sleepMatch[1], 10) > 30) {
525
+ if (parentSupportsAsyncLlm && !hasLoop && sleepMatch && parseInt(sleepMatch[1], 10) > getSetting(ctx.cwd, "enforcement_sleep_threshold_s")) {
498
526
  return {
499
527
  block: true,
500
528
  reason: `⚠️ sleep ${sleepMatch[1]}s blocked — too long. Use subagent with async: true instead of blocking the session.`,
@@ -504,9 +532,28 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
504
532
  const remoteCheck = checkRemoteExecBlock(cmdLower);
505
533
  if (remoteCheck) return remoteCheck;
506
534
 
507
- // Enforce git commit/push only via git-expert agent
535
+ // comment_signature: inject Assisted-by footer into gh pr/issue create|comment|edit bodies
536
+ if (getSetting(ctx.cwd, "comment_signature")) {
537
+ const modelId = (
538
+ process.env.PI_PRIMARY_MODEL ||
539
+ process.env.PI_MODEL ||
540
+ (ctx as any).model?.id ||
541
+ "unknown"
542
+ ).replace(/[^a-zA-Z0-9._:@/ -]/g, "").trim() || "unknown";
543
+ const signature = process.env.PI_COMMENT_SIGNATURE || `Assisted-by: PI (${modelId})`;
544
+ const injected = injectGhBodySignature(event.input.command, signature);
545
+ if (injected !== event.input.command) {
546
+ log.info("comment_signature injected into gh body command", signature);
547
+ event.input.command = injected;
548
+ }
549
+ }
550
+
551
+ // Enforce git commit/push only via git-expert agent.
552
+ // Use isRealGitCommitOrPush (heredoc-stripped + command-position aware) so that
553
+ // file CONTENT / string args / paths that merely MENTION "git commit" do not trip
554
+ // the guard (BUG #3). Only genuine git commit/push invocations are blocked.
508
555
  if (process.env.PI_AGENT_NAME !== "git-expert") {
509
- if (hasGitSub(command, "commit") || hasGitSub(command, "push")) {
556
+ if (isRealGitCommitOrPush(command)) {
510
557
  return {
511
558
  block: true,
512
559
  reason: "⛔ git commit/push blocked. Use git-expert agent for commit and push operations.",
@@ -518,7 +565,9 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
518
565
  // Use resolveEffectiveCwd to detect the worktree from cd commands (e.g., cd .worktrees/issue-622 && git commit)
519
566
  if (process.env.PI_AGENT_NAME === "git-expert" && hasGitSub(command, "commit")) {
520
567
  const commitCwd = resolveEffectiveCwd(command, ctx.cwd);
521
- if (getSetting(ctx.cwd, "review_loop_enforcement") && !isReviewClean(commitCwd)) {
568
+ // Skip review enforcement on version bump branches (release workflow only)
569
+ const commitBranch = getCurrentBranch(commitCwd);
570
+ if (!isBumpVersionBranch(commitBranch) && getSetting(commitCwd, "review_loop_enforcement") && !isCommitAllowed(commitCwd)) {
522
571
  const state = readReviewState(commitCwd);
523
572
  const testInfo = state.status === "clean" && !state.tests_passed ? " Tests have not passed yet — run tests before committing." : "";
524
573
  const reviewAdvice = state.status !== "clean" ? " Fix findings and re-run all reviewers until 0 comments." : "";
@@ -556,9 +605,10 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
556
605
  try {
557
606
  entries = loadEnforcedEntries(ctx.cwd);
558
607
  } catch (e: any) {
559
- console.debug("[enforcement] loadEnforcedEntries failed:", e?.message?.slice(0, 100));
608
+ log.debug("enforcement: loadEnforcedEntries failed:", e?.message?.slice(0, 100));
560
609
  return;
561
610
  }
611
+ log.info("enforcement_loaded", entries.length);
562
612
  if (entries.length === 0) return;
563
613
 
564
614
 
@@ -571,7 +621,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
571
621
  const details = (event as any).details;
572
622
  const results = Array.isArray(details?.results) ? details.results : [];
573
623
  if (!Array.isArray(details?.results) && details?.results) {
574
- console.debug(`[enforcement] subagent details.results is ${typeof details.results} (expected array), skipping bash extraction`);
624
+ log.debug(`enforcement: subagent details.results is ${typeof details.results} (expected array), skipping bash extraction`);
575
625
  }
576
626
  const nonBlockEntries = entries.filter(e => e.action !== "block");
577
627
  const seen = new Set<string>();
@@ -650,6 +700,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
650
700
  }
651
701
 
652
702
  if (rule.action === "warn") {
703
+ log.warn("enforcement_warn", toolName, rule.text);
653
704
  appendMessages.push({
654
705
  type: "text",
655
706
  text: `\n⚠️ ENFORCEMENT WARNING: ${rule.text}`,
@@ -691,6 +742,8 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
691
742
  } catch {
692
743
  effectiveCwd = ctx.cwd; // fallback to session cwd
693
744
  }
745
+ // Skip review tracking on version bump branches (release workflow only)
746
+ if (isBumpVersionBranch(getCachedBranch(effectiveCwd))) return;
694
747
  const relativePath = path.relative(effectiveCwd, absPath);
695
748
 
696
749
  // Skip gitignored files (build artifacts, .pi/tmp/, node_modules, etc.)
@@ -709,7 +762,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
709
762
 
710
763
  // Retry markNeedsReview on lock failure — missing this would leave state
711
764
  // as 'clean' and allow commits to slip through enforcement.
712
- // Uses effectiveCwd so each worktree gets its own pi-config-review-state.json.
765
+ // Uses effectiveCwd so each worktree gets its own pi-config-review-state.jsonl.
713
766
  for (let attempt = 0; attempt < 3; attempt++) {
714
767
  try { markNeedsReview(effectiveCwd); break; } catch {
715
768
  if (attempt === 2) {
@@ -722,7 +775,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
722
775
 
723
776
  // ── Test command detection (auto-mark tests_passed in review state) ──
724
777
  // Runs in ALL processes (orchestrator + subagents) to catch test runs from any agent.
725
- // When a test command exits successfully, marks tests_passed=true in pi-config-review-state.json.
778
+ // When a test command exits successfully, marks tests_passed=true in pi-config-review-state.jsonl.
726
779
  // When it fails, marks tests_passed=false. Any subsequent file edit resets tests_passed
727
780
  // via markNeedsReview(), so stale results are impossible.
728
781
  // NOTE: Code reviewers are excluded — they may run tests to verify behavior but their
@@ -316,6 +316,12 @@ function registerCompletions(
316
316
  }
317
317
  return null;
318
318
  },
319
+ "pi-config-settings": (prefix: string) => {
320
+ return filter([
321
+ { value: "project", label: "project", description: "Edit project-level settings" },
322
+ { value: "global", label: "global", description: "Edit global settings" },
323
+ ], prefix);
324
+ },
319
325
 
320
326
  };
321
327
 
@@ -348,6 +354,7 @@ function setupPromptTemplateInterceptor(
348
354
  const promptTemplateCommands = new Set([
349
355
  "external-ai", "pr-review", "issue-review", "coderabbit-rate-limit",
350
356
  "review-local", "release", "review-handler", "cron", "create-skill", "create-coms-feature-manager",
357
+ "pi-config-settings",
351
358
  ]);
352
359
 
353
360
  // /external-ai-models-refresh command — clears cache and re-fetches
@@ -20,9 +20,9 @@ export function runGit(
20
20
  GCM_INTERACTIVE: "Never",
21
21
  },
22
22
  });
23
- return { stdout: stdout.trim(), code: 0 };
23
+ return { stdout: stdout.trimEnd(), code: 0 };
24
24
  } catch (e: any) {
25
- return { stdout: (e.stdout || "").trim(), code: e.status || 1 };
25
+ return { stdout: (e.stdout || "").trimEnd(), code: e.status || 1 };
26
26
  }
27
27
  }
28
28
 
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
+ import { Type } from "typebox";
17
18
  import { registerAskUser } from "./ask-user.js";
18
19
  import { registerAsyncAgents } from "./async-agents.js";
19
20
  import { registerBtw } from "./btw.js";
@@ -33,24 +34,34 @@ import { registerPreferenceExtractor } from "./preference-extractor.js";
33
34
  import { registerMemoryTools } from "./memory-tools.js";
34
35
  import { registerReviewUI } from "./review-ui.js";
35
36
  import { registerSessionSearch } from "./session-search.js";
37
+ import { registerSettingsTui } from "./settings-tui.js";
38
+ import { registerMarkdownTransformer } from "./markdown-transformer.js";
36
39
  import { ensureGitSshTimeout, isRunningInContainer, terminalNotify } from "./utils.js";
40
+ import { createLogger } from "../shared/logger.js";
41
+ import { setGlobalSessionId } from "../shared/file-logger.js";
37
42
 
38
43
  const IN_CONTAINER = isRunningInContainer();
39
44
  ensureGitSshTimeout();
40
45
 
41
- // Shared command handler registry — pidash uses this to execute commands from the browser
42
- export const commandHandlerRegistry = new Map<string, (args: string, ctx: any) => Promise<void>>();
43
-
44
- // Latest ExtensionCommandContext captured from any command handler.
45
- // Commands provide the full context (switchSession, newSession, etc.)
46
- // while session_start only provides basic ExtensionContext.
47
- export let latestCommandCtx: any = null;
46
+ const log = createLogger("orchestrator");
48
47
 
49
48
  export default function (pi: ExtensionAPI) {
50
- // Wrap registerCommand to capture all handler functions AND command context.
51
- // Any command the user runs upgrades latestCommandCtx to a real ExtensionCommandContext
52
- // which has switchSession()/newSession() methods.
49
+ // Set global session ID FIRST so all fileLog callers get per-session files.
50
+ pi.on("session_start", (_event, ctx) => {
51
+ const reason = (_event as any)?.reason ?? "unknown";
52
+ const sid = ctx.sessionManager?.getSessionId?.();
53
+ log.info("session_start", reason, sid ?? "?");
54
+ if (sid) setGlobalSessionId(sid);
55
+ });
56
+
57
+ // ── Pidash command handler bridge ──────────────────────────────────
58
+ // TODO: Remove once pidash uses PiClient/RemoteSession (#732) — native
59
+ // command dispatch via RemoteSession.submit() eliminates the need for
60
+ // this event-based handler forwarding.
53
61
  const originalRegisterCommand = pi.registerCommand.bind(pi);
62
+ let latestCommandCtx: any = null;
63
+ const commandHandlerRegistry = new Map<string, Function>();
64
+
54
65
  pi.registerCommand = (name: string, options: any) => {
55
66
  if (options?.handler) {
56
67
  const origHandler = options.handler;
@@ -68,6 +79,7 @@ export default function (pi: ExtensionAPI) {
68
79
 
69
80
  // Replay all registered command handlers when pidash requests them
70
81
  // (handles extension load order — pidash may load after orchestrator)
82
+ // TODO: Remove once pidash uses PiClient/RemoteSession (#732)
71
83
  pi.events.on("pidash:request-commands", () => {
72
84
  for (const [name, handler] of commandHandlerRegistry) {
73
85
  pi.events.emit("pidash:register-command", { name, handler });
@@ -97,4 +109,60 @@ export default function (pi: ExtensionAPI) {
97
109
  registerPreferenceExtractor(pi);
98
110
  registerMemoryTools(pi);
99
111
  registerSessionSearch(pi);
112
+ registerSettingsTui(pi);
113
+ registerMarkdownTransformer(pi);
114
+
115
+ // ── list_models tool — LLM-callable model discovery ──
116
+ pi.registerTool({
117
+ name: "list_models",
118
+ label: "List Models",
119
+ description: "List available models and providers. Use when the user asks to run an agent with a specific model, or when you need to discover available models for the subagent model parameter. Returns provider/model pairs that can be passed to subagent(model=\"provider/model-id\").",
120
+ parameters: Type.Object({
121
+ provider: Type.Optional(Type.String({ description: "Filter by provider name (e.g., 'litellm', 'cli-cursor', 'google')" })),
122
+ }),
123
+ async execute(_callId, params, _signal, _onUpdate, ctx) {
124
+ let models: any[];
125
+ try {
126
+ const registry = ctx.modelRegistry;
127
+ if (!registry) {
128
+ return {
129
+ content: [{ type: "text" as const, text: "Model registry not available." }],
130
+ };
131
+ }
132
+ models = registry.getAvailable?.() || registry.getAll?.() || [];
133
+ } catch (e: any) {
134
+ return {
135
+ content: [{ type: "text" as const, text: `Failed to list models: ${e?.message || e}` }],
136
+ };
137
+ }
138
+ // Filter out acpx-* providers — they cannot work in subagent children
139
+ // (PI_SUBAGENT_CHILD=1 skips acpx registration)
140
+ const filtered = (params.provider
141
+ ? models.filter((m: any) => m.provider === params.provider)
142
+ : models
143
+ ).filter((m: any) => !(m.provider || "").startsWith("acpx-"));
144
+ if (filtered.length === 0) {
145
+ return {
146
+ content: [{ type: "text" as const, text: params.provider ? `No models found for provider "${params.provider}".` : "No models available." }],
147
+ };
148
+ }
149
+ // Group by provider for readability
150
+ const byProvider = new Map<string, string[]>();
151
+ for (const m of filtered) {
152
+ const list = byProvider.get(m.provider) || [];
153
+ list.push(m.id);
154
+ byProvider.set(m.provider, list);
155
+ }
156
+ const lines: string[] = [];
157
+ for (const [prov, ids] of byProvider) {
158
+ lines.push(`## ${prov}`);
159
+ for (const id of ids) {
160
+ lines.push(`- ${prov}/${id}`);
161
+ }
162
+ }
163
+ return {
164
+ content: [{ type: "text" as const, text: lines.join("\n") }],
165
+ };
166
+ },
167
+ });
100
168
  }