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.
- package/README.md +76 -67
- package/agents/code-reviewer-docs.md +11 -1
- package/agents/code-reviewer-guidelines.md +11 -1
- package/agents/code-reviewer-quality.md +11 -1
- package/agents/code-reviewer-security.md +11 -1
- package/agents/code-reviewer-spec.md +11 -1
- package/agents/git-expert.md +42 -3
- package/agents/github-expert.md +11 -0
- package/extensions/acpx-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/agents/claude.ts +3 -2
- package/extensions/cli-provider/agents/cursor.ts +3 -5
- package/extensions/cli-provider/agents/gemini.ts +3 -2
- package/extensions/cli-provider/discover.ts +7 -7
- package/extensions/cli-provider/index.ts +5 -0
- package/extensions/cli-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/session-reaper.ts +5 -7
- package/extensions/cli-provider/sessions.ts +4 -0
- package/extensions/cli-provider/shared/discover-cache.ts +17 -176
- package/extensions/coms/coms-p2p.ts +1719 -423
- package/extensions/coms/coms-shared.ts +261 -39
- package/extensions/coms/coms-wrapper.ts +100 -62
- package/extensions/coms/index.ts +8 -4
- package/extensions/coms/mixed-turn.ts +26 -0
- package/extensions/coms/probe-socket.ts +42 -0
- package/extensions/coms/themeMap.ts +0 -1
- package/extensions/image-gen/image-gen.ts +19 -1
- package/extensions/orchestrator/async-agents.ts +563 -102
- package/extensions/orchestrator/async-capability.ts +8 -8
- package/extensions/orchestrator/cron.ts +15 -9
- package/extensions/orchestrator/dreaming.ts +32 -25
- package/extensions/orchestrator/enforcement-helpers.ts +459 -17
- package/extensions/orchestrator/enforcement-rules.ts +5 -4
- package/extensions/orchestrator/enforcement.ts +80 -27
- package/extensions/orchestrator/extended-autocomplete.ts +7 -0
- package/extensions/orchestrator/git-helpers.ts +2 -2
- package/extensions/orchestrator/index.ts +78 -10
- package/extensions/orchestrator/markdown-transformer.ts +313 -0
- package/extensions/orchestrator/memory-embeddings.ts +36 -34
- package/extensions/orchestrator/memory-provenance.ts +29 -25
- package/extensions/orchestrator/memory-scoring.ts +23 -18
- package/extensions/orchestrator/memory-tools.ts +10 -3
- package/extensions/orchestrator/overlay-dashboard.ts +17 -5
- package/extensions/orchestrator/parse-model-override.ts +29 -0
- package/extensions/orchestrator/pi-config-review-state.ts +57 -33
- package/extensions/orchestrator/project-settings.ts +333 -127
- package/extensions/orchestrator/resolve-agent-model.ts +24 -13
- package/extensions/orchestrator/review-porcelain.ts +4 -0
- package/extensions/orchestrator/review-ui.ts +63 -15
- package/extensions/orchestrator/rule-placeholders.ts +411 -2
- package/extensions/orchestrator/rules.ts +80 -132
- package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
- package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
- package/extensions/orchestrator/settings-tui.ts +636 -0
- package/extensions/orchestrator/state-jsonl.ts +431 -0
- package/extensions/orchestrator/subagent-tool.ts +157 -59
- package/extensions/orchestrator/sync-limit.ts +23 -0
- package/extensions/orchestrator/task-lifecycle.ts +77 -67
- package/extensions/orchestrator/utils.ts +8 -5
- package/extensions/pidash/pidash.ts +193 -156
- package/extensions/pidiff/pidiff.ts +96 -38
- package/extensions/pidiff/spawn-lock.ts +62 -0
- package/extensions/pitasks/index.ts +430 -0
- package/extensions/pitasks/reminders.ts +37 -0
- package/extensions/pitasks/task-store.ts +467 -0
- package/extensions/pitasks/task-tools.ts +375 -0
- package/extensions/pitasks/task-widget.ts +181 -0
- package/extensions/pitasks/types.ts +38 -0
- package/extensions/providers/acpx-driver.ts +12 -2
- package/extensions/providers/built-in-drivers.ts +6 -6
- package/extensions/providers/claude-driver.ts +1 -1
- package/extensions/providers/cursor-cli-driver.ts +1 -1
- package/extensions/providers/gemini-driver.ts +1 -1
- package/extensions/providers/index.ts +144 -26
- package/extensions/providers/initialized-guard.ts +27 -0
- package/extensions/providers/restore-default-model.ts +596 -0
- package/extensions/providers/session-shutdown.ts +39 -0
- package/extensions/shared/coms-active.ts +23 -0
- package/extensions/shared/create-runtime-provider.ts +2 -0
- package/extensions/shared/daemon-manager.ts +0 -8
- package/extensions/shared/file-logger.ts +132 -25
- package/extensions/shared/logger.ts +50 -0
- package/extensions/shared/models-dev.ts +421 -0
- package/extensions/shared/oneshot.ts +116 -0
- package/extensions/shared/resolve-binary.ts +191 -0
- package/extensions/shared/types.ts +3 -0
- package/package.json +9 -3
- package/prompts/coderabbit-rate-limit.md +2 -0
- package/rules/00-orchestrator-core.md +3 -0
- package/rules/10-agent-routing.md +12 -1
- package/rules/20-code-review-loop.md +52 -25
- package/rules/30-prompt-templates.md +4 -0
- package/rules/35-memory.md +3 -3
- package/rules/40-critical-rules.md +3 -0
- package/rules/55-coms-protocol.md +84 -18
- package/rules/60-task-tracking.md +2 -0
- package/settings-keys.json +563 -0
- package/templates/coms-feature-manager-prompt.md +4 -6
- package/extensions/coms/coms-net-server.ts +0 -1642
- package/extensions/coms/coms-net-wrapper.ts +0 -521
- 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,
|
|
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) {
|
|
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
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
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.
|
|
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) >
|
|
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) >
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
23
|
+
return { stdout: stdout.trimEnd(), code: 0 };
|
|
24
24
|
} catch (e: any) {
|
|
25
|
-
return { stdout: (e.stdout || "").
|
|
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
|
-
|
|
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
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
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
|
}
|