@kontextmind/kxm 0.7.104 → 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 +12 -0
- package/docs/contributing/test-matrix.md +4 -1
- package/docs/reference/cli-reference.md +42 -35
- package/docs/reference/workflow-definitions.md +4 -4
- package/docs/start/first-workflow.md +6 -2
- package/docs/start/quickstart-claude-code.md +10 -0
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +1361 -709
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +1 -1
- package/plugins/kxm/dist/runtime.js +3 -3
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/project.ts +66 -18
- 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 +1 -1
- 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/suggest.ts +132 -79
- package/plugins/kxm/src/workflow-manager.ts +42 -19
|
@@ -17313,7 +17313,7 @@ function sessionTokenFixHint(policy) {
|
|
|
17313
17313
|
}
|
|
17314
17314
|
|
|
17315
17315
|
// plugins/kxm/src/mcp-server.ts
|
|
17316
|
-
var VERSION = "0.7.
|
|
17316
|
+
var VERSION = "0.7.105";
|
|
17317
17317
|
var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
17318
17318
|
var inbox = /* @__PURE__ */ new Map();
|
|
17319
17319
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
@@ -16015,7 +16015,7 @@ function probeEntry(entry, runCommand, timeoutMs, platform, probe = {}) {
|
|
|
16015
16015
|
return {
|
|
16016
16016
|
id: entry.id,
|
|
16017
16017
|
label: entry.label,
|
|
16018
|
-
default: entry.
|
|
16018
|
+
default: entry.id === (probe.defaultHarness ?? DEFAULT_HARNESS),
|
|
16019
16019
|
mode: entry.mode,
|
|
16020
16020
|
detected,
|
|
16021
16021
|
authenticated,
|
|
@@ -16017,7 +16017,7 @@ function probeEntry(entry, runCommand, timeoutMs, platform, probe = {}) {
|
|
|
16017
16017
|
return {
|
|
16018
16018
|
id: entry.id,
|
|
16019
16019
|
label: entry.label,
|
|
16020
|
-
default: entry.
|
|
16020
|
+
default: entry.id === (probe.defaultHarness ?? DEFAULT_HARNESS),
|
|
16021
16021
|
mode: entry.mode,
|
|
16022
16022
|
detected,
|
|
16023
16023
|
authenticated,
|
|
@@ -16038,7 +16038,7 @@ function probeHarnesses(options = {}) {
|
|
|
16038
16038
|
const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
|
|
16039
16039
|
const platform = options.platform ?? process.platform;
|
|
16040
16040
|
return {
|
|
16041
|
-
defaultHarness: DEFAULT_HARNESS,
|
|
16041
|
+
defaultHarness: options.defaultHarness ?? DEFAULT_HARNESS,
|
|
16042
16042
|
harnesses: BUILTIN_HARNESSES.map((entry) => probeEntry(entry, runCommand, timeoutMs, platform, options))
|
|
16043
16043
|
};
|
|
16044
16044
|
}
|
|
@@ -16449,7 +16449,7 @@ function formatHarnessInventory(inventory) {
|
|
|
16449
16449
|
].join(" ");
|
|
16450
16450
|
});
|
|
16451
16451
|
return [
|
|
16452
|
-
`default harness: ${inventory.defaultHarness} (
|
|
16452
|
+
`default harness: ${inventory.defaultHarness} (used when an agent omits harness:)`,
|
|
16453
16453
|
"enable/disable = Git YAML (.kxm/agents, .kxm/models) or the harness's own plugin CLI",
|
|
16454
16454
|
"governed kxm skills are not auto-updated",
|
|
16455
16455
|
header,
|
package/plugins/kxm/package.json
CHANGED
|
@@ -16,6 +16,7 @@ import { initializeKxmProject } from "../init.ts";
|
|
|
16
16
|
import { diffKxmProjectAgainstRevision, formatKxmPermissionDiff } from "../permission.ts";
|
|
17
17
|
import { readKxmLocalBindings, kxmUserStateRoot } from "../bindings.ts";
|
|
18
18
|
import { loadKxmProject } from "../project-config.ts";
|
|
19
|
+
import { kxmLiveRunPrerequisites, type KxmRunHandoff } from "../engine.ts";
|
|
19
20
|
import {
|
|
20
21
|
attachKxmSupervisor,
|
|
21
22
|
ensureKxmSupervisor,
|
|
@@ -115,6 +116,13 @@ export async function cmdKxmInit(
|
|
|
115
116
|
localStateRoot: kxmUserStateRoot({ env: runtime.env }),
|
|
116
117
|
dryRun: runtime.dryRun,
|
|
117
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
|
+
: [];
|
|
118
126
|
const payload = {
|
|
119
127
|
ok: initialized.action !== "planned" || runtime.dryRun,
|
|
120
128
|
command: "init",
|
|
@@ -128,9 +136,10 @@ export async function cmdKxmInit(
|
|
|
128
136
|
...(initialized.resumePending === undefined ? {} : { resumePending: initialized.resumePending }),
|
|
129
137
|
...(initialized.transactionKind === undefined ? {} : { transactionKind: initialized.transactionKind }),
|
|
130
138
|
plannedOnly: initialized.action === "planned",
|
|
139
|
+
...(starterGuidance.length > 0 ? { guidance: starterGuidance } : {}),
|
|
131
140
|
};
|
|
132
141
|
const finishInit = (code: number, text: string): number => {
|
|
133
|
-
print(runtime.io, runtime.json, payload, text);
|
|
142
|
+
print(runtime.io, runtime.json, payload, [text, ...starterGuidance].join("\n"));
|
|
134
143
|
return code;
|
|
135
144
|
};
|
|
136
145
|
if (initialized.action === "created") {
|
|
@@ -388,11 +397,16 @@ async function readSupervisor(runtime: Runtime, command: string): Promise<Awaite
|
|
|
388
397
|
return attached ?? refuseDryRun(runtime.io, runtime.json, command, "the Runtime supervisor is not running and --dry-run will not start it");
|
|
389
398
|
}
|
|
390
399
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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");
|
|
396
410
|
}
|
|
397
411
|
|
|
398
412
|
/** Where `kxm run <workflow>` would create its run, or the exit code of the
|
|
@@ -400,7 +414,8 @@ export function runEngineNotice(runId: string): string {
|
|
|
400
414
|
export function resolveKxmRunTarget(
|
|
401
415
|
runtime: Runtime,
|
|
402
416
|
workflow: string | undefined,
|
|
403
|
-
|
|
417
|
+
forTask = false,
|
|
418
|
+
): { projectRoot: string; workflowId: string; configRevision: string; defaultHarness: string; prerequisites: KxmRunHandoff[] } | number {
|
|
404
419
|
if (runtime.workspaceFlag !== undefined) {
|
|
405
420
|
print(runtime.io, runtime.json, {
|
|
406
421
|
ok: false,
|
|
@@ -409,7 +424,7 @@ export function resolveKxmRunTarget(
|
|
|
409
424
|
}, "kxm run discovers the authoritative project from the current directory; --workspace is not supported");
|
|
410
425
|
return 2;
|
|
411
426
|
}
|
|
412
|
-
if (!workflow) {
|
|
427
|
+
if (!workflow && !forTask) {
|
|
413
428
|
print(runtime.io, runtime.json, { ok: false, command: "run", error: "workflow_required" }, "usage: kxm run <workflow> [prompt]");
|
|
414
429
|
return 2;
|
|
415
430
|
}
|
|
@@ -419,16 +434,26 @@ export function resolveKxmRunTarget(
|
|
|
419
434
|
return 1;
|
|
420
435
|
}
|
|
421
436
|
const bundle = loadKxmProject(projectRoot, {});
|
|
422
|
-
|
|
423
|
-
|
|
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")}`);
|
|
424
449
|
return 1;
|
|
425
450
|
}
|
|
426
|
-
return { projectRoot, workflowId
|
|
451
|
+
return { projectRoot, workflowId, configRevision: bundle.configRevision, defaultHarness, prerequisites };
|
|
427
452
|
}
|
|
428
453
|
|
|
429
|
-
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> {
|
|
430
455
|
try {
|
|
431
|
-
const target = resolveKxmRunTarget(runtime, workflow);
|
|
456
|
+
const target = resolveKxmRunTarget(runtime, workflow, forTask);
|
|
432
457
|
if (typeof target === "number") return target;
|
|
433
458
|
const { projectRoot } = target;
|
|
434
459
|
if (runtime.dryRun) {
|
|
@@ -451,11 +476,23 @@ export async function cmdKxmRun(runtime: Runtime, workflow: string | undefined,
|
|
|
451
476
|
print(runtime.io, runtime.json, {
|
|
452
477
|
ok: true,
|
|
453
478
|
command: "run",
|
|
454
|
-
|
|
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
|
+
},
|
|
455
492
|
idempotent: acceptance.idempotent === true,
|
|
456
493
|
run,
|
|
457
494
|
supervisor: { runtimeId: supervisor.runtimeId, port: supervisor.port, started: supervisor.started },
|
|
458
|
-
}, `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)}`);
|
|
459
496
|
return 0;
|
|
460
497
|
} catch (error) {
|
|
461
498
|
if (error instanceof KxmConfigError) {
|
|
@@ -782,9 +819,20 @@ export async function cmdTenantStatus(runtime: Runtime): Promise<number> {
|
|
|
782
819
|
}
|
|
783
820
|
|
|
784
821
|
export async function cmdHarnessList(runtime: Runtime): Promise<number> {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
+
}
|
|
788
836
|
}
|
|
789
837
|
|
|
790
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
|
@@ -432,7 +432,7 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
432
432
|
result.code = await cmdRestore(runtimeFrom(ctx, this), manifest, options);
|
|
433
433
|
});
|
|
434
434
|
|
|
435
|
-
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")
|
|
436
436
|
.argument("[workflow]", "Workflow id to run")
|
|
437
437
|
.argument("[prompt...]", "Run prompt (events keep its hash; the full text is kept in a local 0600 sidecar file)")
|
|
438
438
|
.action(async function runAction(this: Command, workflow: string | undefined, promptParts: string[]) {
|