taskplane 0.28.2 → 0.28.3
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/extensions/taskplane/agent-bridge-extension.ts +22 -0
- package/extensions/taskplane/config-loader.ts +3 -0
- package/extensions/taskplane/config-schema.ts +9 -2
- package/extensions/taskplane/engine.ts +7 -3
- package/extensions/taskplane/execution.ts +38 -2
- package/extensions/taskplane/lane-runner.ts +15 -1
- package/extensions/taskplane/merge.ts +9 -2
- package/extensions/taskplane/resume.ts +4 -3
- package/extensions/taskplane/settings-loader.ts +136 -0
- package/extensions/taskplane/settings-tui.ts +147 -3
- package/extensions/taskplane/types.ts +6 -0
- package/package.json +1 -1
|
@@ -27,6 +27,7 @@ import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync, unlinkS
|
|
|
27
27
|
import { join, dirname } from "path";
|
|
28
28
|
import { spawn as nodeSpawn } from "child_process";
|
|
29
29
|
import { resolvePiCliPath, resolveTaskplaneAgentTemplate } from "./path-resolver.ts";
|
|
30
|
+
import { loadPiSettingsPackages, filterExcludedExtensions } from "./settings-loader.ts";
|
|
30
31
|
import { randomBytes } from "crypto";
|
|
31
32
|
import { buildExpansionRequestId, type SegmentExpansionRequest } from "./types.ts";
|
|
32
33
|
|
|
@@ -445,6 +446,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
445
446
|
];
|
|
446
447
|
if (reviewerModel) args.push("--model", reviewerModel);
|
|
447
448
|
if (reviewerThinking) args.push("--thinking", reviewerThinking);
|
|
449
|
+
|
|
450
|
+
// TP-180: Forward user-installed extensions to reviewer agent
|
|
451
|
+
// Use TASKPLANE_STATE_ROOT (canonical project root) for settings resolution,
|
|
452
|
+
// falling back to cwd (which may be a worktree without .pi/settings.json).
|
|
453
|
+
const settingsRoot = process.env.TASKPLANE_STATE_ROOT || cwd;
|
|
454
|
+
const reviewerPackages = loadPiSettingsPackages(settingsRoot);
|
|
455
|
+
// Apply reviewer-specific exclusions from config (JSON array via env)
|
|
456
|
+
let reviewerExclusions: string[] = [];
|
|
457
|
+
try {
|
|
458
|
+
const rawExclude = process.env.TASKPLANE_REVIEWER_EXCLUDE_EXTENSIONS;
|
|
459
|
+
if (rawExclude) {
|
|
460
|
+
const parsed = JSON.parse(rawExclude);
|
|
461
|
+
if (Array.isArray(parsed)) {
|
|
462
|
+
reviewerExclusions = parsed.filter((v: unknown): v is string => typeof v === "string");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch { /* ignore malformed */ }
|
|
466
|
+
const filteredReviewerPackages = filterExcludedExtensions(reviewerPackages, reviewerExclusions);
|
|
467
|
+
for (const pkg of filteredReviewerPackages) {
|
|
468
|
+
args.push("-e", pkg);
|
|
469
|
+
}
|
|
448
470
|
const proc = nodeSpawn(process.execPath, args, {
|
|
449
471
|
shell: false,
|
|
450
472
|
cwd,
|
|
@@ -1125,6 +1125,7 @@ export function toOrchestratorConfig(config: TaskplaneConfig): import("./types.t
|
|
|
1125
1125
|
verify: [...o.merge.verify],
|
|
1126
1126
|
order: o.merge.order,
|
|
1127
1127
|
timeout_minutes: o.merge.timeoutMinutes ?? 90,
|
|
1128
|
+
exclude_extensions: [...(o.merge.excludeExtensions ?? [])],
|
|
1128
1129
|
},
|
|
1129
1130
|
failure: {
|
|
1130
1131
|
on_task_failure: o.failure.onTaskFailure,
|
|
@@ -1183,7 +1184,9 @@ export function toTaskRunnerConfig(config: TaskplaneConfig): import("./types.ts"
|
|
|
1183
1184
|
model: config.taskRunner.reviewer.model,
|
|
1184
1185
|
thinking: config.taskRunner.reviewer.thinking,
|
|
1185
1186
|
tools: config.taskRunner.reviewer.tools,
|
|
1187
|
+
excludeExtensions: [...(config.taskRunner.reviewer.excludeExtensions ?? [])],
|
|
1186
1188
|
},
|
|
1189
|
+
workerExcludeExtensions: [...(config.taskRunner.worker.excludeExtensions ?? [])],
|
|
1187
1190
|
};
|
|
1188
1191
|
}
|
|
1189
1192
|
|
|
@@ -111,6 +111,8 @@ export interface WorkerConfig {
|
|
|
111
111
|
thinking: string;
|
|
112
112
|
/** Optional spawn mode override for task-runner (Runtime V2 subprocess-only). */
|
|
113
113
|
spawnMode?: "subprocess";
|
|
114
|
+
/** Package specifiers to exclude from extension forwarding for worker agents (exact match). @since TP-180 */
|
|
115
|
+
excludeExtensions?: string[];
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
/** Reviewer agent configuration */
|
|
@@ -121,6 +123,8 @@ export interface ReviewerConfig {
|
|
|
121
123
|
tools: string;
|
|
122
124
|
/** Thinking mode for reviewer */
|
|
123
125
|
thinking: string;
|
|
126
|
+
/** Package specifiers to exclude from extension forwarding for reviewer agents (exact match). @since TP-180 */
|
|
127
|
+
excludeExtensions?: string[];
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
/** Context/resource limits for task execution */
|
|
@@ -310,6 +314,8 @@ export interface MergeConfig {
|
|
|
310
314
|
order: "fewest-files-first" | "sequential";
|
|
311
315
|
/** Merge-agent timeout in minutes */
|
|
312
316
|
timeoutMinutes?: number;
|
|
317
|
+
/** Package specifiers to exclude from extension forwarding for merge agents (exact match). @since TP-180 */
|
|
318
|
+
excludeExtensions?: string[];
|
|
313
319
|
}
|
|
314
320
|
|
|
315
321
|
/** Failure policy settings */
|
|
@@ -588,8 +594,8 @@ export const DEFAULT_TASK_RUNNER_SECTION: TaskRunnerSection = {
|
|
|
588
594
|
testing: { commands: {} },
|
|
589
595
|
standards: { docs: [], rules: [] },
|
|
590
596
|
standardsOverrides: {},
|
|
591
|
-
worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "" },
|
|
592
|
-
reviewer: { model: "", tools: "read,bash,grep,find,ls", thinking: "on" },
|
|
597
|
+
worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "", excludeExtensions: [] },
|
|
598
|
+
reviewer: { model: "", tools: "read,bash,grep,find,ls", thinking: "on", excludeExtensions: [] },
|
|
593
599
|
context: {
|
|
594
600
|
workerContextWindow: 0,
|
|
595
601
|
warnPercent: 85,
|
|
@@ -645,6 +651,7 @@ export const DEFAULT_ORCHESTRATOR_SECTION: OrchestratorSection = {
|
|
|
645
651
|
verify: [],
|
|
646
652
|
order: "fewest-files-first",
|
|
647
653
|
timeoutMinutes: 90,
|
|
654
|
+
excludeExtensions: [],
|
|
648
655
|
},
|
|
649
656
|
failure: {
|
|
650
657
|
onTaskFailure: "skip-dependents",
|
|
@@ -6,7 +6,7 @@ import { existsSync, readdirSync, readFileSync, renameSync, unlinkSync } from "f
|
|
|
6
6
|
import { join, resolve } from "path";
|
|
7
7
|
|
|
8
8
|
import { formatDiscoveryResults, runDiscovery } from "./discovery.ts";
|
|
9
|
-
import { buildReviewerEnv, computeTransitiveDependents, execLog, executeLaneV2, executeWave, killV2LaneAgents, resolveCanonicalTaskPaths } from "./execution.ts";
|
|
9
|
+
import { buildReviewerEnv, buildWorkerExcludeEnv, computeTransitiveDependents, execLog, executeLaneV2, executeWave, killV2LaneAgents, resolveCanonicalTaskPaths } from "./execution.ts";
|
|
10
10
|
import type { RuntimeBackend } from "./execution.ts";
|
|
11
11
|
import type { MonitorUpdateCallback } from "./execution.ts";
|
|
12
12
|
// classifyExit no longer called directly — Tier 0 uses exitDiagnostic.classification
|
|
@@ -1381,7 +1381,7 @@ async function attemptWorkerCrashRetry(
|
|
|
1381
1381
|
retryPauseSignal,
|
|
1382
1382
|
wsRoot,
|
|
1383
1383
|
isWsMode,
|
|
1384
|
-
{ ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig?.reviewer) }, // TP-089: ensure mailbox works for retries
|
|
1384
|
+
{ ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig?.reviewer), ...buildWorkerExcludeEnv(runnerConfig?.workerExcludeExtensions) }, // TP-089: ensure mailbox works for retries
|
|
1385
1385
|
);
|
|
1386
1386
|
|
|
1387
1387
|
const retryOutcome = retryResult.tasks[0];
|
|
@@ -1640,7 +1640,7 @@ async function attemptModelFallbackRetry(
|
|
|
1640
1640
|
// Pass TASKPLANE_MODEL_FALLBACK=1 as extra env var to signal
|
|
1641
1641
|
// the task-runner to use the session model instead of configured model.
|
|
1642
1642
|
// TP-089: Also include ORCH_BATCH_ID so mailbox steering works for retries.
|
|
1643
|
-
const modelFallbackEnv = { TASKPLANE_MODEL_FALLBACK: "1", ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig?.reviewer) };
|
|
1643
|
+
const modelFallbackEnv = { TASKPLANE_MODEL_FALLBACK: "1", ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig?.reviewer), ...buildWorkerExcludeEnv(runnerConfig?.workerExcludeExtensions) };
|
|
1644
1644
|
const retryResult = await executeLaneV2(
|
|
1645
1645
|
retryLane,
|
|
1646
1646
|
orchConfig,
|
|
@@ -1887,7 +1887,9 @@ async function attemptStaleWorktreeRecovery(
|
|
|
1887
1887
|
model: runnerConfig?.reviewer?.model || "",
|
|
1888
1888
|
thinking: runnerConfig?.reviewer?.thinking || "",
|
|
1889
1889
|
tools: runnerConfig?.reviewer?.tools || "",
|
|
1890
|
+
excludeExtensions: runnerConfig?.reviewer?.excludeExtensions ?? [],
|
|
1890
1891
|
},
|
|
1892
|
+
runnerConfig?.workerExcludeExtensions ?? [],
|
|
1891
1893
|
);
|
|
1892
1894
|
|
|
1893
1895
|
return retryResult;
|
|
@@ -2490,7 +2492,9 @@ export async function executeOrchBatch(
|
|
|
2490
2492
|
model: runnerConfig?.reviewer?.model || "",
|
|
2491
2493
|
thinking: runnerConfig?.reviewer?.thinking || "",
|
|
2492
2494
|
tools: runnerConfig?.reviewer?.tools || "",
|
|
2495
|
+
excludeExtensions: runnerConfig?.reviewer?.excludeExtensions ?? [],
|
|
2493
2496
|
},
|
|
2497
|
+
runnerConfig?.workerExcludeExtensions ?? [],
|
|
2494
2498
|
);
|
|
2495
2499
|
|
|
2496
2500
|
// ── TP-039: Tier 0 — Stale worktree recovery ────────────
|
|
@@ -1754,7 +1754,8 @@ export async function executeWave(
|
|
|
1754
1754
|
runtimeBackend?: RuntimeBackend,
|
|
1755
1755
|
onSupervisorAlert?: SupervisorAlertCallback,
|
|
1756
1756
|
supervisorAutonomy: "interactive" | "supervised" | "autonomous" = "autonomous",
|
|
1757
|
-
reviewerConfig?: { model?: string; thinking?: string; tools?: string },
|
|
1757
|
+
reviewerConfig?: { model?: string; thinking?: string; tools?: string; excludeExtensions?: string[] },
|
|
1758
|
+
workerExcludeExtensions?: string[],
|
|
1758
1759
|
): Promise<WaveExecutionResult> {
|
|
1759
1760
|
const startedAt = Date.now();
|
|
1760
1761
|
const policy = config.failure.on_task_failure;
|
|
@@ -1862,6 +1863,7 @@ export async function executeWave(
|
|
|
1862
1863
|
ORCH_BATCH_ID: batchId,
|
|
1863
1864
|
TASKPLANE_SUPERVISOR_AUTONOMY: supervisorAutonomy,
|
|
1864
1865
|
...buildReviewerEnv(reviewerConfig),
|
|
1866
|
+
...buildWorkerExcludeEnv(workerExcludeExtensions),
|
|
1865
1867
|
}, onSupervisorAlert),
|
|
1866
1868
|
);
|
|
1867
1869
|
|
|
@@ -2503,13 +2505,44 @@ import { executeTaskV2, type LaneRunnerConfig, type LaneRunnerTaskResult } from
|
|
|
2503
2505
|
*
|
|
2504
2506
|
* @since TP-160
|
|
2505
2507
|
*/
|
|
2508
|
+
/**
|
|
2509
|
+
* Parse a JSON string array from an env var value, returning empty array on failure.
|
|
2510
|
+
* @since TP-180
|
|
2511
|
+
*/
|
|
2512
|
+
function parseJsonArrayEnv(value?: string): string[] {
|
|
2513
|
+
if (!value) return [];
|
|
2514
|
+
try {
|
|
2515
|
+
const parsed = JSON.parse(value);
|
|
2516
|
+
if (Array.isArray(parsed)) return parsed.filter((v: unknown): v is string => typeof v === "string");
|
|
2517
|
+
} catch { /* ignore malformed */ }
|
|
2518
|
+
return [];
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2506
2521
|
export function buildReviewerEnv(
|
|
2507
|
-
reviewerConfig?: { model?: string; thinking?: string; tools?: string } | null,
|
|
2522
|
+
reviewerConfig?: { model?: string; thinking?: string; tools?: string; excludeExtensions?: string[] } | null,
|
|
2508
2523
|
): Record<string, string> {
|
|
2509
2524
|
const env: Record<string, string> = {};
|
|
2510
2525
|
if (reviewerConfig?.model) env.TASKPLANE_REVIEWER_MODEL = reviewerConfig.model;
|
|
2511
2526
|
if (reviewerConfig?.thinking) env.TASKPLANE_REVIEWER_THINKING = reviewerConfig.thinking;
|
|
2512
2527
|
if (reviewerConfig?.tools) env.TASKPLANE_REVIEWER_TOOLS = reviewerConfig.tools;
|
|
2528
|
+
// TP-180: Forward reviewer extension exclusions as JSON array
|
|
2529
|
+
if (reviewerConfig?.excludeExtensions && reviewerConfig.excludeExtensions.length > 0) {
|
|
2530
|
+
env.TASKPLANE_REVIEWER_EXCLUDE_EXTENSIONS = JSON.stringify(reviewerConfig.excludeExtensions);
|
|
2531
|
+
}
|
|
2532
|
+
return env;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
/**
|
|
2536
|
+
* Build worker extension exclusion env vars from config.
|
|
2537
|
+
* @since TP-180
|
|
2538
|
+
*/
|
|
2539
|
+
export function buildWorkerExcludeEnv(
|
|
2540
|
+
workerExcludeExtensions?: string[] | null,
|
|
2541
|
+
): Record<string, string> {
|
|
2542
|
+
const env: Record<string, string> = {};
|
|
2543
|
+
if (workerExcludeExtensions && workerExcludeExtensions.length > 0) {
|
|
2544
|
+
env.TASKPLANE_WORKER_EXCLUDE_EXTENSIONS = JSON.stringify(workerExcludeExtensions);
|
|
2545
|
+
}
|
|
2513
2546
|
return env;
|
|
2514
2547
|
}
|
|
2515
2548
|
|
|
@@ -2606,6 +2639,9 @@ export async function executeLaneV2(
|
|
|
2606
2639
|
reviewerModel: extraEnvVars?.TASKPLANE_REVIEWER_MODEL || "",
|
|
2607
2640
|
reviewerThinking: extraEnvVars?.TASKPLANE_REVIEWER_THINKING || "",
|
|
2608
2641
|
reviewerTools: extraEnvVars?.TASKPLANE_REVIEWER_TOOLS || "",
|
|
2642
|
+
// TP-180: Extension exclusion lists from config
|
|
2643
|
+
workerExcludeExtensions: parseJsonArrayEnv(extraEnvVars?.TASKPLANE_WORKER_EXCLUDE_EXTENSIONS),
|
|
2644
|
+
reviewerExcludeExtensions: parseJsonArrayEnv(extraEnvVars?.TASKPLANE_REVIEWER_EXCLUDE_EXTENSIONS),
|
|
2609
2645
|
supervisorAutonomy,
|
|
2610
2646
|
projectName: config.project?.name || "project",
|
|
2611
2647
|
maxIterations: 20,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "./task-executor-core.ts";
|
|
34
34
|
|
|
35
35
|
import { spawnAgent, type AgentHostOptions, type AgentHostResult } from "./agent-host.ts";
|
|
36
|
+
import { loadPiSettingsPackages, filterExcludedExtensions } from "./settings-loader.ts";
|
|
36
37
|
|
|
37
38
|
import {
|
|
38
39
|
appendAgentEvent,
|
|
@@ -222,6 +223,10 @@ export interface LaneRunnerConfig {
|
|
|
222
223
|
supervisorAutonomy?: "interactive" | "supervised" | "autonomous";
|
|
223
224
|
/** Project name (for review request context) */
|
|
224
225
|
projectName?: string;
|
|
226
|
+
/** Package specifiers to exclude from worker extension forwarding (exact match). @since TP-180 */
|
|
227
|
+
workerExcludeExtensions?: string[];
|
|
228
|
+
/** Package specifiers to exclude from reviewer extension forwarding (exact match). @since TP-180 */
|
|
229
|
+
reviewerExcludeExtensions?: string[];
|
|
225
230
|
/** Max worker iterations before giving up */
|
|
226
231
|
maxIterations: number;
|
|
227
232
|
/** No-progress stall limit */
|
|
@@ -555,6 +560,10 @@ export async function executeTaskV2(
|
|
|
555
560
|
const outboxDir = join(config.stateRoot, ".pi", "mailbox", config.batchId, workerAgentId, "outbox");
|
|
556
561
|
const bridgeExtensionPath = join(LANE_RUNNER_DIR, "agent-bridge-extension.ts");
|
|
557
562
|
|
|
563
|
+
// TP-180: Forward user-installed extensions to worker agent
|
|
564
|
+
const allPackages = loadPiSettingsPackages(config.stateRoot);
|
|
565
|
+
const workerPackages = filterExcludedExtensions(allPackages, config.workerExcludeExtensions ?? []);
|
|
566
|
+
|
|
558
567
|
const hostOpts: AgentHostOptions = {
|
|
559
568
|
agentId: workerAgentId,
|
|
560
569
|
role: "worker",
|
|
@@ -577,7 +586,7 @@ export async function executeTaskV2(
|
|
|
577
586
|
timeoutMs: config.maxWorkerMinutes * 60_000,
|
|
578
587
|
stateRoot: config.stateRoot,
|
|
579
588
|
packet: unit.packet,
|
|
580
|
-
extensions: [bridgeExtensionPath],
|
|
589
|
+
extensions: [bridgeExtensionPath, ...workerPackages],
|
|
581
590
|
env: {
|
|
582
591
|
TASKPLANE_OUTBOX_DIR: outboxDir,
|
|
583
592
|
TASKPLANE_AGENT_ID: workerAgentId,
|
|
@@ -597,6 +606,11 @@ export async function executeTaskV2(
|
|
|
597
606
|
...(config.reviewerModel ? { TASKPLANE_REVIEWER_MODEL: config.reviewerModel } : {}),
|
|
598
607
|
...(config.reviewerThinking ? { TASKPLANE_REVIEWER_THINKING: config.reviewerThinking } : {}),
|
|
599
608
|
...(config.reviewerTools ? { TASKPLANE_REVIEWER_TOOLS: config.reviewerTools } : {}),
|
|
609
|
+
// TP-180: Pass state root and reviewer exclusions for extension forwarding
|
|
610
|
+
TASKPLANE_STATE_ROOT: config.stateRoot,
|
|
611
|
+
...(config.reviewerExcludeExtensions && config.reviewerExcludeExtensions.length > 0
|
|
612
|
+
? { TASKPLANE_REVIEWER_EXCLUDE_EXTENSIONS: JSON.stringify(config.reviewerExcludeExtensions) }
|
|
613
|
+
: {}),
|
|
600
614
|
},
|
|
601
615
|
// TP-172: Exit interception callback — escalate to supervisor when worker
|
|
602
616
|
// exits without making visible progress (no checkboxes, no blocker logged).
|
|
@@ -21,6 +21,7 @@ import { loadOrchestratorConfig } from "./config.ts";
|
|
|
21
21
|
import { captureBaseline, diffFingerprints, runVerificationCommands, parseTestOutput, deduplicateFingerprints } from "./verification.ts";
|
|
22
22
|
import { spawnAgent } from "./agent-host.ts";
|
|
23
23
|
import type { AgentHostOptions, AgentHostResult, AgentTelemetryCallback } from "./agent-host.ts";
|
|
24
|
+
import { loadPiSettingsPackages, filterExcludedExtensions } from "./settings-loader.ts";
|
|
24
25
|
import type { RuntimeBackend } from "./execution.ts";
|
|
25
26
|
import type { VerificationBaseline, FingerprintDiff, TestFingerprint } from "./verification.ts";
|
|
26
27
|
|
|
@@ -716,6 +717,12 @@ export async function spawnMergeAgentV2(
|
|
|
716
717
|
mkdirSync(join(mailboxDir, "inbox"), { recursive: true });
|
|
717
718
|
}
|
|
718
719
|
|
|
720
|
+
// TP-180: Forward user-installed extensions to merge agent
|
|
721
|
+
const mergeStateRoot = stateRoot ?? repoRoot;
|
|
722
|
+
const allMergePackages = loadPiSettingsPackages(mergeStateRoot);
|
|
723
|
+
const mergeExclusions = config.merge.exclude_extensions ?? [];
|
|
724
|
+
const mergePackages = filterExcludedExtensions(allMergePackages, mergeExclusions);
|
|
725
|
+
|
|
719
726
|
const opts: AgentHostOptions = {
|
|
720
727
|
agentId: sessionName,
|
|
721
728
|
role: "merger",
|
|
@@ -733,8 +740,9 @@ export async function spawnMergeAgentV2(
|
|
|
733
740
|
eventsPath,
|
|
734
741
|
exitSummaryPath,
|
|
735
742
|
timeoutMs: (config.merge.timeout_minutes ?? 10) * 60 * 1000,
|
|
736
|
-
stateRoot:
|
|
743
|
+
stateRoot: mergeStateRoot,
|
|
737
744
|
packet: null,
|
|
745
|
+
...(mergePackages.length > 0 ? { extensions: mergePackages } : {}),
|
|
738
746
|
env: {
|
|
739
747
|
ORCH_BATCH_ID: bid,
|
|
740
748
|
},
|
|
@@ -748,7 +756,6 @@ export async function spawnMergeAgentV2(
|
|
|
748
756
|
}
|
|
749
757
|
const mergeNumber = mergeNumberMatch ? parseInt(mergeNumberMatch[1], 10) : 1;
|
|
750
758
|
const mergeStartedAt = Date.now();
|
|
751
|
-
const mergeStateRoot = stateRoot ?? repoRoot;
|
|
752
759
|
|
|
753
760
|
// Helper: build a RuntimeAgentTelemetrySnapshot from a partial AgentHostResult.
|
|
754
761
|
const buildAgentSnap = (tel: Partial<AgentHostResult>, status: RuntimeAgentTelemetrySnapshot["status"]): RuntimeAgentTelemetrySnapshot => ({
|
|
@@ -8,7 +8,7 @@ import { join } from "path";
|
|
|
8
8
|
import { assembleDiagnosticInput, emitDiagnosticReports } from "./diagnostic-reports.ts";
|
|
9
9
|
import { runDiscovery } from "./discovery.ts";
|
|
10
10
|
import { executeOrchBatch, resolveDisplayWaveNumber } from "./engine.ts";
|
|
11
|
-
import { buildReviewerEnv, computeTransitiveDependents, execLog, executeLaneV2, executeWave, resolveCanonicalTaskPaths } from "./execution.ts";
|
|
11
|
+
import { buildReviewerEnv, buildWorkerExcludeEnv, computeTransitiveDependents, execLog, executeLaneV2, executeWave, resolveCanonicalTaskPaths } from "./execution.ts";
|
|
12
12
|
import type { MonitorUpdateCallback, RuntimeBackend } from "./execution.ts";
|
|
13
13
|
import { selectRuntimeBackend } from "./engine.ts";
|
|
14
14
|
import { readRegistrySnapshot, isTerminalStatus, isProcessAlive } from "./process-registry.ts";
|
|
@@ -1506,7 +1506,7 @@ export async function resumeOrchBatch(
|
|
|
1506
1506
|
const laneResult = await executeLaneV2(
|
|
1507
1507
|
lane, orchConfig, laneRepoRoot, batchState.pauseSignal,
|
|
1508
1508
|
workspaceRoot, !!workspaceConfig,
|
|
1509
|
-
{ ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig.reviewer) },
|
|
1509
|
+
{ ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig.reviewer), ...buildWorkerExcludeEnv(runnerConfig.workerExcludeExtensions) },
|
|
1510
1510
|
emitAlert,
|
|
1511
1511
|
);
|
|
1512
1512
|
const taskResult = laneResult.tasks.find(t => t.taskId === task.taskId);
|
|
@@ -1588,7 +1588,7 @@ export async function resumeOrchBatch(
|
|
|
1588
1588
|
const laneResult = await executeLaneV2(
|
|
1589
1589
|
lane, orchConfig, reExecRepoRoot, batchState.pauseSignal,
|
|
1590
1590
|
workspaceRoot, !!workspaceConfig,
|
|
1591
|
-
{ ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig.reviewer) },
|
|
1591
|
+
{ ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig.reviewer), ...buildWorkerExcludeEnv(runnerConfig.workerExcludeExtensions) },
|
|
1592
1592
|
emitAlert,
|
|
1593
1593
|
);
|
|
1594
1594
|
const taskResult = laneResult.tasks.find(t => t.taskId === task.taskId);
|
|
@@ -2048,6 +2048,7 @@ export async function resumeOrchBatch(
|
|
|
2048
2048
|
emitAlert,
|
|
2049
2049
|
supervisorAutonomy,
|
|
2050
2050
|
runnerConfig.reviewer,
|
|
2051
|
+
runnerConfig.workerExcludeExtensions ?? [],
|
|
2051
2052
|
);
|
|
2052
2053
|
|
|
2053
2054
|
batchState.waveResults.push(waveResult);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Loader — Read and merge Pi extension packages from settings files
|
|
3
|
+
*
|
|
4
|
+
* Reads `.pi/settings.json` from both project-level and global locations,
|
|
5
|
+
* extracts the `packages` arrays, merges them (project entries first,
|
|
6
|
+
* deduplicated), and filters out taskplane itself.
|
|
7
|
+
*
|
|
8
|
+
* Used by spawn points (worker, reviewer, merge agent) to forward
|
|
9
|
+
* user-installed extensions as explicit `-e` flags alongside `--no-extensions`.
|
|
10
|
+
*
|
|
11
|
+
* @module taskplane/settings-loader
|
|
12
|
+
* @since TP-180
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
|
|
19
|
+
// ── Constants ────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** Subpath under a project root for the project-level Pi settings file. */
|
|
22
|
+
const PROJECT_SETTINGS_SUBPATH = join(".pi", "settings.json");
|
|
23
|
+
|
|
24
|
+
/** Subpath under the global agent dir for the global Pi settings file. */
|
|
25
|
+
const GLOBAL_SETTINGS_SUBPATH = join(".pi", "agent", "settings.json");
|
|
26
|
+
|
|
27
|
+
// ── Internal Helpers ─────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Safely read and parse a JSON file, returning null on any failure.
|
|
31
|
+
*/
|
|
32
|
+
function readJsonSafe(filePath: string): Record<string, unknown> | null {
|
|
33
|
+
try {
|
|
34
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
37
|
+
return parsed as Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract the `packages` array from a parsed settings object.
|
|
47
|
+
* Returns an empty array if the key is missing or not an array of strings.
|
|
48
|
+
*/
|
|
49
|
+
function extractPackages(settings: Record<string, unknown> | null): string[] {
|
|
50
|
+
if (!settings) return [];
|
|
51
|
+
const packages = settings.packages;
|
|
52
|
+
if (!Array.isArray(packages)) return [];
|
|
53
|
+
// Filter to strings only, skip non-string entries gracefully
|
|
54
|
+
return packages.filter((p): p is string => typeof p === "string" && p.length > 0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the global Pi agent settings path.
|
|
59
|
+
*
|
|
60
|
+
* Resolution order:
|
|
61
|
+
* 1. `PI_CODING_AGENT_DIR` env → `<value>/settings.json`
|
|
62
|
+
* 2. `os.homedir()/.pi/agent/settings.json`
|
|
63
|
+
*/
|
|
64
|
+
function resolveGlobalSettingsPath(): string {
|
|
65
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR;
|
|
66
|
+
if (agentDir) {
|
|
67
|
+
return join(agentDir, "settings.json");
|
|
68
|
+
}
|
|
69
|
+
return join(homedir(), GLOBAL_SETTINGS_SUBPATH);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Load Pi extension packages from project and global settings files.
|
|
76
|
+
*
|
|
77
|
+
* Reads `.pi/settings.json` from the project root (stateRoot) and from
|
|
78
|
+
* the global agent directory, merges the package lists (project first,
|
|
79
|
+
* deduplicated), and filters out any package containing "taskplane"
|
|
80
|
+
* (which is already loaded as the bridge extension).
|
|
81
|
+
*
|
|
82
|
+
* @param stateRoot - Project root directory (used to locate `.pi/settings.json`)
|
|
83
|
+
* @returns Array of package specifiers (e.g., `["npm:pi-sage"]`) or empty array
|
|
84
|
+
*/
|
|
85
|
+
export function loadPiSettingsPackages(stateRoot: string): string[] {
|
|
86
|
+
// Read project-level packages
|
|
87
|
+
const projectSettingsPath = join(stateRoot, PROJECT_SETTINGS_SUBPATH);
|
|
88
|
+
const projectSettings = readJsonSafe(projectSettingsPath);
|
|
89
|
+
const projectPackages = extractPackages(projectSettings);
|
|
90
|
+
|
|
91
|
+
// Read global packages
|
|
92
|
+
const globalSettingsPath = resolveGlobalSettingsPath();
|
|
93
|
+
const globalSettings = readJsonSafe(globalSettingsPath);
|
|
94
|
+
const globalPackages = extractPackages(globalSettings);
|
|
95
|
+
|
|
96
|
+
// Merge: project entries first, then global, deduplicated
|
|
97
|
+
const seen = new Set<string>();
|
|
98
|
+
const merged: string[] = [];
|
|
99
|
+
|
|
100
|
+
for (const pkg of projectPackages) {
|
|
101
|
+
if (!seen.has(pkg)) {
|
|
102
|
+
seen.add(pkg);
|
|
103
|
+
merged.push(pkg);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const pkg of globalPackages) {
|
|
107
|
+
if (!seen.has(pkg)) {
|
|
108
|
+
seen.add(pkg);
|
|
109
|
+
merged.push(pkg);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Filter out taskplane itself (already loaded as bridge extension).
|
|
114
|
+
// Match known specifier patterns: "npm:taskplane", "taskplane", or scoped
|
|
115
|
+
// variants like "npm:@scope/taskplane". Avoid substring matching to prevent
|
|
116
|
+
// false positives on unrelated packages containing "taskplane" in their name.
|
|
117
|
+
return merged.filter((pkg) => {
|
|
118
|
+
// Strip npm:/git: prefix to get the bare package name
|
|
119
|
+
const bare = pkg.replace(/^(?:npm:|git:(?:github\.com\/[^/]+\/)?)/, "").toLowerCase();
|
|
120
|
+
// Exact match on bare name, or scoped exact match (@scope/taskplane)
|
|
121
|
+
return bare !== "taskplane" && !bare.endsWith("/taskplane");
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Filter out excluded extensions from a package list.
|
|
127
|
+
*
|
|
128
|
+
* @param packages - Full list of package specifiers
|
|
129
|
+
* @param exclusions - Package specifiers to exclude (exact match)
|
|
130
|
+
* @returns Filtered list with excluded packages removed
|
|
131
|
+
*/
|
|
132
|
+
export function filterExcludedExtensions(packages: string[], exclusions: string[]): string[] {
|
|
133
|
+
if (!exclusions || exclusions.length === 0) return packages;
|
|
134
|
+
const excludeSet = new Set(exclusions);
|
|
135
|
+
return packages.filter((pkg) => !excludeSet.has(pkg));
|
|
136
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Settings TUI — interactive configuration viewer and editor.
|
|
3
3
|
*
|
|
4
4
|
* Provides a `/taskplane-settings` command that renders a two-level navigation:
|
|
5
|
-
* 1. Section selector (
|
|
5
|
+
* 1. Section selector (14 sections)
|
|
6
6
|
* 2. Per-section SettingsList with field display, source badges,
|
|
7
7
|
* and inline editing for enum/boolean/string/number fields
|
|
8
8
|
*
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
resolveConfigRoot,
|
|
39
39
|
resolveGlobalPreferencesPath,
|
|
40
40
|
} from "./config-loader.ts";
|
|
41
|
+
import { loadPiSettingsPackages } from "./settings-loader.ts";
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
// ── Types ────────────────────────────────────────────────────────────
|
|
@@ -87,7 +88,7 @@ export interface SectionDef {
|
|
|
87
88
|
// ── Section & Field Definitions ──────────────────────────────────────
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
|
-
* Canonical navigation map —
|
|
91
|
+
* Canonical navigation map — 14 sections.
|
|
91
92
|
* Order matches the Step 1 design in STATUS.md.
|
|
92
93
|
*/
|
|
93
94
|
export const SECTIONS: SectionDef[] = [
|
|
@@ -136,6 +137,11 @@ export const SECTIONS: SectionDef[] = [
|
|
|
136
137
|
{ configPath: "orchestrator.merge.timeoutMinutes", label: "Merge Timeout (minutes)", control: "input", layer: "L1", fieldType: "number", description: "Max time for merge agent to complete. Increase for large batches (default: 10)" },
|
|
137
138
|
],
|
|
138
139
|
},
|
|
140
|
+
{
|
|
141
|
+
name: "Agent Extensions",
|
|
142
|
+
readOnly: true, // Dynamically handled — no fixed fields
|
|
143
|
+
fields: [],
|
|
144
|
+
},
|
|
139
145
|
{
|
|
140
146
|
name: "Context Limits",
|
|
141
147
|
fields: [
|
|
@@ -1252,7 +1258,9 @@ async function showSectionSelectorLoop(
|
|
|
1252
1258
|
const sectionIndex = parseInt(selectedSection, 10);
|
|
1253
1259
|
const section = SECTIONS[sectionIndex];
|
|
1254
1260
|
|
|
1255
|
-
if (section.
|
|
1261
|
+
if (section.name === "Agent Extensions") {
|
|
1262
|
+
await showExtensionsSection(ctx, configRoot, pointerConfigRoot, onConfigChanged);
|
|
1263
|
+
} else if (section.readOnly) {
|
|
1256
1264
|
await showAdvancedSection(ctx, state.mergedConfig);
|
|
1257
1265
|
} else {
|
|
1258
1266
|
await showSectionSettingsLoop(ctx, section, configRoot, pointerConfigRoot, onConfigChanged);
|
|
@@ -1312,6 +1320,142 @@ async function showAdvancedSection(
|
|
|
1312
1320
|
});
|
|
1313
1321
|
}
|
|
1314
1322
|
|
|
1323
|
+
/**
|
|
1324
|
+
* TP-180: Agent Extensions section — toggle extensions per agent type.
|
|
1325
|
+
*
|
|
1326
|
+
* Discovers all installed Pi extension packages from project + global settings,
|
|
1327
|
+
* shows per-agent-type toggles (Worker, Reviewer, Merger), and saves
|
|
1328
|
+
* exclusion changes to project taskplane-config.json.
|
|
1329
|
+
*/
|
|
1330
|
+
async function showExtensionsSection(
|
|
1331
|
+
ctx: ExtensionContext,
|
|
1332
|
+
configRoot: string,
|
|
1333
|
+
pointerConfigRoot?: string,
|
|
1334
|
+
onConfigChanged?: () => void,
|
|
1335
|
+
): Promise<void> {
|
|
1336
|
+
while (true) {
|
|
1337
|
+
const resolvedRoot = resolveConfigRoot(configRoot, pointerConfigRoot);
|
|
1338
|
+
const mergedConfig = loadProjectConfig(configRoot, pointerConfigRoot);
|
|
1339
|
+
|
|
1340
|
+
// Discover installed packages (excluding taskplane itself)
|
|
1341
|
+
// Use configRoot (project/state root) for consistency with runtime forwarding,
|
|
1342
|
+
// not resolvedRoot (pointer-resolved config path).
|
|
1343
|
+
const packages = loadPiSettingsPackages(configRoot);
|
|
1344
|
+
|
|
1345
|
+
if (packages.length === 0) {
|
|
1346
|
+
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
1347
|
+
const container = new Container();
|
|
1348
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1349
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Agent Extensions")), 1, 0));
|
|
1350
|
+
container.addChild(new Text("", 0, 0));
|
|
1351
|
+
container.addChild(new Text(theme.fg("dim", "No third-party extensions found."), 1, 0));
|
|
1352
|
+
container.addChild(new Text(theme.fg("dim", "Install extensions via pi settings to see them here."), 1, 0));
|
|
1353
|
+
container.addChild(new Text("", 0, 0));
|
|
1354
|
+
container.addChild(new Text(theme.fg("dim", "esc back"), 1, 0));
|
|
1355
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1356
|
+
return {
|
|
1357
|
+
render: (w: number) => container.render(w),
|
|
1358
|
+
invalidate: () => container.invalidate(),
|
|
1359
|
+
handleInput: (data: string) => { if (data === "\x1b" || data === "\x1b\x1b") done(undefined); },
|
|
1360
|
+
};
|
|
1361
|
+
});
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Read current exclusion lists
|
|
1366
|
+
const workerExclude = new Set(mergedConfig.taskRunner.worker.excludeExtensions ?? []);
|
|
1367
|
+
const reviewerExclude = new Set(mergedConfig.taskRunner.reviewer.excludeExtensions ?? []);
|
|
1368
|
+
const mergeExclude = new Set(mergedConfig.orchestrator.merge.excludeExtensions ?? []);
|
|
1369
|
+
|
|
1370
|
+
const agentTypes = [
|
|
1371
|
+
{ name: "Worker", exclude: workerExclude, configPath: "taskRunner.worker.excludeExtensions" },
|
|
1372
|
+
{ name: "Reviewer", exclude: reviewerExclude, configPath: "taskRunner.reviewer.excludeExtensions" },
|
|
1373
|
+
{ name: "Merger", exclude: mergeExclude, configPath: "orchestrator.merge.excludeExtensions" },
|
|
1374
|
+
];
|
|
1375
|
+
|
|
1376
|
+
// Build toggle items: one per package per agent type
|
|
1377
|
+
const settingsItems: SettingItem[] = [];
|
|
1378
|
+
for (const pkg of packages) {
|
|
1379
|
+
for (const agentType of agentTypes) {
|
|
1380
|
+
const isExcluded = agentType.exclude.has(pkg);
|
|
1381
|
+
const enabled = !isExcluded;
|
|
1382
|
+
settingsItems.push({
|
|
1383
|
+
id: `${agentType.configPath}::${pkg}`,
|
|
1384
|
+
label: `${pkg}`,
|
|
1385
|
+
currentValue: enabled ? "✅ enabled" : "❌ disabled",
|
|
1386
|
+
description: agentType.name,
|
|
1387
|
+
values: [enabled ? "❌ disabled" : "✅ enabled"],
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const result = await ctx.ui.custom<{ id: string; value: string } | null>((tui, theme, _kb, done) => {
|
|
1393
|
+
const container = new Container();
|
|
1394
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1395
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Agent Extensions")), 1, 0));
|
|
1396
|
+
container.addChild(new Text(theme.fg("dim", "Toggle extensions on/off per agent type"), 1, 0));
|
|
1397
|
+
container.addChild(new Text("", 0, 0));
|
|
1398
|
+
|
|
1399
|
+
const settingsList = new SettingsList(
|
|
1400
|
+
settingsItems,
|
|
1401
|
+
Math.min(settingsItems.length + 2, 20),
|
|
1402
|
+
getSettingsListTheme(),
|
|
1403
|
+
(id, newValue) => done({ id, value: newValue }),
|
|
1404
|
+
() => done(null),
|
|
1405
|
+
);
|
|
1406
|
+
container.addChild(settingsList);
|
|
1407
|
+
|
|
1408
|
+
container.addChild(new Text("", 0, 0));
|
|
1409
|
+
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • space toggle • esc back"), 1, 0));
|
|
1410
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1411
|
+
|
|
1412
|
+
return {
|
|
1413
|
+
render: (w: number) => container.render(w),
|
|
1414
|
+
invalidate: () => container.invalidate(),
|
|
1415
|
+
handleInput: (data: string) => { settingsList.handleInput?.(data); tui.requestRender(); },
|
|
1416
|
+
};
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
if (!result) return; // User pressed Esc
|
|
1420
|
+
|
|
1421
|
+
// Parse the toggle result
|
|
1422
|
+
const [configPath, pkg] = result.id.split("::", 2);
|
|
1423
|
+
if (!configPath || !pkg) continue;
|
|
1424
|
+
|
|
1425
|
+
const enabling = result.value.includes("enabled");
|
|
1426
|
+
|
|
1427
|
+
// Read current exclusion array from merged effective config (handles YAML+JSON)
|
|
1428
|
+
const freshConfig = loadProjectConfig(configRoot, pointerConfigRoot);
|
|
1429
|
+
const currentExcludeList: string[] = (getNestedValue(freshConfig, configPath) as string[] | undefined) ?? [];
|
|
1430
|
+
|
|
1431
|
+
let newExcludeList: string[];
|
|
1432
|
+
if (enabling) {
|
|
1433
|
+
// Remove from exclusions → enable
|
|
1434
|
+
newExcludeList = currentExcludeList.filter((e: string) => e !== pkg);
|
|
1435
|
+
} else {
|
|
1436
|
+
// Add to exclusions → disable
|
|
1437
|
+
newExcludeList = currentExcludeList.includes(pkg)
|
|
1438
|
+
? currentExcludeList
|
|
1439
|
+
: [...currentExcludeList, pkg];
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
try {
|
|
1443
|
+
writeProjectConfigField(configRoot, configPath, newExcludeList, pointerConfigRoot);
|
|
1444
|
+
if (onConfigChanged) {
|
|
1445
|
+
try { onConfigChanged(); } catch { /* non-fatal */ }
|
|
1446
|
+
}
|
|
1447
|
+
ctx.ui.notify(
|
|
1448
|
+
`${enabling ? "✅ Enabled" : "❌ Disabled"} ${pkg} for ${configPath.includes("worker") ? "Worker" : configPath.includes("reviewer") ? "Reviewer" : "Merger"}`,
|
|
1449
|
+
"info",
|
|
1450
|
+
);
|
|
1451
|
+
} catch (err: any) {
|
|
1452
|
+
ctx.ui.notify(`❌ Failed to save: ${err.message}`, "error");
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Loop continues → re-render with fresh state
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1315
1459
|
/**
|
|
1316
1460
|
* Format a source badge for display.
|
|
1317
1461
|
*/
|
|
@@ -43,6 +43,8 @@ export interface OrchestratorConfig {
|
|
|
43
43
|
order: "fewest-files-first" | "sequential";
|
|
44
44
|
/** Merge agent timeout in minutes. Default: 10. Increase for large batches. */
|
|
45
45
|
timeout_minutes: number;
|
|
46
|
+
/** Package specifiers to exclude from extension forwarding (exact match). @since TP-180 */
|
|
47
|
+
exclude_extensions?: string[];
|
|
46
48
|
};
|
|
47
49
|
failure: {
|
|
48
50
|
on_task_failure: "skip-dependents" | "stop-wave" | "stop-all";
|
|
@@ -323,7 +325,11 @@ export interface TaskRunnerConfig {
|
|
|
323
325
|
thinking: string;
|
|
324
326
|
/** Comma-separated tool allowlist */
|
|
325
327
|
tools: string;
|
|
328
|
+
/** Package specifiers to exclude from extension forwarding (exact match). @since TP-180 */
|
|
329
|
+
excludeExtensions?: string[];
|
|
326
330
|
};
|
|
331
|
+
/** Worker agent extension exclusion list. @since TP-180 */
|
|
332
|
+
workerExcludeExtensions?: string[];
|
|
327
333
|
}
|
|
328
334
|
|
|
329
335
|
/** Result of a preflight check */
|