@kontextmind/kxm 0.7.104 → 0.7.106
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 +23 -0
- package/docs/contributing/test-matrix.md +6 -1
- package/docs/reference/cli-reference.md +45 -36
- 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 +10 -0
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +1421 -760
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +12 -10
- package/plugins/kxm/dist/runtime.js +14 -12
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/project.ts +66 -18
- package/plugins/kxm/src/cli/roles.ts +47 -40
- 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/project-config.ts +33 -5
- 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.106";
|
|
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,
|
|
@@ -17151,7 +17151,7 @@ function validateModelReferences(models, issues) {
|
|
|
17151
17151
|
};
|
|
17152
17152
|
for (const id of models.keys()) walk(id, []);
|
|
17153
17153
|
}
|
|
17154
|
-
function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot) {
|
|
17154
|
+
function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot, writerRole) {
|
|
17155
17155
|
const issues = [];
|
|
17156
17156
|
const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate) => Boolean(candidate));
|
|
17157
17157
|
const repositoryIds = /* @__PURE__ */ new Set();
|
|
@@ -17222,9 +17222,9 @@ function validateBundle(project, repositories, agents, models, workflows, enviro
|
|
|
17222
17222
|
if (projectRoot) {
|
|
17223
17223
|
const writerRolePath = join2(projectRoot, ".kxm", "roles", "writer.yaml");
|
|
17224
17224
|
const implementerAgent = agents.get("implementer") ?? agents.get("writer");
|
|
17225
|
-
if (existsSync3(writerRolePath) && implementerAgent) {
|
|
17225
|
+
if ((writerRole !== void 0 || existsSync3(writerRolePath)) && implementerAgent) {
|
|
17226
17226
|
try {
|
|
17227
|
-
const rawRole = parseRestrictedYaml2(readFileSync(writerRolePath, "utf8"));
|
|
17227
|
+
const rawRole = parseRestrictedYaml2(writerRole ?? readFileSync(writerRolePath, "utf8"));
|
|
17228
17228
|
const roleObj = objectValue(rawRole);
|
|
17229
17229
|
const rosterEntries = valuesOf(roleObj ?? {}, "roster").map((candidate) => objectValue(candidate)).filter((entry) => Boolean(entry));
|
|
17230
17230
|
const enabledRosterModels = rosterEntries.filter((entry) => entry.enabled !== false).map((entry) => stringValue(entry.model)).filter((m) => Boolean(m));
|
|
@@ -17290,7 +17290,9 @@ function assertNoRegisteredGates(options) {
|
|
|
17290
17290
|
function loadKxmProject(projectRoot, options = {}) {
|
|
17291
17291
|
return loadProjectBundle(projectRoot, options);
|
|
17292
17292
|
}
|
|
17293
|
-
function loadProjectBundle(projectRoot, options,
|
|
17293
|
+
function loadProjectBundle(projectRoot, options, candidate) {
|
|
17294
|
+
const workflowCandidate = candidate?.kind === "workflow" ? candidate : void 0;
|
|
17295
|
+
const writerRole = candidate?.kind === "role" && candidate.id.toLocaleLowerCase("en-US") === "writer" ? candidate.document : void 0;
|
|
17294
17296
|
assertNoRegisteredGates(options);
|
|
17295
17297
|
const root = resolve(projectRoot);
|
|
17296
17298
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -17313,7 +17315,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17313
17315
|
earlyIssues.push(issue2("semantic", "template_provenance_project_mismatch", ".kxm/template-provenance.yaml", "template provenance belongs to a different project identity"));
|
|
17314
17316
|
}
|
|
17315
17317
|
}
|
|
17316
|
-
const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories").map((
|
|
17318
|
+
const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories").map((candidate2) => stringValue(objectValue(candidate2)?.id)).filter((candidate2) => candidate2 !== void 0));
|
|
17317
17319
|
for (const repositoryId of Object.keys(options.repositoryBindings ?? {}).sort(compareCodeUnits3)) {
|
|
17318
17320
|
if (!resourceIdentifier(repositoryId)) {
|
|
17319
17321
|
earlyIssues.push(issue2("path", "repository_binding_id_invalid", ".kxm/project.yaml", `host-local binding identity ${repositoryId} is invalid`));
|
|
@@ -17329,7 +17331,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17329
17331
|
const { id, document } = workflowCandidate;
|
|
17330
17332
|
const logicalPath = `.kxm/workflows/${id}.yaml`;
|
|
17331
17333
|
if (!resourceIdentifier(id)) fail2("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
|
|
17332
|
-
const collision = [...workflows.keys()].find((
|
|
17334
|
+
const collision = [...workflows.keys()].find((candidate2) => candidate2.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
17333
17335
|
if (collision) fail2("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
|
|
17334
17336
|
workflows.set(id, { kind: "workflow", id, file: join2(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
|
|
17335
17337
|
}
|
|
@@ -17348,8 +17350,8 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17348
17350
|
}
|
|
17349
17351
|
}
|
|
17350
17352
|
const seenBindings = /* @__PURE__ */ new Map();
|
|
17351
|
-
for (const
|
|
17352
|
-
const entry = objectValue(
|
|
17353
|
+
for (const candidate2 of valuesOf(project.value, "repositories")) {
|
|
17354
|
+
const entry = objectValue(candidate2);
|
|
17353
17355
|
const repositoryId = entry && stringValue(entry.id);
|
|
17354
17356
|
const pathHint = entry && stringValue(entry.pathHint);
|
|
17355
17357
|
const role = entry && stringValue(entry.role);
|
|
@@ -17446,7 +17448,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17446
17448
|
}
|
|
17447
17449
|
}
|
|
17448
17450
|
if (loadIssues.length > 0) throw new KxmConfigError(loadIssues);
|
|
17449
|
-
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root);
|
|
17451
|
+
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, writerRole);
|
|
17450
17452
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
17451
17453
|
const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...workflows.values(), ...environments, ...gateRegistry ? [gateRegistry] : []].sort((left, right) => compareCodeUnits3(left.logicalPath, right.logicalPath));
|
|
17452
17454
|
return {
|
|
@@ -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,
|
|
@@ -17321,7 +17321,7 @@ function validateModelReferences(models, issues) {
|
|
|
17321
17321
|
};
|
|
17322
17322
|
for (const id of models.keys()) walk(id, []);
|
|
17323
17323
|
}
|
|
17324
|
-
function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot) {
|
|
17324
|
+
function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot, writerRole) {
|
|
17325
17325
|
const issues = [];
|
|
17326
17326
|
const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate) => Boolean(candidate));
|
|
17327
17327
|
const repositoryIds = /* @__PURE__ */ new Set();
|
|
@@ -17392,9 +17392,9 @@ function validateBundle(project, repositories, agents, models, workflows, enviro
|
|
|
17392
17392
|
if (projectRoot) {
|
|
17393
17393
|
const writerRolePath = join2(projectRoot, ".kxm", "roles", "writer.yaml");
|
|
17394
17394
|
const implementerAgent = agents.get("implementer") ?? agents.get("writer");
|
|
17395
|
-
if (existsSync3(writerRolePath) && implementerAgent) {
|
|
17395
|
+
if ((writerRole !== void 0 || existsSync3(writerRolePath)) && implementerAgent) {
|
|
17396
17396
|
try {
|
|
17397
|
-
const rawRole = parseRestrictedYaml2(readFileSync(writerRolePath, "utf8"));
|
|
17397
|
+
const rawRole = parseRestrictedYaml2(writerRole ?? readFileSync(writerRolePath, "utf8"));
|
|
17398
17398
|
const roleObj = objectValue(rawRole);
|
|
17399
17399
|
const rosterEntries = valuesOf(roleObj ?? {}, "roster").map((candidate) => objectValue(candidate)).filter((entry) => Boolean(entry));
|
|
17400
17400
|
const enabledRosterModels = rosterEntries.filter((entry) => entry.enabled !== false).map((entry) => stringValue(entry.model)).filter((m) => Boolean(m));
|
|
@@ -17460,7 +17460,9 @@ function assertNoRegisteredGates(options) {
|
|
|
17460
17460
|
function loadKxmProject(projectRoot, options = {}) {
|
|
17461
17461
|
return loadProjectBundle(projectRoot, options);
|
|
17462
17462
|
}
|
|
17463
|
-
function loadProjectBundle(projectRoot, options,
|
|
17463
|
+
function loadProjectBundle(projectRoot, options, candidate) {
|
|
17464
|
+
const workflowCandidate = candidate?.kind === "workflow" ? candidate : void 0;
|
|
17465
|
+
const writerRole = candidate?.kind === "role" && candidate.id.toLocaleLowerCase("en-US") === "writer" ? candidate.document : void 0;
|
|
17464
17466
|
assertNoRegisteredGates(options);
|
|
17465
17467
|
const root = resolve(projectRoot);
|
|
17466
17468
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -17483,7 +17485,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17483
17485
|
earlyIssues.push(issue2("semantic", "template_provenance_project_mismatch", ".kxm/template-provenance.yaml", "template provenance belongs to a different project identity"));
|
|
17484
17486
|
}
|
|
17485
17487
|
}
|
|
17486
|
-
const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories").map((
|
|
17488
|
+
const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories").map((candidate2) => stringValue(objectValue(candidate2)?.id)).filter((candidate2) => candidate2 !== void 0));
|
|
17487
17489
|
for (const repositoryId of Object.keys(options.repositoryBindings ?? {}).sort(compareCodeUnits3)) {
|
|
17488
17490
|
if (!resourceIdentifier(repositoryId)) {
|
|
17489
17491
|
earlyIssues.push(issue2("path", "repository_binding_id_invalid", ".kxm/project.yaml", `host-local binding identity ${repositoryId} is invalid`));
|
|
@@ -17499,7 +17501,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17499
17501
|
const { id, document } = workflowCandidate;
|
|
17500
17502
|
const logicalPath = `.kxm/workflows/${id}.yaml`;
|
|
17501
17503
|
if (!resourceIdentifier(id)) fail2("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
|
|
17502
|
-
const collision = [...workflows.keys()].find((
|
|
17504
|
+
const collision = [...workflows.keys()].find((candidate2) => candidate2.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
17503
17505
|
if (collision) fail2("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
|
|
17504
17506
|
workflows.set(id, { kind: "workflow", id, file: join2(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
|
|
17505
17507
|
}
|
|
@@ -17518,8 +17520,8 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17518
17520
|
}
|
|
17519
17521
|
}
|
|
17520
17522
|
const seenBindings = /* @__PURE__ */ new Map();
|
|
17521
|
-
for (const
|
|
17522
|
-
const entry = objectValue(
|
|
17523
|
+
for (const candidate2 of valuesOf(project.value, "repositories")) {
|
|
17524
|
+
const entry = objectValue(candidate2);
|
|
17523
17525
|
const repositoryId = entry && stringValue(entry.id);
|
|
17524
17526
|
const pathHint = entry && stringValue(entry.pathHint);
|
|
17525
17527
|
const role = entry && stringValue(entry.role);
|
|
@@ -17616,7 +17618,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17616
17618
|
}
|
|
17617
17619
|
}
|
|
17618
17620
|
if (loadIssues.length > 0) throw new KxmConfigError(loadIssues);
|
|
17619
|
-
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root);
|
|
17621
|
+
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, writerRole);
|
|
17620
17622
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
17621
17623
|
const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...workflows.values(), ...environments, ...gateRegistry ? [gateRegistry] : []].sort((left, right) => compareCodeUnits3(left.logicalPath, right.logicalPath));
|
|
17622
17624
|
return {
|
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> {
|
|
@@ -15,9 +15,10 @@ import {
|
|
|
15
15
|
loadRoleHostsConfig,
|
|
16
16
|
setRoleSeatHost,
|
|
17
17
|
resolveRoleSeat,
|
|
18
|
+
parseRoleFile,
|
|
18
19
|
type KxmRoleDefinition,
|
|
19
20
|
} from "../role.ts";
|
|
20
|
-
import { discoverKxmProjectRoot } from "../project-config.ts";
|
|
21
|
+
import { discoverKxmProjectRoot, kxmRoleWriteIssues } from "../project-config.ts";
|
|
21
22
|
import { ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
|
|
22
23
|
import { projectRuntimeOwnsRun } from "../runtime-store.ts";
|
|
23
24
|
import { resumeWorkflowFromRuling, type WorkflowRun } from "../workflow.ts";
|
|
@@ -156,6 +157,7 @@ export async function cmdRoleAdd(
|
|
|
156
157
|
},
|
|
157
158
|
): Promise<number> {
|
|
158
159
|
const scope = options.scope ?? "local";
|
|
160
|
+
let base: KxmRoleDefinition | undefined;
|
|
159
161
|
if (!roleId || options.pick) {
|
|
160
162
|
const candidates: PickCandidate[] = Object.values(DEFAULT_ROLES).map((r) => ({
|
|
161
163
|
id: r.id,
|
|
@@ -166,8 +168,10 @@ export async function cmdRoleAdd(
|
|
|
166
168
|
if (scope === "local") {
|
|
167
169
|
const globalRoles = listRoles({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
168
170
|
for (const gr of globalRoles) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
// The listing is a summary; the copy needs the definition, read from the listed file (it may be `.yml`).
|
|
172
|
+
const definition = parseRoleFile(gr.filePath);
|
|
173
|
+
if (definition && !candidates.some((c) => c.id === gr.id)) {
|
|
174
|
+
candidates.push({ id: gr.id, description: gr.description, label: "global", payload: definition });
|
|
171
175
|
}
|
|
172
176
|
}
|
|
173
177
|
}
|
|
@@ -179,63 +183,66 @@ export async function cmdRoleAdd(
|
|
|
179
183
|
}
|
|
180
184
|
} else {
|
|
181
185
|
roleId = picked.id;
|
|
182
|
-
|
|
183
|
-
const base = DEFAULT_ROLES[picked.id]!;
|
|
184
|
-
const roleDef: KxmRoleDefinition = {
|
|
185
|
-
...base,
|
|
186
|
-
description: options.description || base.description,
|
|
187
|
-
skills: options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : base.skills,
|
|
188
|
-
roster: options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : base.roster,
|
|
189
|
-
};
|
|
190
|
-
try {
|
|
191
|
-
const res = addRole(roleDef, {
|
|
192
|
-
scope,
|
|
193
|
-
repoRoot: runtime.cwd,
|
|
194
|
-
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
195
|
-
overwrite: options.overwrite,
|
|
196
|
-
dryRun: runtime.dryRun,
|
|
197
|
-
});
|
|
198
|
-
if (runtime.dryRun) {
|
|
199
|
-
printPlan(runtime, { command: "role add", roleId, ...res }, [{ action: "write", target: res.filePath }], `add role '${roleId}' to ${res.scope}`);
|
|
200
|
-
return 0;
|
|
201
|
-
}
|
|
202
|
-
print(
|
|
203
|
-
runtime.io,
|
|
204
|
-
runtime.json,
|
|
205
|
-
{ ok: true, command: "role add", roleId, ...res },
|
|
206
|
-
`Added role '${roleId}' to ${res.scope} (${res.filePath})\n`,
|
|
207
|
-
);
|
|
208
|
-
return 0;
|
|
209
|
-
} catch (err: unknown) {
|
|
210
|
-
runtime.io.stderr(`role add failed: ${(err as Error).message}\n`);
|
|
211
|
-
return 1;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
186
|
+
base = picked.payload as KxmRoleDefinition;
|
|
214
187
|
}
|
|
215
188
|
}
|
|
216
189
|
|
|
190
|
+
const skills = options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
191
|
+
const roster = options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : undefined;
|
|
217
192
|
let roleDef: KxmRoleDefinition;
|
|
218
193
|
if (options.file) {
|
|
219
194
|
const filePath = resolve(runtime.cwd, options.file);
|
|
220
195
|
const content = readFileSync(filePath, "utf8");
|
|
221
196
|
roleDef = parseYaml(content) as KxmRoleDefinition;
|
|
222
197
|
roleDef.id = roleId!;
|
|
198
|
+
} else if (base) {
|
|
199
|
+
roleDef = {
|
|
200
|
+
...base,
|
|
201
|
+
description: options.description || base.description,
|
|
202
|
+
skills: skills ?? base.skills,
|
|
203
|
+
roster: roster ?? base.roster,
|
|
204
|
+
};
|
|
223
205
|
} else {
|
|
224
|
-
const skills = options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
225
|
-
const roster = options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : [];
|
|
226
206
|
roleDef = {
|
|
227
207
|
schema: "kxm.role.v1",
|
|
228
208
|
id: roleId!,
|
|
229
209
|
description: options.description || `Role ${roleId}`,
|
|
230
|
-
skills,
|
|
231
|
-
roster,
|
|
210
|
+
skills: skills ?? [],
|
|
211
|
+
roster: roster ?? [],
|
|
232
212
|
};
|
|
233
213
|
}
|
|
234
214
|
|
|
215
|
+
// A local role file sits in the project the loader reads, and the loader refuses the
|
|
216
|
+
// whole project over a writer roster it rejects, so the role is checked before it lands.
|
|
217
|
+
let repoRoot = runtime.cwd;
|
|
218
|
+
if (scope === "local") {
|
|
219
|
+
const projectRoot = discoverKxmProjectRoot(runtime.cwd);
|
|
220
|
+
if (!projectRoot) {
|
|
221
|
+
print(
|
|
222
|
+
runtime.io,
|
|
223
|
+
runtime.json,
|
|
224
|
+
{ ok: false, command: "role add", error: "project_not_found" },
|
|
225
|
+
"role add failed: local roles belong to a KXM project; run kxm init at the repository root, or pass --scope global",
|
|
226
|
+
);
|
|
227
|
+
return 2;
|
|
228
|
+
}
|
|
229
|
+
const issues = kxmRoleWriteIssues(projectRoot, roleDef.id, stringifyYaml(roleDef));
|
|
230
|
+
if (issues.length > 0) {
|
|
231
|
+
print(
|
|
232
|
+
runtime.io,
|
|
233
|
+
runtime.json,
|
|
234
|
+
{ ok: false, command: "role add", error: "role_invalid", issues },
|
|
235
|
+
`role add failed: with this role the project would not load, so nothing was written\n${issues.map((entry) => ` ${entry.file}: ${entry.code}: ${entry.message}`).join("\n")}`,
|
|
236
|
+
);
|
|
237
|
+
return 2;
|
|
238
|
+
}
|
|
239
|
+
repoRoot = projectRoot;
|
|
240
|
+
}
|
|
241
|
+
|
|
235
242
|
try {
|
|
236
243
|
const res = addRole(roleDef, {
|
|
237
244
|
scope,
|
|
238
|
-
repoRoot
|
|
245
|
+
repoRoot,
|
|
239
246
|
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
240
247
|
overwrite: options.overwrite,
|
|
241
248
|
dryRun: runtime.dryRun,
|
|
@@ -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) => ({
|