@signalridge/pi-subagents 1.11.1 → 1.13.0
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/CHANGELOG.md +27 -0
- package/README.md +77 -10
- package/package.json +3 -3
- package/src/agent-color.ts +1 -1
- package/src/agent-manager.ts +395 -66
- package/src/agent-runner.ts +83 -35
- package/src/extension-resource-names.ts +105 -0
- package/src/index.ts +221 -69
- package/src/output-file.ts +28 -9
- package/src/parent-questions.ts +453 -0
- package/src/question-origin.ts +24 -0
- package/src/supervisor.ts +28 -8
- package/src/types.ts +4 -3
- package/src/ui/agent-display.ts +2 -1
- package/src/ui/conversation-viewer.ts +641 -290
- package/src/ui/fleet-list.ts +258 -66
- package/src/ui/owner-group.ts +110 -0
- package/src/ui/schedule-menu.ts +14 -7
- package/src/ui/workflow-ui-client.ts +189 -0
package/src/agent-runner.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
DefaultResourceLoader,
|
|
18
18
|
type ExtensionAPI,
|
|
19
19
|
getAgentDir,
|
|
20
|
+
type ResolvedResource,
|
|
20
21
|
SessionManager,
|
|
21
22
|
SettingsManager,
|
|
22
23
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -28,6 +29,7 @@ import { extractText } from "./context.js";
|
|
|
28
29
|
import { assertNoRawInheritance, assertSafeChildSession } from "./context-boundary.js";
|
|
29
30
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
30
31
|
import { detectEnv } from "./env.js";
|
|
32
|
+
import { extensionResourceIdentity, resolveExtensionResourceNames } from "./extension-resource-names.js";
|
|
31
33
|
import { formatGateVerdict, type GateExec, runGate, workspaceFingerprint } from "./gate.js";
|
|
32
34
|
import {
|
|
33
35
|
INTERNAL_AGENT_CONFIG_OVERRIDE,
|
|
@@ -40,6 +42,7 @@ import { type ModelRegistry, resolveModel, shortModelLabel } from "./model-resol
|
|
|
40
42
|
import { parentModelSessionOptions } from "./model-runtime-bridge.js";
|
|
41
43
|
import { checkModelScope } from "./model-scope.js";
|
|
42
44
|
import { createNestedSubagentTools, getMaxSubagentDepth, type NestedAgentManager } from "./nested-tools.js";
|
|
45
|
+
import { ANSWER_SUBAGENT_QUESTION, type ParentQuestionHandler } from "./parent-questions.js";
|
|
43
46
|
import { captureProjectTrust, childProjectTrust, configurationMcpFactory, isProjectResource } from "./project-trust.js";
|
|
44
47
|
import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
|
|
45
48
|
import { shutdownAndDisposeSession } from "./session-lifecycle.js";
|
|
@@ -57,6 +60,7 @@ export const SUBAGENT_TOOL_NAMES = {
|
|
|
57
60
|
AGENT: "Agent",
|
|
58
61
|
GET_RESULT: "get_subagent_result",
|
|
59
62
|
STEER: "steer_subagent",
|
|
63
|
+
ANSWER_QUESTION: ANSWER_SUBAGENT_QUESTION,
|
|
60
64
|
} as const;
|
|
61
65
|
|
|
62
66
|
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
@@ -241,8 +245,9 @@ function extensionToolNamesInScope(session: AgentSession, ctx: {
|
|
|
241
245
|
extNames: Set<string>;
|
|
242
246
|
narrowing: Map<string, Set<string>>;
|
|
243
247
|
nestedToolNames: Set<string>;
|
|
248
|
+
resolvedNames: Map<string, string[]>;
|
|
244
249
|
}): () => Set<string> {
|
|
245
|
-
const { loader, toolNames, disallowedSet, extNames, narrowing, nestedToolNames } = ctx;
|
|
250
|
+
const { loader, toolNames, disallowedSet, extNames, narrowing, nestedToolNames, resolvedNames } = ctx;
|
|
246
251
|
const builtins = new Set(toolNames);
|
|
247
252
|
return () => {
|
|
248
253
|
const keep = new Set<string>();
|
|
@@ -258,7 +263,7 @@ function extensionToolNamesInScope(session: AgentSession, ctx: {
|
|
|
258
263
|
const path = tool.sourceInfo.path;
|
|
259
264
|
const extension = extensionsByPath.get(path);
|
|
260
265
|
if (extension) {
|
|
261
|
-
const canons = extensionCanonicalNames(extension.path);
|
|
266
|
+
const canons = resolvedNames.get(extension.path) ?? extensionCanonicalNames(extension.path);
|
|
262
267
|
if (optInActive && !canons.some((c) => extNames.has(c))) continue;
|
|
263
268
|
// First alias that carries a narrowing set — a user won't narrow one
|
|
264
269
|
// extension under two different names, so first-match is correct.
|
|
@@ -684,7 +689,9 @@ export interface RunOptions {
|
|
|
684
689
|
* Defaults to on wherever there is a UI to ask through; `false` withholds the
|
|
685
690
|
* tool entirely rather than injecting one that always refuses.
|
|
686
691
|
*/
|
|
687
|
-
supervisorQuestions?: boolean;
|
|
692
|
+
supervisorQuestions?: boolean | "parent";
|
|
693
|
+
/** Manager-issued root clarification channel; never supplied by child tool args. */
|
|
694
|
+
parentQuestion?: ParentQuestionHandler;
|
|
688
695
|
/** Runtime bridge for opt-in child-safe nested delegation. */
|
|
689
696
|
nestedRuntime?: {
|
|
690
697
|
manager: NestedAgentManager;
|
|
@@ -901,6 +908,9 @@ export async function runAgent(
|
|
|
901
908
|
// an already-cancelled caller. This matters for tool-call signals: the
|
|
902
909
|
// caller may be cancelled before this async function gets its first turn.
|
|
903
910
|
throwIfAborted(options.signal);
|
|
911
|
+
if (options.supervisorQuestions === "parent" && !options.parentQuestion) {
|
|
912
|
+
throw new Error('Parent clarification requires a manager-issued background channel. Use Agent with run_in_background: true and supervisor_questions: "parent".');
|
|
913
|
+
}
|
|
904
914
|
const parentProjectTrust = captureProjectTrust(ctx);
|
|
905
915
|
// Configuration provenance is established synchronously, before definition
|
|
906
916
|
// admission, model setup, callbacks, or any await. Execution cwd is not trust.
|
|
@@ -1085,12 +1095,12 @@ export async function runAgent(
|
|
|
1085
1095
|
const settingsManager = SettingsManager.create(configCwd, agentDir, { projectTrusted });
|
|
1086
1096
|
|
|
1087
1097
|
// Extension loading:
|
|
1088
|
-
// - true → all default-discovered extensions
|
|
1098
|
+
// - true → all child-safe default-discovered extensions
|
|
1089
1099
|
// - false → none (noExtensions)
|
|
1090
1100
|
// - string[] → allowlist. Bare names keep matching default-discovered
|
|
1091
1101
|
// extensions; path entries load explicitly; "*" keeps all defaults.
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
1102
|
+
// Resolve enabled resources before loading: excluded factories cannot
|
|
1103
|
+
// replace selected built-ins or schedule unmanaged asynchronous wakes.
|
|
1094
1104
|
//
|
|
1095
1105
|
// Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md — upstream's
|
|
1096
1106
|
// buildSystemPrompt() re-appends both AFTER systemPromptOverride, which
|
|
@@ -1111,12 +1121,11 @@ export async function runAgent(
|
|
|
1111
1121
|
: undefined;
|
|
1112
1122
|
const keepNames = extensionsSpec?.names ?? new Set<string>();
|
|
1113
1123
|
// `exclude_extensions:` is a denylist applied AFTER the include set — exclude wins.
|
|
1114
|
-
// Plain canonical names only (case-insensitive).
|
|
1115
|
-
//
|
|
1124
|
+
// Plain canonical names only (case-insensitive). Preselection prevents
|
|
1125
|
+
// factory execution, but resolving packages is not an untrusted-code sandbox.
|
|
1116
1126
|
const excludeNames = new Set((excludeExtensions ?? []).map((n) => n.toLowerCase()));
|
|
1117
1127
|
const hasExcludes = excludeNames.size > 0;
|
|
1118
|
-
// The override
|
|
1119
|
-
// and ALWAYS puts our tool policy first. Pi passes one mutable
|
|
1128
|
+
// The override validates preselection and ALWAYS puts our tool policy first. Pi passes one mutable
|
|
1120
1129
|
// tool_call event through handlers in extension order; if an earlier extension
|
|
1121
1130
|
// changes event.toolName, a later gate would inspect a different name from the
|
|
1122
1131
|
// one Pi actually executes. Ordering before untrusted handlers is essential.
|
|
@@ -1130,6 +1139,7 @@ export async function runAgent(
|
|
|
1130
1139
|
let discoveredNames: Set<string> | undefined;
|
|
1131
1140
|
let prefilteredExtensionPaths: Set<string> | undefined;
|
|
1132
1141
|
const resolvedExplicitNames = new Set<string>();
|
|
1142
|
+
let resolvedNames = new Map<string, string[]>();
|
|
1133
1143
|
const interactiveAskNames = new Set(ctx.hasUI ? (agentConfig?.askTools ?? []).map((name) => name.toLowerCase()) : []);
|
|
1134
1144
|
const approvalTimer: ApprovalTimer = {
|
|
1135
1145
|
isInteractiveAsk: (name) => interactiveAskNames.has(name.toLowerCase()),
|
|
@@ -1224,11 +1234,12 @@ export async function runAgent(
|
|
|
1224
1234
|
// discovered extension can register /mcp, replace builtin:mcp, and only then
|
|
1225
1235
|
// get filtered out by our allowlist. Resolve configured resources through the
|
|
1226
1236
|
// public package API first and feed only the selected paths into a loader with
|
|
1227
|
-
// discovery off.
|
|
1228
|
-
//
|
|
1237
|
+
// discovery off. Always preselect, even under default inheritance: a
|
|
1238
|
+
// pi-background factory can schedule model wakes beyond session.prompt(),
|
|
1239
|
+
// after the manager has completed the record and cleaned its worktree.
|
|
1229
1240
|
const builtinNames = extensionFactories.filter((factory) => typeof factory !== "function" && factory.builtin)
|
|
1230
1241
|
.map((factory) => factory.name);
|
|
1231
|
-
const prefilter = !noExtensions
|
|
1242
|
+
const prefilter = !noExtensions;
|
|
1232
1243
|
if (prefilter) {
|
|
1233
1244
|
await settingsManager.reload();
|
|
1234
1245
|
throwIfAborted(options.signal);
|
|
@@ -1238,31 +1249,54 @@ export async function runAgent(
|
|
|
1238
1249
|
// state or replace its source-resolution rules with path heuristics.
|
|
1239
1250
|
const configured = await packages.resolve();
|
|
1240
1251
|
throwIfAborted(options.signal);
|
|
1241
|
-
|
|
1242
|
-
|
|
1252
|
+
// Validate each source before cross-source dedup discards valid alias metadata.
|
|
1253
|
+
const explicitSources = new Map<string, ResolvedResource[]>();
|
|
1243
1254
|
for (const source of extensionsSpec?.paths ?? []) {
|
|
1244
|
-
|
|
1255
|
+
const resolved = await packages.resolveExtensionSources([source], { temporary: true });
|
|
1256
|
+
throwIfAborted(options.signal);
|
|
1257
|
+
const enabled = resolved.extensions.filter(resource => resource.enabled);
|
|
1258
|
+
if (!existsSync(source) || enabled.length === 0) {
|
|
1245
1259
|
throw new Error(`Extension source "${source}" requested by agent "${type}" was not found or has no enabled extensions.`);
|
|
1246
1260
|
}
|
|
1261
|
+
explicitSources.set(source, enabled);
|
|
1247
1262
|
}
|
|
1263
|
+
const explicitResources = [...explicitSources.values()].flat();
|
|
1248
1264
|
settingsBeforeLoad = JSON.stringify([settingsManager.getGlobalSettings(), settingsManager.getProjectSettings()]);
|
|
1249
|
-
const enabledConfigured = configured.extensions.filter((resource) => resource.enabled && !deniedProjectResource(resource.path))
|
|
1250
|
-
const enabledExplicit =
|
|
1265
|
+
const enabledConfigured = configured.extensions.filter((resource) => resource.enabled && !deniedProjectResource(resource.path));
|
|
1266
|
+
const enabledExplicit = explicitResources;
|
|
1251
1267
|
for (const resource of enabledExplicit) {
|
|
1252
1268
|
if (deniedProjectResource(resource.path)) {
|
|
1253
1269
|
throw new Error(`Project extension "${resource.path}" requested by agent "${type}" is not trusted.`);
|
|
1254
1270
|
}
|
|
1255
1271
|
}
|
|
1256
|
-
|
|
1272
|
+
const resolved = await resolveExtensionResourceNames(
|
|
1273
|
+
[...configured.extensions.filter(resource => !deniedProjectResource(resource.path)), ...enabledExplicit],
|
|
1274
|
+
packages, extensionCanonicalNames,
|
|
1275
|
+
);
|
|
1276
|
+
throwIfAborted(options.signal);
|
|
1277
|
+
resolvedNames = resolved.names;
|
|
1278
|
+
discoveredNames = new Set([...enabledConfigured, ...enabledExplicit]
|
|
1279
|
+
.flatMap(resource => resolvedNames.get(resource.path) ?? []));
|
|
1280
|
+
let warnedBackground = false;
|
|
1281
|
+
const admitChildExtension = (resource: ResolvedResource, intentional: boolean): boolean => {
|
|
1282
|
+
if (!resolvedNames.get(resource.path)?.includes("pi-background")) return true;
|
|
1283
|
+
const reason = `pi-background cannot load in agent "${type}": managed child sessions do not support asynchronous model wakes after completion/worktree cleanup`;
|
|
1284
|
+
if (intentional) throw new Error(`${reason} (requested extension "${resource.path}").`);
|
|
1285
|
+
if (!warnedBackground) {
|
|
1286
|
+
warnedBackground = true;
|
|
1287
|
+
options.onToolActivity?.({ type: "end", toolName: `extension-error:${reason}; excluded from inherited resources` });
|
|
1288
|
+
}
|
|
1289
|
+
return false;
|
|
1290
|
+
};
|
|
1257
1291
|
const selectedExplicit = enabledExplicit.filter((resource) =>
|
|
1258
|
-
!
|
|
1259
|
-
const explicitPaths = new Set(selectedExplicit.map((resource) => resource.path));
|
|
1292
|
+
!resolvedNames.get(resource.path)?.some((name) => excludeNames.has(name)) && admitChildExtension(resource, true));
|
|
1293
|
+
const explicitPaths = new Set(selectedExplicit.map((resource) => extensionResourceIdentity(resource.path)));
|
|
1260
1294
|
// For priority, use the package's declared name rather than a generic entry
|
|
1261
1295
|
// alias like "src"; unrelated packages often have src/index.ts too.
|
|
1262
1296
|
const explicitNames = new Set(selectedExplicit.map((resource) =>
|
|
1263
|
-
|
|
1264
|
-
for (const source of
|
|
1265
|
-
if (
|
|
1297
|
+
resolved.packageName(resource) ?? extensionCanonicalName(resource.path)));
|
|
1298
|
+
for (const [source, resources] of explicitSources) {
|
|
1299
|
+
if (resources.some(resource => explicitPaths.has(extensionResourceIdentity(resource.path)))) {
|
|
1266
1300
|
resolvedExplicitNames.add(extensionCanonicalName(source));
|
|
1267
1301
|
}
|
|
1268
1302
|
}
|
|
@@ -1270,15 +1304,23 @@ export async function runAgent(
|
|
|
1270
1304
|
// The loader normally loads temporary paths before discovered paths. Keep
|
|
1271
1305
|
// that priority (including for directory/package paths with many entries).
|
|
1272
1306
|
...selectedExplicit.map((resource) => resource.path),
|
|
1273
|
-
...enabledConfigured.filter((
|
|
1274
|
-
const names = extensionCanonicalNames(path);
|
|
1307
|
+
...enabledConfigured.filter((resource) => {
|
|
1308
|
+
const names = resolvedNames.get(resource.path) ?? extensionCanonicalNames(resource.path);
|
|
1275
1309
|
if (names.some((name) => excludeNames.has(name))) return false;
|
|
1276
|
-
if (explicitPaths.has(
|
|
1277
|
-
|
|
1278
|
-
|
|
1310
|
+
if (explicitPaths.has(extensionResourceIdentity(resource.path)) ||
|
|
1311
|
+
explicitNames.has(resolved.packageName(resource) ?? extensionCanonicalName(resource.path))) return false;
|
|
1312
|
+
const intentional = names.some((name) => bareNames.has(name));
|
|
1313
|
+
return (loadAll || intentional) && admitChildExtension(resource, intentional);
|
|
1314
|
+
}).map((resource) => resource.path),
|
|
1279
1315
|
];
|
|
1280
|
-
|
|
1281
|
-
additionalExtensionPaths =
|
|
1316
|
+
const seen = new Set<string>();
|
|
1317
|
+
additionalExtensionPaths = selectedPaths.filter(path => {
|
|
1318
|
+
const identity = extensionResourceIdentity(path);
|
|
1319
|
+
if (seen.has(identity)) return false;
|
|
1320
|
+
seen.add(identity);
|
|
1321
|
+
return true;
|
|
1322
|
+
});
|
|
1323
|
+
prefilteredExtensionPaths = new Set(additionalExtensionPaths);
|
|
1282
1324
|
}
|
|
1283
1325
|
|
|
1284
1326
|
const loader = new DefaultResourceLoader({
|
|
@@ -1308,7 +1350,12 @@ export async function runAgent(
|
|
|
1308
1350
|
throw new Error(`Pi extension settings changed while loading agent "${type}"; retry with the current settings.`);
|
|
1309
1351
|
}
|
|
1310
1352
|
const loadedPaths = new Set(loader.getExtensions().extensions.map((extension) => extension.path));
|
|
1311
|
-
|
|
1353
|
+
// Preserve ordinary unrestricted inheritance: Pi may drop a faulty factory
|
|
1354
|
+
// or let a replaceable built-in yield to another discovered extension. The
|
|
1355
|
+
// original restrictive preselection still requires every selected path.
|
|
1356
|
+
const validateSelected = !loadAll || hasExcludes || !projectTrusted ||
|
|
1357
|
+
bareNames.size > 0 || (extensionsSpec?.paths.length ?? 0) > 0;
|
|
1358
|
+
for (const path of validateSelected ? prefilteredExtensionPaths : []) {
|
|
1312
1359
|
if (!loadedPaths.has(path)) {
|
|
1313
1360
|
const diagnostic = loader.getExtensions().errors.find((error) => error.path === path);
|
|
1314
1361
|
throw new Error(`Selected extension "${path}" was not loaded for agent "${type}"${diagnostic ? `: ${diagnostic.error}` : " (possibly replaced by another extension)"}.`);
|
|
@@ -1369,7 +1416,7 @@ export async function runAgent(
|
|
|
1369
1416
|
}
|
|
1370
1417
|
if (keepNames.size > 0 || extNames.size > 0) {
|
|
1371
1418
|
const survivingNames = new Set(
|
|
1372
|
-
loader.getExtensions().extensions.flatMap((e) => extensionCanonicalNames(e.path)),
|
|
1419
|
+
loader.getExtensions().extensions.flatMap((e) => resolvedNames.get(e.path) ?? extensionCanonicalNames(e.path)),
|
|
1373
1420
|
);
|
|
1374
1421
|
for (const name of keepNames) {
|
|
1375
1422
|
// A directory/package source can resolve to entries with different
|
|
@@ -1458,8 +1505,9 @@ export async function runAgent(
|
|
|
1458
1505
|
// only a human to answer, which `hasUI` decides. Isolation still suppresses
|
|
1459
1506
|
// it: an `isolated: true` agent is defined as built-in tools only.
|
|
1460
1507
|
const supervisorTools =
|
|
1461
|
-
ctx.hasUI && !isolated && options.supervisorQuestions !== false
|
|
1508
|
+
(ctx.hasUI || options.supervisorQuestions === "parent") && !isolated && options.supervisorQuestions !== false
|
|
1462
1509
|
? createSupervisorTool({
|
|
1510
|
+
...(options.supervisorQuestions === "parent" ? { parentQuestion: options.parentQuestion } : {}),
|
|
1463
1511
|
// Use the canonical agent type, not display_name: the question must
|
|
1464
1512
|
// identify the actual subagent that requested the decision.
|
|
1465
1513
|
agentLabel: agentConfig?.name ?? type,
|
|
@@ -1600,7 +1648,7 @@ export async function runAgent(
|
|
|
1600
1648
|
throwIfAborted(options.signal);
|
|
1601
1649
|
|
|
1602
1650
|
const inScope = noExtensions ? undefined : extensionToolNamesInScope(session, {
|
|
1603
|
-
loader, toolNames, disallowedSet, extNames, narrowing, nestedToolNames,
|
|
1651
|
+
loader, toolNames, disallowedSet, extNames, narrowing, nestedToolNames, resolvedNames,
|
|
1604
1652
|
});
|
|
1605
1653
|
checkToolCall = async (name, input, deadline) => {
|
|
1606
1654
|
const run = async (): Promise<ToolCallEventResult | undefined> => {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import type { DefaultPackageManager, ResolvedResource } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
/** Built-in identifiers and missing resources retain their original identity. */
|
|
7
|
+
export function extensionResourceIdentity(path: string): string {
|
|
8
|
+
if (path.startsWith("builtin:") || path.startsWith("<")) return path;
|
|
9
|
+
try { return realpathSync(path); } catch { return resolve(path); }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function packageShortName(root: string): string | undefined {
|
|
13
|
+
try {
|
|
14
|
+
const name: unknown = JSON.parse(readFileSync(join(root, "package.json"), "utf-8")).name;
|
|
15
|
+
return typeof name === "string" ? name.split("/").pop()?.toLowerCase() : undefined;
|
|
16
|
+
} catch { return undefined; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isDirectory(path: string): boolean {
|
|
20
|
+
try { return statSync(path).isDirectory(); } catch { return false; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Find candidate roots only; the public resolver decides which own the entry. */
|
|
24
|
+
function candidatePackageRoots(entry: string): string[] {
|
|
25
|
+
if (entry.startsWith("builtin:") || entry.startsWith("<")) return [];
|
|
26
|
+
const roots: string[] = [];
|
|
27
|
+
let dir = isDirectory(entry) ? entry : dirname(entry);
|
|
28
|
+
for (;;) {
|
|
29
|
+
if (basename(dir) === "node_modules") break;
|
|
30
|
+
if (existsSync(join(dir, "package.json"))) roots.push(dir);
|
|
31
|
+
const parent = dirname(dir);
|
|
32
|
+
if (parent === dir) break;
|
|
33
|
+
dir = parent;
|
|
34
|
+
}
|
|
35
|
+
return roots;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Only Node's supported module entry, never factory evaluation or sibling scanning. */
|
|
39
|
+
function resourceIdentities(path: string): string[] {
|
|
40
|
+
const identity = extensionResourceIdentity(path);
|
|
41
|
+
if (!isDirectory(identity)) return [identity];
|
|
42
|
+
try {
|
|
43
|
+
return [identity, extensionResourceIdentity(createRequire(join(identity, "package.json")).resolve(identity))];
|
|
44
|
+
} catch { return [identity]; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Union aliases for admission/exclusion; keep declared ownership separate for priority. */
|
|
48
|
+
export async function resolveExtensionResourceNames(
|
|
49
|
+
resources: ResolvedResource[],
|
|
50
|
+
packages: DefaultPackageManager,
|
|
51
|
+
pathNames: (path: string) => string[],
|
|
52
|
+
): Promise<{ names: Map<string, string[]>; packageName: (resource: ResolvedResource) => string | undefined }> {
|
|
53
|
+
const entries = new Map<string, { paths: Set<string>; names: Set<string>; owners: Set<string> }>();
|
|
54
|
+
for (const resource of resources) {
|
|
55
|
+
const identity = extensionResourceIdentity(resource.path);
|
|
56
|
+
let entry = entries.get(identity);
|
|
57
|
+
if (!entry) {
|
|
58
|
+
entry = { paths: new Set([identity]), names: new Set(), owners: new Set() };
|
|
59
|
+
entries.set(identity, entry);
|
|
60
|
+
}
|
|
61
|
+
entry.paths.add(resource.path);
|
|
62
|
+
if (resource.metadata.packageRoot) {
|
|
63
|
+
const name = packageShortName(resource.metadata.packageRoot);
|
|
64
|
+
if (name) entry.owners.add(name);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const probes = new Map<string, Set<string>>();
|
|
68
|
+
const canonicalOwners = new Map<string, string>();
|
|
69
|
+
for (const [identity, entry] of entries) {
|
|
70
|
+
for (const path of entry.paths) {
|
|
71
|
+
for (const name of pathNames(path)) entry.names.add(name);
|
|
72
|
+
}
|
|
73
|
+
// A lexical file alias must not conceal the canonical declaring package.
|
|
74
|
+
// A nearer non-owning manifest must not stop ancestor discovery either.
|
|
75
|
+
for (const root of candidatePackageRoots(identity)) {
|
|
76
|
+
let declared = probes.get(root);
|
|
77
|
+
if (!declared) {
|
|
78
|
+
const resolved = await packages.resolveExtensionSources([root], { temporary: true });
|
|
79
|
+
// Enabled is a selection preference, not evidence of file ownership.
|
|
80
|
+
declared = new Set(resolved.extensions.flatMap(resource => resourceIdentities(resource.path)));
|
|
81
|
+
probes.set(root, declared);
|
|
82
|
+
}
|
|
83
|
+
if (declared.has(identity)) {
|
|
84
|
+
const name = packageShortName(root);
|
|
85
|
+
if (name) {
|
|
86
|
+
entry.owners.add(name);
|
|
87
|
+
if (!canonicalOwners.has(identity)) canonicalOwners.set(identity, name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const name of entry.owners) entry.names.add(name);
|
|
92
|
+
}
|
|
93
|
+
const names = new Map<string, string[]>();
|
|
94
|
+
for (const entry of entries.values()) {
|
|
95
|
+
for (const path of entry.paths) names.set(path, [...entry.names]);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
names,
|
|
99
|
+
// Identical paths can have different declaring metadata. Never key this by
|
|
100
|
+
// path alone. Direct entries use nearest canonical declaring ownership.
|
|
101
|
+
packageName: resource => resource.metadata.packageRoot
|
|
102
|
+
? packageShortName(resource.metadata.packageRoot)
|
|
103
|
+
: canonicalOwners.get(extensionResourceIdentity(resource.path)),
|
|
104
|
+
};
|
|
105
|
+
}
|