@kontextmind/kxm 0.7.86 → 0.7.88

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.
Files changed (35) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/docs/configuration.md +2 -0
  3. package/package.json +1 -1
  4. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  5. package/plugins/kxm/dist/cli.js +897 -500
  6. package/plugins/kxm/dist/core.js +2 -2
  7. package/plugins/kxm/dist/extension.js +72 -63
  8. package/plugins/kxm/dist/mcp-server.js +3 -3
  9. package/plugins/kxm/dist/runtime.js +44 -26
  10. package/plugins/kxm/dist/server.js +56 -31
  11. package/plugins/kxm/package.json +1 -1
  12. package/plugins/kxm/src/cli/context-skills.ts +51 -6
  13. package/plugins/kxm/src/cli/hub.ts +39 -14
  14. package/plugins/kxm/src/cli/project.ts +78 -23
  15. package/plugins/kxm/src/cli/roles.ts +48 -4
  16. package/plugins/kxm/src/cli/system.ts +18 -1
  17. package/plugins/kxm/src/cli/tasks.ts +34 -5
  18. package/plugins/kxm/src/cli/types.ts +30 -0
  19. package/plugins/kxm/src/cli/workflows.ts +20 -4
  20. package/plugins/kxm/src/cli.ts +57 -0
  21. package/plugins/kxm/src/commands.ts +2 -2
  22. package/plugins/kxm/src/config.ts +4 -3
  23. package/plugins/kxm/src/context/providers.ts +4 -0
  24. package/plugins/kxm/src/database.ts +72 -33
  25. package/plugins/kxm/src/hub.ts +26 -8
  26. package/plugins/kxm/src/local-snapshot.ts +4 -4
  27. package/plugins/kxm/src/mcp-server.ts +1 -1
  28. package/plugins/kxm/src/memory.ts +10 -7
  29. package/plugins/kxm/src/role.ts +18 -6
  30. package/plugins/kxm/src/session-work.ts +4 -1
  31. package/plugins/kxm/src/skills.ts +29 -15
  32. package/plugins/kxm/src/sqlite.ts +18 -0
  33. package/plugins/kxm/src/state.ts +4 -1
  34. package/plugins/kxm/src/task-manager.ts +20 -13
  35. package/plugins/kxm/src/workflow-manager.ts +12 -4
