pi-session-mcp 0.13.0 → 0.13.1
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/.pi-session-mcp-cli/config.js +21 -2
- package/.pi-session-mcp-cli/doctor.js +8 -3
- package/CHANGELOG.md +73 -0
- package/CONTRIBUTING.md +8 -1
- package/README.md +4 -2
- package/dist/config.d.ts +11 -1
- package/dist/config.js +21 -2
- package/dist/confined-tools.d.ts +1 -0
- package/dist/confined-tools.js +6 -2
- package/dist/doctor.js +8 -3
- package/dist/main.js +6 -2
- package/dist/sdk-pi-adapter.js +3 -2
- package/dist/setup-cli.d.ts +1 -2
- package/dist/setup-cli.js +0 -3
- package/dist/setup-writers-common.d.ts +78 -0
- package/dist/setup-writers-common.js +177 -0
- package/dist/setup-writers-plan.d.ts +34 -0
- package/dist/setup-writers-plan.js +105 -0
- package/dist/setup-writers-reconcile.d.ts +8 -0
- package/dist/setup-writers-reconcile.js +169 -0
- package/dist/setup-writers.d.ts +6 -30
- package/dist/setup-writers.js +18 -382
- package/dist/wake-hook.d.ts +11 -0
- package/dist/wake-hook.js +51 -8
- package/docs/acceptance.md +130 -0
- package/docs/adr/0009-execution-modes.md +3 -2
- package/docs/adr/0012-leased-writer-pool.md +3 -3
- package/docs/adr/0013-hardened-read-only-profiles.md +6 -4
- package/docs/architecture.md +1 -1
- package/docs/client-setup.md +3 -3
- package/docs/doctor-design.md +1 -1
- package/docs/execution-modes.md +1 -1
- package/docs/roadmap.md +13 -5
- package/docs/session-mcp.md +5 -3
- package/docs/threat-model.md +2 -1
- package/docs/tool-contracts.md +4 -2
- package/package.json +1 -1
- package/skills/pi-session-orchestration/SKILL.md +2 -2
- package/skills/pi-session-orchestration/references/start-errors.md +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ContainerSettings, type WorkspaceModeEntry } from "./execution-modes.js";
|
|
2
|
+
import type { ModeRequest } from "./setup-modes.js";
|
|
3
|
+
import { type SetupOperation, type SetupResult } from "./setup-result.js";
|
|
4
|
+
import type * as lifecycle from "./writer-lifecycle.js";
|
|
5
|
+
import { type WriterPlan, type WriterSetupDependencies } from "./setup-writers-common.js";
|
|
6
|
+
/** Base inference, the settings check and the plans a mode change shows or checks before it writes the modes file. */
|
|
7
|
+
export type SettingsDrift = (name: string, ctx?: lifecycle.WriterContext) => Promise<lifecycle.WriterOutcome | undefined>;
|
|
8
|
+
/** What a writer's configuration is rendered from: the raw configuration file and the skill override directory. */
|
|
9
|
+
export interface RenderInputs {
|
|
10
|
+
readonly rawConfig: unknown;
|
|
11
|
+
readonly overrideDirectory: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function renderInputs(configPath: string): Promise<RenderInputs>;
|
|
14
|
+
/**
|
|
15
|
+
* The one settings check (#134, #163) of the pre-write gate, the dry run and reconciliation: whether a writer's
|
|
16
|
+
* provisioned settings or rendered configuration differ from `settings`. Reconcile passes its fenced context per writer.
|
|
17
|
+
*/
|
|
18
|
+
export declare function settingsDriftCheck(inputs: RenderInputs, ctx: lifecycle.WriterContext, settings: ContainerSettings, deps: WriterSetupDependencies): SettingsDrift;
|
|
19
|
+
/**
|
|
20
|
+
* The request as the modes file should record it: `--base` as provisioning resolves it, so a refused or
|
|
21
|
+
* remote-tracking form never reaches the modes file; without `--base`, the one base the existing writers share, or
|
|
22
|
+
* for a first provisioning the base it will resolve (origin/HEAD, else main), so default starts find writers.
|
|
23
|
+
* Returns a refusal instead when the base cannot be resolved or existing writers disagree.
|
|
24
|
+
*/
|
|
25
|
+
export declare function inferBase(configPath: string, request: ModeRequest, deps: WriterSetupDependencies, containerRuntime: () => Promise<boolean>, failed: (code: string) => SetupResult): Promise<ModeRequest | SetupResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Apply only: the Doctor gate, a fence and the destructive-work refusal for each surplus writer, and the settings
|
|
28
|
+
* check of kept writers before the write, so a kept writer with changed settings refuses the mode change itself and
|
|
29
|
+
* the modes file never records a mode no writer matches (#163). Read-only like the dry run, which fences nothing;
|
|
30
|
+
* reconcile re-checks under its fence. Missing writers have no settings yet. Returns a refusal or `undefined`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function gateBeforeWrite(configPath: string, ctx: lifecycle.WriterContext, entry: WorkspaceModeEntry, planned: WriterPlan, discard: boolean, operation: SetupOperation, deps: WriterSetupDependencies): Promise<SetupResult | undefined>;
|
|
33
|
+
/** The dry run's writer plan: kept writers with the same settings check as apply (#134), then the surplus to remove. */
|
|
34
|
+
export declare function plannedWriters(configPath: string, ctx: lifecycle.WriterContext, proposed: WorkspaceModeEntry, planned: WriterPlan, deps: WriterSetupDependencies): Promise<SetupResult>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { MAX_WRITERS } from "./execution-modes.js";
|
|
5
|
+
import { result } from "./setup-result.js";
|
|
6
|
+
import { skillOverrideDirectory } from "./skill-library.js";
|
|
7
|
+
import { sameWriterLimits, writerLimits } from "./writer-limits.js";
|
|
8
|
+
import { readWriterState, writerName } from "./writer-state.js";
|
|
9
|
+
import { codeFor, context, doctorGate, FENCE_CODES, fenced, modesTarget, preflight, services, targetFor, writerIndex } from "./setup-writers-common.js";
|
|
10
|
+
export async function renderInputs(configPath) {
|
|
11
|
+
return { rawConfig: JSON.parse(await readFile(configPath, "utf8")), overrideDirectory: skillOverrideDirectory(configPath) };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The one settings check (#134, #163) of the pre-write gate, the dry run and reconciliation: whether a writer's
|
|
15
|
+
* provisioned settings or rendered configuration differ from `settings`. Reconcile passes its fenced context per writer.
|
|
16
|
+
*/
|
|
17
|
+
export function settingsDriftCheck(inputs, ctx, settings, deps) {
|
|
18
|
+
const { life } = services(deps);
|
|
19
|
+
return (name, scoped = ctx) => life.writerSettingsDrift(scoped, { index: writerIndex(name), settings, ...inputs });
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The request as the modes file should record it: `--base` as provisioning resolves it, so a refused or
|
|
23
|
+
* remote-tracking form never reaches the modes file; without `--base`, the one base the existing writers share, or
|
|
24
|
+
* for a first provisioning the base it will resolve (origin/HEAD, else main), so default starts find writers.
|
|
25
|
+
* Returns a refusal instead when the base cannot be resolved or existing writers disagree.
|
|
26
|
+
*/
|
|
27
|
+
export async function inferBase(configPath, request, deps, containerRuntime, failed) {
|
|
28
|
+
const operation = request.apply ? "apply" : "dry-run";
|
|
29
|
+
if (request.baseRef !== undefined) {
|
|
30
|
+
try {
|
|
31
|
+
return { ...request, baseRef: await services(deps).life.baseBranchName(await context(request.alias, deps), request.baseRef) };
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return failed(codeFor(error));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!(request.mode === "container" || request.mode === undefined && await containerRuntime()))
|
|
38
|
+
return request;
|
|
39
|
+
const config = await loadConfig(configPath);
|
|
40
|
+
if (config.workspaceModes.get(request.alias)?.container?.base)
|
|
41
|
+
return request;
|
|
42
|
+
const bases = new Set();
|
|
43
|
+
for (let index = 1; index <= MAX_WRITERS; index++) {
|
|
44
|
+
const state = await readWriterState(deps.home ?? homedir(), writerName(request.alias, index));
|
|
45
|
+
if (state)
|
|
46
|
+
bases.add(state.base.ref);
|
|
47
|
+
}
|
|
48
|
+
if (bases.size > 1)
|
|
49
|
+
return result(operation, [{ target: modesTarget(request.alias), status: "failed", code: "workspace_base_ambiguous" }]);
|
|
50
|
+
if (bases.size === 1)
|
|
51
|
+
return { ...request, baseRef: [...bases][0].replace(/^refs\/heads\//, "") };
|
|
52
|
+
if (request.mode !== "container")
|
|
53
|
+
return request;
|
|
54
|
+
try {
|
|
55
|
+
return { ...request, baseRef: await services(deps).life.baseBranchName(await context(request.alias, deps)) };
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return failed(codeFor(error));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Apply only: the Doctor gate, a fence and the destructive-work refusal for each surplus writer, and the settings
|
|
63
|
+
* check of kept writers before the write, so a kept writer with changed settings refuses the mode change itself and
|
|
64
|
+
* the modes file never records a mode no writer matches (#163). Read-only like the dry run, which fences nothing;
|
|
65
|
+
* reconcile re-checks under its fence. Missing writers have no settings yet. Returns a refusal or `undefined`.
|
|
66
|
+
*/
|
|
67
|
+
export async function gateBeforeWrite(configPath, ctx, entry, planned, discard, operation, deps) {
|
|
68
|
+
await doctorGate(ctx, deps);
|
|
69
|
+
for (const name of planned.surplus) {
|
|
70
|
+
try {
|
|
71
|
+
await fenced(ctx, name, deps, (leased) => preflight(leased, [name], discard, deps));
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// Refuse the mode change, but name the writer that could not be fenced.
|
|
75
|
+
if (FENCE_CODES.includes(codeFor(error)))
|
|
76
|
+
return result(operation, [{ target: targetFor(ctx.alias, name), status: "failed", code: codeFor(error) }]);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (entry.mode !== "container")
|
|
81
|
+
return undefined;
|
|
82
|
+
const drift = settingsDriftCheck(await renderInputs(configPath), ctx, entry.container, deps);
|
|
83
|
+
const drifted = [];
|
|
84
|
+
for (const name of planned.keep) {
|
|
85
|
+
if (planned.missing.includes(name))
|
|
86
|
+
continue;
|
|
87
|
+
const outcome = await drift(name);
|
|
88
|
+
if (outcome)
|
|
89
|
+
drifted.push({ target: targetFor(ctx.alias, name), ...outcome });
|
|
90
|
+
}
|
|
91
|
+
return drifted.length ? result(operation, drifted) : undefined;
|
|
92
|
+
}
|
|
93
|
+
/** The dry run's writer plan: kept writers with the same settings check as apply (#134), then the surplus to remove. */
|
|
94
|
+
export async function plannedWriters(configPath, ctx, proposed, planned, deps) {
|
|
95
|
+
const drift = settingsDriftCheck(await renderInputs(configPath), ctx, proposed.container, deps);
|
|
96
|
+
const kept = [];
|
|
97
|
+
for (const name of planned.keep) {
|
|
98
|
+
const outcome = await drift(name);
|
|
99
|
+
const state = await readWriterState(ctx.home, name);
|
|
100
|
+
const updatingLimits = state && state.phase !== "provisioning" && !sameWriterLimits(state.limits, writerLimits(proposed.container));
|
|
101
|
+
kept.push({ target: targetFor(ctx.alias, name), ...(outcome ?? { status: "planned", code: updatingLimits ? "writer_limits_update_planned" : "writer_planned" }) });
|
|
102
|
+
}
|
|
103
|
+
const refused = kept.some((finding) => finding.status === "failed" || finding.status === "divergent");
|
|
104
|
+
return result("dry-run", [...kept, ...planned.surplus.map((name) => ({ target: targetFor(ctx.alias, name), status: "planned", code: "writer_remove_planned" }))], refused ? "failed" : "planned");
|
|
105
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SetupResult } from "./setup-result.js";
|
|
2
|
+
import { type WriterSetupDependencies } from "./setup-writers-common.js";
|
|
3
|
+
/** Writer reconciliation after a mode change is written: provisioning, migration to the relay launch, acceptance and removal. */
|
|
4
|
+
export type ReconcileRequest = {
|
|
5
|
+
discard: boolean;
|
|
6
|
+
baseRef?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function reconcileUnlocked(configPath: string, alias: string, request: ReconcileRequest, deps: WriterSetupDependencies): Promise<SetupResult>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { result } from "./setup-result.js";
|
|
6
|
+
import { unregisterWriter } from "./client-adapters/writer-registration.js";
|
|
7
|
+
import { sameWriterLimits, writerLimits } from "./writer-limits.js";
|
|
8
|
+
import { renderWrapper, wrapperDigest } from "./writer-wrapper.js";
|
|
9
|
+
import { readWriterState, writeWriterState, writerPaths } from "./writer-state.js";
|
|
10
|
+
import { codeFor, context, doctorGate, FENCE_CODES, fenced, INFRASTRUCTURE_CODE, intent, plan, preflight, remove, services, targetFor, writerHook, writerIndex } from "./setup-writers-common.js";
|
|
11
|
+
import { renderInputs, settingsDriftCheck } from "./setup-writers-plan.js";
|
|
12
|
+
/**
|
|
13
|
+
* Moves a kept writer to the relay launch: its owned registrations are removed (both clients are decided before
|
|
14
|
+
* anything is removed, so a divergent registration of one client never costs the writer its owned registration of the
|
|
15
|
+
* other; a client that is not installed has nothing to keep), and so is its wake hook; the wrapper is rebound to this
|
|
16
|
+
* release. A changed launch or wrapper leaves the writer unaccepted. Returns a refusal outcome or `undefined`.
|
|
17
|
+
*/
|
|
18
|
+
async function migrateToRelay(ctx, name, deps, findings) {
|
|
19
|
+
const { ownership, adapter } = services(deps);
|
|
20
|
+
let divergent = false;
|
|
21
|
+
const owned = [];
|
|
22
|
+
for (const client of ["claude-code", "codex"]) {
|
|
23
|
+
const expected = intent(ctx, name, client);
|
|
24
|
+
const record = await ownership.get({ client, scope: client === "codex" ? "user" : "local", alias: expected.name });
|
|
25
|
+
const current = await adapter.inspect(expected);
|
|
26
|
+
if (record ? current === "divergent" : current === "equivalent" || current === "divergent")
|
|
27
|
+
divergent = true;
|
|
28
|
+
else if (record)
|
|
29
|
+
owned.push(expected);
|
|
30
|
+
}
|
|
31
|
+
if (!divergent) {
|
|
32
|
+
for (const expected of owned) {
|
|
33
|
+
try {
|
|
34
|
+
await unregisterWriter(ownership, adapter, expected);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (codeFor(error) !== "ownership_diverged")
|
|
38
|
+
throw error;
|
|
39
|
+
divergent = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const hook = await writerHook(ctx, name, deps, "remove", false);
|
|
44
|
+
if (hook)
|
|
45
|
+
findings.push({ target: targetFor(ctx.alias, name), ...hook });
|
|
46
|
+
const state = await readWriterState(ctx.home, name);
|
|
47
|
+
if (!state)
|
|
48
|
+
throw new Error("writer_state_invalid");
|
|
49
|
+
if (divergent) {
|
|
50
|
+
if (state.launch !== "legacy")
|
|
51
|
+
await writeWriterState(ctx.home, name, { ...state, launch: "legacy" });
|
|
52
|
+
return { status: "failed", code: "registration_divergent" };
|
|
53
|
+
}
|
|
54
|
+
const paths = writerPaths(ctx.home, name), wrapper = renderWrapper(ctx.nodePath, ctx.releaseRoot, name);
|
|
55
|
+
if (!(await lstat(paths.wrapper).catch(() => undefined))?.isFile())
|
|
56
|
+
throw new Error("writer_state_invalid");
|
|
57
|
+
const wrapperChanged = wrapperDigest(await readFile(paths.wrapper, "utf8")) !== wrapperDigest(wrapper);
|
|
58
|
+
if (wrapperChanged) {
|
|
59
|
+
const temporary = join(paths.root, `.wrapper-${randomUUID()}.tmp`);
|
|
60
|
+
try {
|
|
61
|
+
await writeFile(temporary, wrapper, { flag: "wx", mode: 0o700 });
|
|
62
|
+
await rename(temporary, paths.wrapper);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await rm(temporary, { force: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// A launch change also needs fresh provider-free acceptance, even when wrapper bytes match.
|
|
69
|
+
if (state.launch !== "relay" || wrapperChanged) {
|
|
70
|
+
const { acceptance: _oldAcceptance, ...unaccepted } = state;
|
|
71
|
+
await writeWriterState(ctx.home, name, { ...unaccepted, launch: "relay", phase: "not_ready" });
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
export async function reconcileUnlocked(configPath, alias, request, deps) {
|
|
76
|
+
const findings = [];
|
|
77
|
+
const ctx = await context(alias, { ...deps, configPath });
|
|
78
|
+
const config = await loadConfig(configPath), entry = config.workspaceModes.get(alias);
|
|
79
|
+
const planned = await plan(ctx, entry), { life, accept } = services(deps);
|
|
80
|
+
const add = (name, outcome) => { findings.push({ target: targetFor(alias, name), ...outcome }); };
|
|
81
|
+
await doctorGate(ctx, deps);
|
|
82
|
+
// A surplus writer that cannot be fenced is reported on its own target and left in place; the others proceed.
|
|
83
|
+
const unfenced = new Set();
|
|
84
|
+
for (const name of planned.surplus) {
|
|
85
|
+
try {
|
|
86
|
+
await fenced(ctx, name, deps, (leased) => preflight(leased, [name], request.discard, deps));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (!FENCE_CODES.includes(codeFor(error)))
|
|
90
|
+
throw error;
|
|
91
|
+
unfenced.add(name);
|
|
92
|
+
add(name, { status: "failed", code: codeFor(error) });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const inputs = await renderInputs(configPath), drift = settingsDriftCheck(inputs, ctx, entry.container, deps);
|
|
96
|
+
// Each writer's reconciliation, including first provisioning and acceptance, owns one fence.
|
|
97
|
+
// A writer added without --base joins the recorded default base, so default starts find it. A recorded base that is
|
|
98
|
+
// the repository default stays unset: provisioning then resolves origin/HEAD, as the first provisioning did.
|
|
99
|
+
let baseRef = request.baseRef;
|
|
100
|
+
const recordedBase = entry.container?.base;
|
|
101
|
+
if (baseRef === undefined && recordedBase !== undefined && recordedBase !== await life.baseBranchName(ctx).catch(() => undefined))
|
|
102
|
+
baseRef = recordedBase;
|
|
103
|
+
for (const name of planned.keep) {
|
|
104
|
+
try {
|
|
105
|
+
await fenced(ctx, name, deps, async (ctx) => {
|
|
106
|
+
if (planned.missing.includes(name)) {
|
|
107
|
+
const outcome = await life.provisionWriter(ctx, { index: writerIndex(name), settings: entry.container, ...inputs, ...(baseRef ? { baseRef } : {}) });
|
|
108
|
+
add(name, outcome);
|
|
109
|
+
if (outcome.status === "failed")
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Settings first: a drifted writer is left exactly as it is, neither migrated nor accepted again (#134).
|
|
113
|
+
if (!planned.missing.includes(name)) {
|
|
114
|
+
const drifted = await drift(name, ctx);
|
|
115
|
+
if (drifted) {
|
|
116
|
+
add(name, drifted);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const state = await readWriterState(ctx.home, name);
|
|
120
|
+
if (!state)
|
|
121
|
+
throw new Error("writer_state_invalid");
|
|
122
|
+
const limits = writerLimits(entry.container);
|
|
123
|
+
if (!sameWriterLimits(state.limits, limits)) {
|
|
124
|
+
const { limits: _previous, ...rest } = state;
|
|
125
|
+
await writeWriterState(ctx.home, name, { ...rest, ...(limits ? { limits } : {}) });
|
|
126
|
+
add(name, { status: "ok", code: "writer_limits_updated" });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const refused = await migrateToRelay(ctx, name, deps, findings);
|
|
130
|
+
if (refused) {
|
|
131
|
+
add(name, refused);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// A ready phase with a stale binding (package upgrade, new image, Podman version) is accepted again (#130).
|
|
135
|
+
const current = await accept.acceptanceCurrent(ctx, name);
|
|
136
|
+
if (current === "stale") {
|
|
137
|
+
const outcome = await accept.acceptWriter(ctx, name);
|
|
138
|
+
add(name, outcome);
|
|
139
|
+
if (outcome.status === "failed")
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
else if (current === "unknown") {
|
|
143
|
+
add(name, { status: "failed", code: "podman_unavailable" });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// A broken writer is reported on its own target and the others are still reconciled.
|
|
150
|
+
if (INFRASTRUCTURE_CODE.test(codeFor(error)))
|
|
151
|
+
throw error;
|
|
152
|
+
add(name, { status: "failed", code: codeFor(error) });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const name of planned.surplus) {
|
|
156
|
+
if (unfenced.has(name))
|
|
157
|
+
continue;
|
|
158
|
+
const extras = [];
|
|
159
|
+
try {
|
|
160
|
+
add(name, await fenced(ctx, name, deps, (leased) => remove(leased, name, request.discard, deps, extras)));
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
add(name, { status: "failed", code: codeFor(error) });
|
|
164
|
+
}
|
|
165
|
+
for (const extra of extras)
|
|
166
|
+
findings.push({ target: targetFor(alias, name), ...extra });
|
|
167
|
+
}
|
|
168
|
+
return result("apply", findings);
|
|
169
|
+
}
|
package/dist/setup-writers.d.ts
CHANGED
|
@@ -1,41 +1,17 @@
|
|
|
1
|
-
import { runDoctor } from "./doctor.js";
|
|
2
1
|
import { type ModeRequest } from "./setup-modes.js";
|
|
3
|
-
import { acquireOverrideLock } from "./override-lock.js";
|
|
4
|
-
import { createCommandMcpStdioLauncher } from "./setup-process.js";
|
|
5
|
-
import { type OwnershipStore } from "./setup-ownership.js";
|
|
6
2
|
import { type SetupResult } from "./setup-result.js";
|
|
7
|
-
import { type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export declare const SETUP_LEASE_WAIT_MS = 10000;
|
|
3
|
+
import { type WriterSetupDependencies } from "./setup-writers-common.js";
|
|
4
|
+
/**
|
|
5
|
+
* Writer setup entry points: a repository's mode change with its writers (planning and base inference in
|
|
6
|
+
* `setup-writers-plan.ts`, reconciliation in `setup-writers-reconcile.ts`), single-writer operations and the isolation control.
|
|
7
|
+
*/
|
|
8
|
+
export { SETUP_LEASE_WAIT_MS, type WriterSetupDependencies } from "./setup-writers-common.js";
|
|
14
9
|
export type WriterOperation = "collect" | "reset" | "recreate" | "finish" | "remove" | "verify";
|
|
15
10
|
export interface WriterOptions {
|
|
16
11
|
apply: boolean;
|
|
17
12
|
discard: boolean;
|
|
18
13
|
baseRef?: string;
|
|
19
14
|
}
|
|
20
|
-
export interface WriterSetupDependencies {
|
|
21
|
-
configPath: string;
|
|
22
|
-
releaseRoot: string;
|
|
23
|
-
home?: string;
|
|
24
|
-
platform?: NodeJS.Platform;
|
|
25
|
-
signal?: AbortSignal;
|
|
26
|
-
runner?: lifecycle.WriterContext["runner"];
|
|
27
|
-
lifecycle?: typeof lifecycle;
|
|
28
|
-
acceptance?: typeof acceptance;
|
|
29
|
-
ownership?: OwnershipStore;
|
|
30
|
-
adapter?: WriterRegistrationAdapter;
|
|
31
|
-
doctor?: typeof runDoctor;
|
|
32
|
-
verify?: typeof verifyMcpCapabilities;
|
|
33
|
-
launch?: typeof createCommandMcpStdioLauncher;
|
|
34
|
-
lock?: typeof acquireOverrideLock;
|
|
35
|
-
lease?: typeof acquireWriterLease;
|
|
36
|
-
ensureAbsent?: typeof ensureWriterAbsent;
|
|
37
|
-
containerRuntime?: () => Promise<boolean>;
|
|
38
|
-
}
|
|
39
15
|
/** Serializes planning, destructive-work refusal, mode commit and writer reconciliation. */
|
|
40
16
|
export declare function changeWriterMode(configPath: string, request: ModeRequest, deps: WriterSetupDependencies): Promise<SetupResult>;
|
|
41
17
|
export declare function runWriterOperation(name: string, operation: WriterOperation, opts: WriterOptions, deps: WriterSetupDependencies): Promise<SetupResult>;
|