pi-orchestrator-config 4.2.1 → 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 (92) hide show
  1. package/README.md +52 -61
  2. package/agents/git-expert.md +5 -1
  3. package/agents/github-expert.md +4 -1
  4. package/extensions/acpx-provider/runtime-models.ts +18 -6
  5. package/extensions/cli-provider/agents/claude.ts +3 -2
  6. package/extensions/cli-provider/agents/cursor.ts +3 -5
  7. package/extensions/cli-provider/agents/gemini.ts +3 -2
  8. package/extensions/cli-provider/discover.ts +7 -7
  9. package/extensions/cli-provider/index.ts +5 -0
  10. package/extensions/cli-provider/runtime-models.ts +18 -6
  11. package/extensions/cli-provider/session-reaper.ts +5 -7
  12. package/extensions/cli-provider/sessions.ts +4 -0
  13. package/extensions/cli-provider/shared/discover-cache.ts +17 -176
  14. package/extensions/coms/coms-p2p.ts +1719 -423
  15. package/extensions/coms/coms-shared.ts +261 -39
  16. package/extensions/coms/coms-wrapper.ts +100 -62
  17. package/extensions/coms/index.ts +8 -4
  18. package/extensions/coms/mixed-turn.ts +26 -0
  19. package/extensions/coms/probe-socket.ts +42 -0
  20. package/extensions/coms/themeMap.ts +0 -1
  21. package/extensions/image-gen/image-gen.ts +19 -1
  22. package/extensions/orchestrator/async-agents.ts +509 -85
  23. package/extensions/orchestrator/cron.ts +14 -8
  24. package/extensions/orchestrator/dreaming.ts +31 -24
  25. package/extensions/orchestrator/enforcement-helpers.ts +459 -17
  26. package/extensions/orchestrator/enforcement-rules.ts +5 -4
  27. package/extensions/orchestrator/enforcement.ts +79 -25
  28. package/extensions/orchestrator/extended-autocomplete.ts +7 -0
  29. package/extensions/orchestrator/git-helpers.ts +2 -2
  30. package/extensions/orchestrator/index.ts +23 -10
  31. package/extensions/orchestrator/markdown-transformer.ts +313 -0
  32. package/extensions/orchestrator/memory-embeddings.ts +36 -34
  33. package/extensions/orchestrator/memory-provenance.ts +29 -25
  34. package/extensions/orchestrator/memory-scoring.ts +23 -18
  35. package/extensions/orchestrator/memory-tools.ts +10 -3
  36. package/extensions/orchestrator/overlay-dashboard.ts +17 -5
  37. package/extensions/orchestrator/pi-config-review-state.ts +44 -33
  38. package/extensions/orchestrator/project-settings.ts +220 -21
  39. package/extensions/orchestrator/review-porcelain.ts +4 -0
  40. package/extensions/orchestrator/review-ui.ts +11 -5
  41. package/extensions/orchestrator/rule-placeholders.ts +384 -1
  42. package/extensions/orchestrator/rules.ts +80 -132
  43. package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
  44. package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
  45. package/extensions/orchestrator/settings-tui.ts +636 -0
  46. package/extensions/orchestrator/state-jsonl.ts +431 -0
  47. package/extensions/orchestrator/subagent-tool.ts +105 -50
  48. package/extensions/orchestrator/sync-limit.ts +23 -0
  49. package/extensions/orchestrator/task-lifecycle.ts +77 -67
  50. package/extensions/orchestrator/utils.ts +8 -5
  51. package/extensions/pidash/pidash.ts +193 -156
  52. package/extensions/pidiff/pidiff.ts +64 -38
  53. package/extensions/pidiff/spawn-lock.ts +62 -0
  54. package/extensions/pitasks/index.ts +430 -0
  55. package/extensions/pitasks/reminders.ts +37 -0
  56. package/extensions/pitasks/task-store.ts +467 -0
  57. package/extensions/pitasks/task-tools.ts +375 -0
  58. package/extensions/pitasks/task-widget.ts +181 -0
  59. package/extensions/pitasks/types.ts +38 -0
  60. package/extensions/providers/acpx-driver.ts +12 -2
  61. package/extensions/providers/built-in-drivers.ts +6 -6
  62. package/extensions/providers/claude-driver.ts +1 -1
  63. package/extensions/providers/cursor-cli-driver.ts +1 -1
  64. package/extensions/providers/gemini-driver.ts +1 -1
  65. package/extensions/providers/index.ts +144 -26
  66. package/extensions/providers/initialized-guard.ts +27 -0
  67. package/extensions/providers/restore-default-model.ts +596 -0
  68. package/extensions/providers/session-shutdown.ts +39 -0
  69. package/extensions/shared/coms-active.ts +23 -0
  70. package/extensions/shared/create-runtime-provider.ts +2 -0
  71. package/extensions/shared/daemon-manager.ts +0 -8
  72. package/extensions/shared/file-logger.ts +132 -25
  73. package/extensions/shared/logger.ts +50 -0
  74. package/extensions/shared/models-dev.ts +421 -0
  75. package/extensions/shared/oneshot.ts +116 -0
  76. package/extensions/shared/resolve-binary.ts +191 -0
  77. package/extensions/shared/types.ts +3 -0
  78. package/package.json +7 -2
  79. package/prompts/coderabbit-rate-limit.md +2 -0
  80. package/rules/00-orchestrator-core.md +3 -0
  81. package/rules/10-agent-routing.md +1 -1
  82. package/rules/20-code-review-loop.md +38 -16
  83. package/rules/30-prompt-templates.md +4 -0
  84. package/rules/35-memory.md +3 -3
  85. package/rules/40-critical-rules.md +3 -0
  86. package/rules/55-coms-protocol.md +84 -18
  87. package/rules/60-task-tracking.md +2 -0
  88. package/settings-keys.json +481 -20
  89. package/templates/coms-feature-manager-prompt.md +4 -6
  90. package/extensions/coms/coms-net-server.ts +0 -1642
  91. package/extensions/coms/coms-net-wrapper.ts +0 -521
  92. 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";
