@kontextmind/kxm 0.7.85 → 0.7.87

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.
@@ -18372,7 +18372,7 @@ function discoverProjectStores(projectRoot, options = {}) {
18372
18372
  }
18373
18373
  return stores;
18374
18374
  }
18375
- function createBackup(options = {}) {
18375
+ function planBackup(options = {}) {
18376
18376
  const projectRoot = options.projectRoot ? resolve3(options.projectRoot) : process.cwd();
18377
18377
  const stores = discoverProjectStores(projectRoot, {
18378
18378
  ...options.hubDataPath !== void 0 ? { hubDataPath: options.hubDataPath } : {}
@@ -18380,30 +18380,35 @@ function createBackup(options = {}) {
18380
18380
  if (stores.length === 0) {
18381
18381
  throw databaseError("backup_no_stores", projectRoot, "no existing SQLite stores found to backup");
18382
18382
  }
18383
- const now = /* @__PURE__ */ new Date();
18384
- const timestamp = now.toISOString().replace(/[:.]/g, "-");
18385
- const backupId = `bk_${randomBytes(8).toString("hex")}`;
18383
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
18384
+ const timestamp = createdAt.replace(/[:.]/g, "-");
18386
18385
  const outDir = options.outDir ? resolve3(options.outDir) : join4(projectRoot, ".kxm", "backups", `backup-${timestamp}`);
18387
- if (!existsSync4(outDir)) {
18388
- mkdirSync(outDir, { recursive: true, mode: 448 });
18389
- }
18390
- const backedUpStores = [];
18391
18386
  const usedFilenames = /* @__PURE__ */ new Set();
18392
- for (const store of stores) {
18387
+ const planned = stores.map((store) => {
18393
18388
  let filename = basename2(store.sourcePath);
18394
18389
  if (usedFilenames.has(filename)) {
18395
18390
  const sanitizedId = store.storeId.replace(/[^a-zA-Z0-9_.-]/g, "_");
18396
18391
  filename = `${sanitizedId}-${filename}`;
18397
18392
  }
18398
18393
  usedFilenames.add(filename);
18399
- const targetFile = join4(outDir, filename);
18400
- const record2 = backupDatabaseFile(store.sourcePath, targetFile, store.storeId);
18401
- backedUpStores.push(record2);
18394
+ return { storeId: store.storeId, sourcePath: store.sourcePath, backupFile: filename };
18395
+ });
18396
+ return { projectRoot, outDir, createdAt, stores: planned };
18397
+ }
18398
+ function createBackup(options = {}) {
18399
+ const { projectRoot, outDir, createdAt, stores } = planBackup(options);
18400
+ const backupId = `bk_${randomBytes(8).toString("hex")}`;
18401
+ if (!existsSync4(outDir)) {
18402
+ mkdirSync(outDir, { recursive: true, mode: 448 });
18403
+ }
18404
+ const backedUpStores = [];
18405
+ for (const store of stores) {
18406
+ backedUpStores.push(backupDatabaseFile(store.sourcePath, join4(outDir, store.backupFile), store.storeId));
18402
18407
  }
18403
18408
  const manifest = {
18404
18409
  schema: "kxm.backup-manifest.v1",
18405
18410
  backupId,
18406
- createdAt: now.toISOString(),
18411
+ createdAt,
18407
18412
  projectRoot,
18408
18413
  stores: backedUpStores
18409
18414
  };
@@ -18415,7 +18420,7 @@ function createBackup(options = {}) {
18415
18420
  writeFileSync(manifestPath, finalJson, "utf8");
18416
18421
  return { manifest, outDir };
18417
18422
  }
18418
- function restoreBackup(manifestPathOrDir, options = {}) {
18423
+ function planRestore(manifestPathOrDir, options = {}) {
18419
18424
  let manifestPath = resolve3(manifestPathOrDir);
18420
18425
  const stat = lstatSync2(manifestPath, { throwIfNoEntry: false });
18421
18426
  if (!stat) {
@@ -18438,7 +18443,7 @@ function restoreBackup(manifestPathOrDir, options = {}) {
18438
18443
  if (manifest.schema !== "kxm.backup-manifest.v1" || !Array.isArray(manifest.stores) || manifest.stores.length === 0) {
18439
18444
  throw databaseError("restore_manifest_invalid", manifestPath, "manifest is not a valid kxm.backup-manifest.v1 document");
18440
18445
  }
18441
- const restoredStores = [];
18446
+ const stores = [];
18442
18447
  for (const store of manifest.stores) {
18443
18448
  const backupFilePath = join4(manifestDir, store.backupFile);
18444
18449
  if (!existsSync4(backupFilePath)) {
@@ -18452,24 +18457,35 @@ function restoreBackup(manifestPathOrDir, options = {}) {
18452
18457
  `backup file ${store.backupFile} sha256 ${actualSha256} does not match manifest hash ${store.sha256}`
18453
18458
  );
18454
18459
  }
18455
- const maxSupported = kxmBackupCeiling(store.storeId);
18460
+ const maxSupportedVersion = kxmBackupCeiling(store.storeId);
18461
+ if (store.schemaVersion > maxSupportedVersion) {
18462
+ throw databaseError(
18463
+ "runtime_schema_newer",
18464
+ backupFilePath,
18465
+ `backup store ${store.storeId} schema version ${store.schemaVersion} is newer than supported maximum ${maxSupportedVersion}`
18466
+ );
18467
+ }
18456
18468
  let targetPath = store.sourcePath;
18457
18469
  if (options.projectRoot && manifest.projectRoot && targetPath.startsWith(manifest.projectRoot)) {
18458
18470
  const rel = targetPath.slice(manifest.projectRoot.length).replace(/^[\\/]+/, "");
18459
18471
  targetPath = join4(resolve3(options.projectRoot), rel);
18460
18472
  }
18461
- const result = restoreDatabaseFile(
18462
- backupFilePath,
18463
- targetPath,
18464
- store.storeId,
18465
- store.schemaVersion,
18466
- maxSupported
18467
- );
18468
- restoredStores.push(result);
18473
+ stores.push({ storeId: store.storeId, backupFilePath, targetPath, schemaVersion: store.schemaVersion, maxSupportedVersion });
18469
18474
  }
18475
+ return { manifestPath, backupId: manifest.backupId, stores };
18476
+ }
18477
+ function restoreBackup(manifestPathOrDir, options = {}) {
18478
+ const plan = planRestore(manifestPathOrDir, options);
18479
+ const restoredStores = plan.stores.map((store) => restoreDatabaseFile(
18480
+ store.backupFilePath,
18481
+ store.targetPath,
18482
+ store.storeId,
18483
+ store.schemaVersion,
18484
+ store.maxSupportedVersion
18485
+ ));
18470
18486
  return {
18471
- manifestPath,
18472
- backupId: manifest.backupId,
18487
+ manifestPath: plan.manifestPath,
18488
+ backupId: plan.backupId,
18473
18489
  restoredStores
18474
18490
  };
18475
18491
  }
@@ -29003,17 +29019,38 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
29003
29019
  }
29004
29020
  }, 1e3);
29005
29021
  heartbeat.unref();
29006
- for (const reg of registry.projectsForRuntime(activeRuntimeId)) {
29007
- try {
29008
- contextFor(reg.projectRoot);
29009
- } catch {
29010
- logger.warn({
29011
- event: "runtime_sync_context_unavailable",
29012
- projectId: reg.projectId,
29013
- message: `cannot reopen ${reg.projectRoot}: its outbox rows stay pending`
29014
- });
29022
+ const reopenRegisteredProjects = () => {
29023
+ for (const reg of registry.projectsForRuntime(activeRuntimeId)) {
29024
+ const key = projectRuntimeKey(reg.projectRoot);
29025
+ if (contexts.has(key)) continue;
29026
+ try {
29027
+ contextFor(reg.projectRoot);
29028
+ syncStatuses.delete(key);
29029
+ } catch (error) {
29030
+ const reason = syncFailureText(error);
29031
+ const prior = syncStatuses.get(key);
29032
+ if (prior === void 0 || prior.lastError !== reason) {
29033
+ logger.warn({
29034
+ event: "runtime_sync_context_unavailable",
29035
+ projectId: reg.projectId,
29036
+ message: `cannot reopen ${reg.projectRoot}: its outbox rows stay pending`,
29037
+ reason
29038
+ });
29039
+ }
29040
+ syncStatuses.set(key, {
29041
+ projectId: reg.projectId,
29042
+ projectRoot: reg.projectRoot,
29043
+ homeRuntimeId: activeRuntimeId,
29044
+ state: "blocked",
29045
+ outbox: { pending: 0, acked: 0, refused: 0, refusals: [] },
29046
+ storeReadable: false,
29047
+ consecutiveFailures: 1,
29048
+ lastError: reason
29049
+ });
29050
+ }
29015
29051
  }
29016
- }
29052
+ };
29053
+ reopenRegisteredProjects();
29017
29054
  const recordSyncStatus = (context, next) => {
29018
29055
  const key = projectRuntimeKey(context.projectRoot);
29019
29056
  const previous = syncStatuses.get(key);
@@ -29042,6 +29079,7 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
29042
29079
  if (syncing || stopping) return;
29043
29080
  syncing = true;
29044
29081
  void (async () => {
29082
+ reopenRegisteredProjects();
29045
29083
  for (const context of [...contexts.values()]) {
29046
29084
  const key = projectRuntimeKey(context.projectRoot);
29047
29085
  const prior = syncStatuses.get(key);
@@ -31414,7 +31452,9 @@ export {
31414
31452
  parsePiOneShotUsage,
31415
31453
  parseSshConfig,
31416
31454
  persistKxmRunState,
31455
+ planBackup,
31417
31456
  planHarnessUpdate,
31457
+ planRestore,
31418
31458
  probeHarnessAssignment,
31419
31459
  probeHarnessAssignmentAsync,
31420
31460
  probeHarnesses,
@@ -14783,7 +14783,7 @@ import { createHash as createHash5, createHmac } from "node:crypto";
14783
14783
  import { existsSync as existsSync6 } from "node:fs";
14784
14784
  import { createServer } from "node:http";
14785
14785
  import { isIP } from "node:net";
14786
- import { dirname as dirname5, join as join6, resolve as resolve6 } from "node:path";
14786
+ import { dirname as dirname6, join as join6, resolve as resolve6 } from "node:path";
14787
14787
 
14788
14788
  // plugins/kxm/src/protocol.ts
14789
14789
  import { randomUUID } from "node:crypto";
@@ -18213,7 +18213,7 @@ function boundedRefs(value, field) {
18213
18213
  var import_yaml4 = __toESM(require_dist(), 1);
18214
18214
  import { createHash as createHash4 } from "node:crypto";
18215
18215
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, renameSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
18216
- import { join as join4 } from "node:path";
18216
+ import { dirname as dirname4, join as join4 } from "node:path";
18217
18217
  var SKILL_CANDIDATE_SCHEMA = "kxm.skill-candidate.v1";
18218
18218
  var SKILL_EVALUATION_SCHEMA = "kxm.skill-evaluation.v1";
18219
18219
  var SKILL_DECISION_SCHEMA = "kxm.skill-decision.v1";
@@ -18301,10 +18301,22 @@ var SkillLifecycle = class {
18301
18301
  root;
18302
18302
  now;
18303
18303
  allowOptimizationEvals;
18304
+ dryRun;
18305
+ /** What a `dryRun` lifecycle would have written or moved, in order. */
18306
+ planned = [];
18304
18307
  constructor(root, options = {}) {
18305
18308
  this.root = root;
18306
18309
  this.now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
18307
18310
  this.allowOptimizationEvals = options.allowOptimizationEvals === true;
18311
+ this.dryRun = options.dryRun === true;
18312
+ }
18313
+ write(file, content) {
18314
+ if (this.dryRun) {
18315
+ this.planned.push({ action: "write", target: file });
18316
+ return;
18317
+ }
18318
+ mkdirSync2(dirname4(file), { recursive: true });
18319
+ writeFileSync2(file, content);
18308
18320
  }
18309
18321
  dir(state) {
18310
18322
  return join4(this.root, state === "candidate" ? "candidates" : `${state}s`.replace("rejecteds", "rejected").replace("promoteds", "promoted"));
@@ -18317,15 +18329,14 @@ var SkillLifecycle = class {
18317
18329
  return { dir, metadata: join4(dir, "metadata.json"), skill: join4(dir, "SKILL.md") };
18318
18330
  }
18319
18331
  appendHistory(id, record) {
18320
- mkdirSync2(join4(this.root, "history"), { recursive: true });
18321
18332
  const line = `${JSON.stringify(record)}
18322
18333
  `;
18323
18334
  if (existsSync4(this.historyFile(id))) {
18324
18335
  const existing = readFileSync3(this.historyFile(id), "utf8");
18325
18336
  const lines = existing.split("\n").filter((entry) => entry.trim());
18326
- writeFileSync2(this.historyFile(id), [...lines.slice(-499), line.trim()].join("\n") + "\n");
18337
+ this.write(this.historyFile(id), [...lines.slice(-499), line.trim()].join("\n") + "\n");
18327
18338
  } else {
18328
- writeFileSync2(this.historyFile(id), line);
18339
+ this.write(this.historyFile(id), line);
18329
18340
  }
18330
18341
  }
18331
18342
  history(id) {
@@ -18346,6 +18357,10 @@ var SkillLifecycle = class {
18346
18357
  if (!existsSync4(fromDir)) {
18347
18358
  throw new SkillLifecycleError("skill_not_found", `skill ${id} not found in ${from}`);
18348
18359
  }
18360
+ if (this.dryRun) {
18361
+ this.planned.push({ action: "move", target: `${fromDir} -> ${toDir}` });
18362
+ return;
18363
+ }
18349
18364
  mkdirSync2(this.dir(to), { recursive: true });
18350
18365
  if (existsSync4(toDir)) rmSync(toDir, { recursive: true, force: true });
18351
18366
  renameSync(fromDir, toDir);
@@ -18389,7 +18404,7 @@ var SkillLifecycle = class {
18389
18404
  if (!harness) throw new SkillLifecycleError("invalid_skill_compatibility", "compatibility.harness is required");
18390
18405
  const contentSha256 = skillContentSha256(content);
18391
18406
  const id = skillIdFor(name, contentSha256);
18392
- const { dir, metadata, skill } = this.paths("candidate", id);
18407
+ const { metadata, skill } = this.paths("candidate", id);
18393
18408
  if (existsSync4(metadata)) {
18394
18409
  throw new SkillLifecycleError(
18395
18410
  "skill_candidate_exists",
@@ -18409,9 +18424,8 @@ var SkillLifecycle = class {
18409
18424
  createdAt: this.now(),
18410
18425
  ...input.supersedes ? { supersedes: input.supersedes } : {}
18411
18426
  };
18412
- mkdirSync2(dir, { recursive: true });
18413
- writeFileSync2(skill, content);
18414
- writeFileSync2(metadata, `${JSON.stringify(record, null, 2)}
18427
+ this.write(skill, content);
18428
+ this.write(metadata, `${JSON.stringify(record, null, 2)}
18415
18429
  `);
18416
18430
  this.appendHistory(id, { schema: "kxm.skill-history-event.v1", event: "candidate_created", by: createdBy, supersedes: input.supersedes, at: record.createdAt });
18417
18431
  return record;
@@ -18500,18 +18514,15 @@ var SkillLifecycle = class {
18500
18514
  };
18501
18515
  const candidatePaths = this.paths("candidate", candidateId);
18502
18516
  const promotedPaths = this.paths("promoted", candidateId);
18503
- mkdirSync2(promotedPaths.dir, { recursive: true });
18504
18517
  const skillContent = readFileSync3(candidatePaths.skill, "utf8");
18505
18518
  const metadataContent = readFileSync3(candidatePaths.metadata, "utf8");
18506
- writeFileSync2(promotedPaths.skill, skillContent);
18507
- writeFileSync2(promotedPaths.metadata, metadataContent);
18508
- const patchesDir = join4(this.root, "patches");
18509
- mkdirSync2(patchesDir, { recursive: true });
18510
- const patchPath = join4(patchesDir, `${candidateId}.patch`);
18519
+ this.write(promotedPaths.skill, skillContent);
18520
+ this.write(promotedPaths.metadata, metadataContent);
18521
+ const patchPath = join4(this.root, "patches", `${candidateId}.patch`);
18511
18522
  const relSkillPath = `.kxm/skills/promoted/${candidateId}/SKILL.md`;
18512
18523
  const relMetaPath = `.kxm/skills/promoted/${candidateId}/metadata.json`;
18513
18524
  const patch = `${createUnifiedPatch(relSkillPath, skillContent)}${createUnifiedPatch(relMetaPath, metadataContent)}`;
18514
- writeFileSync2(patchPath, patch, "utf8");
18525
+ this.write(patchPath, patch);
18515
18526
  this.appendHistory(candidateId, record);
18516
18527
  return { ...metadata, patch, patchPath };
18517
18528
  }
@@ -19031,7 +19042,7 @@ import {
19031
19042
  unlinkSync,
19032
19043
  writeFileSync as writeFileSync4
19033
19044
  } from "node:fs";
19034
- import { basename as basename3, dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
19045
+ import { basename as basename3, dirname as dirname5, join as join5, resolve as resolve4 } from "node:path";
19035
19046
 
19036
19047
  // plugins/kxm/src/sqlite.ts
19037
19048
  import { createRequire } from "node:module";
@@ -19078,7 +19089,7 @@ function databaseError(code, file, message) {
19078
19089
  return new KxmConfigError([issue2]);
19079
19090
  }
19080
19091
  function checkedParent(path, description) {
19081
- const parent = dirname4(path);
19092
+ const parent = dirname5(path);
19082
19093
  if (!existsSync5(parent)) mkdirSync4(parent, { recursive: true, mode: 448 });
19083
19094
  const stat = lstatSync2(parent, { throwIfNoEntry: false });
19084
19095
  if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
@@ -20359,7 +20370,7 @@ function createMeshHub(options = {}) {
20359
20370
  const logger = options.logger ?? (() => void 0);
20360
20371
  const hubNow = options.now ?? (() => Date.now());
20361
20372
  const assetsDir2 = options.assetsDir;
20362
- const hubRepoRoot = options.repoRoot ?? (options.dataPath && options.dataPath !== ":memory:" ? resolve6(dirname5(dirname5(options.dataPath))) : process.cwd());
20373
+ const hubRepoRoot = options.repoRoot ?? (options.dataPath && options.dataPath !== ":memory:" ? resolve6(dirname6(dirname6(options.dataPath))) : process.cwd());
20363
20374
  const store = new MeshStore(options.dataPath);
20364
20375
  const agents = store.agents;
20365
20376
  const messages = store.messages;
@@ -22556,11 +22567,11 @@ data: ${JSON.stringify({ agent: publicAgent(current, staleAfterMs) })}
22556
22567
 
22557
22568
  // plugins/kxm/src/server.ts
22558
22569
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync5 } from "node:fs";
22559
- import { dirname as dirname7, join as join7, resolve as resolve7 } from "node:path";
22570
+ import { dirname as dirname8, join as join7, resolve as resolve7 } from "node:path";
22560
22571
 
22561
22572
  // plugins/kxm/src/logger.ts
22562
22573
  import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync5, renameSync as renameSync3, statSync as statSync2, unlinkSync as unlinkSync2 } from "node:fs";
22563
- import { dirname as dirname6 } from "node:path";
22574
+ import { dirname as dirname7 } from "node:path";
22564
22575
  var LOG_LEVEL_PRIORITY = {
22565
22576
  debug: 10,
22566
22577
  info: 20,
@@ -22666,7 +22677,7 @@ function createLogger(options) {
22666
22677
  currentSize = 0;
22667
22678
  }
22668
22679
  try {
22669
- mkdirSync5(dirname6(filePath), { recursive: true });
22680
+ mkdirSync5(dirname7(filePath), { recursive: true });
22670
22681
  appendFileSync(filePath, line, { encoding: "utf8", mode: 384 });
22671
22682
  currentSize += lineBytes;
22672
22683
  } catch {
@@ -22729,7 +22740,7 @@ var rateLimitWindowMs = Number.parseInt(
22729
22740
  process.env.KXM_RATE_LIMIT_WINDOW_MS ?? String(DEFAULT_RATE_LIMIT_WINDOW_MS),
22730
22741
  10
22731
22742
  );
22732
- for (const directory of [configDir, logsDir, assetsDir, stateDir, dirname7(logPath)]) {
22743
+ for (const directory of [configDir, logsDir, assetsDir, stateDir, dirname8(logPath)]) {
22733
22744
  mkdirSync6(directory, { recursive: true });
22734
22745
  }
22735
22746
  var structuredLog = createLogger({
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.85",
3
+ "version": "0.7.87",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { join, relative } from "node:path";
2
+ import { join, relative, resolve } from "node:path";
3
3
  import { redactSecrets } from "../redact.ts";
4
4
  import {
5
5
  createMemoryNote,
@@ -10,7 +10,7 @@ import {
10
10
  } from "../memory.ts";
11
11
  import { SkillLifecycle, type SkillEvaluationKind, type SkillState } from "../skills.ts";
12
12
  import { writeCompiledWiki } from "../wiki.ts";
13
- import { print, type Runtime } from "./types.ts";
13
+ import { print, printPlan, type Runtime } from "./types.ts";
14
14
 
15
15
  /** Authenticated hub POST for context operations. The CLI operates as the
16
16
  * control plane: the administrative token scopes one project per request. */
@@ -122,6 +122,15 @@ export async function cmdContextPromote(runtime: Runtime, project: string, propo
122
122
  runtime.io.stderr("context promote --evidence must contain at least one durable evidence reference\n");
123
123
  return 2;
124
124
  }
125
+ if (runtime.dryRun) {
126
+ printPlan(
127
+ runtime,
128
+ { command: "context promote", project, proposalId, evidence },
129
+ [{ action: "request", target: `POST ${runtime.serverUrl.replace(/\/$/, "")}/v1/context/state/promote` }],
130
+ `promote proposal ${proposalId} in ${project}`,
131
+ );
132
+ return 0;
133
+ }
125
134
  const response = await hubContextPost({
126
135
  serverUrl: runtime.serverUrl,
127
136
  path: "/v1/context/state/promote",
@@ -158,6 +167,16 @@ export async function cmdContextWikiCompile(runtime: Runtime, project: string, o
158
167
  return 1;
159
168
  }
160
169
  const compiled = response.body as { audit: { pages: string[]; contradictions: number }; pages: { path: string; content: string }[] };
170
+ if (options.out && runtime.dryRun) {
171
+ const outDir = options.out;
172
+ printPlan(
173
+ runtime,
174
+ { command: "context wiki-compile", project, pages: compiled.audit.pages, openContradictions: compiled.audit.contradictions, outDir },
175
+ compiled.pages.map((page) => ({ action: "write", target: resolve(outDir, page.path) })),
176
+ `write ${compiled.pages.length} compiled wiki page(s) to ${outDir}`,
177
+ );
178
+ return 0;
179
+ }
161
180
  let written: string[] = [];
162
181
  if (options.out) {
163
182
  const pages = new Map(compiled.pages.map((page) => [page.path, page.content]));
@@ -233,7 +252,7 @@ export async function cmdSkillsCreate(runtime: Runtime, options: { file: string;
233
252
  }
234
253
 
235
254
  export async function cmdSkillsEvaluate(runtime: Runtime, skillId: string, options: { kind: string; evaluator: string; fail?: boolean | undefined; score?: string | undefined; details?: string | undefined }): Promise<number> {
236
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
255
+ const lifecycle = new SkillLifecycle(skillsRoot(runtime), { dryRun: runtime.dryRun });
237
256
  try {
238
257
  const outcome = lifecycle.evaluate(skillId, {
239
258
  kind: options.kind as SkillEvaluationKind,
@@ -242,6 +261,10 @@ export async function cmdSkillsEvaluate(runtime: Runtime, skillId: string, optio
242
261
  ...(options.score !== undefined ? { score: Number(options.score) } : {}),
243
262
  ...(options.details ? { details: options.details } : {}),
244
263
  });
264
+ if (runtime.dryRun) {
265
+ printPlan(runtime, { command: "skills evaluate", skillId, quarantined: outcome.quarantined, evaluation: outcome.evaluation }, lifecycle.planned, `record ${options.kind} evaluation${outcome.quarantined ? " (candidate quarantined)" : ""}`);
266
+ return 0;
267
+ }
245
268
  print(runtime.io, runtime.json, { ok: true, command: "skills evaluate", skillId, quarantined: outcome.quarantined, evaluation: outcome.evaluation }, `recorded ${options.kind} evaluation${outcome.quarantined ? " (candidate quarantined)" : ""}`);
246
269
  return 0;
247
270
  } catch (error) {
@@ -252,13 +275,17 @@ export async function cmdSkillsEvaluate(runtime: Runtime, skillId: string, optio
252
275
  }
253
276
 
254
277
  export async function cmdSkillsPromote(runtime: Runtime, skillId: string, options: { decidedBy: string; evidence: string; reason?: string | undefined }): Promise<number> {
255
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
278
+ const lifecycle = new SkillLifecycle(skillsRoot(runtime), { dryRun: runtime.dryRun });
256
279
  try {
257
280
  const promoted = lifecycle.promote(skillId, {
258
281
  decidedBy: options.decidedBy,
259
282
  reason: options.reason ?? "passed protected evaluation",
260
283
  evidenceRefs: csv(options.evidence) ?? [],
261
284
  });
285
+ if (runtime.dryRun) {
286
+ printPlan(runtime, { command: "skills promote", skillId, metadata: promoted, patchPath: promoted.patchPath }, lifecycle.planned, `promote skill ${skillId}`);
287
+ return 0;
288
+ }
262
289
  print(runtime.io, runtime.json, { ok: true, command: "skills promote", skillId, metadata: promoted, patchPath: promoted.patchPath }, `promoted skill ${skillId} (patch: ${promoted.patchPath})`);
263
290
  return 0;
264
291
  } catch (error) {
@@ -269,9 +296,13 @@ export async function cmdSkillsPromote(runtime: Runtime, skillId: string, option
269
296
  }
270
297
 
271
298
  export async function cmdSkillsReject(runtime: Runtime, skillId: string, options: { decidedBy: string; reason?: string | undefined }): Promise<number> {
272
- const lifecycle = new SkillLifecycle(skillsRoot(runtime));
299
+ const lifecycle = new SkillLifecycle(skillsRoot(runtime), { dryRun: runtime.dryRun });
273
300
  try {
274
301
  const metadata = lifecycle.reject(skillId, { decidedBy: options.decidedBy, reason: options.reason ?? "rejected" });
302
+ if (runtime.dryRun) {
303
+ printPlan(runtime, { command: "skills reject", skillId, metadata }, lifecycle.planned, `reject skill ${skillId} (history retained)`);
304
+ return 0;
305
+ }
275
306
  print(runtime.io, runtime.json, { ok: true, command: "skills reject", skillId, metadata }, `rejected skill ${skillId} (history retained)`);
276
307
  return 0;
277
308
  } catch (error) {
@@ -339,8 +370,13 @@ export async function cmdMemoryNote(
339
370
  scope: (options.scope ?? "project") as MemoryScope,
340
371
  ...(options.kind !== undefined ? { kind: options.kind } : {}),
341
372
  ...(options.body !== undefined ? { body: options.body } : {}),
373
+ dryRun: runtime.dryRun,
342
374
  });
343
375
  const relPath = relative(runtime.cwd, path);
376
+ if (runtime.dryRun) {
377
+ printPlan(runtime, { command: "memory note", candidate: record, path: relPath }, [{ action: "write", target: path }], `record memory candidate in ${relPath} (the id is assigned when it is recorded)`);
378
+ return 0;
379
+ }
344
380
  print(
345
381
  runtime.io,
346
382
  runtime.json,
@@ -357,7 +393,16 @@ export async function cmdMemoryNote(
357
393
 
358
394
  export async function cmdMemorySync(runtime: Runtime): Promise<number> {
359
395
  try {
360
- const result = syncHarnessMemory(runtime.cwd);
396
+ const result = syncHarnessMemory(runtime.cwd, { dryRun: runtime.dryRun });
397
+ if (runtime.dryRun) {
398
+ printPlan(
399
+ runtime,
400
+ { command: "memory sync", ...result },
401
+ [...result.created, ...result.updated].map((file) => ({ action: "write", target: join(runtime.cwd, file) })),
402
+ "regenerate project memory blocks in AGENTS.md, CLAUDE.md, and GEMINI.md",
403
+ );
404
+ return 0;
405
+ }
361
406
  print(
362
407
  runtime.io,
363
408
  runtime.json,
@@ -6,7 +6,7 @@ import { defaultProjectName } from "../project-name.ts";
6
6
  import { hasClientHubCredential, resolveClientHubAuthToken } from "../hub-env.ts";
7
7
  import { agentWorker, type Worker } from "../envelope.ts";
8
8
  import { MESH_TUI_PANELS, runMeshTui, type MeshTuiPanel } from "../tui.ts";
9
- import { formatSessionBriefText, loadSessionBriefAsync, type SessionHubStatus } from "../session-work.ts";
9
+ import { formatSessionBriefText, loadSessionBriefAsync, readCachedSessionBrief, type SessionHubStatus } from "../session-work.ts";
10
10
  import {
11
11
  HUB_BINDING_SCHEMA,
12
12
  HubBindingError,
@@ -47,11 +47,13 @@ import {
47
47
  import { createSession, loadNamedWorkers, rosterNames, sessionAssetDirs, workflowAssetDirs, writeSession } from "../session.ts";
48
48
  import {
49
49
  print,
50
+ printPlan,
50
51
  printWorker,
51
52
  hostMode,
52
53
  workspaceEnv,
53
54
  spawnScript,
54
55
  processExists,
56
+ type PlannedChange,
55
57
  type Runtime,
56
58
  } from "./types.ts";
57
59
 
@@ -140,7 +142,7 @@ export async function refreshKxmUpdateNotice(
140
142
  }
141
143
  const fetched = await fetchLatestKxmVersion(resolved.source, runtime.env, runtime.fetchImpl);
142
144
  const notice = noticeFromVersions(installed, fetched.latest, resolved, fetched.error, fetched.asset);
143
- writeUpdateCache(runtime.dirs.state, notice);
145
+ if (!runtime.dryRun) writeUpdateCache(runtime.dirs.state, notice);
144
146
  return notice;
145
147
  }
146
148
 
@@ -556,6 +558,12 @@ export async function cmdSessionStatus(runtime: Runtime): Promise<number> {
556
558
  }
557
559
 
558
560
  export async function cmdAuthToken(runtime: Runtime, options: { status?: boolean | undefined; clear?: boolean | undefined; issue?: boolean | undefined } = {}): Promise<number> {
561
+ const tokenFile = sessionTokenPath(runtime.env.KXM_USER_CONFIG_DIR);
562
+ if (options.clear && runtime.dryRun) {
563
+ const present = existsSync(tokenFile);
564
+ printPlan(runtime, { command: "auth token", cleared: present }, present ? [{ action: "delete", target: tokenFile }] : [], present ? "clear the session token from disk" : "no session token file to clear");
565
+ return 0;
566
+ }
559
567
  if (options.clear) {
560
568
  const cleared = clearSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
561
569
  print(
@@ -615,18 +623,19 @@ export async function cmdAuthToken(runtime: Runtime, options: { status?: boolean
615
623
  return 1;
616
624
  }
617
625
 
626
+ const existingToken = options.issue ? undefined : readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
627
+ if (runtime.dryRun && !existingToken) {
628
+ // The token is only worth printing once it is on disk; a dry run never is.
629
+ printPlan(runtime, { command: "auth token" }, [{ action: "write", target: tokenFile }], "issue an operator session token and persist it");
630
+ return 0;
631
+ }
632
+
618
633
  let token: string;
619
- if (options.issue) {
634
+ if (existingToken) {
635
+ token = existingToken.token;
636
+ } else {
620
637
  token = mintSessionToken({ preset: "operator" });
621
638
  persistSessionTokenToDisk(token, { userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
622
- } else {
623
- const existing = readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
624
- if (existing) {
625
- token = existing.token;
626
- } else {
627
- token = mintSessionToken({ preset: "operator" });
628
- persistSessionTokenToDisk(token, { userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
629
- }
630
639
  }
631
640
 
632
641
  print(runtime.io, runtime.json, { ok: true, command: "auth token", token }, token);
@@ -635,11 +644,17 @@ export async function cmdAuthToken(runtime: Runtime, options: { status?: boolean
635
644
 
636
645
  export async function cmdSessionBrief(runtime: Runtime, options: { status?: boolean | undefined; token?: boolean | undefined } = {}): Promise<number> {
637
646
  const existing = readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
638
- const sessionToken = existing ? existing.token : mintSessionToken({ preset: "operator" });
639
- if (!existing) {
647
+ // A dry run neither mints nor persists a token; it names the file it would write.
648
+ const planned: PlannedChange[] = existing ? [] : [{ action: "write", target: sessionTokenPath(runtime.env.KXM_USER_CONFIG_DIR) }];
649
+ const sessionToken = existing?.token ?? (runtime.dryRun ? undefined : mintSessionToken({ preset: "operator" }));
650
+ if (!existing && sessionToken) {
640
651
  persistSessionTokenToDisk(sessionToken, { userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
641
652
  }
642
653
  if (options.token) {
654
+ if (!sessionToken) {
655
+ printPlan(runtime, { command: "session brief" }, planned, "issue an operator session token and persist it");
656
+ return 0;
657
+ }
643
658
  print(runtime.io, runtime.json, { ok: true, command: "session brief", sessionToken }, sessionToken);
644
659
  return 0;
645
660
  }
@@ -668,9 +683,19 @@ export async function cmdSessionBrief(runtime: Runtime, options: { status?: bool
668
683
 
669
684
  const brief = await loadSessionBriefAsync(runtime.dirs.workdir, env, undefined, hub, {
670
685
  fetchImpl: runtime.fetchImpl,
671
- sessionToken,
686
+ ...(sessionToken ? { sessionToken } : {}),
687
+ writeCache: !runtime.dryRun,
672
688
  });
673
689
 
690
+ if (runtime.dryRun) {
691
+ if (!readCachedSessionBrief(runtime.dirs.state)) planned.push({ action: "write", target: join(runtime.dirs.state, "session-brief.json") });
692
+ print(runtime.io, runtime.json, { ...brief, dryRun: true, planned }, [
693
+ options.status ? brief.statusLine : formatSessionBriefText(brief),
694
+ ...planned.map((change) => `dry run: would ${change.action} ${change.target}`),
695
+ ].join("\n"));
696
+ return 0;
697
+ }
698
+
674
699
  if (options.status) {
675
700
  print(runtime.io, runtime.json, brief, brief.statusLine);
676
701
  return 0;