@kontextmind/kxm 0.7.53 → 0.7.55

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.
@@ -12,7 +12,6 @@ import {
12
12
  type KxmInitializationPlan,
13
13
  } from "../project-config.ts";
14
14
  import { initializeKxmProject } from "../init.ts";
15
- import { applyKxmMigration, planKxmMigration, verifyKxmMigration } from "../migrate.ts";
16
15
  import { diffKxmProjectAgainstRevision, formatKxmPermissionDiff } from "../permission.ts";
17
16
  import { readKxmLocalBindings, kxmUserStateRoot } from "../bindings.ts";
18
17
  import { loadKxmProject } from "../project-config.ts";
@@ -151,8 +150,8 @@ export async function cmdKxmInit(
151
150
  if (runtime.dryRun) {
152
151
  return finishInit(0, `init plan: ${initialized.plan.mode}`);
153
152
  }
154
- const next = initialized.plan.mode === "migrate"
155
- ? "legacy state requires reviewed migration; conversion is not available in this implementation slice"
153
+ const next = initialized.plan.mode === "legacy"
154
+ ? "legacy state is not migrated by this build: initialise a fresh project directory and copy the YAML definitions you want to keep"
156
155
  : initialized.repairPlan?.issues.length
157
156
  ? "managed-template repair is blocked by conflicts or authority changes; local files were preserved"
158
157
  : "partial or provenance-free KXM state requires explicit repair; no files were overwritten";
@@ -176,136 +175,6 @@ export async function cmdKxmInit(
176
175
  }
177
176
  }
178
177
 