@@ -34,6 +35,9 @@ import {
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,7 +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 = (process.env.PI_MODEL || (ctx as any).model?.id || "unknown").replace(/[^a-zA-Z0-9._:@/ -]/g, "").trim() || "unknown";
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";
86
97
 
87
98
  const piIdentity = `PI (${modelId}) <noreply@pi.dev>`;
88
99
  let trailerName: string;
@@ -122,10 +133,6 @@ async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd
122
133
  trailerName = trailerSetting;
123
134
  }
124
135
 
125
- // Raw trailer line for duplicate detection (what the commit message should contain)
126
- const rawTrailerLine = `${trailerName}: ${piIdentity}`;
127
- if (command.includes(rawTrailerLine)) return undefined;
128
-
129
136
  // Pattern A: echo "..." | git commit -F -
130
137
  const pipeIdx = command.lastIndexOf("|");
131
138
  if (pipeIdx !== -1 && /git\s+commit\s+.*-F\s*-/.test(command.slice(pipeIdx))) {
@@ -137,6 +144,14 @@ async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd
137
144
  if (lastQuoteIdx > 0) {
138
145
  // Determine quote context for correct escaping
139
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
+ }
140
155
  const escaped = quoteChar === "'"
141
156
  ? `${escapeForSingleQuote(trailerName)}: ${piIdentity}`
142
157
  : `${escapeForDoubleQuote(trailerName)}: ${piIdentity}`;
@@ -149,6 +164,14 @@ async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd
149
164
  const mFlagMatch = command.match(/git\s+commit\s+.*-m\s+(["'])([\s\S]*?)\1/);
150
165
  if (mFlagMatch) {
151
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
+ }
152
175
  const escaped = quoteChar === "'"
153
176
  ? `${escapeForSingleQuote(trailerName)}: ${piIdentity}`
154
177
  : `${escapeForDoubleQuote(trailerName)}: ${piIdentity}`;
@@ -268,12 +291,17 @@ async function checkGitProtection(command: string, event: any, ctx: any, gitCwd:
268
291
  const trailerResult = await handleCommitTrailer(command, event, ctx, gitCwd);
269
292
  if (trailerResult) return trailerResult;
270
293
 
271
- // DCO enforcement — inject --signoff when dco setting is enabled
272
- if (getSetting(gitCwd, "dco") && !/(?:^|\s)--signoff(?:\s|$)/.test(command) && !/(?:^|\s)-s(?:\s|$)/.test(command)) {
273
- event.input.command = event.input.command.replace(
274
- /\bgit\b((?:\s+(?:-[a-zA-Z]\s+\S+|-\S+))*\s+)commit\b/,
275
- "git$1commit --signoff",
276
- );
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
+ }
277
305
  }
278
306
  }
279
307
 
@@ -361,7 +389,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
361
389
  cachedTrailerSelection = null;
362
390
  // Set comment signature env var for CLI tools to read
363
391
  if (getSetting(ctx.cwd, "comment_signature")) {
364
- const modelId = (process.env.PI_MODEL || (ctx as any).model?.id || "unknown").replace(/[^a-zA-Z0-9._:@/ -]/g, "").trim() || "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";
365
393
  process.env.PI_COMMENT_SIGNATURE = `Assisted-by: PI (${modelId})`;
366
394
  } else {
367
395
  delete process.env.PI_COMMENT_SIGNATURE;
@@ -383,6 +411,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
383
411
  const matches = matchToolCall(blockEntries, toolName, input);
384
412
  if (matches.length > 0) {
385
413
  const rule = matches[0].rule;
414
+ log.info("enforcement_block", toolName, rule.text);
386
415
  return { block: true, reason: `⛔ ENFORCEMENT [${rule.entry.class}]: ${rule.text}` };
387
416
  }
388
417
  }
@@ -395,7 +424,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
395
424
  const toolUsed = isToolCallEventType("edit", event) ? "edit" : "write";
396
425
  return {
397
426
  block: true,
398
- 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.`,
399
428
  };
400
429
  }
401
430
  }
@@ -406,7 +435,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
406
435
  if (filePath && resolve(ctx.cwd, filePath) === statePath(ctx.cwd)) {
407
436
  return {
408
437
  block: true,
409
- 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.",
410
439
  };
411
440
  }
412
441
  }
@@ -421,7 +450,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
421
450
  if (getSetting(ctx.cwd, "review_loop_enforcement") && cmdLower.includes("pi-config-review-state")) {
422
451
  return {
423
452
  block: true,
424
- 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.",
425
454
  };
426
455
  }
427
456
 
@@ -486,14 +515,14 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
486
515
  // On acpx, do not push async (unsupported); foreground waits are allowed.
487
516
  const hasLoop = /\b(while|for|until)\b/.test(command);
488
517
  const sleepMatch = command.match(/\bsleep\s+(\d+)/);
489
- 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")) {
490
519
  return {
491
520
  block: true,
492
521
  reason: `⚠️ Polling loop with sleep ${sleepMatch[1]}s blocked. Use subagent with async: true for polling/monitoring tasks instead of blocking the session.`,
493
522
  };
494
523
  }
495
524
 
496
- 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")) {
497
526
  return {
498
527
  block: true,
499
528
  reason: `⚠️ sleep ${sleepMatch[1]}s blocked — too long. Use subagent with async: true instead of blocking the session.`,
@@ -503,9 +532,28 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
503
532
  const remoteCheck = checkRemoteExecBlock(cmdLower);
504
533
  if (remoteCheck) return remoteCheck;
505
534
 
506
- // 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.
507
555
  if (process.env.PI_AGENT_NAME !== "git-expert") {
508
- if (hasGitSub(command, "commit") || hasGitSub(command, "push")) {
556
+ if (isRealGitCommitOrPush(command)) {
509
557
  return {
510
558
  block: true,
511
559
  reason: "⛔ git commit/push blocked. Use git-expert agent for commit and push operations.",
@@ -517,7 +565,9 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
517
565
  // Use resolveEffectiveCwd to detect the worktree from cd commands (e.g., cd .worktrees/issue-622 && git commit)
518
566
  if (process.env.PI_AGENT_NAME === "git-expert" && hasGitSub(command, "commit")) {
519
567
  const commitCwd = resolveEffectiveCwd(command, ctx.cwd);
520
- if (getSetting(ctx.cwd, "review_loop_enforcement") && !isCommitAllowed(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)) {
521
571
  const state = readReviewState(commitCwd);
522
572
  const testInfo = state.status === "clean" && !state.tests_passed ? " Tests have not passed yet — run tests before committing." : "";
523
573
  const reviewAdvice = state.status !== "clean" ? " Fix findings and re-run all reviewers until 0 comments." : "";
@@ -555,9 +605,10 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
555
605
  try {
556
606
  entries = loadEnforcedEntries(ctx.cwd);
557
607
  } catch (e: any) {
558
- console.debug("[enforcement] loadEnforcedEntries failed:", e?.message?.slice(0, 100));
608
+ log.debug("enforcement: loadEnforcedEntries failed:", e?.message?.slice(0, 100));
559
609
  return;
560
610
  }
611
+ log.info("enforcement_loaded", entries.length);
561
612
  if (entries.length === 0) return;
562
613
 
563
614
 
@@ -570,7 +621,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
570
621
  const details = (event as any).details;
571
622
  const results = Array.isArray(details?.results) ? details.results : [];
572
623
  if (!Array.isArray(details?.results) && details?.results) {
573
- 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`);
574
625
  }
575
626
  const nonBlockEntries = entries.filter(e => e.action !== "block");
576
627
  const seen = new Set<string>();
@@ -649,6 +700,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
649
700
  }
650
701
 
651
702
  if (rule.action === "warn") {
703
+ log.warn("enforcement_warn", toolName, rule.text);
652
704
  appendMessages.push({
653
705
  type: "text",
654
706
  text: `\n⚠️ ENFORCEMENT WARNING: ${rule.text}`,
@@ -690,6 +742,8 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
690
742
  } catch {
691
743
  effectiveCwd = ctx.cwd; // fallback to session cwd
692
744
  }
745
+ // Skip review tracking on version bump branches (release workflow only)
746
+ if (isBumpVersionBranch(getCachedBranch(effectiveCwd))) return;
693
747
  const relativePath = path.relative(effectiveCwd, absPath);
694
748
 
695
749
  // Skip gitignored files (build artifacts, .pi/tmp/, node_modules, etc.)
@@ -708,7 +762,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
708
762
 
709
763
  // Retry markNeedsReview on lock failure — missing this would leave state
710
764
  // as 'clean' and allow commits to slip through enforcement.
711
- // 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.
712
766
  for (let attempt = 0; attempt < 3; attempt++) {
713
767
  try { markNeedsReview(effectiveCwd); break; } catch {
714
768
  if (attempt === 2) {
@@ -721,7 +775,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
721
775
 
722
776
  // ── Test command detection (auto-mark tests_passed in review state) ──
723
777
  // Runs in ALL processes (orchestrator + subagents) to catch test runs from any agent.
724
- // 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.
725
779
  // When it fails, marks tests_passed=false. Any subsequent file edit resets tests_passed
726
780
  // via markNeedsReview(), so stale results are impossible.
727
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
 
@@ -34,24 +34,34 @@ import { registerPreferenceExtractor } from "./preference-extractor.js";
34
34
  import { registerMemoryTools } from "./memory-tools.js";
35
35
  import { registerReviewUI } from "./review-ui.js";
36
36
  import { registerSessionSearch } from "./session-search.js";
37
+ import { registerSettingsTui } from "./settings-tui.js";
38
+ import { registerMarkdownTransformer } from "./markdown-transformer.js";
37
39
  import { ensureGitSshTimeout, isRunningInContainer, terminalNotify } from "./utils.js";
40
+ import { createLogger } from "../shared/logger.js";
41
+ import { setGlobalSessionId } from "../shared/file-logger.js";
38
42
 
39
43
  const IN_CONTAINER = isRunningInContainer();
40
44
  ensureGitSshTimeout();
41
45
 
42
- // Shared command handler registry — pidash uses this to execute commands from the browser
43
- export const commandHandlerRegistry = new Map<string, (args: string, ctx: any) => Promise<void>>();
44
-
45
- // Latest ExtensionCommandContext captured from any command handler.
46
- // Commands provide the full context (switchSession, newSession, etc.)
47
- // while session_start only provides basic ExtensionContext.
48
- export let latestCommandCtx: any = null;
46
+ const log = createLogger("orchestrator");
49
47
 
50
48
  export default function (pi: ExtensionAPI) {
51
- // Wrap registerCommand to capture all handler functions AND command context.
52
- // Any command the user runs upgrades latestCommandCtx to a real ExtensionCommandContext
53
- // 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.
54
61
  const originalRegisterCommand = pi.registerCommand.bind(pi);
62
+ let latestCommandCtx: any = null;
63
+ const commandHandlerRegistry = new Map<string, Function>();
64
+
55
65
  pi.registerCommand = (name: string, options: any) => {
56
66
  if (options?.handler) {
57
67
  const origHandler = options.handler;
@@ -69,6 +79,7 @@ export default function (pi: ExtensionAPI) {
69
79
 
70
80
  // Replay all registered command handlers when pidash requests them
71
81
  // (handles extension load order — pidash may load after orchestrator)
82
+ // TODO: Remove once pidash uses PiClient/RemoteSession (#732)
72
83
  pi.events.on("pidash:request-commands", () => {
73
84
  for (const [name, handler] of commandHandlerRegistry) {
74
85
  pi.events.emit("pidash:register-command", { name, handler });
@@ -98,6 +109,8 @@ export default function (pi: ExtensionAPI) {
98
109
  registerPreferenceExtractor(pi);
99
110
  registerMemoryTools(pi);
100
111
  registerSessionSearch(pi);
112
+ registerSettingsTui(pi);
113
+ registerMarkdownTransformer(pi);
101
114
 
102
115
  // ── list_models tool — LLM-callable model discovery ──
103
116
  pi.registerTool({