taskplane 0.28.1 → 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/dashboard/public/app.js +24 -18
- 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/extension.ts +5135 -5135
- 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
|
@@ -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 */
|