@@ -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";
@@ -17303,8 +17303,8 @@ var AGENT_COMMANDS = [
17303
17303
  summary: { type: "string", description: "Promotion summary" },
17304
17304
  authority: {
17305
17305
  type: "string",
17306
- enum: ["policy", "instruction", "evidence", "hypothesis"],
17307
- description: "Authority class"
17306
+ enum: ["evidence", "hypothesis"],
17307
+ description: "Authority class; an agent's proposal is peer origin, so evidence at most"
17308
17308
  },
17309
17309
  confidence: {
17310
17310
  type: "string",
@@ -18082,6 +18082,9 @@ var NativeStateProvider = class {
18082
18082
  if (change?.schema !== "kxm.state-change-proposal.v1") {
18083
18083
  throw new ProtocolError(400, "invalid state change proposal schema", "invalid_state_proposal");
18084
18084
  }
18085
+ if (change.origin !== "peer" && change.origin !== "human") {
18086
+ throw new ProtocolError(400, "state change proposal origin must be peer or human", "invalid_state_proposal");
18087
+ }
18085
18088
  const project = requireNonEmpty(change.project, "proposal project");
18086
18089
  const key = requireNonEmpty(change.key, "proposal key");
18087
18090
  const evidenceRefs = boundedRefs(change.evidenceRefs, "proposal evidenceRefs");
@@ -18091,7 +18094,7 @@ var NativeStateProvider = class {
18091
18094
  project,
18092
18095
  summary: change.summary,
18093
18096
  provenance: {
18094
- sourceType: change.proposedBy.startsWith("agent_") ? "peer" : "human",
18097
+ sourceType: change.origin,
18095
18098
  sourceRef: `proposed-by:${change.proposedBy}`
18096
18099
  },
18097
18100
  authority: change.authority,
@@ -18213,7 +18216,7 @@ function boundedRefs(value, field) {
18213
18216
  var import_yaml4 = __toESM(require_dist(), 1);
18214
18217
  import { createHash as createHash4 } from "node:crypto";
18215
18218
  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";
18219
+ import { dirname as dirname4, join as join4 } from "node:path";
18217
18220
  var SKILL_CANDIDATE_SCHEMA = "kxm.skill-candidate.v1";
18218
18221
  var SKILL_EVALUATION_SCHEMA = "kxm.skill-evaluation.v1";
18219
18222
  var SKILL_DECISION_SCHEMA = "kxm.skill-decision.v1";
@@ -18301,10 +18304,22 @@ var SkillLifecycle = class {
18301
18304
  root;
18302
18305
  now;
18303
18306
  allowOptimizationEvals;
18307
+ dryRun;
18308
+ /** What a `dryRun` lifecycle would have written or moved, in order. */
18309
+ planned = [];
18304
18310
  constructor(root, options = {}) {
18305
18311
  this.root = root;
18306
18312
  this.now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
18307
18313
  this.allowOptimizationEvals = options.allowOptimizationEvals === true;
18314
+ this.dryRun = options.dryRun === true;
18315
+ }
18316
+ write(file, content) {
18317
+ if (this.dryRun) {
18318
+ this.planned.push({ action: "write", target: file });
18319
+ return;
18320
+ }
18321
+ mkdirSync2(dirname4(file), { recursive: true });
18322
+ writeFileSync2(file, content);
18308
18323
  }
18309
18324
  dir(state) {
18310
18325
  return join4(this.root, state === "candidate" ? "candidates" : `${state}s`.replace("rejecteds", "rejected").replace("promoteds", "promoted"));
@@ -18317,15 +18332,14 @@ var SkillLifecycle = class {
18317
18332
  return { dir, metadata: join4(dir, "metadata.json"), skill: join4(dir, "SKILL.md") };
18318
18333
  }
18319
18334
  appendHistory(id, record) {
18320
- mkdirSync2(join4(this.root, "history"), { recursive: true });
18321
18335
  const line = `${JSON.stringify(record)}
18322
18336
  `;
18323
18337
  if (existsSync4(this.historyFile(id))) {
18324
18338
  const existing = readFileSync3(this.historyFile(id), "utf8");
18325
18339
  const lines = existing.split("\n").filter((entry) => entry.trim());
18326
- writeFileSync2(this.historyFile(id), [...lines.slice(-499), line.trim()].join("\n") + "\n");
18340
+ this.write(this.historyFile(id), [...lines.slice(-499), line.trim()].join("\n") + "\n");
18327
18341
  } else {
18328
- writeFileSync2(this.historyFile(id), line);
18342
+ this.write(this.historyFile(id), line);
18329
18343
  }
18330
18344
  }
18331
18345
  history(id) {
@@ -18346,6 +18360,10 @@ var SkillLifecycle = class {
18346
18360
  if (!existsSync4(fromDir)) {
18347
18361
  throw new SkillLifecycleError("skill_not_found", `skill ${id} not found in ${from}`);
18348
18362
  }
18363
+ if (this.dryRun) {
18364
+ this.planned.push({ action: "move", target: `${fromDir} -> ${toDir}` });
18365
+ return;
18366
+ }
18349
18367
  mkdirSync2(this.dir(to), { recursive: true });
18350
18368
  if (existsSync4(toDir)) rmSync(toDir, { recursive: true, force: true });
18351
18369
  renameSync(fromDir, toDir);
@@ -18389,7 +18407,7 @@ var SkillLifecycle = class {
18389
18407
  if (!harness) throw new SkillLifecycleError("invalid_skill_compatibility", "compatibility.harness is required");
18390
18408
  const contentSha256 = skillContentSha256(content);
18391
18409
  const id = skillIdFor(name, contentSha256);
18392
- const { dir, metadata, skill } = this.paths("candidate", id);
18410
+ const { metadata, skill } = this.paths("candidate", id);
18393
18411
  if (existsSync4(metadata)) {
18394
18412
  throw new SkillLifecycleError(
18395
18413
  "skill_candidate_exists",
@@ -18409,9 +18427,8 @@ var SkillLifecycle = class {
18409
18427
  createdAt: this.now(),
18410
18428
  ...input.supersedes ? { supersedes: input.supersedes } : {}
18411
18429
  };
18412
- mkdirSync2(dir, { recursive: true });
18413
- writeFileSync2(skill, content);
18414
- writeFileSync2(metadata, `${JSON.stringify(record, null, 2)}
18430
+ this.write(skill, content);
18431
+ this.write(metadata, `${JSON.stringify(record, null, 2)}
18415
18432
  `);
18416
18433
  this.appendHistory(id, { schema: "kxm.skill-history-event.v1", event: "candidate_created", by: createdBy, supersedes: input.supersedes, at: record.createdAt });
18417
18434
  return record;
@@ -18500,18 +18517,15 @@ var SkillLifecycle = class {
18500
18517
  };
18501
18518
  const candidatePaths = this.paths("candidate", candidateId);
18502
18519
  const promotedPaths = this.paths("promoted", candidateId);
18503
- mkdirSync2(promotedPaths.dir, { recursive: true });
18504
18520
  const skillContent = readFileSync3(candidatePaths.skill, "utf8");
18505
18521
  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`);
18522
+ this.write(promotedPaths.skill, skillContent);
18523
+ this.write(promotedPaths.metadata, metadataContent);
18524
+ const patchPath = join4(this.root, "patches", `${candidateId}.patch`);
18511
18525
  const relSkillPath = `.kxm/skills/promoted/${candidateId}/SKILL.md`;
18512
18526
  const relMetaPath = `.kxm/skills/promoted/${candidateId}/metadata.json`;
18513
18527
  const patch = `${createUnifiedPatch(relSkillPath, skillContent)}${createUnifiedPatch(relMetaPath, metadataContent)}`;
18514
- writeFileSync2(patchPath, patch, "utf8");
18528
+ this.write(patchPath, patch);
18515
18529
  this.appendHistory(candidateId, record);
18516
18530
  return { ...metadata, patch, patchPath };
18517
18531
  }
@@ -19031,7 +19045,7 @@ import {
19031
19045
  unlinkSync,
19032
19046
  writeFileSync as writeFileSync4
19033
19047
  } from "node:fs";
19034
- import { basename as basename3, dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
19048
+ import { basename as basename3, dirname as dirname5, join as join5, resolve as resolve4 } from "node:path";
19035
19049
 
19036
19050
  // plugins/kxm/src/sqlite.ts
19037
19051
  import { createRequire } from "node:module";
@@ -19078,7 +19092,7 @@ function databaseError(code, file, message) {
19078
19092
  return new KxmConfigError([issue2]);
19079
19093
  }
19080
19094
  function checkedParent(path, description) {
19081
- const parent = dirname4(path);
19095
+ const parent = dirname5(path);
19082
19096
  if (!existsSync5(parent)) mkdirSync4(parent, { recursive: true, mode: 448 });
19083
19097
  const stat = lstatSync2(parent, { throwIfNoEntry: false });
19084
19098
  if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
@@ -20359,7 +20373,7 @@ function createMeshHub(options = {}) {
20359
20373
  const logger = options.logger ?? (() => void 0);
20360
20374
  const hubNow = options.now ?? (() => Date.now());
20361
20375
  const assetsDir2 = options.assetsDir;
20362
- const hubRepoRoot = options.repoRoot ?? (options.dataPath && options.dataPath !== ":memory:" ? resolve6(dirname5(dirname5(options.dataPath))) : process.cwd());
20376
+ const hubRepoRoot = options.repoRoot ?? (options.dataPath && options.dataPath !== ":memory:" ? resolve6(dirname6(dirname6(options.dataPath))) : process.cwd());
20363
20377
  const store = new MeshStore(options.dataPath);
20364
20378
  const agents = store.agents;
20365
20379
  const messages = store.messages;
@@ -20470,12 +20484,12 @@ function createMeshHub(options = {}) {
20470
20484
  if (agent.project !== project) {
20471
20485
  throw new ProtocolError(403, "context requests are limited to the agent's project", "context_isolation_violation");
20472
20486
  }
20473
- return { project, caller: agent.id };
20487
+ return { project, caller: agent.id, credential: "agent" };
20474
20488
  }
20475
20489
  requireAdminAuth(request);
20476
20490
  const callerHeader = request.headers["x-kxm-caller-id"];
20477
20491
  const caller = typeof callerHeader === "string" && callerHeader.trim() ? callerHeader.trim() : "kxm-admin";
20478
- return { project, caller };
20492
+ return { project, caller, credential: "admin" };
20479
20493
  }
20480
20494
  function requireAdminAuth(request) {
20481
20495
  if (!authToken2 && isLoopback(host2)) return;
@@ -21582,7 +21596,17 @@ data: ${JSON.stringify({ type: "ops", project, topic: "agents", at: nowIso() })}
21582
21596
  const contextStateProposeMatch = url.pathname.match(/^\/v1\/context\/state\/propose$/);
21583
21597
  if (method === "POST" && contextStateProposeMatch) {
21584
21598
  const body = await readJson(request);
21585
- const { project: callerProject, caller: callerId } = contextCallerProject(request, body.project);
21599
+ const { project: callerProject, caller: callerId, credential } = contextCallerProject(request, body.project);
21600
+ if (credential === "admin") requireConfiguredAdminAuth(request, "human-origin state proposals");
21601
+ if (body.proposedBy !== void 0 && body.proposedBy !== callerId) {
21602
+ logger({ event: "security_alert", alert: "state_proposer_mismatch", project: callerProject, callerId, credential });
21603
+ throw new ProtocolError(
21604
+ 403,
21605
+ "proposedBy must name the authenticated caller",
21606
+ "state_proposer_mismatch"
21607
+ );
21608
+ }
21609
+ const origin = credential === "agent" ? "peer" : "human";
21586
21610
  const proposalId = await stateProvider.propose({
21587
21611
  schema: "kxm.state-change-proposal.v1",
21588
21612
  project: callerProject,
@@ -21591,11 +21615,12 @@ data: ${JSON.stringify({ type: "ops", project, topic: "agents", at: nowIso() })}
21591
21615
  authority: parseContextAuthority(body.authority),
21592
21616
  confidence: parseContextConfidence(body.confidence),
21593
21617
  evidenceRefs: boundedStringList(body.evidenceRefs, "evidenceRefs", 32),
21594
- proposedBy: typeof body.proposedBy === "string" && body.proposedBy.trim() ? body.proposedBy.trim() : callerId
21618
+ proposedBy: callerId,
21619
+ origin
21595
21620
  });
21596
21621
  counters.contextRequests += 1;
21597
21622
  publishOps(callerProject, "workflows");
21598
- logger({ event: "context_state_proposed", project: callerProject, proposalId, proposedBy: callerId });
21623
+ logger({ event: "context_state_proposed", project: callerProject, proposalId, proposedBy: callerId, origin });
21599
21624
  json(response, 201, { proposalId });
21600
21625
  return;
21601
21626
  }
@@ -22556,11 +22581,11 @@ data: ${JSON.stringify({ agent: publicAgent(current, staleAfterMs) })}
22556
22581
 
22557
22582
  // plugins/kxm/src/server.ts
22558
22583
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync5 } from "node:fs";
22559
- import { dirname as dirname7, join as join7, resolve as resolve7 } from "node:path";
22584
+ import { dirname as dirname8, join as join7, resolve as resolve7 } from "node:path";
22560
22585
 
22561
22586
  // plugins/kxm/src/logger.ts
22562
22587
  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";
22588
+ import { dirname as dirname7 } from "node:path";
22564
22589
  var LOG_LEVEL_PRIORITY = {
22565
22590
  debug: 10,
22566
22591
  info: 20,
@@ -22666,7 +22691,7 @@ function createLogger(options) {
22666
22691
  currentSize = 0;
22667
22692
  }
22668
22693
  try {
22669
- mkdirSync5(dirname6(filePath), { recursive: true });
22694
+ mkdirSync5(dirname7(filePath), { recursive: true });
22670
22695
  appendFileSync(filePath, line, { encoding: "utf8", mode: 384 });
22671
22696
  currentSize += lineBytes;
22672
22697
  } catch {
@@ -22729,7 +22754,7 @@ var rateLimitWindowMs = Number.parseInt(
22729
22754
  process.env.KXM_RATE_LIMIT_WINDOW_MS ?? String(DEFAULT_RATE_LIMIT_WINDOW_MS),
22730
22755
  10
22731
22756
  );
22732
- for (const directory of [configDir, logsDir, assetsDir, stateDir, dirname7(logPath)]) {
22757
+ for (const directory of [configDir, logsDir, assetsDir, stateDir, dirname8(logPath)]) {
22733
22758
  mkdirSync6(directory, { recursive: true });
22734
22759
  }
22735
22760
  var structuredLog = createLogger({
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.86",
3
+ "version": "0.7.88",
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;