@topolo/cli 0.13.8 → 0.13.9

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.
@@ -24,7 +24,8 @@ async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoi
24
24
  if (plan.noEffects && (plan.mode !== "recover" || previous?.effectAttempted)) throw new Error("No-effect recovery is not qualified.");
25
25
  if (apply) manager[fenceKey] = { operationId, running: true, effectAttempted: false };
26
26
  try {
27
- verifyContext();
27
+ const incremental = (plan.mode ?? "configure") === "configure";
28
+ verifyContext(incremental ? [] : void 0);
28
29
  const checkDeadline = () => {
29
30
  if (Date.now() >= plan.deadline) throw new Error("Native operation deadline expired; reconcile journal before retry.");
30
31
  };
@@ -64,18 +65,28 @@ async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoi
64
65
  const result = [];
65
66
  for (const member of members) {
66
67
  checkDeadline();
67
- const session = await manager.getSession(member.sessionId);
68
+ let session = await manager.getSession(member.sessionId);
68
69
  if (!session || session.sessionId !== member.sessionId || session.cwd !== member.cwd || session.title !== member.title || session.isArchived && !["switch", "recover"].includes(plan.mode ?? "configure"))
69
70
  throw new Error("Owned session identity or active membership changed: " + member.key);
70
- if (manager.hasLosableWork(member.sessionId) || session.isRunning || session.starting || session.pendingToolPermissions?.length)
71
+ let descriptor;
72
+ if (!incremental && (manager.hasLosableWork(member.sessionId) || session.isRunning || session.starting || session.pendingToolPermissions?.length))
71
73
  throw new Error("Session has live or pending work; no changes permitted: " + member.key);
72
74
  if (!plan.noEffects) {
73
- const descriptor = await nativeDescriptor(member);
74
- if (manager.hasLosableWork(member.sessionId) || descriptor.isRunning || descriptor.starting || descriptor.pendingToolPermissions?.length)
75
+ descriptor = await nativeDescriptor(member);
76
+ session = await manager.getSession(member.sessionId);
77
+ checkDeadline();
78
+ if (!session || session.sessionId !== member.sessionId || session.cwd !== member.cwd || session.title !== member.title || (session.isArchived || descriptor.isArchived) && incremental) throw new Error("Owned session identity or active membership changed: " + member.key);
79
+ if (!incremental && (manager.hasLosableWork(member.sessionId) || session.isRunning || session.starting || session.pendingToolPermissions?.length || descriptor.isRunning || descriptor.starting || descriptor.pendingToolPermissions?.length))
75
80
  throw new Error("Session has live or pending work; no changes permitted: " + member.key);
76
81
  if (descriptor.backend?.kind !== "local" || descriptor.backend.remoteTarget || descriptor.worktreePath || descriptor.worktreeLazy || descriptor.spawnedFrom)
77
82
  throw new Error("Incoming session is not a qualified local role.");
78
83
  }
84
+ const busyReasons = [
85
+ ...manager.hasLosableWork(member.sessionId) ? ["losable"] : [],
86
+ ...session.isRunning || descriptor?.isRunning ? ["running"] : [],
87
+ ...session.starting || descriptor?.starting ? ["starting"] : [],
88
+ ...session.pendingToolPermissions?.length || descriptor?.pendingToolPermissions?.length ? ["pending-permissions"] : []
89
+ ];
79
90
  result.push({
80
91
  key: member.key,
81
92
  sessionId: session.sessionId,
@@ -86,7 +97,8 @@ async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoi
86
97
  model: session.model ?? null,
87
98
  effort: session.effort ?? null,
88
99
  permissionMode: session.permissionMode ?? null,
89
- archived: session.isArchived === true
100
+ archived: session.isArchived === true,
101
+ ...incremental ? { busy: busyReasons.length > 0, busyReasons } : {}
90
102
  });
91
103
  }
92
104
  return result;
@@ -118,6 +130,124 @@ async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoi
118
130
  const outgoingBefore = await outgoingSnapshot();
119
131
  const sections = await command({ kind: "list_sections" });
120
132
  checkpoint({ kind: "baseline", before, outgoingBefore, sections: sections.sections, at: Date.now() });
133
+ if (incremental) {
134
+ const settings = ["group", "pinned", "model", "effort", "permissionMode"];
135
+ const idleBefore = plan.members.filter((_, i) => !before[i].busy);
136
+ verifyContext([]);
137
+ if (!apply) return {
138
+ state: "preflight",
139
+ before,
140
+ outgoingBefore,
141
+ sections: sections.sections,
142
+ availableModels: models,
143
+ applied: false,
144
+ contextVerifiedSessionIds: idleBefore.map((m) => m.sessionId)
145
+ };
146
+ const appliedFields = [];
147
+ const deferredReasons = /* @__PURE__ */ new Map();
148
+ const differences = (member, row) => settings.filter((field) => member[field] !== row[field]);
149
+ for (const member of plan.members) {
150
+ const initial = (await snapshot([member]))[0];
151
+ if (initial.busy) {
152
+ deferredReasons.set(member.sessionId, initial.busyReasons);
153
+ continue;
154
+ }
155
+ for (const setting of differences(member, initial)) {
156
+ let row = (await snapshot([member]))[0];
157
+ if (row.busy) {
158
+ deferredReasons.set(member.sessionId, row.busyReasons);
159
+ break;
160
+ }
161
+ if (row[setting] === member[setting]) continue;
162
+ verifyContext([member]);
163
+ if (setting === "group" && member.group !== null) {
164
+ let matching = manager.listCustomGroups()?.filter((g) => g.name === member.group);
165
+ if (!matching || matching.length > 1) throw new Error("Ambiguous native sidebar group: " + member.group);
166
+ if (!matching.length) {
167
+ row = (await snapshot([member]))[0];
168
+ if (row.busy) {
169
+ deferredReasons.set(member.sessionId, row.busyReasons);
170
+ break;
171
+ }
172
+ verifyContext([member]);
173
+ checkDeadline();
174
+ checkpoint({ kind: "attempt", operation: "create_group", key: member.key, name: member.group, at: Date.now() });
175
+ manager[fenceKey].effectAttempted = true;
176
+ const created = await command({ kind: "create_group", name: member.group });
177
+ checkpoint({ kind: "acknowledged", operation: "create_group", key: member.key, name: member.group, at: Date.now() });
178
+ matching = manager.listCustomGroups()?.filter((g) => g.name === member.group);
179
+ if (!created.groupId) throw new Error("Native create_group did not return an identity.");
180
+ if (matching?.length !== 1 || matching[0].id !== created.groupId)
181
+ throw new Error("Native group read-back mismatch.");
182
+ verifyContext([member]);
183
+ appliedFields.push({ key: member.key, operation: "create_group", name: member.group });
184
+ }
185
+ }
186
+ row = (await snapshot([member]))[0];
187
+ if (row.busy) {
188
+ deferredReasons.set(member.sessionId, row.busyReasons);
189
+ break;
190
+ }
191
+ if (row[setting] === member[setting]) continue;
192
+ verifyContext([member]);
193
+ checkDeadline();
194
+ if (setting === "model" && !manager.getAvailableCodeModels().includes(member.model))
195
+ throw new Error("Requested model is absent from the native account catalog: " + member.model);
196
+ const operation = setting === "group" ? "move_sessions" : setting === "pinned" ? "set_pinned" : setting === "model" ? "set_model" : setting === "effort" ? "set_effort" : "set_permission_mode";
197
+ checkpoint({ kind: "attempt", operation, setting, key: member.key, sessionId: member.sessionId, at: Date.now() });
198
+ manager[fenceKey].effectAttempted = true;
199
+ if (setting === "group") {
200
+ const matching = member.group === null ? [] : manager.listCustomGroups()?.filter((g) => g.name === member.group);
201
+ if (!matching || member.group !== null && matching.length !== 1) throw new Error("Native group identity changed.");
202
+ await command({
203
+ kind: "move_sessions",
204
+ sessionIds: [member.sessionId],
205
+ ...member.group === null ? {} : { groupId: matching[0].id }
206
+ });
207
+ } else if (setting === "pinned") await command({ kind: "set_pinned", sessionId: member.sessionId, pinned: member.pinned });
208
+ else if (setting === "model") await manager.setModel(member.sessionId, member.model);
209
+ else if (setting === "effort") await manager.setEffort(member.sessionId, member.effort);
210
+ else if (!await manager.setPermissionMode(member.sessionId, member.permissionMode, "picker"))
211
+ throw new Error("Native permission mode was refused: " + member.key);
212
+ checkpoint({ kind: "acknowledged", operation, setting, key: member.key, sessionId: member.sessionId, at: Date.now() });
213
+ const actual = (await snapshot([member]))[0];
214
+ if (actual[setting] !== member[setting]) throw new Error("Native read-back mismatch: " + member.key + " / " + setting);
215
+ verifyContext([member]);
216
+ appliedFields.push({ key: member.key, sessionId: member.sessionId, setting, value: member[setting] });
217
+ checkpoint({ kind: "verified-field", operation, setting, key: member.key, sessionId: member.sessionId, at: Date.now() });
218
+ }
219
+ }
220
+ const after2 = await snapshot();
221
+ const idleAfter = plan.members.filter((member, i) => !after2[i].busy && !deferredReasons.has(member.sessionId));
222
+ verifyContext(idleAfter);
223
+ const deferred = plan.members.flatMap((member, i) => {
224
+ const row = after2[i], fields = differences(member, row);
225
+ if (!fields.length) return [];
226
+ const reasons = deferredReasons.get(member.sessionId) ?? row.busyReasons;
227
+ if (!reasons?.length) throw new Error("Native read-back mismatch: " + member.key + " / " + fields.join(","));
228
+ return [{ key: member.key, sessionId: member.sessionId, fields, reasons }];
229
+ });
230
+ const attestedBusyKeys = after2.filter((row) => row.busy && !deferred.some((d) => d.sessionId === row.sessionId)).map((row) => row.key);
231
+ checkpoint({ kind: deferred.length ? "partial-verified" : "verified", after: after2, appliedFields, deferred, attestedBusyKeys, at: Date.now() });
232
+ delete manager[fenceKey];
233
+ return {
234
+ state: deferred.length ? "native-settings-partial" : "native-settings-verified",
235
+ before,
236
+ after: after2,
237
+ outgoingAfter: [],
238
+ verifiedAgents: after2.length - deferred.length,
239
+ complete: deferred.length === 0,
240
+ applied: appliedFields.length > 0,
241
+ appliedFields,
242
+ deferred,
243
+ attestedBusyKeys,
244
+ contextVerifiedSessionIds: idleAfter.map((m) => m.sessionId),
245
+ busyObservations: [...deferredReasons].map(([sessionId, reasons]) => ({ key: plan.members.find((m) => m.sessionId === sessionId).key, sessionId, reasons })),
246
+ modelInferenceVerified: false,
247
+ readOnlyConfinementVerified: false,
248
+ fullPackSwitchSupported: false
249
+ };
250
+ }
121
251
  if (plan.mode === "recover") {
122
252
  if (plan.noEffects) {
123
253
  verifyContext();
@@ -672,6 +802,7 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
672
802
  if (!records.length || records.length !== policy.agents.length) throw new Error("Reviewed policy must cover the complete installed roster.");
673
803
  const members = [];
674
804
  const context = {};
805
+ const roleContextFiles = {};
675
806
  const contextInventories = {};
676
807
  const captureMarkdown = (cwd) => {
677
808
  contextInventories[cwd] = snapshotClaudeMarkdown(cwd, filesystem, cryptography, paths);
@@ -690,6 +821,7 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
690
821
  const transcript = realpathSync(record.transcript);
691
822
  context[charter] = createHash("sha256").update(readFileSync(charter)).digest("hex");
692
823
  context[transcript] = createHash("sha256").update(readFileSync(transcript)).digest("hex");
824
+ roleContextFiles[record.desktopSessionId] = [charter, transcript];
693
825
  members.push({
694
826
  key: record.key,
695
827
  sessionId: record.desktopSessionId,
@@ -744,6 +876,8 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
744
876
  outgoing,
745
877
  context,
746
878
  contextInventories,
879
+ roleContextFiles,
880
+ globalContextFiles: [resolve(root, "claude/state.json"), realpathSync(policyPath)],
747
881
  approvalRoutes,
748
882
  installationConsent: policy.installationConsent,
749
883
  libraryRoot: root,
@@ -752,6 +886,27 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
752
886
  activeBefore: active
753
887
  };
754
888
  }
889
+ function verifyClaudePlanContext(plan, members, fs, crypto, path, snapshotMarkdown) {
890
+ if (!plan.contextInventories || !Object.keys(plan.contextInventories).length) throw new Error("Complete context inventory proof is unavailable.");
891
+ if (members && (!plan.roleContextFiles || !plan.globalContextFiles?.length)) throw new Error("Scoped role context proof is unavailable.");
892
+ const selected = members ?? plan.members;
893
+ if (new Set(selected.map((m) => m.sessionId)).size !== selected.length || selected.some((m) => !plan.members.some((p) => p.sessionId === m.sessionId && p.key === m.key && p.cwd === m.cwd && p.title === m.title)))
894
+ throw new Error("Scoped context identity differs from the complete roster.");
895
+ const roots = members ? selected.map((m) => m.cwd) : Object.keys(plan.contextInventories);
896
+ for (const root of roots) {
897
+ const inventory = plan.contextInventories[root];
898
+ if (!inventory || JSON.stringify(snapshotMarkdown(root, fs, crypto, path)) !== JSON.stringify(inventory))
899
+ throw new Error("Context or transcript changed during configuration: Markdown inventory " + root);
900
+ }
901
+ const files = members ? [...plan.globalContextFiles, ...selected.flatMap((m) => {
902
+ const files2 = plan.roleContextFiles[m.sessionId];
903
+ if (!files2?.length) throw new Error("Scoped role context proof is unavailable.");
904
+ return files2;
905
+ })] : Object.keys(plan.context);
906
+ for (const file of files)
907
+ if (!plan.context[file] || crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") !== plan.context[file])
908
+ throw new Error("Context or transcript changed during configuration: " + file);
909
+ }
755
910
  function qualifyPermissionConsent(policy, key, records, full) {
756
911
  const consent = policy.installationConsent;
757
912
  const intakeKey = consent?.intakeRoutes?.[key];
@@ -784,15 +939,9 @@ function nativePackExpression(pid, plan, journal, apply) {
784
939
  const context = ${JSON.stringify(plan.context)};
785
940
  const inventories = ${JSON.stringify(plan.contextInventories)};
786
941
  const snapshotMarkdown = (${snapshotClaudeMarkdown.toString()});
787
- const verifyContext = () => {
788
- if (!inventories || !Object.keys(inventories).length) throw new Error('Complete context inventory proof is unavailable.');
789
- for (const [root, inventory] of Object.entries(inventories))
790
- if (JSON.stringify(snapshotMarkdown(root, fs, crypto, path)) !== JSON.stringify(inventory))
791
- throw new Error('Context or transcript changed during configuration: Markdown inventory ' + root);
792
- for (const [file, hash] of Object.entries(context))
793
- if (crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') !== hash)
794
- throw new Error('Context or transcript changed during configuration: ' + file);
795
- };
942
+ const contextPlan = {context, contextInventories: inventories, members: ${JSON.stringify(plan.members)},
943
+ roleContextFiles: ${JSON.stringify(plan.roleContextFiles)}, globalContextFiles: ${JSON.stringify(plan.globalContextFiles)}};
944
+ const verifyContext = members => (${verifyClaudePlanContext.toString()})(contextPlan, members, fs, crypto, path, snapshotMarkdown);
796
945
  const result = await (${nativeClaudePackOperation.toString()})(manager,
797
946
  value => transport.P(value, transport.M),
798
947
  { members: ${JSON.stringify(plan.members)}, outgoing: ${JSON.stringify(plan.outgoing)},
@@ -815,15 +964,61 @@ function nativePackExpression(pid, plan, journal, apply) {
815
964
  row => checkpoint({kind: 'approval', ...row}),
816
965
  fs.readFileSync(expected.journal, 'utf8').split('\\n').filter(Boolean).map(line => JSON.parse(line)));
817
966
  if (expected.apply) {
818
- result.approvalRouting = await invoke({kind: 'install'});
819
967
  const control = ${JSON.stringify(plan.control)};
820
968
  if (!control) throw new Error('Owned native control setup is unavailable.');
821
- const stat = fs.lstatSync(control.secretPath);
822
- if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid() || (stat.mode & 63)) throw new Error('Unowned native control secret.');
823
- const secret = JSON.parse(fs.readFileSync(control.secretPath, 'utf8'));
824
- result.approvalRouting.control = await (${nativeClaudeApprovalControl.toString()})(manager,
825
- {socket: control.socket, pid: expected.pid, identity: fingerprint(routes)}, secret.token,
826
- load('node:net'), fs, invoke, qualifyNative, checkpoint);
969
+ const identity = fingerprint(routes);
970
+ const current = manager[Symbol.for('topolo.agent-pack.approval-control.v1')];
971
+ const listener = manager[Symbol.for('topolo.agent-pack.approvals.v1')];
972
+ const incremental = ${JSON.stringify(plan.mode === "configure" && !plan.recovery)};
973
+ const unchanged = listener?.identity === identity && listener.listener && !listener.auditFailed &&
974
+ current?.descriptor.pid === expected.pid && current.descriptor.identity === identity &&
975
+ current.descriptor.socket === control.socket && current.server?.listening === true && !current.failure;
976
+ if (!incremental) {
977
+ result.approvalRouting = await invoke({kind: 'install'});
978
+ const stat = fs.lstatSync(control.secretPath);
979
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid() || (stat.mode & 63)) throw new Error('Unowned native control secret.');
980
+ const secret = JSON.parse(fs.readFileSync(control.secretPath, 'utf8'));
981
+ result.approvalRouting.control = await (${nativeClaudeApprovalControl.toString()})(manager,
982
+ {socket: control.socket, pid: expected.pid, identity}, secret.token,
983
+ load('node:net'), fs, invoke, qualifyNative, checkpoint);
984
+ } else if (unchanged) {
985
+ // Re-attest the owned listener/control; do not replace matching hooks
986
+ // or interrupt an in-flight request merely to configure idle settings.
987
+ result.approvalRouting = {state: 'native-approval-listener-installed', roles: routes.length,
988
+ identity, control: current.descriptor, unchanged: true};
989
+ } else {
990
+ qualifyNative();
991
+ const deferred = [];
992
+ for (const route of routes) {
993
+ const descriptor = await manager.getSessionWhenLoaded(route.sessionId, 'topolo-agent-pack-approval', {warnOnMiss: false});
994
+ const session = await manager.getSession(route.sessionId);
995
+ if (!descriptor || descriptor.sessionId !== route.sessionId || descriptor.cwd !== route.cwd || descriptor.title !== route.title ||
996
+ descriptor.isArchived || descriptor.backend?.kind !== 'local' || descriptor.backend.remoteTarget || descriptor.worktreePath ||
997
+ descriptor.worktreeLazy || descriptor.spawnedFrom || !session || session.sessionId !== route.sessionId ||
998
+ session.cwd !== route.cwd || session.title !== route.title || session.isArchived)
999
+ throw new Error('Native approval route ownership changed.');
1000
+ if (session.pendingToolPermissions?.length || descriptor.pendingToolPermissions?.length)
1001
+ deferred.push({key: route.key, fields: ['intakeHooks'], reason: 'pending-permissions'});
1002
+ }
1003
+ if (current?.active || current?.failure || listener?.auditFailed ||
1004
+ (listener && listener.identity !== identity) ||
1005
+ (current && (current.descriptor.pid !== expected.pid || current.descriptor.identity !== identity)))
1006
+ for (const route of routes) deferred.push({key: route.key, fields: ['intakeHooks'], reason: 'existing-control-custody'});
1007
+ if (deferred.length) {
1008
+ result.routingDeferred = deferred;
1009
+ result.state = 'native-settings-partial';result.complete = false;
1010
+ checkpoint({kind: 'routing-deferred', deferred, at: Date.now()});
1011
+ } else {
1012
+ verifyContext(result.after.filter(row => !row.busy).map(row => contextPlan.members.find(m => m.sessionId === row.sessionId)));
1013
+ result.approvalRouting = await invoke({kind: 'install'});
1014
+ const stat = fs.lstatSync(control.secretPath);
1015
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid() || (stat.mode & 63)) throw new Error('Unowned native control secret.');
1016
+ const secret = JSON.parse(fs.readFileSync(control.secretPath, 'utf8'));
1017
+ result.approvalRouting.control = await (${nativeClaudeApprovalControl.toString()})(manager,
1018
+ {socket: control.socket, pid: expected.pid, identity}, secret.token,
1019
+ load('node:net'), fs, invoke, qualifyNative, checkpoint);
1020
+ }
1021
+ }
827
1022
  } else if (${JSON.stringify(plan.recovery === true && !plan.noEffects)}) {
828
1023
  const current = manager[Symbol.for('topolo.agent-pack.approval-control.v1')];
829
1024
  if (!current || current.descriptor.pid !== expected.pid || current.descriptor.identity !== fingerprint(routes))
@@ -920,13 +1115,8 @@ function syncDirectory(path) {
920
1115
  closeSync(fd);
921
1116
  }
922
1117
  }
923
- function verifyPlanContext(plan) {
924
- if (!plan.contextInventories || !Object.keys(plan.contextInventories).length) throw new Error("Complete context inventory proof is unavailable.");
925
- for (const [root, inventory] of Object.entries(plan.contextInventories))
926
- if (JSON.stringify(snapshotClaudeMarkdown(root, filesystem, cryptography, paths)) !== JSON.stringify(inventory))
927
- throw new Error("Context or transcript changed during configuration: Markdown inventory " + root);
928
- for (const [file, hash] of Object.entries(plan.context))
929
- if (createHash("sha256").update(readFileSync(file)).digest("hex") !== hash) throw new Error("Context or transcript changed during configuration: " + file);
1118
+ function verifyPlanContext(plan, members) {
1119
+ verifyClaudePlanContext(plan, members, filesystem, cryptography, paths, snapshotClaudeMarkdown);
930
1120
  }
931
1121
  function verifyClaudeOrganizationPolicy(root, pack, consent, run = spawnSync, locate = existsSync) {
932
1122
  const here = dirname(fileURLToPath(import.meta.url));
@@ -976,18 +1166,35 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
976
1166
  } else {
977
1167
  plan = { ...prepareClaudeNativePlan(root, options.pack, options.policy, options.mode === "switch" ? "switch" : "configure"), operationId };
978
1168
  if (options.apply && plan.approvalRoutes.length) {
979
- const directory2 = resolve(root, "claude/private");
980
- mkdirSync(directory2, { recursive: true, mode: 448 });
981
- const socketDirectory = resolve("/tmp", "topolo-claude-pack-" + createHash("sha256").update(root).digest("hex").slice(0, 16));
982
- mkdirSync(socketDirectory, { recursive: true, mode: 448 });
983
- for (const path of [directory2, socketDirectory]) {
984
- const stat = lstatSync(path);
985
- if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || stat.mode & 63)
986
- throw new Error("Native control directory must be owned and private.");
1169
+ const connectionPath = resolve(root, "claude/approval-connection.json");
1170
+ if (plan.mode === "configure" && existsSync(connectionPath)) {
1171
+ const entry = lstatSync(connectionPath);
1172
+ if (!entry.isFile() || entry.isSymbolicLink() || typeof process.getuid !== "function" || entry.uid !== process.getuid() || entry.mode & 63)
1173
+ throw new Error("Unowned native approval connection.");
1174
+ const connection = JSON.parse(readFileSync(connectionPath, "utf8"));
1175
+ if (connection.pid === options.pid && connection.pack === options.pack && JSON.stringify(connection.routes) === JSON.stringify(plan.approvalRoutes) && JSON.stringify(connection.installationConsent) === JSON.stringify(plan.installationConsent)) {
1176
+ if (typeof connection.secretPath !== "string" || typeof connection.socket !== "string" || !resolve(connection.secretPath).startsWith(realpathSync(resolve(root, "claude/private")) + sep) || realpathSync(connection.secretPath) !== resolve(connection.secretPath) || dirname(resolve(connection.socket)) !== resolve("/tmp", "topolo-claude-pack-" + createHash("sha256").update(root).digest("hex").slice(0, 16)))
1177
+ throw new Error("Native approval connection is outside owned control paths.");
1178
+ const secret = lstatSync(connection.secretPath);
1179
+ if (!secret.isFile() || secret.isSymbolicLink() || typeof process.getuid !== "function" || secret.uid !== process.getuid() || secret.mode & 63)
1180
+ throw new Error("Unowned native control secret.");
1181
+ plan.control = { socket: connection.socket, secretPath: connection.secretPath };
1182
+ }
1183
+ }
1184
+ if (!plan.control) {
1185
+ const directory2 = resolve(root, "claude/private");
1186
+ mkdirSync(directory2, { recursive: true, mode: 448 });
1187
+ const socketDirectory = resolve("/tmp", "topolo-claude-pack-" + createHash("sha256").update(root).digest("hex").slice(0, 16));
1188
+ mkdirSync(socketDirectory, { recursive: true, mode: 448 });
1189
+ for (const path of [directory2, socketDirectory]) {
1190
+ const stat = lstatSync(path);
1191
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || stat.mode & 63)
1192
+ throw new Error("Native control directory must be owned and private.");
1193
+ }
1194
+ plan.control = { socket: resolve(socketDirectory, operationId + ".sock"), secretPath: resolve(directory2, operationId + ".json") };
1195
+ durableRecord(plan.control.secretPath, { token: randomBytes(32).toString("hex") }, "wx");
1196
+ syncDirectory(directory2);
987
1197
  }
988
- plan.control = { socket: resolve(socketDirectory, operationId + ".sock"), secretPath: resolve(directory2, operationId + ".json") };
989
- durableRecord(plan.control.secretPath, { token: randomBytes(32).toString("hex") }, "wx");
990
- syncDirectory(directory2);
991
1198
  }
992
1199
  journal = resolve(directory, operationId + ".jsonl");
993
1200
  const record = { kind: "plan", pack: options.pack, ...plan, apply: options.apply };
@@ -998,7 +1205,8 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
998
1205
  syncDirectory(resolve(root, "claude"));
999
1206
  }
1000
1207
  try {
1001
- verifyPlanContext(plan);
1208
+ const incremental = !recovering && plan.mode === "configure";
1209
+ verifyPlanContext(plan, incremental ? [] : void 0);
1002
1210
  const organizationPolicy = options.apply && plan.approvalRoutes.length ? qualify(root, options.pack, plan.installationConsent) : null;
1003
1211
  if (options.apply) {
1004
1212
  const dispatched = { ...marker, dispatchState: "may-have-effects" };
@@ -1009,7 +1217,18 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
1009
1217
  marker = dispatched;
1010
1218
  }
1011
1219
  const result = await evaluate(options.endpoint, nativePackExpression(options.pid, plan, journal, options.apply));
1012
- verifyPlanContext(plan);
1220
+ if (incremental && result.contextVerifiedSessionIds !== void 0) {
1221
+ if (!Array.isArray(result.contextVerifiedSessionIds) || new Set(result.contextVerifiedSessionIds).size !== result.contextVerifiedSessionIds.length || result.contextVerifiedSessionIds.some((id) => !plan.members.some((m) => m.sessionId === id)))
1222
+ throw new Error("Native scoped context identities differ.");
1223
+ const observations = result.busyObservations ?? [];
1224
+ if (!Array.isArray(observations) || observations.some((row) => !plan.members.some((m) => m.key === row.key && m.sessionId === row.sessionId) || !Array.isArray(row.reasons) || !row.reasons.length || row.reasons.some((reason) => !["running", "starting", "losable", "pending-permissions"].includes(reason))))
1225
+ throw new Error("Native busy context attestation differs.");
1226
+ for (let i = 0; i < plan.members.length; i++)
1227
+ if (!result.contextVerifiedSessionIds.includes(plan.members[i].sessionId) && result.after?.[i]?.busy !== true && !observations.some((row) => row.sessionId === plan.members[i].sessionId))
1228
+ throw new Error("Native result omits idle context qualification.");
1229
+ }
1230
+ const contextMembers = incremental && Array.isArray(result.after) ? plan.members.filter((m, i) => result.after[i]?.busy !== true && (result.contextVerifiedSessionIds === void 0 || result.contextVerifiedSessionIds.includes(m.sessionId))) : void 0;
1231
+ verifyPlanContext(plan, contextMembers);
1013
1232
  if (options.apply || recovering) {
1014
1233
  if (plan.noEffects) {
1015
1234
  if (result.state !== "native-reconciled-no-effects" || result.nativeEffectsReplayed !== false || result.after?.length !== plan.members.length || result.after.some((actual, i) => ["key", "sessionId", "cwd", "title"].some((key) => actual[key] !== plan.members[i][key])))
@@ -1023,6 +1242,46 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
1023
1242
  syncDirectory(resolve(root, "claude"));
1024
1243
  return { ...result, journal, contextPreserved: true };
1025
1244
  }
1245
+ if (incremental && result.state === "native-settings-partial") {
1246
+ const settings = ["group", "pinned", "model", "effort", "permissionMode"];
1247
+ if (result.after?.length !== plan.members.length || !Array.isArray(result.deferred) || !result.deferred.length && !result.routingDeferred?.length) throw new Error("Native partial result has no qualified complete roster.");
1248
+ if (new Set(result.deferred.map((d) => d.sessionId)).size !== result.deferred.length || result.deferred.some((d) => !plan.members.some((m) => m.key === d.key && m.sessionId === d.sessionId) || !Array.isArray(d.fields) || !d.fields.length || new Set(d.fields).size !== d.fields.length || d.fields.some((field) => !settings.includes(field)) || !Array.isArray(d.reasons) || !d.reasons.length || d.reasons.some((reason) => !["running", "starting", "losable", "pending-permissions"].includes(reason))))
1249
+ throw new Error("Native deferred identity or fields differ.");
1250
+ if (result.routingDeferred !== void 0 && (!Array.isArray(result.routingDeferred) || result.routingDeferred.some((d) => !plan.approvalRoutes.some((r) => r.key === d.key) || JSON.stringify(d.fields) !== JSON.stringify(["intakeHooks"]) || !["pending-permissions", "existing-control-custody"].includes(d.reason))))
1251
+ throw new Error("Native deferred routing ownership differs.");
1252
+ for (let i = 0; i < plan.members.length; i++) {
1253
+ const wanted = plan.members[i], actual = result.after[i];
1254
+ if (actual.archived !== false || ["key", "sessionId", "cwd", "title"].some((key) => actual[key] !== wanted[key]))
1255
+ throw new Error("Native partial roster identity differs.");
1256
+ const mismatch = settings.filter((key) => actual[key] !== wanted[key]);
1257
+ const deferred = result.deferred.find((d) => d.sessionId === wanted.sessionId);
1258
+ if (JSON.stringify(mismatch.slice().sort()) !== JSON.stringify((deferred?.fields ?? []).slice().sort()))
1259
+ throw new Error("Native partial result hides an undeferred mismatch.");
1260
+ }
1261
+ const activePath2 = resolve(root, "claude/active.json");
1262
+ const active2 = existsSync(activePath2) ? JSON.parse(readFileSync(activePath2, "utf8")) : null;
1263
+ if (JSON.stringify(active2) !== JSON.stringify(plan.activeBefore)) throw new Error("Active roster changed during partial configuration.");
1264
+ durableRecord(journal, {
1265
+ kind: "partial-verified",
1266
+ after: result.after,
1267
+ deferred: result.deferred,
1268
+ routingDeferred: result.routingDeferred ?? [],
1269
+ appliedFields: result.appliedFields ?? [],
1270
+ activeUnchanged: true
1271
+ });
1272
+ if (JSON.stringify(JSON.parse(readFileSync(markerPath, "utf8"))) !== JSON.stringify(marker)) throw new Error("Recovery marker identity changed.");
1273
+ unlinkSync(markerPath);
1274
+ syncDirectory(resolve(root, "claude"));
1275
+ return {
1276
+ ...result,
1277
+ complete: false,
1278
+ journal,
1279
+ contextPreserved: contextMembers?.length === plan.members.length,
1280
+ idleContextsVerified: contextMembers?.map((m) => m.key),
1281
+ activeRosterUnchanged: true,
1282
+ resume: "explicit-configure-only"
1283
+ };
1284
+ }
1026
1285
  if (result.state !== (recovering ? "native-reconciled" : "native-settings-verified") || result.after?.length !== plan.members.length) throw new Error("Native result has no complete verified roster.");
1027
1286
  for (let i = 0; i < plan.members.length; i++) {
1028
1287
  const wanted = plan.members[i];
@@ -1066,7 +1325,12 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
1066
1325
  unlinkSync(markerPath);
1067
1326
  syncDirectory(resolve(root, "claude"));
1068
1327
  } else durableRecord(journal, { kind: "context-verified", context: plan.context });
1069
- return { ...result, journal, contextPreserved: true };
1328
+ return {
1329
+ ...result,
1330
+ journal,
1331
+ contextPreserved: !incremental || contextMembers?.length === plan.members.length,
1332
+ ...incremental ? { idleContextsVerified: contextMembers?.map((m) => m.key) } : {}
1333
+ };
1070
1334
  } catch (error) {
1071
1335
  durableRecord(journal, { kind: "recovery-required" });
1072
1336
  throw new Error(`Claude configuration is incomplete. Reconcile ${journal}. ${error instanceof Error ? error.message : String(error)}`);
@@ -1203,5 +1467,6 @@ export {
1203
1467
  runClaudeSmokeHelper,
1204
1468
  snapshotClaudeMarkdown,
1205
1469
  validateInspectorEndpoint,
1206
- verifyClaudeOrganizationPolicy
1470
+ verifyClaudeOrganizationPolicy,
1471
+ verifyClaudePlanContext
1207
1472
  };
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ function registerAgentPacksCommand(program2) {
14
14
  throw new Error("Exact closed smoke operation, role/case and UUID required.");
15
15
  const store = packs.opts().store ?? resolve(homedir(), "AgentPacks");
16
16
  if (options.harness === "claude") {
17
- const { operateClaudeSmoke } = await import("./claude-pack-backend-3YJNKMYD.js");
17
+ const { operateClaudeSmoke } = await import("./claude-pack-backend-4F4VHMI5.js");
18
18
  console.log(JSON.stringify(await operateClaudeSmoke({
19
19
  store,
20
20
  kind: "smoke-" + operation,
@@ -44,7 +44,7 @@ function registerAgentPacksCommand(program2) {
44
44
  });
45
45
  packs.command("configure").description("Qualify and configure an adopted Claude roster through its live native backend").argument("<pack>").option("--harness <name>", "Host adapter", "claude").requiredOption("--policy <path>", "Reviewed execution policy JSON").requiredOption("--endpoint <url>", "Explicit loopback main-process inspector endpoint, enabled separately with authorization").requiredOption("--pid <pid>", "Expected Claude main-process PID").option("--apply", "Apply native groups, pins, models, effort and permission modes; otherwise preflight only").action(async (pack, options) => {
46
46
  if (options.harness !== "claude") throw new Error("Native configure is currently qualified only for Claude Desktop.");
47
- const { configureClaudePack } = await import("./claude-pack-backend-3YJNKMYD.js");
47
+ const { configureClaudePack } = await import("./claude-pack-backend-4F4VHMI5.js");
48
48
  const result = await configureClaudePack({
49
49
  store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
50
50
  pack,
@@ -96,7 +96,7 @@ function registerAgentPacksCommand(program2) {
96
96
  throw new Error("Only a one-request once/deny response is supported.");
97
97
  decision = response.decision;
98
98
  }
99
- const { operateClaudeApprovals } = await import("./claude-pack-backend-3YJNKMYD.js");
99
+ const { operateClaudeApprovals } = await import("./claude-pack-backend-4F4VHMI5.js");
100
100
  console.log(JSON.stringify(await operateClaudeApprovals({
101
101
  store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
102
102
  kind: args[0],
@@ -111,7 +111,7 @@ function registerAgentPacksCommand(program2) {
111
111
  if (options.policy || options.endpoint || options.pid || options.apply) {
112
112
  if (options.harness !== "claude" || !options.policy || !options.endpoint || !options.pid || args.length !== 1)
113
113
  throw new Error("Native switch/recovery requires one Claude pack, --policy, --endpoint and --pid.");
114
- const { configureClaudePack } = await import("./claude-pack-backend-3YJNKMYD.js");
114
+ const { configureClaudePack } = await import("./claude-pack-backend-4F4VHMI5.js");
115
115
  const result2 = await configureClaudePack({
116
116
  store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
117
117
  pack: args[0],
@@ -1741,7 +1741,7 @@ var TopoloClient = class _TopoloClient {
1741
1741
  } catch (error) {
1742
1742
  const transportFailure = !(error instanceof TopoloSdkError) && !opts.signal?.aborted;
1743
1743
  if (!transportFailure || attempt === maxAttempts) throw error;
1744
- await new Promise((resolve4) => setTimeout(resolve4, 250));
1744
+ await new Promise((resolve5) => setTimeout(resolve5, 250));
1745
1745
  }
1746
1746
  }
1747
1747
  throw new TopoloSdkError("network_error", "Topolo read retry loop exhausted.");
@@ -3740,7 +3740,7 @@ function createTopolo(options) {
3740
3740
  async function pollUntilApproved(params) {
3741
3741
  let interval = Math.max(params.intervalSeconds ?? 5, 1);
3742
3742
  while (Date.now() < params.expiresAtMs) {
3743
- await new Promise((resolve4) => setTimeout(resolve4, Math.min(interval * 1e3, params.expiresAtMs - Date.now())));
3743
+ await new Promise((resolve5) => setTimeout(resolve5, Math.min(interval * 1e3, params.expiresAtMs - Date.now())));
3744
3744
  if (Date.now() >= params.expiresAtMs) break;
3745
3745
  try {
3746
3746
  return await params.oauth.pollDeviceToken({
@@ -6521,7 +6521,7 @@ import { spawnSync as spawnSync4 } from "child_process";
6521
6521
  import { existsSync as existsSync6 } from "fs";
6522
6522
  import { readFile as readFile6, rm as rm2 } from "fs/promises";
6523
6523
  import { homedir as homedir5 } from "os";
6524
- import { dirname as dirname6, join as join7, resolve as resolve2 } from "path";
6524
+ import { dirname as dirname6, join as join7, resolve as resolve3 } from "path";
6525
6525
  import { fileURLToPath as fileURLToPath3 } from "url";
6526
6526
  import { createRequire } from "module";
6527
6527
 
@@ -6530,7 +6530,7 @@ import { spawnSync as spawnSync3 } from "child_process";
6530
6530
  import { existsSync as existsSync4 } from "fs";
6531
6531
  import { readFile as readFile4 } from "fs/promises";
6532
6532
  import { homedir as homedir4 } from "os";
6533
- import { isAbsolute, join as join6 } from "path";
6533
+ import { isAbsolute, join as join6, resolve as resolve2 } from "path";
6534
6534
  var SUPPORTED_AGENT_IDS = [
6535
6535
  "claude",
6536
6536
  "claude-desktop",
@@ -6570,6 +6570,18 @@ var AGENT_NAME_FOR = {
6570
6570
  goose: "goose",
6571
6571
  "amazon-q": "amazon-q"
6572
6572
  };
6573
+ function resolveAgentHostTarget(options) {
6574
+ const homeDir = resolve2(options.homeDir ?? homedir4());
6575
+ const codexHome = options.codexHome ?? (options.homeDir === void 0 ? process.env.CODEX_HOME || void 0 : void 0);
6576
+ if (codexHome !== void 0 && (!codexHome.trim() || !isAbsolute(codexHome))) {
6577
+ throw new Error("Codex home must be an absolute nonempty directory.");
6578
+ }
6579
+ return Object.freeze({
6580
+ homeDir,
6581
+ ...options.projectDir ? { projectDir: resolve2(options.projectDir) } : {},
6582
+ codexHome: codexHome ?? join6(homeDir, ".codex")
6583
+ });
6584
+ }
6573
6585
  var RESERVED_OPERATOR_REGISTRATION = ["topolo", "platform"].join("-");
6574
6586
  function isPublicRegistrationName(name) {
6575
6587
  return name === "topolo" || name.startsWith("topolo-") && name !== RESERVED_OPERATOR_REGISTRATION;
@@ -6585,14 +6597,14 @@ function stableNodeCommand() {
6585
6597
  });
6586
6598
  return selectStableNodeCommand(probe.status === 0 ? probe.stdout : void 0);
6587
6599
  }
6588
- function agentSkillPath(agent, homeDir = homedir4(), projectDir) {
6600
+ function agentSkillPath(agent, homeDir = homedir4(), projectDir, codexHome) {
6589
6601
  switch (agent) {
6590
6602
  case "claude":
6591
6603
  return projectDir ? join6(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
6592
6604
  case "claude-desktop":
6593
6605
  return join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
6594
6606
  case "codex":
6595
- return join6(homeDir, ".codex", "skills", "topolo", "SKILL.md");
6607
+ return join6(codexHome ?? join6(homeDir, ".codex"), "skills", "topolo", "SKILL.md");
6596
6608
  case "cursor":
6597
6609
  return join6(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
6598
6610
  case "grok":
@@ -6610,10 +6622,10 @@ function agentSkillPath(agent, homeDir = homedir4(), projectDir) {
6610
6622
  return void 0;
6611
6623
  }
6612
6624
  }
6613
- function agentConfigPaths(agent, homeDir = homedir4(), projectDir) {
6625
+ function agentConfigPaths(agent, homeDir = homedir4(), projectDir, codexHome) {
6614
6626
  switch (agent) {
6615
6627
  case "claude":
6616
- return [{ path: join6(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
6628
+ return [{ path: projectDir ? join6(projectDir, ".mcp.json") : join6(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
6617
6629
  case "claude-desktop":
6618
6630
  return [{
6619
6631
  path: claudeDesktopConfigPath(homeDir),
@@ -6621,7 +6633,7 @@ function agentConfigPaths(agent, homeDir = homedir4(), projectDir) {
6621
6633
  rootKey: "mcpServers"
6622
6634
  }];
6623
6635
  case "codex":
6624
- return [{ path: join6(homeDir, ".codex", "config.toml"), format: "toml" }];
6636
+ return [{ path: join6(codexHome ?? join6(homeDir, ".codex"), "config.toml"), format: "toml" }];
6625
6637
  case "cursor":
6626
6638
  return [{
6627
6639
  path: projectDir ? join6(projectDir, ".cursor", "mcp.json") : join6(homeDir, ".cursor", "mcp.json"),
@@ -6702,8 +6714,36 @@ function agentCliNames(agent) {
6702
6714
  return ["qchat", "q"];
6703
6715
  }
6704
6716
  }
6705
- function agentConfigExists(agent, homeDir = homedir4(), projectDir) {
6706
- return agentConfigPaths(agent, homeDir, projectDir).some((config) => existsSync4(config.path));
6717
+ function agentConfigExists(agent, homeDir = homedir4(), projectDir, codexHome) {
6718
+ return agentConfigPaths(agent, homeDir, projectDir, codexHome).some((config) => existsSync4(config.path));
6719
+ }
6720
+ async function assertClaudeProjectScope(homeDir, projectDir, name) {
6721
+ const path = join6(homeDir, ".claude.json");
6722
+ let contents;
6723
+ try {
6724
+ contents = await readFile4(path, "utf8");
6725
+ } catch (error) {
6726
+ if (error.code === "ENOENT") return;
6727
+ throw error;
6728
+ }
6729
+ let config;
6730
+ try {
6731
+ config = JSON.parse(contents);
6732
+ if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error();
6733
+ } catch {
6734
+ throw new Error("Cannot verify malformed Claude scope configuration at " + path + ".");
6735
+ }
6736
+ const object = (value) => {
6737
+ if (value === void 0) return void 0;
6738
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6739
+ throw new Error("Cannot verify malformed Claude scope configuration at " + path + ".");
6740
+ }
6741
+ return value;
6742
+ };
6743
+ const local = object(object(config.projects)?.[resolve2(projectDir)]);
6744
+ if (Object.hasOwn(object(config.mcpServers) ?? {}, name) || Object.hasOwn(object(local?.mcpServers) ?? {}, name)) {
6745
+ throw new Error("Claude MCP scope conflict for " + name + ": a user or local definition can override the project registration. Resolve that scope before setup.");
6746
+ }
6707
6747
  }
6708
6748
  function claudeDesktopConfigPath(homeDir) {
6709
6749
  if (process.platform === "darwin") return join6(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
@@ -6715,9 +6755,9 @@ function vscodeUserConfigPath(homeDir) {
6715
6755
  if (process.platform === "win32") return join6(process.env.APPDATA ?? join6(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
6716
6756
  return join6(process.env.XDG_CONFIG_HOME ?? join6(homeDir, ".config"), "Code", "User", "mcp.json");
6717
6757
  }
6718
- async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir) {
6758
+ async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir, codexHome) {
6719
6759
  const registrations = [];
6720
- for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
6760
+ for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
6721
6761
  if (!existsSync4(config.path)) continue;
6722
6762
  if (config.format === "json") {
6723
6763
  registrations.push(...await readJsonRegistrations(config.path, config.rootKey ?? "mcpServers", agent));
@@ -6982,17 +7022,19 @@ function message(error) {
6982
7022
 
6983
7023
  // src/commands/agent-config.ts
6984
7024
  import { existsSync as existsSync5 } from "fs";
6985
- import { mkdir as mkdir3, readFile as readFile5, rename as rename2, writeFile as writeFile3 } from "fs/promises";
7025
+ import { lstat as lstat2, mkdir as mkdir3, open, readFile as readFile5, rename as rename2, unlink, writeFile as writeFile3 } from "fs/promises";
7026
+ import { randomUUID as randomUUID3 } from "crypto";
6986
7027
  import { dirname as dirname5 } from "path";
6987
- async function installAgentSkill(agent, source, homeDir, projectDir) {
6988
- const path = agentSkillPath(agent, homeDir, projectDir);
7028
+ async function installAgentSkill(agent, source, homeDir, projectDir, codexHome) {
7029
+ const path = agentSkillPath(agent, homeDir, projectDir, codexHome);
6989
7030
  if (!path) return { installed: false };
6990
7031
  const contents = formatSkillForAgent(agent, source);
6991
7032
  await atomicWrite(path, contents);
6992
7033
  return { path, installed: true };
6993
7034
  }
6994
- async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir) {
6995
- for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
7035
+ async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir, codexHome) {
7036
+ if (agent === "claude" && projectDir) await assertClaudeProjectScope(homeDir, projectDir, registration.name);
7037
+ for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
6996
7038
  if (config.format === "json") {
6997
7039
  await registerJson(config, agent, mcp, registration);
6998
7040
  } else if (config.format === "toml") {
@@ -7002,8 +7044,8 @@ async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir
7002
7044
  }
7003
7045
  }
7004
7046
  }
7005
- async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
7006
- for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
7047
+ async function unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome) {
7048
+ for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
7007
7049
  if (!existsSync5(config.path)) continue;
7008
7050
  if (config.format === "json") {
7009
7051
  await unregisterJson(config, agent, name);
@@ -7016,18 +7058,39 @@ async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
7016
7058
  }
7017
7059
  }
7018
7060
  function registrationEnvironment(agent, registration) {
7061
+ let privateAliases = {};
7062
+ if (registration.configPath) {
7063
+ const prefixes = {
7064
+ production: "TOPOLO_PRODUCTION",
7065
+ staging: "TOPOLO_STAGING",
7066
+ development: "TOPOLO_DEVELOPMENT",
7067
+ local: "TOPOLO_LOCAL"
7068
+ };
7069
+ const prefix = prefixes[registration.environment];
7070
+ if (!prefix) throw new Error("Cannot isolate a private profile for an unknown environment.");
7071
+ privateAliases = {
7072
+ TOPOLO_API_KEY: "",
7073
+ TOPOLO_ACCESS_TOKEN: "",
7074
+ [prefix + "_API_KEY"]: "",
7075
+ [prefix + "_ACCESS_TOKEN"]: "",
7076
+ TOPOLO_ENV: registration.environment
7077
+ };
7078
+ }
7019
7079
  return {
7020
7080
  TOPOLO_AGENT_NAME: AGENT_NAME_FOR[agent],
7021
7081
  ...registration.configPath ? { TOPOLO_CONFIG_PATH: registration.configPath } : {},
7022
7082
  ...registration.installationId ? { TOPOLO_AGENT_INSTALLATION_ID: registration.installationId } : {},
7023
- ...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {}
7083
+ ...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {},
7084
+ ...privateAliases
7024
7085
  };
7025
7086
  }
7026
7087
  async function registerJson(config, agent, mcp, registration) {
7088
+ const before = await fileSnapshot(config.path);
7027
7089
  let parsed = {};
7028
- if (existsSync5(config.path)) {
7090
+ if (before.contents !== void 0) {
7029
7091
  try {
7030
- parsed = JSON.parse(await readFile5(config.path, "utf8"));
7092
+ parsed = JSON.parse(before.contents);
7093
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Expected an object.");
7031
7094
  } catch (error) {
7032
7095
  throw new Error(
7033
7096
  "Cannot update malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
@@ -7048,7 +7111,7 @@ async function registerJson(config, agent, mcp, registration) {
7048
7111
  if (agent === "claude" || agent === "claude-desktop" || rootKey === "servers") entry.type = "stdio";
7049
7112
  servers[registration.name] = entry;
7050
7113
  parsed[rootKey] = servers;
7051
- await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
7114
+ await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n", before);
7052
7115
  }
7053
7116
  async function unregisterJson(config, agent, name) {
7054
7117
  let parsed;
@@ -7070,13 +7133,14 @@ async function unregisterJson(config, agent, name) {
7070
7133
  await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
7071
7134
  }
7072
7135
  async function registerToml(path, agent, mcp, registration) {
7073
- const existing = existsSync5(path) ? await readFile5(path, "utf8") : "";
7136
+ const before = await fileSnapshot(path);
7137
+ const existing = before.contents ?? "";
7074
7138
  const text2 = withoutMcpServerSections(existing, registration.name);
7075
7139
  const args = mcp.args.length ? "\nargs = [" + mcp.args.map((item) => JSON.stringify(item)).join(", ") + "]" : "";
7076
7140
  const environment = registrationEnvironment(agent, registration);
7077
7141
  const environmentText = Object.entries(environment).map(([key, value]) => JSON.stringify(key) + " = " + JSON.stringify(value)).join(", ");
7078
7142
  const block = agent === "grok" ? "\n\n[mcp_servers." + registration.name + "]\ncommand = " + JSON.stringify(mcp.command) + args + "\nenv = { " + environmentText + " }\n" : "\n\n[mcp_servers." + registration.name + "]\ncommand = " + JSON.stringify(mcp.command) + args + "\n\n[mcp_servers." + registration.name + ".env]\n" + Object.entries(environment).map(([key, value]) => key + " = " + JSON.stringify(value)).join("\n") + "\n";
7079
- await atomicWrite(path, text2 + block);
7143
+ await atomicWrite(path, text2 + block, before);
7080
7144
  }
7081
7145
  async function registerGoose(path, mcp, registration) {
7082
7146
  const existing = existsSync5(path) ? await readFile5(path, "utf8") : "";
@@ -7146,11 +7210,82 @@ function formatSkillForAgent(agent, source) {
7146
7210
  }
7147
7211
  return "---\ntrigger: model_decision\ndescription: Use Topolo MCP for Topolo platform operations.\n---\n\n" + body;
7148
7212
  }
7149
- async function atomicWrite(path, contents) {
7213
+ function fileIdentity(stat2) {
7214
+ return [stat2.dev, stat2.ino, stat2.size, stat2.mtimeMs, stat2.ctimeMs, stat2.mode, stat2.nlink].join(":");
7215
+ }
7216
+ async function fileSnapshot(path) {
7217
+ const parents = [];
7218
+ for (let parent = dirname5(path); ; parent = dirname5(parent)) {
7219
+ try {
7220
+ const stat2 = await lstat2(parent);
7221
+ if (!stat2.isDirectory() || stat2.isSymbolicLink()) throw new Error("Host destination contains a symbolic link or non-directory at " + parent + ".");
7222
+ parents.push({ path: parent, identity: [stat2.dev, stat2.ino].join(":") });
7223
+ } catch (error) {
7224
+ if (error.code !== "ENOENT") throw error;
7225
+ }
7226
+ if (dirname5(parent) === parent) break;
7227
+ }
7228
+ try {
7229
+ const stat2 = await lstat2(path);
7230
+ if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1) throw new Error("Host configuration must be a single regular file at " + path + ".");
7231
+ const contents = await readFile5(path, "utf8");
7232
+ if (fileIdentity(await lstat2(path)) !== fileIdentity(stat2)) throw new Error("Host configuration changed while reading at " + path + ".");
7233
+ return { contents, identity: fileIdentity(stat2), parents };
7234
+ } catch (error) {
7235
+ if (error.code === "ENOENT") return { parents };
7236
+ throw error;
7237
+ }
7238
+ }
7239
+ async function assertSnapshot(path, before) {
7240
+ const current = await fileSnapshot(path);
7241
+ if (current.contents !== before.contents || current.identity !== before.identity || before.parents.some((parent) => !current.parents.some((candidate) => candidate.path === parent.path && candidate.identity === parent.identity))) {
7242
+ throw new Error("Host destination changed before write at " + path + ".");
7243
+ }
7244
+ }
7245
+ async function atomicWrite(path, contents, before) {
7246
+ before ??= await fileSnapshot(path);
7247
+ await assertSnapshot(path, before);
7248
+ if (before.contents === contents) return;
7150
7249
  await mkdir3(dirname5(path), { recursive: true });
7151
- const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
7152
- await writeFile3(temporaryPath, contents, { encoding: "utf8", mode: 384 });
7153
- await rename2(temporaryPath, path);
7250
+ const destination = await fileSnapshot(path);
7251
+ await assertSnapshot(path, before);
7252
+ const temporaryPath = path + ".topolo-" + randomUUID3() + ".tmp";
7253
+ const handle = await open(temporaryPath, "wx", 384);
7254
+ let temporaryIdentity;
7255
+ let handleClosed = false;
7256
+ let temporaryPending = true;
7257
+ try {
7258
+ const created = await handle.stat();
7259
+ temporaryIdentity = [created.dev, created.ino].join(":");
7260
+ await assertSnapshot(path, destination);
7261
+ await writeFile3(handle, contents, { encoding: "utf8" });
7262
+ await handle.close();
7263
+ handleClosed = true;
7264
+ await assertSnapshot(path, destination);
7265
+ const temporary = await lstat2(temporaryPath);
7266
+ if (!temporary.isFile() || temporary.isSymbolicLink() || temporary.nlink !== 1 || [temporary.dev, temporary.ino].join(":") !== temporaryIdentity) {
7267
+ throw new Error("Host temporary file custody changed before rename at " + path + ".");
7268
+ }
7269
+ await rename2(temporaryPath, path);
7270
+ temporaryPending = false;
7271
+ const installed = await fileSnapshot(path);
7272
+ if (installed.contents !== contents) throw new Error("Host configuration read-back failed at " + path + ".");
7273
+ } finally {
7274
+ if (!handleClosed) await handle.close();
7275
+ if (temporaryPending && temporaryIdentity) {
7276
+ let cleanupOwned = false;
7277
+ try {
7278
+ const parentsOwned = await Promise.all(destination.parents.map(async (parent) => {
7279
+ const current2 = await lstat2(parent.path);
7280
+ return current2.isDirectory() && !current2.isSymbolicLink() && [current2.dev, current2.ino].join(":") === parent.identity;
7281
+ }));
7282
+ const current = await lstat2(temporaryPath);
7283
+ cleanupOwned = parentsOwned.every(Boolean) && current.isFile() && !current.isSymbolicLink() && current.nlink === 1 && [current.dev, current.ino].join(":") === temporaryIdentity;
7284
+ } catch {
7285
+ }
7286
+ if (cleanupOwned) await unlink(temporaryPath);
7287
+ }
7288
+ }
7154
7289
  }
7155
7290
  function withoutMcpServerSections(text2, name) {
7156
7291
  const section = "[mcp_servers." + name + "]";
@@ -7171,8 +7306,8 @@ function withoutMcpServerSections(text2, name) {
7171
7306
  var AGENT_LABELS2 = AGENT_LABELS;
7172
7307
  function skillsSourceDir() {
7173
7308
  const here = dirname6(fileURLToPath3(import.meta.url));
7174
- const distCandidate = resolve2(here, "../skills");
7175
- const srcCandidate = resolve2(here, "../../skills");
7309
+ const distCandidate = resolve3(here, "../skills");
7310
+ const srcCandidate = resolve3(here, "../../skills");
7176
7311
  for (const candidate of [distCandidate, srcCandidate]) {
7177
7312
  if (existsSync6(join7(candidate, "topolo", "SKILL.md"))) return candidate;
7178
7313
  }
@@ -7224,14 +7359,22 @@ function resolveMcpRegistration(options) {
7224
7359
  };
7225
7360
  }
7226
7361
  async function installForAgent(agent, sourceDir, mcp, opts) {
7362
+ const { homeDir, projectDir, codexHome } = opts.target;
7363
+ if (opts.withMcp && agent === "claude" && projectDir) {
7364
+ try {
7365
+ await assertClaudeProjectScope(homeDir, projectDir, opts.registration.name);
7366
+ } catch (err) {
7367
+ return { agent, skillInstalled: false, mcp: "failed", detail: err instanceof Error ? err.message : String(err) };
7368
+ }
7369
+ }
7227
7370
  const source = await readFile6(join7(sourceDir, "topolo", "SKILL.md"), "utf8");
7228
- const skill = await installAgentSkill(agent, source, opts.homeDir, opts.projectDir);
7371
+ const skill = await installAgentSkill(agent, source, homeDir, projectDir, codexHome);
7229
7372
  if (!opts.withMcp) {
7230
7373
  return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
7231
7374
  }
7232
7375
  try {
7233
- await registerAgentConfig(agent, mcp, opts.homeDir, opts.registration, opts.projectDir);
7234
- const installed = (await readAgentRegistrations(agent, opts.homeDir, opts.projectDir)).find((registration) => registration.name === opts.registration.name);
7376
+ await registerAgentConfig(agent, mcp, homeDir, opts.registration, projectDir, codexHome);
7377
+ const installed = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).find((registration) => registration.name === opts.registration.name);
7235
7378
  const expectedEnvironment = registrationEnvironment(agent, opts.registration);
7236
7379
  if (!installed || installed.command !== mcp.command || JSON.stringify(installed.args) !== JSON.stringify(mcp.args) || Object.entries(expectedEnvironment).some(([key, value]) => installed.env[key] !== value)) {
7237
7380
  throw new Error(`Host configuration did not retain the ${opts.registration.name} MCP registration.`);
@@ -7265,12 +7408,12 @@ function resolveTargets(opts) {
7265
7408
  if (opts.all) return [...SUPPORTED_AGENT_IDS];
7266
7409
  const homeDir = opts.homeDir ?? homedir5();
7267
7410
  const detectBinaries = homeDir === homedir5();
7268
- const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project) || detectBinaries && agentBinaryExists(agent));
7411
+ const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project, opts.codexHome) || detectBinaries && agentBinaryExists(agent));
7269
7412
  return [.../* @__PURE__ */ new Set(["claude", "codex", ...detected])];
7270
7413
  }
7271
7414
  function registerSkillsCommand(program2) {
7272
7415
  const skills = program2.command("skills").description("Install the Topolo agent skill + MCP server into supported agent hosts");
7273
- skills.command("install").description("Copy the skill file and register the topolo MCP server (credential-free)").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id (for example cursor gemini)").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--no-mcp", "Copy the skill file but do not register the MCP server").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
7416
+ skills.command("install").description("Copy the skill file and register the topolo MCP server (credential-free)").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id (for example cursor gemini)").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--no-mcp", "Copy the skill file but do not register the MCP server").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
7274
7417
  async (opts) => {
7275
7418
  const mode = resolveOutputMode(opts.json);
7276
7419
  try {
@@ -7286,11 +7429,12 @@ function registerSkillsCommand(program2) {
7286
7429
  }
7287
7430
  }
7288
7431
  );
7289
- skills.command("status").description("List public Topolo skill and MCP registrations for each agent").option("--agent <host...>", "Inspect one or more hosts by id").option("--all", "Inspect every supported host").option("--project <dir>", "Inspect project-scoped host files under <dir>").option("--agent-home <dir>", "Inspect an alternate agent home").option("--name <name>", "Inspect one registration name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7432
+ skills.command("status").description("List public Topolo skill and MCP registrations for each agent").option("--agent <host...>", "Inspect one or more hosts by id").option("--all", "Inspect every supported host").option("--project <dir>", "Inspect project-scoped host files under <dir>").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Inspect an alternate agent home").option("--name <name>", "Inspect one registration name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7290
7433
  const mode = resolveOutputMode(opts.json);
7291
7434
  try {
7292
7435
  const rows = await agentSurfaceStatus({
7293
- homeDir: opts.agentHome ? resolve2(opts.agentHome) : void 0,
7436
+ homeDir: opts.agentHome ? resolve3(opts.agentHome) : void 0,
7437
+ codexHome: opts.codexHome,
7294
7438
  name: opts.name,
7295
7439
  agent: opts.agent,
7296
7440
  all: opts.all,
@@ -7301,12 +7445,12 @@ function registerSkillsCommand(program2) {
7301
7445
  fail2(err, mode);
7302
7446
  }
7303
7447
  });
7304
- skills.command("uninstall").description("Remove the skill file and MCP registration for each agent").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Remove from every supported host").option("--project <dir>", "Remove project-scoped host files under <dir>").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Remove one registration (defaults to topolo)").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7448
+ skills.command("uninstall").description("Remove the skill file and MCP registration for each agent").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Remove from every supported host").option("--project <dir>", "Remove project-scoped host files under <dir>").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Remove one registration (defaults to topolo)").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7305
7449
  const mode = resolveOutputMode(opts.json);
7306
7450
  try {
7307
7451
  const removed = await uninstallAgentSurfaces({
7308
7452
  ...opts,
7309
- homeDir: opts.agentHome ? resolve2(opts.agentHome) : void 0
7453
+ homeDir: opts.agentHome ? resolve3(opts.agentHome) : void 0
7310
7454
  });
7311
7455
  emit(
7312
7456
  { removed },
@@ -7325,20 +7469,20 @@ async function installAgentSurfaces(opts) {
7325
7469
  }
7326
7470
  const mcp = resolveMcpCommand();
7327
7471
  const registration = resolveMcpRegistration(opts);
7328
- const homeDir = opts.homeDir ?? homedir5();
7472
+ const target = resolveAgentHostTarget({ homeDir: opts.homeDir, projectDir: opts.project, codexHome: opts.codexHome });
7473
+ const { homeDir, projectDir, codexHome } = target;
7329
7474
  const results = [];
7330
- for (const agent of resolveTargets({ ...opts, homeDir, project: opts.project })) {
7475
+ for (const agent of resolveTargets({ ...opts, homeDir, project: projectDir, codexHome })) {
7331
7476
  results.push(await installForAgent(agent, sourceDir, mcp, {
7332
7477
  withMcp: opts.mcp !== false,
7333
- projectDir: opts.project,
7334
- homeDir,
7478
+ target,
7335
7479
  registration
7336
7480
  }));
7337
7481
  }
7338
7482
  return { results, mcp };
7339
7483
  }
7340
7484
  function registerSetupCommand(program2) {
7341
- program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7485
+ program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7342
7486
  const mode = resolveOutputMode(opts.json);
7343
7487
  try {
7344
7488
  const { results, mcp } = await installAgentSurfaces({
@@ -7360,18 +7504,20 @@ function assertAgentSetupSucceeded(results) {
7360
7504
  }
7361
7505
  }
7362
7506
  async function agentSurfaceStatus(options = {}) {
7363
- const homeDir = options.homeDir ?? homedir5();
7507
+ const target = resolveAgentHostTarget({ homeDir: options.homeDir, projectDir: options.project, codexHome: options.codexHome });
7508
+ const { homeDir, projectDir, codexHome } = target;
7364
7509
  const sourceSkill = await readFile6(join7(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
7365
7510
  const agents = resolveTargets({
7366
7511
  agent: options.agent,
7367
7512
  all: options.all,
7368
7513
  homeDir,
7369
- project: options.project
7514
+ project: projectDir,
7515
+ codexHome
7370
7516
  });
7371
7517
  return Promise.all(agents.map(async (agent) => {
7372
- const skillPath = agentSkillPath(agent, homeDir, options.project);
7518
+ const skillPath = agentSkillPath(agent, homeDir, projectDir, codexHome);
7373
7519
  const installedSkill = skillPath && existsSync6(skillPath) ? await readFile6(skillPath, "utf8") : void 0;
7374
- const registrations = (await readAgentRegistrations(agent, homeDir, options.project)).filter((registration) => !options.name || registration.name === options.name);
7520
+ const registrations = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).filter((registration) => !options.name || registration.name === options.name);
7375
7521
  return {
7376
7522
  agent,
7377
7523
  skillInstalled: installedSkill !== void 0,
@@ -7384,16 +7530,17 @@ async function agentSurfaceStatus(options = {}) {
7384
7530
  }));
7385
7531
  }
7386
7532
  async function uninstallAgentSurfaces(options) {
7387
- const homeDir = options.homeDir ?? homedir5();
7533
+ const target = resolveAgentHostTarget({ homeDir: options.homeDir, projectDir: options.project, codexHome: options.codexHome });
7534
+ const { homeDir, projectDir, codexHome } = target;
7388
7535
  const name = options.name?.trim() || "topolo";
7389
7536
  resolveMcpRegistration({ name, ...name === "topolo" ? {} : { context: "uninstall-validation" } });
7390
7537
  const removed = [];
7391
- for (const agent of resolveTargets({ ...options, homeDir, project: options.project })) {
7392
- const before = await readAgentRegistrations(agent, homeDir, options.project);
7538
+ for (const agent of resolveTargets({ ...options, homeDir, project: projectDir, codexHome })) {
7539
+ const before = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
7393
7540
  const registrationRemoved = before.some((entry) => entry.name === name);
7394
- await unregisterAgentConfig(agent, homeDir, name, options.project);
7395
- const remaining = await readAgentRegistrations(agent, homeDir, options.project);
7396
- const skillPath = agentSkillPath(agent, homeDir, options.project);
7541
+ await unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome);
7542
+ const remaining = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
7543
+ const skillPath = agentSkillPath(agent, homeDir, projectDir, codexHome);
7397
7544
  const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync6(skillPath));
7398
7545
  if (skillRemoved && skillPath) await rm2(dirname6(skillPath), { recursive: true, force: true });
7399
7546
  removed.push({ agent, name, registrationRemoved, skillRemoved });
@@ -7426,7 +7573,7 @@ function formatStatus(rows) {
7426
7573
  // src/commands/doctor.ts
7427
7574
  import { access } from "fs/promises";
7428
7575
  import { homedir as homedir6 } from "os";
7429
- import { resolve as resolve3 } from "path";
7576
+ import { resolve as resolve4 } from "path";
7430
7577
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7431
7578
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7432
7579
  var defaultDependencies = {
@@ -7488,7 +7635,7 @@ async function verifyMcpProtocol(command, args, env = definedEnvironment(process
7488
7635
  }
7489
7636
  async function resolveInstalledDoctorTarget(options) {
7490
7637
  const name = options.name?.trim() || "topolo";
7491
- const homeDir = options.agentHome ? resolve3(options.agentHome) : homedir6();
7638
+ const homeDir = options.agentHome ? resolve4(options.agentHome) : homedir6();
7492
7639
  const expected = resolveMcpCommand();
7493
7640
  const requestedAgents = resolveTargets({
7494
7641
  ...options,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
3
  "packageManager": "pnpm@10.17.1",
4
- "version": "0.13.8",
5
- "topoloPublishedVersion": "0.13.8",
4
+ "version": "0.13.9",
5
+ "topoloPublishedVersion": "0.13.9",
6
6
  "description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
7
7
  "license": "UNLICENSED",
8
8
  "author": "Topolo",