@kontextmind/kxm 0.7.103 → 0.7.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +39 -8
- package/docs/concepts/data-and-storage.md +1 -1
- package/docs/contributing/test-matrix.md +11 -6
- package/docs/operations/backup-and-restore.md +59 -27
- package/docs/operations/deploy.md +1 -1
- package/docs/reference/cli-reference.md +73 -55
- package/docs/reference/config-reference.md +1 -1
- package/docs/reference/workflow-definitions.md +4 -4
- package/docs/start/first-workflow.md +6 -2
- package/docs/start/quickstart-claude-code.md +11 -1
- package/docs/templates/runbook.md +2 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +1802 -1011
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +322 -292
- package/plugins/kxm/dist/runtime.js +427 -351
- package/plugins/kxm/dist/server.js +78 -78
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +14 -7
- package/plugins/kxm/src/cli/project.ts +132 -30
- package/plugins/kxm/src/cli/system.ts +19 -1
- package/plugins/kxm/src/cli/tasks.ts +55 -31
- package/plugins/kxm/src/cli/workflows.ts +52 -54
- package/plugins/kxm/src/cli.ts +8 -6
- package/plugins/kxm/src/database.ts +193 -65
- package/plugins/kxm/src/engine.ts +79 -3
- package/plugins/kxm/src/harness.ts +6 -5
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/runtime-paths.ts +50 -0
- package/plugins/kxm/src/runtime-store.ts +22 -53
- package/plugins/kxm/src/runtime-supervisor.ts +38 -17
- package/plugins/kxm/src/suggest.ts +132 -79
- package/plugins/kxm/src/workflow-manager.ts +42 -19
- package/schemas/backup-manifest.schema.json +15 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
|
-
import { createBackup, planBackup, planRestore,
|
|
6
|
+
import { applyRestorePlan, createBackup, databaseError, planBackup, planRestore, type RestorePlan } from "../database.ts";
|
|
7
|
+
import { readLiveHubClaim } from "../hub-autostart.ts";
|
|
7
8
|
import { refreshModelInventory } from "../model-inventory.ts";
|
|
8
9
|
import { listInventoryModels, listRoleBindings, loadRoutePolicy, setRouteState, updateRouteState } from "../routes.ts";
|
|
9
10
|
import {
|
|
@@ -15,6 +16,7 @@ import { initializeKxmProject } from "../init.ts";
|
|
|
15
16
|
import { diffKxmProjectAgainstRevision, formatKxmPermissionDiff } from "../permission.ts";
|
|
16
17
|
import { readKxmLocalBindings, kxmUserStateRoot } from "../bindings.ts";
|
|
17
18
|
import { loadKxmProject } from "../project-config.ts";
|
|
19
|
+
import { kxmLiveRunPrerequisites, type KxmRunHandoff } from "../engine.ts";
|
|
18
20
|
import {
|
|
19
21
|
attachKxmSupervisor,
|
|
20
22
|
ensureKxmSupervisor,
|
|
@@ -114,6 +116,13 @@ export async function cmdKxmInit(
|
|
|
114
116
|
localStateRoot: kxmUserStateRoot({ env: runtime.env }),
|
|
115
117
|
dryRun: runtime.dryRun,
|
|
116
118
|
});
|
|
119
|
+
const starterGuidance = initialized.action === "created" || (initialized.action === "planned" && initialized.plan.mode === "create")
|
|
120
|
+
? [
|
|
121
|
+
"defaultHarness: pi and the npm test gate are generic starter settings, not repository detection.",
|
|
122
|
+
"For Claude, set defaultHarness: claude in .kxm/project.yaml, update any explicit harness overrides in .kxm/agents/*.yaml, and configure compatible agent models.",
|
|
123
|
+
"For .NET or other non-npm repositories, set gates.test.argv in .kxm/gates.yaml to the repository's actual test runner before driving a workflow.",
|
|
124
|
+
]
|
|
125
|
+
: [];
|
|
117
126
|
const payload = {
|
|
118
127
|
ok: initialized.action !== "planned" || runtime.dryRun,
|
|
119
128
|
command: "init",
|
|
@@ -127,9 +136,10 @@ export async function cmdKxmInit(
|
|
|
127
136
|
...(initialized.resumePending === undefined ? {} : { resumePending: initialized.resumePending }),
|
|
128
137
|
...(initialized.transactionKind === undefined ? {} : { transactionKind: initialized.transactionKind }),
|
|
129
138
|
plannedOnly: initialized.action === "planned",
|
|
139
|
+
...(starterGuidance.length > 0 ? { guidance: starterGuidance } : {}),
|
|
130
140
|
};
|
|
131
141
|
const finishInit = (code: number, text: string): number => {
|
|
132
|
-
print(runtime.io, runtime.json, payload, text);
|
|
142
|
+
print(runtime.io, runtime.json, payload, [text, ...starterGuidance].join("\n"));
|
|
133
143
|
return code;
|
|
134
144
|
};
|
|
135
145
|
if (initialized.action === "created") {
|
|
@@ -181,23 +191,30 @@ export async function cmdKxmInit(
|
|
|
181
191
|
}
|
|
182
192
|
}
|
|
183
193
|
|
|
184
|
-
|
|
194
|
+
/** The checkout backup and restore act for, found as the Runtime finds it, so the
|
|
195
|
+
* project's event store key is the one the Runtime derives. */
|
|
196
|
+
function backupProjectRoot(runtime: Runtime): string {
|
|
197
|
+
return discoverKxmProjectRoot(runtime.cwd) ?? runtime.cwd;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function cmdBackup(runtime: Runtime, options: { out?: string | undefined; allProjects?: boolean | undefined }): Promise<number> {
|
|
185
201
|
try {
|
|
186
202
|
const backupOptions = {
|
|
187
|
-
projectRoot: runtime
|
|
203
|
+
projectRoot: backupProjectRoot(runtime),
|
|
188
204
|
env: runtime.env,
|
|
205
|
+
...(options.allProjects === true ? { allProjects: true } : {}),
|
|
189
206
|
...(options.out ? { outDir: resolve(runtime.cwd, options.out) } : {}),
|
|
190
207
|
};
|
|
191
208
|
if (runtime.dryRun) {
|
|
192
209
|
const plan = planBackup(backupOptions);
|
|
193
210
|
printPlan(
|
|
194
211
|
runtime,
|
|
195
|
-
{ command: "backup", outDir: plan.outDir, stores: plan.stores, files: plan.files },
|
|
212
|
+
{ command: "backup", scope: plan.scope, outDir: plan.outDir, stores: plan.stores, files: plan.files },
|
|
196
213
|
[
|
|
197
214
|
...[...plan.stores, ...plan.files].map((entry) => ({ action: "write" as const, target: join(plan.outDir, entry.backupFile) })),
|
|
198
215
|
{ action: "write", target: join(plan.outDir, "manifest.json") },
|
|
199
216
|
],
|
|
200
|
-
`back up ${plan.stores.length} store(s) and ${plan.files.length} file(s) to ${plan.outDir} (sources are not opened, so their WAL is not checkpointed)`,
|
|
217
|
+
`back up ${plan.stores.length} store(s) and ${plan.files.length} file(s) (${plan.scope} scope) to ${plan.outDir} (sources are not opened, so their WAL is not checkpointed)`,
|
|
201
218
|
);
|
|
202
219
|
return 0;
|
|
203
220
|
}
|
|
@@ -212,7 +229,7 @@ export async function cmdBackup(runtime: Runtime, options: { out?: string | unde
|
|
|
212
229
|
};
|
|
213
230
|
const summary = [
|
|
214
231
|
complete
|
|
215
|
-
? `Created SQLite backup with ${manifest.stores.length} store(s):`
|
|
232
|
+
? `Created SQLite backup with ${manifest.stores.length} store(s) (${manifest.scope ?? "project"} scope):`
|
|
216
233
|
: `Backup is incomplete (${manifest.omitted?.length ?? 0} omitted); not ok:`,
|
|
217
234
|
...manifest.stores.map((s) => ` - ${s.storeId}: ${s.sourcePath} -> ${s.backupFile} (schema v${s.schemaVersion}, ${s.bytes} bytes, sha256 ${s.sha256.slice(0, 12)}...)`),
|
|
218
235
|
...(manifest.omitted ?? []).map((id) => ` - omitted ${id}`),
|
|
@@ -230,16 +247,63 @@ export async function cmdBackup(runtime: Runtime, options: { out?: string | unde
|
|
|
230
247
|
}
|
|
231
248
|
}
|
|
232
249
|
|
|
233
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Refuse while anything holds the stores a restore would overwrite. Replacing a
|
|
252
|
+
* SQLite file under an open connection loses the writer's next commit or corrupts
|
|
253
|
+
* the store, and the supervisor holds the registry and every project's event store
|
|
254
|
+
* open. Reads only: the registry is opened read-only and the hub claim is a file,
|
|
255
|
+
* so the check is the same under --dry-run.
|
|
256
|
+
*/
|
|
257
|
+
function assertRestoreTargetsStopped(runtime: Runtime, plan: RestorePlan): void {
|
|
258
|
+
const paths = kxmRuntimePaths({ env: runtime.env });
|
|
259
|
+
let supervisor: ReturnType<typeof kxmSupervisorStatus>;
|
|
234
260
|
try {
|
|
261
|
+
supervisor = kxmSupervisorStatus(paths, { readOnly: true });
|
|
262
|
+
} catch (error) {
|
|
263
|
+
// Fail closed, but name the way out: a registry too broken to read is also one a
|
|
264
|
+
// restore may be meant to replace.
|
|
265
|
+
throw databaseError(
|
|
266
|
+
"restore_runtime_unverified",
|
|
267
|
+
paths.registryDb,
|
|
268
|
+
`cannot read the Runtime registry to check whether the supervisor is running (${error instanceof Error ? error.message : String(error)}); stop the Runtime, move the registry aside, and restore again`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (supervisor.running) {
|
|
272
|
+
throw databaseError(
|
|
273
|
+
"restore_runtime_running",
|
|
274
|
+
paths.registryDb,
|
|
275
|
+
`the Runtime supervisor is running (pid ${String(supervisor.pid)}); stop it with \`kxm runtime stop\` and keep it stopped until the restore finishes`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
for (const store of plan.stores) {
|
|
279
|
+
if (store.storeId !== "hub-store") continue;
|
|
280
|
+
const claim = readLiveHubClaim(dirname(store.targetPath));
|
|
281
|
+
if (claim) {
|
|
282
|
+
throw databaseError(
|
|
283
|
+
"restore_hub_running",
|
|
284
|
+
store.targetPath,
|
|
285
|
+
`a hub (pid ${String(claim.pid)}) is running on ${store.targetPath}; stop it with \`kxm hub stop\` before restoring`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function cmdRestore(runtime: Runtime, manifestArg: string, options: { allProjects?: boolean | undefined } = {}): Promise<number> {
|
|
292
|
+
try {
|
|
293
|
+
const plan = planRestore(resolve(runtime.cwd, manifestArg), {
|
|
294
|
+
projectRoot: backupProjectRoot(runtime),
|
|
295
|
+
env: runtime.env,
|
|
296
|
+
...(options.allProjects === true ? { allProjects: true } : {}),
|
|
297
|
+
});
|
|
298
|
+
assertRestoreTargetsStopped(runtime, plan);
|
|
235
299
|
if (runtime.dryRun) {
|
|
236
|
-
const plan = planRestore(resolve(runtime.cwd, manifestArg), { projectRoot: runtime.cwd });
|
|
237
300
|
printPlan(
|
|
238
301
|
runtime,
|
|
239
302
|
{
|
|
240
303
|
command: "restore",
|
|
241
304
|
backupId: plan.backupId,
|
|
242
305
|
manifestPath: plan.manifestPath,
|
|
306
|
+
...(plan.scope !== undefined ? { scope: plan.scope } : {}),
|
|
243
307
|
stores: plan.stores.map(({ storeId, targetPath, schemaVersion }) => ({ storeId, targetPath, schemaVersion })),
|
|
244
308
|
files: plan.files.map(({ id, targetPath }) => ({ id, targetPath })),
|
|
245
309
|
},
|
|
@@ -256,14 +320,13 @@ export async function cmdRestore(runtime: Runtime, manifestArg: string): Promise
|
|
|
256
320
|
);
|
|
257
321
|
return 0;
|
|
258
322
|
}
|
|
259
|
-
const result =
|
|
260
|
-
projectRoot: runtime.cwd,
|
|
261
|
-
});
|
|
323
|
+
const result = applyRestorePlan(plan);
|
|
262
324
|
const payload = {
|
|
263
325
|
ok: true,
|
|
264
326
|
command: "restore",
|
|
265
327
|
backupId: result.backupId,
|
|
266
328
|
manifestPath: result.manifestPath,
|
|
329
|
+
...(plan.scope !== undefined ? { scope: plan.scope } : {}),
|
|
267
330
|
restoredStores: result.restoredStores,
|
|
268
331
|
};
|
|
269
332
|
const summary = [
|
|
@@ -334,11 +397,16 @@ async function readSupervisor(runtime: Runtime, command: string): Promise<Awaite
|
|
|
334
397
|
return attached ?? refuseDryRun(runtime.io, runtime.json, command, "the Runtime supervisor is not running and --dry-run will not start it");
|
|
335
398
|
}
|
|
336
399
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
400
|
+
/** Creation is not execution; local runs are driven and inspected in the runs namespace. */
|
|
401
|
+
export function runEngineNotice(runId: string, defaultHarness: string, prerequisites: readonly KxmRunHandoff[]): string {
|
|
402
|
+
return [
|
|
403
|
+
`No steps executed. Project default harness: ${defaultHarness}; per-agent harness settings take precedence.`,
|
|
404
|
+
...prerequisites.map((item) => `Live prerequisite${item.stepId ? ` (${item.stepId})` : ""}: ${item.detail}`),
|
|
405
|
+
"Live execution uses one-shot harness calls; no hub or Pi worker is required. Check installation/authentication with kxm harness list.",
|
|
406
|
+
`${prerequisites.length > 0 ? "Resolve the prerequisites above, then execute" : "Execute"}: kxm runs drive ${runId} --wait`,
|
|
407
|
+
`Inspect: kxm runs status ${runId} --json; receipt: kxm runs receipt ${runId} --json; cancel: kxm runs cancel ${runId}`,
|
|
408
|
+
"These are local Runtime runs, not webhook workflows; use kxm runs, not kxm workflow get.",
|
|
409
|
+
].join("\n");
|
|
342
410
|
}
|
|
343
411
|
|
|
344
412
|
/** Where `kxm run <workflow>` would create its run, or the exit code of the
|
|
@@ -346,7 +414,8 @@ export function runEngineNotice(runId: string): string {
|
|
|
346
414
|
export function resolveKxmRunTarget(
|
|
347
415
|
runtime: Runtime,
|
|
348
416
|
workflow: string | undefined,
|
|
349
|
-
|
|
417
|
+
forTask = false,
|
|
418
|
+
): { projectRoot: string; workflowId: string; configRevision: string; defaultHarness: string; prerequisites: KxmRunHandoff[] } | number {
|
|
350
419
|
if (runtime.workspaceFlag !== undefined) {
|
|
351
420
|
print(runtime.io, runtime.json, {
|
|
352
421
|
ok: false,
|
|
@@ -355,7 +424,7 @@ export function resolveKxmRunTarget(
|
|
|
355
424
|
}, "kxm run discovers the authoritative project from the current directory; --workspace is not supported");
|
|
356
425
|
return 2;
|
|
357
426
|
}
|
|
358
|
-
if (!workflow) {
|
|
427
|
+
if (!workflow && !forTask) {
|
|
359
428
|
print(runtime.io, runtime.json, { ok: false, command: "run", error: "workflow_required" }, "usage: kxm run <workflow> [prompt]");
|
|
360
429
|
return 2;
|
|
361
430
|
}
|
|
@@ -365,16 +434,26 @@ export function resolveKxmRunTarget(
|
|
|
365
434
|
return 1;
|
|
366
435
|
}
|
|
367
436
|
const bundle = loadKxmProject(projectRoot, {});
|
|
368
|
-
|
|
369
|
-
|
|
437
|
+
const workflowId = workflow ?? String(bundle.project.value.defaultWorkflow ?? "default");
|
|
438
|
+
if (!bundle.workflows.has(workflowId)) {
|
|
439
|
+
print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_workflow_unknown", workflow: workflowId }, `workflow ${workflowId} does not exist in this project`);
|
|
440
|
+
return 1;
|
|
441
|
+
}
|
|
442
|
+
const defaultHarness = String(bundle.project.value.defaultHarness ?? "pi");
|
|
443
|
+
const prerequisites = kxmLiveRunPrerequisites(bundle, workflowId, projectRoot);
|
|
444
|
+
if (forTask && prerequisites.length > 0) {
|
|
445
|
+
print(runtime.io, runtime.json, {
|
|
446
|
+
ok: false, command: "task run", error: "run_execution_unavailable", workflowId, defaultHarness,
|
|
447
|
+
execution: { status: "not_started", mode: "live", prerequisites },
|
|
448
|
+
}, `task run refused; no run created or task changed (default harness: ${defaultHarness}).\n${prerequisites.map((item) => `${item.stepId ?? workflowId}: ${item.detail}`).join("\n")}`);
|
|
370
449
|
return 1;
|
|
371
450
|
}
|
|
372
|
-
return { projectRoot, workflowId
|
|
451
|
+
return { projectRoot, workflowId, configRevision: bundle.configRevision, defaultHarness, prerequisites };
|
|
373
452
|
}
|
|
374
453
|
|
|
375
|
-
export async function cmdKxmRun(runtime: Runtime, workflow: string | undefined, promptParts: string[]): Promise<number> {
|
|
454
|
+
export async function cmdKxmRun(runtime: Runtime, workflow: string | undefined, promptParts: string[], forTask = false): Promise<number> {
|
|
376
455
|
try {
|
|
377
|
-
const target = resolveKxmRunTarget(runtime, workflow);
|
|
456
|
+
const target = resolveKxmRunTarget(runtime, workflow, forTask);
|
|
378
457
|
if (typeof target === "number") return target;
|
|
379
458
|
const { projectRoot } = target;
|
|
380
459
|
if (runtime.dryRun) {
|
|
@@ -397,11 +476,23 @@ export async function cmdKxmRun(runtime: Runtime, workflow: string | undefined,
|
|
|
397
476
|
print(runtime.io, runtime.json, {
|
|
398
477
|
ok: true,
|
|
399
478
|
command: "run",
|
|
400
|
-
|
|
479
|
+
execution: {
|
|
480
|
+
status: "not_started",
|
|
481
|
+
mode: "live",
|
|
482
|
+
defaultHarness: target.defaultHarness,
|
|
483
|
+
authentication: "not_checked",
|
|
484
|
+
prerequisites: target.prerequisites,
|
|
485
|
+
nextSteps: {
|
|
486
|
+
harnesses: "kxm harness list",
|
|
487
|
+
drive: `kxm runs drive ${run.runId} --wait`,
|
|
488
|
+
status: `kxm runs status ${run.runId} --json`,
|
|
489
|
+
receipt: `kxm runs receipt ${run.runId} --json`,
|
|
490
|
+
},
|
|
491
|
+
},
|
|
401
492
|
idempotent: acceptance.idempotent === true,
|
|
402
493
|
run,
|
|
403
494
|
supervisor: { runtimeId: supervisor.runtimeId, port: supervisor.port, started: supervisor.started },
|
|
404
|
-
}, `run ${run.status}: ${run.runId} (home ${run.homeRuntimeId.slice(0, 12)}…, config ${run.configRevision.slice(0, 19)}…)\n${runEngineNotice(run.runId)}`);
|
|
495
|
+
}, `run ${run.status}: ${run.runId} (home ${run.homeRuntimeId.slice(0, 12)}…, config ${run.configRevision.slice(0, 19)}…)\n${runEngineNotice(run.runId, target.defaultHarness, target.prerequisites)}`);
|
|
405
496
|
return 0;
|
|
406
497
|
} catch (error) {
|
|
407
498
|
if (error instanceof KxmConfigError) {
|
|
@@ -728,9 +819,20 @@ export async function cmdTenantStatus(runtime: Runtime): Promise<number> {
|
|
|
728
819
|
}
|
|
729
820
|
|
|
730
821
|
export async function cmdHarnessList(runtime: Runtime): Promise<number> {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
822
|
+
try {
|
|
823
|
+
const projectRoot = discoverKxmProjectRoot(runtime.cwd);
|
|
824
|
+
const defaultHarness = projectRoot ? String(loadKxmProject(projectRoot).project.value.defaultHarness ?? "pi") : undefined;
|
|
825
|
+
const inventory = await probeHarnessesAsync({ env: runtime.env, defaultHarness });
|
|
826
|
+
print(runtime.io, runtime.json, { ok: true, command: "harness list", ...inventory }, formatHarnessInventory(inventory));
|
|
827
|
+
return 0;
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if (error instanceof KxmConfigError) {
|
|
830
|
+
print(runtime.io, runtime.json, { ok: false, command: "harness list", error: "harness_list_failed", issues: error.issues }, `harness list failed: ${error.message}`);
|
|
831
|
+
return 1;
|
|
832
|
+
}
|
|
833
|
+
print(runtime.io, runtime.json, { ok: false, command: "harness list", error: "harness_list_io_failed" }, "harness list failed because a local operation did not complete");
|
|
834
|
+
return 1;
|
|
835
|
+
}
|
|
734
836
|
}
|
|
735
837
|
|
|
736
838
|
export async function selectInventoryModel(runtime: Runtime, requested?: string | undefined): Promise<string | undefined> {
|
|
@@ -2,10 +2,12 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
5
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline";
|
|
7
7
|
import { verifyArtifactExists } from "../artifacts-exist.ts";
|
|
8
8
|
import { parseWorkflowDefinitions } from "../workflow.ts";
|
|
9
|
+
import { KxmConfigError, KxmSchemaRegistry, parseRestrictedYaml } from "../project-config.ts";
|
|
10
|
+
import { compileKxmWorkflow } from "../engine-compile.ts";
|
|
9
11
|
import { redactSecrets } from "../redact.ts";
|
|
10
12
|
import {
|
|
11
13
|
telemetryPath,
|
|
@@ -478,6 +480,22 @@ export async function cmdValidate(runtime: Runtime, fileFlag?: string | undefine
|
|
|
478
480
|
}
|
|
479
481
|
try {
|
|
480
482
|
const raw = file ? readFileSync(file, "utf8") : inline!;
|
|
483
|
+
// Local workflow mappings use the runner's restricted YAML/schema/compiler.
|
|
484
|
+
// Webhook sources remain JSON arrays with their existing secret checks.
|
|
485
|
+
if (explicitFile && file && !raw.trimStart().startsWith("[")) {
|
|
486
|
+
const value = parseRestrictedYaml(raw, file);
|
|
487
|
+
const issues = new KxmSchemaRegistry().validate("workflow", value, file);
|
|
488
|
+
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
489
|
+
const id = basename(file, extname(file));
|
|
490
|
+
compileKxmWorkflow({ id, value, logicalPath: file });
|
|
491
|
+
printWorker(
|
|
492
|
+
runtime,
|
|
493
|
+
worker,
|
|
494
|
+
{ ok: true, command: "validate", source: "file", file, workflows: [{ id, schema: value.schema }], warnings: [] },
|
|
495
|
+
`validated local workflow ${id} (schema and transitions); kxm init validates project references`,
|
|
496
|
+
);
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
481
499
|
const warnings: string[] = [];
|
|
482
500
|
const definitions = parseWorkflowDefinitions(raw, runtime.env, (message) => warnings.push(message));
|
|
483
501
|
const secretEnvs = definitions.map((definition) => ({
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
listGoals,
|
|
10
10
|
listTasks,
|
|
11
11
|
getTask,
|
|
12
|
-
updateTaskStatus,
|
|
13
12
|
syncTaskWithTracker,
|
|
14
13
|
goalFilePath,
|
|
15
14
|
taskFilePath,
|
|
@@ -27,40 +26,71 @@ import { readSessionTokenFromDisk } from "../commands.ts";
|
|
|
27
26
|
import { print, printPlan, type Runtime } from "./types.ts";
|
|
28
27
|
import { cmdKxmRun, resolveKxmRunTarget } from "./project.ts";
|
|
29
28
|
|
|
30
|
-
export async function cmdSuggest(
|
|
29
|
+
export async function cmdSuggest(
|
|
30
|
+
runtime: Runtime,
|
|
31
|
+
promptParts: string[],
|
|
32
|
+
probeHarnesses = probeHarnessesAsync,
|
|
33
|
+
): Promise<number> {
|
|
31
34
|
try {
|
|
32
35
|
const prompt = promptParts.join(" ").trim();
|
|
33
36
|
if (!prompt) {
|
|
34
|
-
runtime.io.
|
|
37
|
+
print(runtime.io, runtime.json, { ok: false, command: "suggest", error: "prompt_required" }, "prompt must be non-empty");
|
|
35
38
|
return 2;
|
|
36
39
|
}
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
// Native authentication probes may initialize state. A dry run must not invoke them.
|
|
41
|
+
const inventory = runtime.dryRun ? undefined : await probeHarnesses({ env: runtime.env });
|
|
42
|
+
const suggestion = suggestWorkflowAndRoles(prompt, { availableHarnesses: inventory?.harnesses });
|
|
43
|
+
if (suggestion.execution.supported && [".yaml", ".yml"].some((extension) =>
|
|
44
|
+
existsSync(join(runtime.cwd, ".kxm", "workflows", `${suggestion.workflowId}${extension}`)))) {
|
|
45
|
+
suggestion.roles = [];
|
|
46
|
+
suggestion.execution = {
|
|
47
|
+
supported: false,
|
|
48
|
+
error: "workflow_already_exists",
|
|
49
|
+
reason: `Workflow ${suggestion.workflowId} already exists; its agents and permissions may differ from the recommended template.`,
|
|
50
|
+
nextSteps: ["Review the existing definition and every agent's harness/model before running it, or install the recommended template under a fresh flat ID. No existing workflow is overwritten or recommended for execution."],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const execution = suggestion.execution;
|
|
44
54
|
const text = [
|
|
45
55
|
`Suggested Workflow: ${suggestion.workflowId} (${suggestion.area})`,
|
|
56
|
+
`Template: ${suggestion.template}`,
|
|
46
57
|
`Confidence: ${(suggestion.confidence * 100).toFixed(0)}%`,
|
|
47
58
|
`Reasons: ${suggestion.reasons.join("; ")}`,
|
|
48
59
|
`Suggested Skills: ${suggestion.suggestedSkills.join(", ") || "none"}`,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
` Writer: ${suggestion.roles.writer.harness} (${suggestion.roles.writer.model})`,
|
|
52
|
-
` Critics: ${suggestion.roles.critics.map((c) => `${c.harness}:${c.model}`).join(", ")}`,
|
|
53
|
-
` Verifier: ${suggestion.roles.verifier.command}`,
|
|
54
|
-
``,
|
|
55
|
-
`Execute with:`,
|
|
60
|
+
"",
|
|
61
|
+
"Install definition only (requires kxm init; does not execute):",
|
|
56
62
|
` ${suggestion.suggestedCommand}`,
|
|
63
|
+
"",
|
|
64
|
+
...(execution.supported ? [
|
|
65
|
+
"Suggested agent routing (not applied):",
|
|
66
|
+
...suggestion.roles.map((role) => ` ${role.agent}: ${role.harness} (${role.role}; use a configured compatible model)`),
|
|
67
|
+
"Prerequisites:",
|
|
68
|
+
...execution.prerequisites.map((step) => ` ${step}`),
|
|
69
|
+
`Create a run only (${execution.shell}; does not execute steps):`,
|
|
70
|
+
` ${execution.createCommand}`,
|
|
71
|
+
"Then drive the returned run ID with live calls and inspect its result:",
|
|
72
|
+
` ${execution.driveCommand}`,
|
|
73
|
+
` ${execution.statusCommand}`,
|
|
74
|
+
` ${execution.receiptCommand}`,
|
|
75
|
+
] : [
|
|
76
|
+
`Execution unavailable: ${execution.reason}`,
|
|
77
|
+
...execution.nextSteps.map((step) => ` ${step}`),
|
|
78
|
+
...(runtime.dryRun ? ["Harness authentication was not probed during --dry-run."] : []),
|
|
79
|
+
]),
|
|
57
80
|
].join("\n");
|
|
58
81
|
|
|
59
|
-
print(runtime.io, runtime.json, {
|
|
60
|
-
|
|
82
|
+
print(runtime.io, runtime.json, {
|
|
83
|
+
ok: execution.supported,
|
|
84
|
+
command: "suggest",
|
|
85
|
+
prompt,
|
|
86
|
+
...suggestion,
|
|
87
|
+
...(runtime.dryRun ? { dryRun: true } : {}),
|
|
88
|
+
...(!execution.supported ? { error: execution.error } : {}),
|
|
89
|
+
}, text);
|
|
90
|
+
return execution.supported ? 0 : 1;
|
|
61
91
|
} catch (error) {
|
|
62
92
|
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
-
runtime.io.
|
|
93
|
+
print(runtime.io, runtime.json, { ok: false, command: "suggest", error: "suggest_failed", detail: message }, `suggest failed: ${message}`);
|
|
64
94
|
return 1;
|
|
65
95
|
}
|
|
66
96
|
}
|
|
@@ -196,27 +226,21 @@ export async function cmdTaskRun(runtime: Runtime, taskId: string): Promise<numb
|
|
|
196
226
|
runtime.io.stderr(`Task ${taskId} not found\n`);
|
|
197
227
|
return 1;
|
|
198
228
|
}
|
|
199
|
-
const workflow = task.assignedWorkflow
|
|
229
|
+
const workflow = task.assignedWorkflow;
|
|
200
230
|
if (runtime.dryRun) {
|
|
201
|
-
const target = resolveKxmRunTarget(runtime, workflow);
|
|
231
|
+
const target = resolveKxmRunTarget(runtime, workflow, true);
|
|
202
232
|
if (typeof target === "number") return target;
|
|
203
|
-
const started = updateTaskStatus(runtime.cwd, taskId, "in_progress", { dryRun: true });
|
|
204
233
|
printPlan(
|
|
205
234
|
runtime,
|
|
206
|
-
{ command: "task run", taskId, ...target, status:
|
|
235
|
+
{ command: "task run", taskId, ...target, status: task.status, execution: { status: "not_started", mode: "live" } },
|
|
207
236
|
[
|
|
208
237
|
{ action: "request", target: "POST kxm-runtime /v1/runs (starts the Runtime supervisor if it is not running)" },
|
|
209
|
-
{ action: "write", target: taskFilePath(runtime.cwd, taskId) },
|
|
210
238
|
],
|
|
211
|
-
`
|
|
239
|
+
`create workflow ${target.workflowId} for task ${taskId}; task status stays ${task.status} until work actually starts`,
|
|
212
240
|
);
|
|
213
241
|
return 0;
|
|
214
242
|
}
|
|
215
|
-
|
|
216
|
-
if (exitCode === 0) {
|
|
217
|
-
updateTaskStatus(runtime.cwd, taskId, "in_progress");
|
|
218
|
-
}
|
|
219
|
-
return exitCode;
|
|
243
|
+
return await cmdKxmRun(runtime, workflow, [task.objective], true);
|
|
220
244
|
} catch (error) {
|
|
221
245
|
const message = error instanceof Error ? error.message : String(error);
|
|
222
246
|
runtime.io.stderr(`task run failed: ${message}\n`);
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
addWorkflowDefinition,
|
|
12
12
|
removeWorkflowDefinition,
|
|
13
13
|
modifyWorkflowDefinition,
|
|
14
|
-
parseWorkflowFile,
|
|
15
14
|
scaffoldWorkflowDefinition,
|
|
16
15
|
WORKFLOW_TEMPLATES,
|
|
17
16
|
} from "../workflow-manager.ts";
|
|
@@ -23,7 +22,7 @@ import {
|
|
|
23
22
|
type WorkflowJournalEntry,
|
|
24
23
|
type WorkflowRun,
|
|
25
24
|
} from "../workflow.ts";
|
|
26
|
-
import { discoverKxmProjectRoot, kxmWorkflowWriteIssues } from "../project-config.ts";
|
|
25
|
+
import { discoverKxmProjectRoot, kxmWorkflowWriteIssues, parseRestrictedYaml } from "../project-config.ts";
|
|
27
26
|
import { ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
|
|
28
27
|
import { projectRuntimeOwnsRun } from "../runtime-store.ts";
|
|
29
28
|
import type { WorkerOutcome } from "../envelope.ts";
|
|
@@ -215,63 +214,63 @@ export async function cmdWorkflowAdd(
|
|
|
215
214
|
},
|
|
216
215
|
): Promise<number> {
|
|
217
216
|
const scope = options.scope ?? "local";
|
|
218
|
-
const refuse = (error: string,
|
|
219
|
-
print(runtime.io, runtime.json, { ok: false, command: "workflow add", error }, `workflow add failed: ${
|
|
220
|
-
return
|
|
217
|
+
const refuse = (error: string, message: string, code = 2): number => {
|
|
218
|
+
print(runtime.io, runtime.json, { ok: false, command: "workflow add", error, message }, `workflow add failed: ${message}`);
|
|
219
|
+
return code;
|
|
221
220
|
};
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (options.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
id,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
221
|
+
try {
|
|
222
|
+
let content: Record<string, unknown> | string | undefined;
|
|
223
|
+
if (options.template !== undefined) {
|
|
224
|
+
if (options.file !== undefined || options.pick !== undefined) {
|
|
225
|
+
return refuse("workflow_add_conflict", "--template cannot be combined with --file or --pick");
|
|
226
|
+
}
|
|
227
|
+
if (!workflowId) return refuse("workflow_id_required", "usage: kxm workflow add <workflowId> --template <name>");
|
|
228
|
+
const template = Object.hasOwn(WORKFLOW_TEMPLATES, options.template) ? WORKFLOW_TEMPLATES[options.template] : undefined;
|
|
229
|
+
if (!template) {
|
|
230
|
+
return refuse("workflow_template_unknown", `unknown template ${options.template}; choose ${Object.keys(WORKFLOW_TEMPLATES).join(", ")}`);
|
|
231
|
+
}
|
|
232
|
+
content = { ...template, ...(options.description !== undefined ? { description: options.description } : {}) };
|
|
233
|
+
} else if (!workflowId || options.pick) {
|
|
234
|
+
const candidates: PickCandidate[] = Object.entries(WORKFLOW_TEMPLATES).map(([id, tmpl]) => ({
|
|
235
|
+
id,
|
|
236
|
+
description: String(tmpl.description ?? id),
|
|
237
|
+
label: "template",
|
|
238
|
+
payload: tmpl,
|
|
239
|
+
}));
|
|
240
|
+
if (scope === "local") {
|
|
241
|
+
const globalDefs = listWorkflowDefinitions({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
242
|
+
for (const gd of globalDefs) {
|
|
243
|
+
// Read the listed path when selected: globals may use `.yml`, not only `.yaml`.
|
|
244
|
+
if (!candidates.some((c) => c.id === gd.id)) {
|
|
245
|
+
candidates.push({ id: gd.id, description: gd.description, label: "global", payload: gd.filePath });
|
|
246
|
+
}
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (!workflowId) {
|
|
253
|
-
runtime.io.stderr("workflow add failed: missing workflowId or pick selection\n");
|
|
254
|
-
return 1;
|
|
249
|
+
const picked = await resolvePickItem(runtime.io, `Select a workflow template to add (${scope})`, candidates, options.pick, runtime.env);
|
|
250
|
+
if (!picked) {
|
|
251
|
+
return refuse("workflow_id_required", "provide a workflowId or select an available workflow with --pick <id>", 1);
|
|
255
252
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
253
|
+
workflowId ??= picked.id;
|
|
254
|
+
if (options.file === undefined) {
|
|
255
|
+
const pickedContent = typeof picked.payload === "string"
|
|
256
|
+
? readFileSync(picked.payload, "utf8")
|
|
257
|
+
: picked.payload;
|
|
258
|
+
content = options.description !== undefined
|
|
259
|
+
? {
|
|
260
|
+
...(typeof pickedContent === "string" ? parseRestrictedYaml(pickedContent, String(picked.payload)) : pickedContent),
|
|
261
|
+
description: options.description,
|
|
262
|
+
}
|
|
263
|
+
: pickedContent;
|
|
263
264
|
}
|
|
264
265
|
}
|
|
265
|
-
}
|
|
266
266
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
267
|
+
if (options.file !== undefined) {
|
|
268
|
+
const filePath = resolve(runtime.cwd, options.file);
|
|
269
|
+
content = readFileSync(filePath, "utf8");
|
|
270
|
+
} else if (!content) {
|
|
271
|
+
content = scaffoldWorkflowDefinition(options.description || `Workflow ${workflowId}`);
|
|
272
|
+
}
|
|
273
273
|
|
|
274
|
-
try {
|
|
275
274
|
const document = typeof content === "string" ? content : stringify(content);
|
|
276
275
|
// A local workflow is read by the project loader, which refuses the whole
|
|
277
276
|
// project over one bad file, so it is checked by that loader before it lands.
|
|
@@ -286,7 +285,7 @@ export async function cmdWorkflowAdd(
|
|
|
286
285
|
print(
|
|
287
286
|
runtime.io,
|
|
288
287
|
runtime.json,
|
|
289
|
-
{ ok: false, command: "workflow add", error: "workflow_invalid", issues },
|
|
288
|
+
{ ok: false, command: "workflow add", error: "workflow_invalid", message: "with this workflow the project would not load, so nothing was written", issues },
|
|
290
289
|
`workflow add failed: with this workflow the project would not load, so nothing was written\n${issues.map((entry) => ` ${entry.file}: ${entry.code}: ${entry.message}`).join("\n")}`,
|
|
291
290
|
);
|
|
292
291
|
return 2;
|
|
@@ -312,8 +311,7 @@ export async function cmdWorkflowAdd(
|
|
|
312
311
|
);
|
|
313
312
|
return 0;
|
|
314
313
|
} catch (err: unknown) {
|
|
315
|
-
|
|
316
|
-
return 1;
|
|
314
|
+
return refuse("workflow_add_failed", err instanceof Error ? err.message : String(err), 1);
|
|
317
315
|
}
|
|
318
316
|
}
|
|
319
317
|
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -419,18 +419,20 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
419
419
|
});
|
|
420
420
|
});
|
|
421
421
|
|
|
422
|
-
addGlobalOptions(program.command("backup").description("Create a verified SQLite backup of
|
|
422
|
+
addGlobalOptions(program.command("backup").description("Create a verified SQLite backup of this project's hub store and Runtime event store, with a hashed manifest"))
|
|
423
423
|
.option("--out <dir>", "Directory to write backup and manifest")
|
|
424
|
-
.
|
|
424
|
+
.option("--all-projects", "Also back up the shared Runtime registry and every project's event store on this machine")
|
|
425
|
+
.action(async function backupAction(this: Command, options: { out?: string; allProjects?: boolean }) {
|
|
425
426
|
result.code = await cmdBackup(runtimeFrom(ctx, this), options);
|
|
426
427
|
});
|
|
427
428
|
|
|
428
|
-
addGlobalOptions(program.command("restore <manifest>").description("Restore SQLite stores from a verified backup manifest"))
|
|
429
|
-
.
|
|
430
|
-
|
|
429
|
+
addGlobalOptions(program.command("restore <manifest>").description("Restore this project's SQLite stores from a verified backup manifest; refuses while the Runtime or hub is running"))
|
|
430
|
+
.option("--all-projects", "Also restore the shared Runtime registry and other projects' event stores the backup holds")
|
|
431
|
+
.action(async function restoreAction(this: Command, manifest: string, options: { allProjects?: boolean }) {
|
|
432
|
+
result.code = await cmdRestore(runtimeFrom(ctx, this), manifest, options);
|
|
431
433
|
});
|
|
432
434
|
|
|
433
|
-
addGlobalOptions(program.command("run").description("Create a KXM run
|
|
435
|
+
addGlobalOptions(program.command("run").description("Create a KXM run without executing steps; follow its prerequisites, then kxm runs drive <runId> --wait")
|
|
434
436
|
.argument("[workflow]", "Workflow id to run")
|
|
435
437
|
.argument("[prompt...]", "Run prompt (events keep its hash; the full text is kept in a local 0600 sidecar file)")
|
|
436
438
|
.action(async function runAction(this: Command, workflow: string | undefined, promptParts: string[]) {
|