pi-profile-switch 0.4.9 → 0.5.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/README.md +1 -1
- package/bin/pi-profile.ts +8 -5
- package/package.json +1 -1
- package/src/extension-discovery.ts +1 -1
- package/src/launcher/initial-profile.ts +1 -1
- package/src/launcher/runtime-cleanup.ts +112 -32
- package/src/settings-generator.ts +63 -10
- package/src/switching/apply-plan.ts +1 -1
- package/src/switching/status.ts +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | [中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
Named profiles for [Pi](https://github.com/badlogic/pi-mono). A profile is a named
|
|
5
|
+
Named profiles for [Pi](https://github.com/badlogic/pi-mono). A profile is a named set of resources you define: skills, extensions, MCP servers, tools (including tools exposed by MCP servers and extensions), model defaults, and extra system-prompt instructions. Switch profiles inside a running Pi session — no restart.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
package/bin/pi-profile.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
16
16
|
import { ExtensionError } from "../src/extension-discovery.ts";
|
|
17
17
|
import { parseLauncherArgs } from "../src/launcher/args.ts";
|
|
18
18
|
import { UnknownProfileError, resolveInitialProfile } from "../src/launcher/initial-profile.ts";
|
|
19
|
-
import {
|
|
19
|
+
import { sweepStaleInstances } from "../src/launcher/runtime-cleanup.ts";
|
|
20
20
|
import { spawnPi } from "../src/launcher/spawn.ts";
|
|
21
21
|
import { McpConfigError, MissingMcpAdapterError } from "../src/mcp-config.ts";
|
|
22
22
|
import { CatalogError } from "../src/profile-catalog.ts";
|
|
@@ -36,10 +36,13 @@ try {
|
|
|
36
36
|
for (const warning of warnings) {
|
|
37
37
|
console.error(`pi-profile: warning: ${warning}`);
|
|
38
38
|
}
|
|
39
|
-
// Stale per-launch
|
|
40
|
-
// window) are swept before this launch materializes its own.
|
|
41
|
-
//
|
|
42
|
-
|
|
39
|
+
// Stale per-launch instance dirs (dead pid, or no pid past the grace
|
|
40
|
+
// window) are swept before this launch materializes its own. Directories
|
|
41
|
+
// holding state pi-profile did not generate are kept and reported instead of
|
|
42
|
+
// deleted (ADR-0010). Best-effort: sweep errors never block the launch.
|
|
43
|
+
for (const warning of await sweepStaleInstances()) {
|
|
44
|
+
console.error(`pi-profile: warning: ${warning}`);
|
|
45
|
+
}
|
|
43
46
|
const generated = await generateRuntimeDir(plan, { agentDir, discovery, projectSettings, projectDir });
|
|
44
47
|
process.exitCode = await spawnPi({
|
|
45
48
|
generated,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-profile-switch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Named profiles for Pi: reference skills, extensions, MCP servers, and tools per workflow, switched without restarting. Install: npm install -g pi-profile-switch (not pi install).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ExtensionDiscovery: implicit, read-only discovery of selectable extensions
|
|
3
|
-
* (ADR-
|
|
3
|
+
* (ADR-0007/0008), so profiles can reference extensions without any
|
|
4
4
|
* registration step.
|
|
5
5
|
*
|
|
6
6
|
* Two implicit sources, both resolved by Pi itself:
|
|
@@ -31,7 +31,7 @@ export class UnknownProfileError extends Error {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/** Zero-match glob references surface as launch warnings (ADR-
|
|
34
|
+
/** Zero-match glob references surface as launch warnings (ADR-0009):
|
|
35
35
|
* visible, but never blocking — globs re-expand on every resolution. */
|
|
36
36
|
function unmatchedWarnings(plan: ActivationPlan): string[] {
|
|
37
37
|
return (plan.unmatched ?? []).map(
|
|
@@ -1,35 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* InstanceCleanup: sweeps stale per-launch instance directories at startup.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* (ADR-
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* logic on the signal path.
|
|
4
|
+
* Every launch materializes its own instance dir under
|
|
5
|
+
* `<PI_PROFILE_SWITCH_DIR>/instances/launch-*` (ADR-0010), which would
|
|
6
|
+
* otherwise accumulate forever. Startup sweep is the ONLY cleanup mechanism by
|
|
7
|
+
* design: any exit — graceful, signal, SIGKILL, power loss — kills the child
|
|
8
|
+
* pid, so the next launch's sweep converges. There is no exit-time deletion; it
|
|
9
|
+
* would only buy immediacy at the cost of deletion logic on the signal path.
|
|
10
10
|
*
|
|
11
|
-
* Liveness token: a `pid` file written by spawnPi into the
|
|
12
|
-
* (Naming the dir after the pid is impossible — the pid does not exist
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Liveness token: a `pid` file written by spawnPi into the instance dir.
|
|
12
|
+
* (Naming the dir after the pid is impossible — the pid does not exist before
|
|
13
|
+
* spawn, and the running process's PI_CODING_AGENT_DIR path is frozen.) Rules
|
|
14
|
+
* per instance dir:
|
|
15
15
|
* - pid file parses and the process is alive (or EPERM) → keep;
|
|
16
|
-
* ESRCH →
|
|
17
|
-
* - no/unparsable pid file →
|
|
16
|
+
* ESRCH → candidate.
|
|
17
|
+
* - no/unparsable pid file → candidate only when the dir mtime is older than
|
|
18
18
|
* NO_PID_GRACE_MS. The grace window guards the concurrent-launch race (a
|
|
19
|
-
* second launcher between
|
|
20
|
-
*
|
|
19
|
+
* second launcher between mkdir and its pid write must not be reaped); it
|
|
20
|
+
* also covers post-mkdir crashes.
|
|
21
21
|
* PID reuse needs no /proc check: a wrong keep only delays cleanup and
|
|
22
22
|
* self-heals once the reused pid dies.
|
|
23
23
|
*
|
|
24
|
+
* A candidate is deleted only when every entry in it is pi-profile's own: a
|
|
25
|
+
* symlink (the mirror's link into the real agent dir) or a managed generated
|
|
26
|
+
* file. Anything else is state an extension created at runtime, so the
|
|
27
|
+
* directory is kept and reported — the sweep never destroys data it cannot
|
|
28
|
+
* attribute. The check deliberately does NOT consult the real agent dir: a
|
|
29
|
+
* same-named entry there (a seeded `missions`, say) must not turn an
|
|
30
|
+
* instance-local real directory into a deletable one.
|
|
31
|
+
*
|
|
24
32
|
* Everything is best-effort: sweep errors never block a launch.
|
|
25
33
|
*/
|
|
26
34
|
|
|
27
|
-
import {
|
|
35
|
+
import type { Dirent } from "node:fs";
|
|
36
|
+
import { lstat, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
28
37
|
import path from "node:path";
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
import { MANAGED_INSTANCE_FILES } from "../settings-generator.ts";
|
|
40
|
+
import { getInstancesRootDir } from "../workspace.ts";
|
|
41
|
+
|
|
42
|
+
/** Prefix of the directories this module owns under the instances root. */
|
|
43
|
+
const INSTANCE_DIR_PREFIX = "launch-";
|
|
44
|
+
|
|
45
|
+
/** Grace period for instance dirs without a (parseable) pid file. */
|
|
31
46
|
export const NO_PID_GRACE_MS = 10 * 60 * 1000;
|
|
32
47
|
|
|
48
|
+
/** Managed directories whose contents are not necessarily generated: a package
|
|
49
|
+
* may write its own files inside them (e.g. an extension's own config). */
|
|
50
|
+
const MANAGED_DIRS_WITH_RUNTIME_CONTENT = new Set(["extensions"]);
|
|
51
|
+
|
|
33
52
|
function isProcessAlive(pid: number): boolean {
|
|
34
53
|
try {
|
|
35
54
|
process.kill(pid, 0);
|
|
@@ -40,11 +59,47 @@ function isProcessAlive(pid: number): boolean {
|
|
|
40
59
|
}
|
|
41
60
|
}
|
|
42
61
|
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
/** Paths inside `dir` that pi-profile did not generate, relative to `dir`.
|
|
63
|
+
* `undefined` means the directory could not be inspected — the caller must
|
|
64
|
+
* treat that as unrecognized rather than as empty. */
|
|
65
|
+
async function unrecognizedEntries(dir: string): Promise<string[] | undefined> {
|
|
66
|
+
let entries: Dirent<string>[];
|
|
67
|
+
try {
|
|
68
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const found: string[] = [];
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const entryPath = path.join(dir, entry.name);
|
|
76
|
+
let linkStat;
|
|
77
|
+
try {
|
|
78
|
+
linkStat = await lstat(entryPath);
|
|
79
|
+
} catch {
|
|
80
|
+
continue; // Vanished under us: nothing to protect.
|
|
81
|
+
}
|
|
82
|
+
// Symlinks only point at the real agent dir and hold no data of their own.
|
|
83
|
+
if (linkStat.isSymbolicLink()) continue;
|
|
84
|
+
if (!MANAGED_INSTANCE_FILES.has(entry.name)) {
|
|
85
|
+
found.push(entry.name);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (MANAGED_DIRS_WITH_RUNTIME_CONTENT.has(entry.name) && linkStat.isDirectory()) {
|
|
89
|
+
const nested = await unrecognizedEntries(entryPath);
|
|
90
|
+
if (nested === undefined) {
|
|
91
|
+
found.push(entry.name);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
found.push(...nested.map((name) => path.join(entry.name, name)));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return found;
|
|
45
98
|
}
|
|
46
99
|
|
|
47
|
-
|
|
100
|
+
/** Reclaims `dir` if it is both dead and free of unrecognized entries.
|
|
101
|
+
* Returns the warnings for the cases where it was intentionally kept. */
|
|
102
|
+
async function sweepEntry(dir: string): Promise<string[]> {
|
|
48
103
|
let pid: number | undefined;
|
|
49
104
|
try {
|
|
50
105
|
const raw = await readFile(path.join(dir, "pid"), "utf8");
|
|
@@ -55,31 +110,56 @@ async function sweepEntry(dir: string): Promise<void> {
|
|
|
55
110
|
}
|
|
56
111
|
|
|
57
112
|
if (pid !== undefined) {
|
|
58
|
-
if (
|
|
59
|
-
|
|
113
|
+
if (isProcessAlive(pid)) return [];
|
|
114
|
+
} else {
|
|
115
|
+
const info = await stat(dir);
|
|
116
|
+
if (Date.now() - info.mtimeMs <= NO_PID_GRACE_MS) return [];
|
|
60
117
|
}
|
|
61
118
|
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
119
|
+
const unrecognized = await unrecognizedEntries(dir);
|
|
120
|
+
if (unrecognized === undefined) {
|
|
121
|
+
return [
|
|
122
|
+
`${dir} was not reclaimed: it could not be inspected (permissions?). Check its contents and delete it manually.`,
|
|
123
|
+
];
|
|
65
124
|
}
|
|
125
|
+
if (unrecognized.length > 0) {
|
|
126
|
+
return [
|
|
127
|
+
`${dir} was not reclaimed: it holds state pi-profile did not generate (${unrecognized.join(", ")}). ` +
|
|
128
|
+
`Move that state into the real agent dir (it is mirrored on the next launch), or point the extension that ` +
|
|
129
|
+
`created it at a fixed path via that extension's own configuration, then delete ${dir}.`,
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await rm(dir, { recursive: true, force: true });
|
|
134
|
+
return [];
|
|
66
135
|
}
|
|
67
136
|
|
|
68
|
-
/** Deletes stale
|
|
69
|
-
|
|
137
|
+
/** Deletes stale instance dirs under the instances root and returns the
|
|
138
|
+
* warnings for directories it deliberately kept. Never throws. */
|
|
139
|
+
export async function sweepStaleInstances(): Promise<string[]> {
|
|
140
|
+
const warnings: string[] = [];
|
|
141
|
+
const root = getInstancesRootDir();
|
|
142
|
+
|
|
70
143
|
let entries: string[];
|
|
71
144
|
try {
|
|
72
|
-
entries = await readdir(
|
|
145
|
+
entries = await readdir(root);
|
|
73
146
|
} catch {
|
|
74
|
-
return; // No
|
|
147
|
+
return warnings; // No instances root yet: nothing to sweep.
|
|
75
148
|
}
|
|
149
|
+
|
|
76
150
|
for (const entry of entries) {
|
|
77
|
-
|
|
78
|
-
|
|
151
|
+
// Only directories this module generated are candidates. Anything else
|
|
152
|
+
// under the root — 0.4.x's per-profile dirs included — is not ours to
|
|
153
|
+
// delete, and is left alone without a warning.
|
|
154
|
+
if (!entry.startsWith(INSTANCE_DIR_PREFIX)) continue;
|
|
155
|
+
const dir = path.join(root, entry);
|
|
79
156
|
try {
|
|
80
|
-
if ((await stat(dir)).isDirectory())
|
|
157
|
+
if (!(await stat(dir)).isDirectory()) continue;
|
|
158
|
+
warnings.push(...(await sweepEntry(dir)));
|
|
81
159
|
} catch {
|
|
82
160
|
// Best-effort: one bad entry must not stop the sweep or the launch.
|
|
83
161
|
}
|
|
84
162
|
}
|
|
163
|
+
|
|
164
|
+
return warnings;
|
|
85
165
|
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* runtime directory (ADR-0005).
|
|
4
4
|
*
|
|
5
5
|
* Two entry points:
|
|
6
|
-
* - `generateRuntimeDir` (launcher):
|
|
7
|
-
*
|
|
8
|
-
* default), derive env.
|
|
6
|
+
* - `generateRuntimeDir` (launcher): create a fresh per-launch runtime dir
|
|
7
|
+
* under the workspace instances root, write the files, mirror the real
|
|
8
|
+
* agent dir as symlinks (trust.json only for default), derive env.
|
|
9
9
|
* - `writeRuntimeFiles` (in-session switch, ticket 05): rewrite
|
|
10
10
|
* settings.json + pi-profile.json inside the EXISTING runtime dir (the
|
|
11
11
|
* running process's PI_CODING_AGENT_DIR cannot move), and transition the
|
|
@@ -99,6 +99,24 @@ export const MANAGED_INSTANCE_FILES = new Set([
|
|
|
99
99
|
"extensions",
|
|
100
100
|
]);
|
|
101
101
|
|
|
102
|
+
/** State directories that Pi and its extensions resolve under the agent dir,
|
|
103
|
+
* and which therefore appear at runtime rather than at install time. They are
|
|
104
|
+
* seeded in the REAL agent dir before mirroring, so the instance gets a
|
|
105
|
+
* symlink instead of a private real directory: runtime-created state then
|
|
106
|
+
* lands where native Pi puts it, and third-party records never embed an
|
|
107
|
+
* instance path (ADR-0010). Adding a name here needs observed evidence that a
|
|
108
|
+
* package creates that directory under the agent dir; anything unlisted shows
|
|
109
|
+
* up as an unrecognized entry in the sweep (src/launcher/runtime-cleanup.ts). */
|
|
110
|
+
const SEEDED_STATE_DIRS = ["sessions", "missions"] as const;
|
|
111
|
+
|
|
112
|
+
/** State FILES Pi creates at runtime (same evidence rule as the dirs). They
|
|
113
|
+
* cannot be created up front — the content is Pi's, not pi-profile's — so the
|
|
114
|
+
* instance gets a symlink into the real agent dir that is deliberately allowed
|
|
115
|
+
* to dangle: Pi sees no file, writes through the link, and the real agent dir
|
|
116
|
+
* gets the file. A real file left here instead would be unrecognized state and
|
|
117
|
+
* would strand credentials in a directory the sweep refuses to delete. */
|
|
118
|
+
const SEEDED_STATE_FILES = ["auth.json", "models-store.json"] as const;
|
|
119
|
+
|
|
102
120
|
/** Resource dirs rooted at the real agent dir, re-included for the default
|
|
103
121
|
* profile because PI_CODING_AGENT_DIR moves the discovery root. */
|
|
104
122
|
const RESOURCE_DIR_KINDS = ["skills", "extensions", "prompts", "themes"] as const;
|
|
@@ -405,7 +423,7 @@ export async function writeRuntimeFiles(
|
|
|
405
423
|
skills: plan.skills.map((skill) => ({ name: skill.name, filePath: skill.filePath })),
|
|
406
424
|
extensions: plan.extensions,
|
|
407
425
|
},
|
|
408
|
-
// Zero-match glob references (ADR-
|
|
426
|
+
// Zero-match glob references (ADR-0009) — surfaced by /profile status
|
|
409
427
|
// so a typo'd glob is visible instead of silently selecting nothing.
|
|
410
428
|
...(plan.unmatched !== undefined ? { unmatched: plan.unmatched } : {}),
|
|
411
429
|
...options.planExtras,
|
|
@@ -494,9 +512,6 @@ export async function syncAgentSymlinks(agentDir: string, runtimeDir: string): P
|
|
|
494
512
|
if (!existsSync(agentDir)) return;
|
|
495
513
|
if (path.resolve(agentDir) === path.resolve(runtimeDir)) return;
|
|
496
514
|
|
|
497
|
-
// Ensure the real sessions directory exists so it is always mirrored
|
|
498
|
-
await mkdir(path.join(agentDir, "sessions"), { recursive: true });
|
|
499
|
-
|
|
500
515
|
// 1. Clean up dangling or obsolete symlinks in runtimeDir
|
|
501
516
|
try {
|
|
502
517
|
const runtimeEntries = await readdir(runtimeDir);
|
|
@@ -517,7 +532,13 @@ export async function syncAgentSymlinks(agentDir: string, runtimeDir: string): P
|
|
|
517
532
|
}
|
|
518
533
|
} catch {}
|
|
519
534
|
|
|
520
|
-
// 2.
|
|
535
|
+
// 2. Seed the state paths this process's Pi will create at runtime, so their
|
|
536
|
+
// writes land in the real agent dir instead of an instance-local copy
|
|
537
|
+
// (ADR-0010). Runs after the cleanup above, which would otherwise remove the
|
|
538
|
+
// deliberately dangling file links.
|
|
539
|
+
await seedRuntimeState(agentDir, runtimeDir);
|
|
540
|
+
|
|
541
|
+
// 3. Mirror files and directories from agentDir to runtimeDir
|
|
521
542
|
try {
|
|
522
543
|
const entries = await readdir(agentDir);
|
|
523
544
|
for (const name of entries) {
|
|
@@ -547,13 +568,45 @@ export async function syncAgentSymlinks(agentDir: string, runtimeDir: string): P
|
|
|
547
568
|
} catch {}
|
|
548
569
|
}
|
|
549
570
|
|
|
571
|
+
/** Ensures the runtime state paths exist (or are linked) in the real agent dir
|
|
572
|
+
* and the instance. Best-effort: a failure here leaves the path unseeded, and
|
|
573
|
+
* the sweep's unrecognized-entry warning names it later. */
|
|
574
|
+
async function seedRuntimeState(agentDir: string, runtimeDir: string): Promise<void> {
|
|
575
|
+
for (const name of SEEDED_STATE_DIRS) {
|
|
576
|
+
try {
|
|
577
|
+
await mkdir(path.join(agentDir, name), { recursive: true });
|
|
578
|
+
} catch {
|
|
579
|
+
// Best-effort: the mirror then simply links nothing for this name.
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
for (const name of SEEDED_STATE_FILES) {
|
|
584
|
+
const linkPath = path.join(runtimeDir, name);
|
|
585
|
+
// A real file here belongs to an earlier run of a different layout, and a
|
|
586
|
+
// link may already point somewhere else: leave both alone rather than
|
|
587
|
+
// replacing state pi-profile cannot attribute.
|
|
588
|
+
if (await existsLexical(linkPath)) continue;
|
|
589
|
+
try {
|
|
590
|
+
await symlink(path.join(agentDir, name), linkPath);
|
|
591
|
+
} catch {
|
|
592
|
+
// Best-effort: Pi then creates the file inside the instance, and the
|
|
593
|
+
// sweep keeps that directory instead of deleting it silently.
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
550
598
|
export async function generateRuntimeDir(
|
|
551
599
|
plan: ActivationPlan,
|
|
552
600
|
options: GenerateOptions,
|
|
553
601
|
): Promise<GeneratedRuntime> {
|
|
554
602
|
const { agentDir } = options;
|
|
555
|
-
const
|
|
556
|
-
await mkdir(
|
|
603
|
+
const runtimeRoot = getInstancesRootDir();
|
|
604
|
+
await mkdir(runtimeRoot, { recursive: true });
|
|
605
|
+
// One instance per launch, never reused: PI_CODING_AGENT_DIR is frozen for
|
|
606
|
+
// the life of the spawned process, so a stable path cannot follow an
|
|
607
|
+
// in-session switch, and a shared path would make concurrent launches (and
|
|
608
|
+
// their switches) rewrite each other's files (ADR-0010).
|
|
609
|
+
const runtimeDir = await mkdtemp(path.join(runtimeRoot, "launch-"));
|
|
557
610
|
|
|
558
611
|
await writeRuntimeFiles(runtimeDir, plan, options);
|
|
559
612
|
|
|
@@ -48,7 +48,7 @@ export interface LaunchPlanFile {
|
|
|
48
48
|
skills: Array<{ name: string; filePath: string }>;
|
|
49
49
|
extensions: Array<{ id: string; entry: string }>;
|
|
50
50
|
};
|
|
51
|
-
/** Glob references that matched nothing at resolution (ADR-
|
|
51
|
+
/** Glob references that matched nothing at resolution (ADR-0009). */
|
|
52
52
|
unmatched?: string[];
|
|
53
53
|
previousResolved?: {
|
|
54
54
|
skills: string[];
|
package/src/switching/status.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface StatusReport {
|
|
|
36
36
|
mcp: { enabled: string[]; disabled: string[]; missing: string[] };
|
|
37
37
|
/** Glob delta versus the previous activation (prefixed names). */
|
|
38
38
|
delta?: { added: string[]; removed: string[] };
|
|
39
|
-
/** Glob references that matched nothing at resolution (ADR-
|
|
39
|
+
/** Glob references that matched nothing at resolution (ADR-0009). */
|
|
40
40
|
unmatched?: string[];
|
|
41
41
|
conflicts: StatusConflict[];
|
|
42
42
|
}
|