179
- export async function cmdKxmMigratePlan(runtime: Runtime): Promise<number> {
180
- if (runtime.workspaceFlag !== undefined) {
181
- print(runtime.io, runtime.json, {
182
- ok: false,
183
- command: "migrate plan",
184
- error: "workspace_option_unsupported",
185
- }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
186
- return 2;
187
- }
188
- try {
189
- const result = planKxmMigration(runtime.cwd, {});
190
- const ambiguities = (result.plan.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
191
- const unmapped = (result.plan.unmapped as unknown[] | undefined) ?? [];
192
- const payload = {
193
- ok: result.plan.canApply === true,
194
- command: "migrate plan",
195
- plan: result.plan,
196
- plannedOnly: result.plan.canApply !== true,
197
- };
198
- if (result.plan.canApply === true) {
199
- print(runtime.io, runtime.json, payload, `migration plan: ${ambiguities.length} ambiguities, ${unmapped.length} preserved fields; ready to apply`);
200
- return 0;
201
- }
202
- print(
203
- runtime.io,
204
- runtime.json,
205
- payload,
206
- `migration plan requires ${ambiguities.length} reviewed decision(s):\n${ambiguities.map((candidate) => ` - ${candidate.key}: ${candidate.message}`).join("\n")}`,
207
- );
208
- return 1;
209
- } catch (error) {
210
- if (error instanceof KxmConfigError) {
211
- print(runtime.io, runtime.json, { ok: false, command: "migrate plan", error: "migration_plan_failed", issues: error.issues }, `migration plan failed: ${error.message}`);
212
- return 1;
213
- }
214
- print(runtime.io, runtime.json, { ok: false, command: "migrate plan", error: "migration_plan_io_failed" }, "migration plan failed because a local filesystem operation did not complete");
215
- return 1;
216
- }
217
- }
218
-
219
- export async function cmdKxmMigrateApply(runtime: Runtime, options: { decisions?: string | undefined; projectId?: string | undefined; name?: string | undefined }): Promise<number> {
220
- if (runtime.workspaceFlag !== undefined) {
221
- print(runtime.io, runtime.json, {
222
- ok: false,
223
- command: "migrate apply",
224
- error: "workspace_option_unsupported",
225
- }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
226
- return 2;
227
- }
228
- try {
229
- const result = applyKxmMigration(runtime.cwd, {
230
- ...(options.decisions?.trim() ? { decisionsFile: options.decisions.trim() } : {}),
231
- ...(options.projectId?.trim() ? { projectId: options.projectId.trim() } : {}),
232
- ...(options.name?.trim() ? { projectName: options.name.trim() } : {}),
233
- localStateRoot: kxmUserStateRoot({ env: runtime.env }),
234
- dryRun: runtime.dryRun,
235
- });
236
- const payload = {
237
- ok: result.action !== "planned" || (runtime.dryRun === true && result.plan?.canApply === true),
238
- command: "migrate apply",
239
- action: result.action,
240
- files: result.files,
241
- ...(result.configRevision ? { configRevision: result.configRevision } : {}),
242
- ...(result.receiptPath ? { receiptPath: result.receiptPath } : {}),
243
- plannedOnly: result.action === "planned",
244
- };
245
- if (result.action === "applied") {
246
- print(runtime.io, runtime.json, payload, `migration applied: ${result.files.length} resources installed, receipt at ${result.receiptPath ?? ""}`);
247
- return 0;
248
- }
249
- if (result.action === "already-migrated") {
250
- print(runtime.io, runtime.json, payload, "migration receipt already exists; nothing to apply");
251
- return 0;
252
- }
253
- if (runtime.dryRun && result.plan?.canApply === true) {
254
- print(runtime.io, runtime.json, payload, `migration dry run: ${result.files.length} resources would be installed`);
255
- return 0;
256
- }
257
- const ambiguities = (result.plan?.ambiguities as Array<{ key: string; message: string }> | undefined) ?? [];
258
- print(
259
- runtime.io,
260
- runtime.json,
261
- { ...payload, plan: result.plan },
262
- `migration blocked by ${ambiguities.length} unresolved decision(s); review 'kxm migrate plan' and pass --decisions:\n${ambiguities.map((candidate) => ` - ${candidate.key}: ${candidate.message}`).join("\n")}`,
263
- );
264
- return 1;
265
- } catch (error) {
266
- if (error instanceof KxmConfigError) {
267
- print(runtime.io, runtime.json, { ok: false, command: "migrate apply", error: "migration_apply_failed", issues: error.issues }, `migration apply failed: ${error.message}`);
268
- return 1;
269
- }
270
- print(runtime.io, runtime.json, { ok: false, command: "migrate apply", error: "migration_apply_io_failed" }, "migration apply failed because a local filesystem operation did not complete");
271
- return 1;
272
- }
273
- }
274
-
275
- export async function cmdKxmMigrateVerify(runtime: Runtime): Promise<number> {
276
- if (runtime.workspaceFlag !== undefined) {
277
- print(runtime.io, runtime.json, {
278
- ok: false,
279
- command: "migrate verify",
280
- error: "workspace_option_unsupported",
281
- }, "kxm migrate discovers the authoritative Git root from the current directory; --workspace is not supported");
282
- return 2;
283
- }
284
- let result: ReturnType<typeof verifyKxmMigration>;
285
- try {
286
- result = verifyKxmMigration(runtime.cwd, {});
287
- } catch (error) {
288
- if (error instanceof KxmConfigError) {
289
- print(runtime.io, runtime.json, { ok: false, command: "migrate verify", error: "migration_verify_failed", issues: error.issues }, `migration verification failed: ${error.message}`);
290
- return 1;
291
- }
292
- print(runtime.io, runtime.json, { ok: false, command: "migrate verify", error: "migration_verify_io_failed" }, "migration verification failed because a local filesystem operation did not complete");
293
- return 1;
294
- }
295
- const payload = {
296
- ok: result.ok,
297
- command: "migrate verify",
298
- ...(result.configRevision ? { configRevision: result.configRevision } : {}),
299
- issues: result.issues,
300
- };
301
- if (result.ok) {
302
- print(runtime.io, runtime.json, payload, `migration receipt verified: legacy sources unchanged, target bundle matches ${result.configRevision ?? ""}`);
303
- return 0;
304
- }
305
- print(runtime.io, runtime.json, payload, `migration verification failed:\n${result.issues.map((issue) => ` - ${issue.file}: ${issue.code}: ${issue.message}`).join("\n")}`);
306
- return 1;
307
- }
308
-
309
178
  export async function cmdBackup(runtime: Runtime, options: { out?: string | undefined }): Promise<number> {
310
179
  try {
311
180
  const { manifest, outDir } = createBackup({
@@ -7,7 +7,7 @@
7
7
  * - Role & workflow governance (`kxm role`, `kxm workflow`, `kxm gate`, `kxm signal`)
8
8
  * - Task & goal management (`kxm goal`, `kxm task`, `kxm suggest`, `kxm studio`)
9
9
  * - Knowledge, skills, and memory (`kxm context`, `kxm skills`, `kxm memory`)
10
- * - KXM runtime & initialization (`kxm init`, `kxm migrate`, `kxm trust`, `kxm run`, `kxm harness`)
10
+ * - KXM runtime & initialization (`kxm init`, `kxm trust`, `kxm run`, `kxm harness`)
11
11
  * - Hub, workers, and dashboard (`kxm hub`, `kxm worker`, `kxm dash`, `kxm session`, `kxm auth`)
12
12
  * - System, update, and configuration (`kxm update`, `kxm config`, `kxm completion`, `kxm improve`)
13
13
  */
@@ -98,10 +98,7 @@ import {
98
98
 
99
99
  import {
100
100
  cmdKxmInit,
101
- cmdKxmMigratePlan,
102
- cmdKxmMigrateApply,
103
- cmdKxmMigrateVerify,
104
- cmdBackup,
101
+ cmdBackup,
105
102
  cmdRestore,
106
103
  cmdKxmTrust,
107
104
  cmdKxmRun,
@@ -334,7 +331,7 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
334
331
  .helpCommand("help", "Show help");
335
332
  addGlobalOptions(program);
336
333
 
337
- program.command("init").description("Create, validate, or plan migration of a KXM project")
334
+ program.command("init").description("Create, validate, repair, or join a KXM project")
338
335
  .option("--json", "Print machine-readable JSON")
339
336
  .option("--dry-run", "Plan without making changes")
340
337
  .option("--name <name>", "Project display name for a new project")
@@ -347,24 +344,6 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
347
344
  });
348
345
  });
349
346
 
350
- const migrate = addGlobalOptions(program.command("migrate").description("Plan, apply, and verify legacy JSON configuration migration"));
351
- migrate.helpCommand("help", "Show migrate help");
352
- addGlobalOptions(migrate.command("plan").description("Compute the deterministic legacy-to-KXM migration plan without writes"))
353
- .action(async function migratePlanAction(this: Command) {
354
- result.code = await cmdKxmMigratePlan(runtimeFrom(ctx, this));
355
- });
356
- addGlobalOptions(migrate.command("apply").description("Install a reviewed migration with a hash-linked receipt"))
357
- .option("--decisions <file>", "Reviewed kxm.migration-decision.v1 YAML file")
358
- .option("--project-id <id>", "Stable project ID for controlled provisioning")
359
- .option("--name <name>", "Project display name")
360
- .action(async function migrateApplyAction(this: Command, options: { decisions?: string; projectId?: string; name?: string }) {
361
- result.code = await cmdKxmMigrateApply(runtimeFrom(ctx, this), options);
362
- });
363
- addGlobalOptions(migrate.command("verify").description("Verify a migration receipt against current sources and target bundle"))
364
- .action(async function migrateVerifyAction(this: Command) {
365
- result.code = await cmdKxmMigrateVerify(runtimeFrom(ctx, this));
366
- });
367
-
368
347
  addGlobalOptions(program.command("backup").description("Create a verified SQLite backup of all stores with a hashed manifest"))
369
348
  .option("--out <dir>", "Directory to write backup and manifest")
370
349
  .action(async function backupAction(this: Command, options: { out?: string }) {
@@ -179,7 +179,7 @@ export function openDatabase(file: string, description: string, spec: DatabaseSc
179
179
  throw databaseError(
180
180
  "runtime_schema_outdated",
181
181
  file,
182
- `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file (or re-run \`kxm init\`) to start fresh — upgrading old state in place is deliberately unsupported`,
182
+ `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for registry/event stores); \`kxm init\` is project-only and rebuilds no database — upgrading old state in place is deliberately unsupported`,
183
183
  );
184
184
  }
185
185
 
@@ -207,9 +207,18 @@ function initializeKxmProjectAtGitRoot(
207
207
  const loaderOptions = configOptions(options, repositoryBindings);
208
208
  const transactionOptions = repairOptions(options, repositoryBindings);
209
209
 
210
+ const plan = planKxmInitialization(start, loaderOptions);
211
+ if (plan.mode === "legacy") {
212
+ // Classified **before** any transaction or lock work. A legacy tree used to reach the
213
+ // recovery branch first, so an interrupted create/repair journal made ordinary init
214
+ // acquire the project mutation lock, clean or resume the transaction, and write files
215
+ // while reporting `mode: "legacy"`. Nothing is resumed, cleaned, or written here: the
216
+ // pending journal stays exactly as it is until the legacy inputs are dealt with.
217
+ return { action: "planned", plan, ...(plan.projectRoot ? { projectRoot: plan.projectRoot } : {}), files: [] };
218
+ }
219
+
210
220
  if (hasKxmInitTransaction(gitRoot)) {
211
221
  const operation = inspectKxmInitTransaction(gitRoot, options.schemasDir);
212
- const plan = planKxmInitialization(start, loaderOptions);
213
222
  if (!operation) {
214
223
  if (options.dryRun) return { action: "planned", plan, projectRoot: gitRoot, resumePending: true, files: [] };
215
224
  if (!mutationLock) throw new Error("project mutation lock is required to clean an empty transaction");
@@ -267,11 +276,6 @@ function initializeKxmProjectAtGitRoot(
267
276
  }
268
277
  }
269
278
 
270
- const plan = planKxmInitialization(start, loaderOptions);
271
- if (plan.mode === "migrate") {
272
- return { action: "planned", plan, ...(plan.projectRoot ? { projectRoot: plan.projectRoot } : {}), files: [] };
273
- }
274
-
275
279
  if (plan.mode === "repair") {
276
280
  const projectRoot = plan.projectRoot ?? gitRoot;
277
281
  let templateRepair: KxmTemplateRepairPlan | undefined;
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
8
8
  import { deliverInboxNotification } from "./inbox.ts";
9
9
  import type { HubEvent, MessageRecord } from "./protocol.ts";
10
10
 
11
- const VERSION = "0.7.53";
11
+ const VERSION = "0.7.55";
12
12
  const inbox = new Map<string, MessageRecord>();
13
13
  const notifiedInbox = new Set<string>();
14
14
  let meshClient: HubClient | undefined;
@@ -82,20 +82,62 @@ function extractText(content: unknown): string {
82
82
  return "";
83
83
  }
84
84
 
85
- function determineOutcome(text: string, allowedOutcomes: readonly string[]): string {
86
- const normalized = text.trim();
87
- const jsonMatch = /"outcome"\s*:\s*"([^"]+)"/.exec(normalized);
88
- if (jsonMatch && allowedOutcomes.includes(jsonMatch[1]!)) {
89
- return jsonMatch[1]!;
85
+ function asJsonObject(text: string): Record<string, unknown> | undefined {
86
+ try {
87
+ const parsed: unknown = JSON.parse(text);
88
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;
89
+ } catch {
90
+ return undefined;
90
91
  }
91
- for (const outcome of allowedOutcomes) {
92
- const regex = new RegExp(`\\b${outcome}\\b`, "i");
93
- if (regex.test(normalized)) {
94
- return outcome;
95
- }
92
+ }
93
+
94
+ function outcomeField(value: Record<string, unknown>): string | undefined {
95
+ const outcome = value.outcome;
96
+ return typeof outcome === "string" ? outcome : undefined;
97
+ }
98
+
99
+ /**
100
+ * The result a reply actually declares, or `undefined` when it declares none.
101
+ *
102
+ * Two shapes count: the whole reply is one JSON object, or a **standalone** result object on a
103
+ * line of its own — and when there is more than one of those, the **last** one wins, because
104
+ * that is where a reply puts its answer after showing an example. If anything in the tail after
105
+ * that declaration still looks like an outcome key, the reply is **ambiguous and settles
106
+ * `failed`**. What is deliberately not accepted: an outcome *word* anywhere in prose, and an
107
+ * object embedded mid-sentence, so
108
+ * `Example: {"outcome": "passed"}. Actual result: {"outcome": "failed"}` declares nothing at
109
+ * all and settles as `failed` rather than letting the illustration outrank the answer.
110
+ */
111
+ function declaredOutcomeOf(text: string): string | undefined {
112
+ const trimmed = text.trim();
113
+ if (!trimmed) return undefined;
114
+ const whole = asJsonObject(trimmed);
115
+ if (whole) return outcomeField(whole);
116
+ const lines = trimmed.split(/\r?\n/);
117
+ let declaration: { index: number; outcome: string } | undefined;
118
+ for (let index = 0; index < lines.length; index += 1) {
119
+ const outcome = outcomeField(asJsonObject(lines[index]!.trim()) ?? {});
120
+ if (outcome !== undefined) declaration = { index, outcome };
96
121
  }
97
- if (allowedOutcomes.includes("passed")) return "passed";
98
- return allowedOutcomes[0] ?? "completed";
122
+ if (!declaration) return undefined;
123
+ // Ambiguity after the declaration fails closed. Anything in the tail that still looks like
124
+ // an outcome key — an inline `Actual result: {"outcome": "failed"}` on the next line, a
125
+ // pretty-printed object, or a second mention — means we cannot tell which one the reply is
126
+ // reporting, and guessing is exactly the behaviour this function exists to remove. Trailing
127
+ // prose that says nothing about outcomes is fine, which is what lets a real reply put its
128
+ // usage or sign-off after the result block.
129
+ const tail = lines.slice(declaration.index + 1).join("\n");
130
+ if (/"outcome"\s*:/.test(tail)) return undefined;
131
+ return declaration.outcome;
132
+ }
133
+
134
+ function determineOutcome(text: string, allowedOutcomes: readonly string[]): string {
135
+ // Structured result only. What does **not** count: an outcome *word* anywhere in the text —
136
+ // "the tests did not pass" used to settle a step as `passed` — and an empty or unstructured
137
+ // reply, which used to default to success. Undeclared here means `failed`; if the step does
138
+ // not declare `failed` the engine records `outcome_unknown` and terminates as `failed` anyway.
139
+ const declared = declaredOutcomeOf(text);
140
+ return declared !== undefined && allowedOutcomes.includes(declared) ? declared : "failed";
99
141
  }
100
142
 
101
143
  export class PiSession {
@@ -255,9 +297,11 @@ export class PiSession {
255
297
  const isAborted = active.aborted || active.signal?.aborted;
256
298
  let outcome: string;
257
299
  if (isAborted) {
258
- outcome = active.allowedOutcomes.includes("cancelled")
259
- ? "cancelled"
260
- : (active.allowedOutcomes.includes("failed") ? "failed" : active.allowedOutcomes[0]!);
300
+ // A cancel is a cancel. The old chain fell through to `allowedOutcomes[0]` when the
301
+ // step declared neither `cancelled` nor `failed`, so aborting a step whose only
302
+ // declared outcome was `passed` reported `passed`. Returning `cancelled` instead lets
303
+ // the engine record `outcome_unknown` and terminate `failed` — never a borrowed success.
304
+ outcome = "cancelled";
261
305
  } else {
262
306
  outcome = determineOutcome(active.text, active.allowedOutcomes);
263
307
  }
@@ -316,8 +360,8 @@ export class PiSession {
316
360
  throw new Error("pi_session_busy");
317
361
  }
318
362
  if (signal?.aborted) {
319
- const outcome = allowedOutcomes.includes("cancelled") ? "cancelled" : (allowedOutcomes.includes("failed") ? "failed" : allowedOutcomes[0]!);
320
- return { outcome, text: "aborted", usage: {} };
363
+ // Same rule as the in-flight abort: never fall back to the first declared outcome.
364
+ return { outcome: "cancelled", text: "aborted", usage: {} };
321
365
  }
322
366
 
323
367
  this.status = "busy";