@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
|
@@ -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[]) {
|
|
@@ -3,7 +3,6 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
import { isRouteAdmitted, listRoleBindings } from "./routes.ts";
|
|
6
|
-
import { oneShotWriterArgs } from "./harness.ts";
|
|
7
6
|
import { applyAuthoringWitness, captureWorktreeWitness } from "./worktree-witness.ts";
|
|
8
7
|
import {
|
|
9
8
|
buildFormalContextPacket,
|
|
@@ -14,6 +13,7 @@ import {
|
|
|
14
13
|
type HandoffManifestV1,
|
|
15
14
|
} from "./context-packet.ts";
|
|
16
15
|
import { compileKxmWorkflow, type KxmCompiledPlan, type KxmCompiledStep } from "./engine-compile.ts";
|
|
16
|
+
import { BUILTIN_HARNESSES, oneShotPermissionArgs, oneShotWriterArgs, validateHarnessModelPair } from "./harness.ts";
|
|
17
17
|
import {
|
|
18
18
|
effectiveRunDurationBudget,
|
|
19
19
|
isTerminalRunStatus,
|
|
@@ -2693,7 +2693,83 @@ function armRunDurationBudgetTimer(context: KxmRuntimeContext, runId: string): (
|
|
|
2693
2693
|
};
|
|
2694
2694
|
}
|
|
2695
2695
|
|
|
2696
|
-
|
|
2696
|
+
/** Read-only admission guidance for a live run, using the same policies as dispatch. */
|
|
2697
|
+
export function kxmLiveRunPrerequisites(
|
|
2698
|
+
bundle: KxmProjectBundle,
|
|
2699
|
+
workflowId: string,
|
|
2700
|
+
projectRoot: string,
|
|
2701
|
+
): KxmRunHandoff[] {
|
|
2702
|
+
const workflow = bundle.workflows.get(workflowId);
|
|
2703
|
+
if (!workflow) throw runtimeError("run_workflow_unknown", workflowId, `workflow ${workflowId} does not exist in this project`);
|
|
2704
|
+
const plan = compileKxmWorkflow({ id: workflowId, value: workflow.value, logicalPath: workflow.logicalPath });
|
|
2705
|
+
const envelope = { plan, projectLimits: kxmProjectAdmissionLimits(bundle), gates: pinnedGatesForCompiledPlan(plan, bundle, projectRoot) };
|
|
2706
|
+
const prerequisites: KxmRunHandoff[] = [];
|
|
2707
|
+
const limit = unsupportedLimit(envelope);
|
|
2708
|
+
if (limit) prerequisites.push(limit);
|
|
2709
|
+
const visited = new Set<string>();
|
|
2710
|
+
const pending = [plan.entryStepId];
|
|
2711
|
+
while (pending.length > 0) {
|
|
2712
|
+
const stepId = pending.pop()!;
|
|
2713
|
+
if (visited.has(stepId)) continue;
|
|
2714
|
+
visited.add(stepId);
|
|
2715
|
+
const step = plan.steps[stepId]!;
|
|
2716
|
+
for (const transition of Object.values(step.transitions)) {
|
|
2717
|
+
if (transition.to === "step") pending.push(transition.target);
|
|
2718
|
+
}
|
|
2719
|
+
const unsupported = step.kind === "gate"
|
|
2720
|
+
? unsupportedGateStep(plan, step, envelope, { projectRoot }) ?? starterGatePrerequisite(step, envelope.gates, projectRoot)
|
|
2721
|
+
: unsupportedStep(plan, step, "oneshot");
|
|
2722
|
+
if (unsupported) {
|
|
2723
|
+
prerequisites.push({ ...unsupported, stepId });
|
|
2724
|
+
continue;
|
|
2725
|
+
}
|
|
2726
|
+
if (step.kind !== "agent" && step.kind !== "moa") continue;
|
|
2727
|
+
const agentIds = step.assignments.allowedAgents.length > 0 ? step.assignments.allowedAgents : [step.agent];
|
|
2728
|
+
for (const agentId of agentIds) {
|
|
2729
|
+
const agent = bundle.agents.get(agentId);
|
|
2730
|
+
const harness = String(agent?.value.harness ?? bundle.project.value.defaultHarness ?? "pi");
|
|
2731
|
+
const permission = Object.values(step.repositories).includes("write") ? "edit" : "read-only";
|
|
2732
|
+
if (!BUILTIN_HARNESSES.some((entry) => entry.id === harness && entry.oneShot) || !oneShotPermissionArgs(harness, permission)) {
|
|
2733
|
+
prerequisites.push({ reason: "step_unsupported", stepId, field: "harness", detail: `${agentId}: ${harness} has no audited ${permission} one-shot profile; use a supported workflow or execute this work directly in ${harness}, without substituting another harness` });
|
|
2734
|
+
continue;
|
|
2735
|
+
}
|
|
2736
|
+
const route = resolveProducerRoute(projectRoot, step, agentId);
|
|
2737
|
+
if ("error" in route) {
|
|
2738
|
+
prerequisites.push({ ...route.error, stepId, detail: `${agentId}: ${route.error.detail}; configure its model in .kxm/agents/${agentId}.yaml and admit the installed model with kxm routes admit --model <provider/model>` });
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
const writeRefusal = unsupportedLiveWrite(projectRoot, step, agentId, route.selector, envelope.projectLimits.maxConcurrentRuns);
|
|
2742
|
+
if (writeRefusal) {
|
|
2743
|
+
prerequisites.push({ ...writeRefusal, stepId });
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2746
|
+
const compatible = validateHarnessModelPair(harness, route);
|
|
2747
|
+
if (!compatible.valid) {
|
|
2748
|
+
prerequisites.push({ reason: "step_unsupported", stepId, field: "model", detail: `${agentId}: ${compatible.message ?? `${harness} cannot run ${route.selector}`}; configure a model supported by ${harness} in .kxm/agents/${agentId}.yaml` });
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
return prerequisites;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
function starterGatePrerequisite(step: KxmCompiledStep & { kind: "gate" }, gates: KxmPinnedGates, projectRoot: string): Omit<KxmRunHandoff, "stepId"> | undefined {
|
|
2756
|
+
const definition = gates.definitions[step.gate];
|
|
2757
|
+
if (!definition || definition.kind !== "command" || definition.argv.length !== 2 || definition.argv[0] !== "npm" || definition.argv[1] !== "test") return undefined;
|
|
2758
|
+
let testScript: unknown;
|
|
2759
|
+
try {
|
|
2760
|
+
const manifest: unknown = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf8"));
|
|
2761
|
+
if (manifest && typeof manifest === "object" && "scripts" in manifest) {
|
|
2762
|
+
const scripts = manifest.scripts;
|
|
2763
|
+
if (scripts && typeof scripts === "object" && "test" in scripts) testScript = scripts.test;
|
|
2764
|
+
}
|
|
2765
|
+
} catch { /* A missing or unreadable manifest cannot satisfy the starter gate. */ }
|
|
2766
|
+
if (typeof testScript !== "string" || testScript.trim().length === 0) {
|
|
2767
|
+
return { reason: "gate_unsupported", field: `gates.${step.gate}.argv`, detail: `gate ${step.gate} runs npm test but this repository has no readable package.json with scripts.test; configure .kxm/gates.yaml gates.${step.gate}.argv for the repository's actual test runner before driving this workflow` };
|
|
2768
|
+
}
|
|
2769
|
+
return undefined;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
function unsupportedLimit(envelope: Pick<KxmRunPlanEnvelope, "plan" | "projectLimits">): KxmRunHandoff | undefined {
|
|
2697
2773
|
if (envelope.plan.limits.maxAgentTimeMs !== undefined) {
|
|
2698
2774
|
return { reason: "limit_unsupported", field: "limits.maxAgentTimeMs", detail: "agent-time budget enforcement is not available in this slice" };
|
|
2699
2775
|
}
|
|
@@ -2847,7 +2923,7 @@ function unsupportedLiveWrite(
|
|
|
2847
2923
|
return undefined;
|
|
2848
2924
|
}
|
|
2849
2925
|
|
|
2850
|
-
function unsupportedGateStep(plan: KxmCompiledPlan, step: KxmCompiledStep & { kind: "gate" }, envelope: KxmRunPlanEnvelope, context?: { projectRoot: string }): Omit<KxmRunHandoff, "stepId"> | undefined {
|
|
2926
|
+
function unsupportedGateStep(plan: KxmCompiledPlan, step: KxmCompiledStep & { kind: "gate" }, envelope: Pick<KxmRunPlanEnvelope, "gates">, context?: { projectRoot: string }): Omit<KxmRunHandoff, "stepId"> | undefined {
|
|
2851
2927
|
if (!step.gate) return { reason: "step_unsupported", field: "gate", detail: "gate step is missing gate id" };
|
|
2852
2928
|
if (envelope.gates.registry === null) {
|
|
2853
2929
|
return { reason: "step_unsupported", field: "gate", detail: `step ${step.id} refers to a gate but registry is null` };
|
|
@@ -62,6 +62,7 @@ export type HarnessRunCommand = (command: string, args: readonly string[], timeo
|
|
|
62
62
|
export type HarnessRunCommandAsync = (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult | Promise<HarnessCommandResult>;
|
|
63
63
|
|
|
64
64
|
export interface HarnessProbeOptions {
|
|
65
|
+
defaultHarness?: string | undefined;
|
|
65
66
|
env?: NodeJS.ProcessEnv | undefined;
|
|
66
67
|
runCommand?: HarnessRunCommand | undefined;
|
|
67
68
|
timeoutMs?: number | undefined;
|
|
@@ -96,7 +97,7 @@ export interface HarnessStatus {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
export interface HarnessInventory {
|
|
99
|
-
defaultHarness:
|
|
100
|
+
defaultHarness: string;
|
|
100
101
|
harnesses: readonly HarnessStatus[];
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -958,7 +959,7 @@ function probeEntry(
|
|
|
958
959
|
runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
|
|
959
960
|
timeoutMs: number,
|
|
960
961
|
platform: NodeJS.Platform,
|
|
961
|
-
probe: Pick<HarnessProbeOptions, "env" | "existsSync"> = {},
|
|
962
|
+
probe: Pick<HarnessProbeOptions, "env" | "existsSync" | "defaultHarness"> = {},
|
|
962
963
|
): HarnessStatus {
|
|
963
964
|
const issues: string[] = [];
|
|
964
965
|
const found = detectHarnessCommand(entry, runCommand, timeoutMs, platform, probe);
|
|
@@ -987,7 +988,7 @@ function probeEntry(
|
|
|
987
988
|
return {
|
|
988
989
|
id: entry.id,
|
|
989
990
|
label: entry.label,
|
|
990
|
-
default: entry.
|
|
991
|
+
default: entry.id === (probe.defaultHarness ?? DEFAULT_HARNESS),
|
|
991
992
|
mode: entry.mode,
|
|
992
993
|
detected,
|
|
993
994
|
authenticated,
|
|
@@ -1009,7 +1010,7 @@ export function probeHarnesses(options: HarnessProbeOptions = {}): HarnessInvent
|
|
|
1009
1010
|
const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
|
|
1010
1011
|
const platform = options.platform ?? process.platform;
|
|
1011
1012
|
return {
|
|
1012
|
-
defaultHarness: DEFAULT_HARNESS,
|
|
1013
|
+
defaultHarness: options.defaultHarness ?? DEFAULT_HARNESS,
|
|
1013
1014
|
harnesses: BUILTIN_HARNESSES.map((entry) => probeEntry(entry, runCommand, timeoutMs, platform, options)),
|
|
1014
1015
|
};
|
|
1015
1016
|
}
|
|
@@ -1508,7 +1509,7 @@ export function formatHarnessInventory(inventory: HarnessInventory): string {
|
|
|
1508
1509
|
].join(" ");
|
|
1509
1510
|
});
|
|
1510
1511
|
return [
|
|
1511
|
-
`default harness: ${inventory.defaultHarness} (
|
|
1512
|
+
`default harness: ${inventory.defaultHarness} (used when an agent omits harness:)`,
|
|
1512
1513
|
"enable/disable = Git YAML (.kxm/agents, .kxm/models) or the harness's own plugin CLI",
|
|
1513
1514
|
"governed kxm skills are not auto-updated",
|
|
1514
1515
|
header,
|
|
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
|
|
|
11
11
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
12
12
|
import { sessionTokenFixHint } from "./session-token-hint.ts";
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.7.
|
|
14
|
+
const VERSION = "0.7.106";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -1124,6 +1124,7 @@ function validateBundle(
|
|
|
1124
1124
|
options: KxmConfigOptions,
|
|
1125
1125
|
gateRegistry?: KxmResource,
|
|
1126
1126
|
projectRoot?: string,
|
|
1127
|
+
writerRole?: string,
|
|
1127
1128
|
): KxmConfigIssue[] {
|
|
1128
1129
|
const issues: KxmConfigIssue[] = [];
|
|
1129
1130
|
const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate): candidate is JsonObject => Boolean(candidate));
|
|
@@ -1200,9 +1201,9 @@ function validateBundle(
|
|
|
1200
1201
|
if (projectRoot) {
|
|
1201
1202
|
const writerRolePath = join(projectRoot, ".kxm", "roles", "writer.yaml");
|
|
1202
1203
|
const implementerAgent = agents.get("implementer") ?? agents.get("writer");
|
|
1203
|
-
if (existsSync(writerRolePath) && implementerAgent) {
|
|
1204
|
+
if ((writerRole !== undefined || existsSync(writerRolePath)) && implementerAgent) {
|
|
1204
1205
|
try {
|
|
1205
|
-
const rawRole = parseRestrictedYaml(readFileSync(writerRolePath, "utf8"));
|
|
1206
|
+
const rawRole = parseRestrictedYaml(writerRole ?? readFileSync(writerRolePath, "utf8"));
|
|
1206
1207
|
const roleObj = objectValue(rawRole);
|
|
1207
1208
|
const rosterEntries = valuesOf(roleObj ?? {}, "roster")
|
|
1208
1209
|
.map((candidate) => objectValue(candidate))
|
|
@@ -1304,8 +1305,32 @@ export function kxmWorkflowWriteIssues(
|
|
|
1304
1305
|
document: string,
|
|
1305
1306
|
options: KxmConfigOptions = {},
|
|
1306
1307
|
): readonly KxmConfigIssue[] {
|
|
1308
|
+
return candidateIssues(projectRoot, options, { kind: "workflow", id: workflowId, document });
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* What `loadKxmProject` would refuse once `.kxm/roles/<roleId>.yaml` held `document`,
|
|
1313
|
+
* found without writing it. The loader reads only the writer role, so for any other
|
|
1314
|
+
* id this is the project's load as it stands.
|
|
1315
|
+
*/
|
|
1316
|
+
export function kxmRoleWriteIssues(
|
|
1317
|
+
projectRoot: string,
|
|
1318
|
+
roleId: string,
|
|
1319
|
+
document: string,
|
|
1320
|
+
options: KxmConfigOptions = {},
|
|
1321
|
+
): readonly KxmConfigIssue[] {
|
|
1322
|
+
return candidateIssues(projectRoot, options, { kind: "role", id: roleId, document });
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
interface WriteCandidate {
|
|
1326
|
+
kind: "workflow" | "role";
|
|
1327
|
+
id: string;
|
|
1328
|
+
document: string;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function candidateIssues(projectRoot: string, options: KxmConfigOptions, candidate: WriteCandidate): readonly KxmConfigIssue[] {
|
|
1307
1332
|
try {
|
|
1308
|
-
loadProjectBundle(projectRoot, options,
|
|
1333
|
+
loadProjectBundle(projectRoot, options, candidate);
|
|
1309
1334
|
return [];
|
|
1310
1335
|
} catch (error) {
|
|
1311
1336
|
if (error instanceof KxmConfigError) return error.issues;
|
|
@@ -1316,8 +1341,11 @@ export function kxmWorkflowWriteIssues(
|
|
|
1316
1341
|
function loadProjectBundle(
|
|
1317
1342
|
projectRoot: string,
|
|
1318
1343
|
options: KxmConfigOptions,
|
|
1319
|
-
|
|
1344
|
+
candidate?: WriteCandidate,
|
|
1320
1345
|
): KxmProjectBundle {
|
|
1346
|
+
const workflowCandidate = candidate?.kind === "workflow" ? candidate : undefined;
|
|
1347
|
+
// A case-insensitive filesystem serves `Writer.yaml` to the loader's `writer.yaml` lookup.
|
|
1348
|
+
const writerRole = candidate?.kind === "role" && candidate.id.toLocaleLowerCase("en-US") === "writer" ? candidate.document : undefined;
|
|
1321
1349
|
assertNoRegisteredGates(options);
|
|
1322
1350
|
const root = resolve(projectRoot);
|
|
1323
1351
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -1484,7 +1512,7 @@ function loadProjectBundle(
|
|
|
1484
1512
|
}
|
|
1485
1513
|
if (loadIssues.length > 0) throw new KxmConfigError(loadIssues);
|
|
1486
1514
|
|
|
1487
|
-
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root);
|
|
1515
|
+
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, writerRole);
|
|
1488
1516
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
1489
1517
|
const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...workflows.values(), ...environments, ...(gateRegistry ? [gateRegistry] : [])]
|
|
1490
1518
|
.sort((left, right) => compareCodeUnits(left.logicalPath, right.logicalPath));
|