@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
|
@@ -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.105";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -1,156 +1,209 @@
|
|
|
1
|
+
import { BUILTIN_HARNESS_IDS, oneShotWriterArgs, type HarnessStatus } from "./harness.ts";
|
|
2
|
+
import { WORKFLOW_TEMPLATES } from "./workflow-manager.ts";
|
|
3
|
+
|
|
1
4
|
export interface RoleSpec {
|
|
5
|
+
agent: string;
|
|
2
6
|
harness: string;
|
|
3
|
-
model: string;
|
|
4
7
|
role: string;
|
|
5
8
|
}
|
|
6
9
|
|
|
10
|
+
export type SuggestionExecution = {
|
|
11
|
+
supported: true;
|
|
12
|
+
prerequisites: string[];
|
|
13
|
+
shell: "powershell" | "posix";
|
|
14
|
+
createCommand: string;
|
|
15
|
+
driveCommand: string;
|
|
16
|
+
statusCommand: string;
|
|
17
|
+
receiptCommand: string;
|
|
18
|
+
} | {
|
|
19
|
+
supported: false;
|
|
20
|
+
error: "harness_unavailable" | "live_write_unsupported" | "workflow_already_exists";
|
|
21
|
+
reason: string;
|
|
22
|
+
nextSteps: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
7
25
|
export interface SuggestionResult {
|
|
8
26
|
workflowId: string;
|
|
27
|
+
template: string;
|
|
9
28
|
area: string;
|
|
10
29
|
confidence: number;
|
|
11
30
|
reasons: string[];
|
|
12
31
|
suggestedSkills: string[];
|
|
13
|
-
roles:
|
|
14
|
-
|
|
15
|
-
writer: RoleSpec;
|
|
16
|
-
critics: RoleSpec[];
|
|
17
|
-
verifier: { kind: "witness"; command: string };
|
|
18
|
-
};
|
|
32
|
+
roles: RoleSpec[];
|
|
33
|
+
/** Installs a definition; it does not create or execute a run. */
|
|
19
34
|
suggestedCommand: string;
|
|
35
|
+
execution: SuggestionExecution;
|
|
20
36
|
}
|
|
21
37
|
|
|
22
38
|
interface WorkflowPattern {
|
|
23
39
|
id: string;
|
|
40
|
+
template: string;
|
|
24
41
|
area: string;
|
|
25
42
|
keywords: string[];
|
|
26
43
|
skills: string[];
|
|
27
|
-
defaultCommand: (prompt: string) => string;
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
const WORKFLOW_PATTERNS: WorkflowPattern[] = [
|
|
31
47
|
{
|
|
32
|
-
id: "
|
|
48
|
+
id: "bug-fix",
|
|
49
|
+
template: "implement-and-verify",
|
|
33
50
|
area: "software-engineering",
|
|
34
51
|
keywords: ["fix", "bug", "flaky", "failure", "timeout", "error", "repro", "crash", "broken", "hang"],
|
|
35
52
|
skills: ["kxm-workflow", "kxm-runs", "kxm-context-memory"],
|
|
36
|
-
defaultCommand: (p) => `kxm run software-engineering/bug-fix "${p}"`,
|
|
37
53
|
},
|
|
38
54
|
{
|
|
39
|
-
id: "
|
|
55
|
+
id: "feature-implementation",
|
|
56
|
+
template: "implement-and-verify",
|
|
40
57
|
area: "software-engineering",
|
|
41
58
|
keywords: ["feature", "implement", "add", "build", "create", "develop", "support", "endpoint", "ui", "tui"],
|
|
42
59
|
skills: ["kxm-workflow", "kxm-peer", "kxm-context-memory"],
|
|
43
|
-
defaultCommand: (p) => `kxm run software-engineering/feature-implementation "${p}"`,
|
|
44
60
|
},
|
|
45
61
|
{
|
|
46
|
-
id: "
|
|
62
|
+
id: "refactoring",
|
|
63
|
+
template: "dual-critic-review",
|
|
47
64
|
area: "software-engineering",
|
|
48
65
|
keywords: ["refactor", "cleanup", "reorganize", "modularize", "deduplicate", "split", "simplify", "deprecate"],
|
|
49
66
|
skills: ["kxm-workflow", "kxm-runs"],
|
|
50
|
-
defaultCommand: (p) => `kxm run software-engineering/refactoring "${p}"`,
|
|
51
67
|
},
|
|
52
68
|
{
|
|
53
|
-
id: "
|
|
69
|
+
id: "vulnerability-remediation",
|
|
70
|
+
template: "dual-critic-review",
|
|
54
71
|
area: "security-reliability",
|
|
55
72
|
keywords: ["cve", "vulnerability", "security", "exploit", "sanitize", "leak", "secret", "injection", "redact", "auth"],
|
|
56
73
|
skills: ["kxm-workflow", "kxm-definitions"],
|
|
57
|
-
defaultCommand: (p) => `kxm run security-reliability/vulnerability-remediation "${p}"`,
|
|
58
74
|
},
|
|
59
75
|
{
|
|
60
|
-
id: "
|
|
76
|
+
id: "reliability-hardening",
|
|
77
|
+
template: "dual-critic-review",
|
|
61
78
|
area: "security-reliability",
|
|
62
79
|
keywords: ["idempotency", "retry", "circuit-breaker", "cas", "lock", "concurrency", "deadlock", "race", "crash-recovery"],
|
|
63
80
|
skills: ["kxm-workflow", "kxm-peer"],
|
|
64
|
-
defaultCommand: (p) => `kxm run security-reliability/reliability-hardening "${p}"`,
|
|
65
81
|
},
|
|
66
82
|
{
|
|
67
|
-
id: "
|
|
83
|
+
id: "pipeline-migration",
|
|
84
|
+
template: "implement-and-verify",
|
|
68
85
|
area: "data-analytics",
|
|
69
86
|
keywords: ["database", "sqlite", "migration", "pipeline", "schema", "transform", "table", "wal", "foreign", "cascading"],
|
|
70
87
|
skills: ["kxm-runs", "kxm-context-memory"],
|
|
71
|
-
defaultCommand: (p) => `kxm run data-analytics/pipeline-migration "${p}"`,
|
|
72
88
|
},
|
|
73
89
|
{
|
|
74
|
-
id: "
|
|
90
|
+
id: "architecture-spike",
|
|
91
|
+
template: "spec-and-plan",
|
|
75
92
|
area: "research-strategy",
|
|
76
93
|
keywords: ["spike", "investigate", "prototype", "research", "feasibility", "benchmark", "explore", "evaluate"],
|
|
77
94
|
skills: ["kxm-session", "kxm-context-memory", "kxm-routing-improve"],
|
|
78
|
-
defaultCommand: (p) => `kxm run research-strategy/architecture-spike "${p}"`,
|
|
79
95
|
},
|
|
80
96
|
];
|
|
81
97
|
|
|
98
|
+
type TemplateStep = { id: string; kind: string; agent?: string; repositories?: Record<string, string> };
|
|
99
|
+
type SuggestionHarness = Pick<HarnessStatus, "id" | "detected" | "authenticated" | "dispatch">;
|
|
100
|
+
|
|
82
101
|
export function suggestWorkflowAndRoles(
|
|
83
102
|
prompt: string,
|
|
84
|
-
options: {
|
|
85
|
-
availableHarnesses?: Array<{ harness: string; auth?: string | undefined }> | undefined;
|
|
86
|
-
} = {},
|
|
103
|
+
options: { availableHarnesses?: readonly SuggestionHarness[] | undefined } = {},
|
|
87
104
|
): SuggestionResult {
|
|
88
105
|
const normalized = prompt.toLowerCase();
|
|
89
106
|
const words = normalized.split(/\W+/).filter(Boolean);
|
|
90
|
-
|
|
91
|
-
let bestPattern: WorkflowPattern = WORKFLOW_PATTERNS[
|
|
92
|
-
let bestScore =
|
|
93
|
-
const
|
|
107
|
+
const claudeOnly = /\bclaude(?:\s+code)?(?:[\s-]+only|\s+exclusively)\b|\b(?:only|exclusively)(?:\s+(?:use|using|with|via))?\s+claude(?:\s+code)?\b/i.test(prompt);
|
|
108
|
+
let bestPattern: WorkflowPattern = WORKFLOW_PATTERNS[1]!;
|
|
109
|
+
let bestScore = 0;
|
|
110
|
+
const reasons: string[] = [];
|
|
94
111
|
|
|
95
112
|
for (const pattern of WORKFLOW_PATTERNS) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (words.includes(keyword) || normalized.includes(keyword)) {
|
|
100
|
-
score += 1;
|
|
101
|
-
matchedWords.push(keyword);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (score > bestScore) {
|
|
105
|
-
bestScore = score;
|
|
113
|
+
const matchedWords = pattern.keywords.filter((keyword) => words.includes(keyword) || normalized.includes(keyword));
|
|
114
|
+
if (matchedWords.length > bestScore) {
|
|
115
|
+
bestScore = matchedWords.length;
|
|
106
116
|
bestPattern = pattern;
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
reasons.length = 0;
|
|
118
|
+
reasons.push(`Matched keywords: ${matchedWords.join(", ")}`);
|
|
109
119
|
}
|
|
110
120
|
}
|
|
121
|
+
if (bestScore === 0) reasons.push("Default fallback: general feature implementation workflow");
|
|
122
|
+
if (claudeOnly) reasons.push("Explicit Claude-only constraint; other harnesses are excluded");
|
|
111
123
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const activeHarnesses = new Set(
|
|
118
|
-
(options.availableHarnesses ?? [])
|
|
119
|
-
.filter((h) => h.auth === "active" || h.auth === "ready" || h.auth === "configured")
|
|
120
|
-
.map((h) => h.harness.toLowerCase()),
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
const hasClaude = activeHarnesses.size === 0 || activeHarnesses.has("claude");
|
|
124
|
-
const hasGrok = activeHarnesses.size === 0 || activeHarnesses.has("grok");
|
|
125
|
-
const hasCodex = activeHarnesses.size === 0 || activeHarnesses.has("codex");
|
|
126
|
-
|
|
127
|
-
const planner: RoleSpec = hasClaude
|
|
128
|
-
? { harness: "claude", model: "fable", role: "planner" }
|
|
129
|
-
: { harness: "codex", model: "gpt-5.6-sol", role: "planner" };
|
|
130
|
-
|
|
131
|
-
const writer: RoleSpec = hasGrok
|
|
132
|
-
? { harness: "grok", model: "grok-4.6", role: "writer" }
|
|
133
|
-
: (activeHarnesses.has("agy")
|
|
134
|
-
? { harness: "agy", model: "gemini-2.5-pro", role: "writer" }
|
|
135
|
-
: { harness: "pi", model: "qwen/qwen3-coder-plus", role: "writer" });
|
|
136
|
-
|
|
137
|
-
const critics: RoleSpec[] = [
|
|
138
|
-
{ harness: "claude", model: "fable", role: "reviewer-arch" },
|
|
139
|
-
{ harness: "codex", model: "gpt-5.6-sol", role: "reviewer-cli" },
|
|
140
|
-
];
|
|
141
|
-
|
|
142
|
-
return {
|
|
124
|
+
const template = WORKFLOW_TEMPLATES[bestPattern.template]!;
|
|
125
|
+
const steps = template.steps as TemplateStep[];
|
|
126
|
+
const base = {
|
|
143
127
|
workflowId: bestPattern.id,
|
|
128
|
+
template: bestPattern.template,
|
|
144
129
|
area: bestPattern.area,
|
|
145
130
|
confidence: bestScore > 0 ? Math.min(1, 0.5 + bestScore * 0.15) : 0.5,
|
|
146
|
-
reasons
|
|
131
|
+
reasons,
|
|
147
132
|
suggestedSkills: bestPattern.skills,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
133
|
+
suggestedCommand: `kxm workflow add ${bestPattern.id} --template ${bestPattern.template}`,
|
|
134
|
+
};
|
|
135
|
+
const writeStep = steps.find((step) => step.kind === "agent" && Object.values(step.repositories ?? {}).includes("write"));
|
|
136
|
+
|
|
137
|
+
const available = (options.availableHarnesses ?? []).filter((entry) =>
|
|
138
|
+
BUILTIN_HARNESS_IDS.includes(entry.id) && entry.detected && entry.authenticated === true
|
|
139
|
+
&& entry.dispatch?.supported === true && entry.dispatch.status === "yes"
|
|
140
|
+
&& (!claudeOnly || entry.id === "claude"));
|
|
141
|
+
const selected = available.find((entry) => entry.id === "claude") ?? available[0];
|
|
142
|
+
if (!selected) {
|
|
143
|
+
const relevant = (options.availableHarnesses ?? []).filter((entry) => !claudeOnly || entry.id === "claude");
|
|
144
|
+
const detail = relevant.map((entry) => `${entry.id}: ${entry.dispatch?.reason ?? (!entry.detected ? "not_detected" : entry.authenticated !== true ? "authentication_unverified" : "dispatch_unavailable")}`).join("; ");
|
|
145
|
+
return {
|
|
146
|
+
...base,
|
|
147
|
+
roles: [],
|
|
148
|
+
execution: {
|
|
149
|
+
supported: false,
|
|
150
|
+
error: "harness_unavailable",
|
|
151
|
+
reason: `${claudeOnly ? "Claude-only execution requires Claude Code" : "Execution requires a supported harness"} detected, authenticated, and ready for dispatch. ${detail || "No verified harness inventory is available."}`,
|
|
152
|
+
nextSteps: claudeOnly
|
|
153
|
+
? ["Install Claude Code if missing, authenticate with claude auth login, then confirm claude auth status and kxm harness list. Other harnesses will not be substituted."]
|
|
154
|
+
: ["Install and authenticate a supported harness, then run kxm harness list to check detection, authentication, and dispatch readiness. Pi requires a configured model/provider auth context."],
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (writeStep && !oneShotWriterArgs(selected.id)) {
|
|
160
|
+
return {
|
|
161
|
+
...base,
|
|
162
|
+
roles: [],
|
|
163
|
+
execution: {
|
|
164
|
+
supported: false,
|
|
165
|
+
error: "live_write_unsupported",
|
|
166
|
+
reason: `${bestPattern.template} requires repository writes at step ${writeStep.id}, but the selected ${selected.id} harness has no audited live writer profile. No other harness will be substituted.`,
|
|
167
|
+
nextSteps: [
|
|
168
|
+
`Run the implementation directly in ${claudeOnly ? "Claude Code" : selected.id} in the target worktree, and run the repository's actual verification command.`,
|
|
169
|
+
`For read-only KXM planning, try kxm suggest "Investigate an architecture spike${claudeOnly ? " using Claude only" : ""}"; the shipped spec-and-plan template does not implement changes.`,
|
|
170
|
+
"Installing a definition or creating a run does not execute it. Simulation is not evidence of a bug fix or workflow completion.",
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const roles: RoleSpec[] = [];
|
|
177
|
+
for (const step of steps) {
|
|
178
|
+
if (!step.agent) continue;
|
|
179
|
+
const role = roles.find((entry) => entry.agent === step.agent);
|
|
180
|
+
if (role) role.role += `, ${step.id}`;
|
|
181
|
+
else roles.push({ agent: step.agent, harness: selected.id, role: step.id });
|
|
182
|
+
}
|
|
183
|
+
const shell = process.platform === "win32" ? "powershell" : "posix";
|
|
184
|
+
const quotedPrompt = shell === "powershell"
|
|
185
|
+
? `'${prompt.replace(/['\u2018-\u201b]/g, "$&$&")}'`
|
|
186
|
+
: `'${prompt.replace(/'/g, "'\\''")}'`;
|
|
187
|
+
return {
|
|
188
|
+
...base,
|
|
189
|
+
roles,
|
|
190
|
+
execution: {
|
|
191
|
+
supported: true,
|
|
192
|
+
prerequisites: [
|
|
193
|
+
"Run kxm init in the target repository if needed, then install the exact template with the suggested command. If that workflow ID already exists, stop and review it; the commands below apply only to a newly installed template, not an existing definition with potentially different agents or permissions.",
|
|
194
|
+
...roles.map((role) => `Set harness: ${role.harness} and model: <your authenticated compatible model selector> in .kxm/agents/${role.agent}.yaml. Admit that exact selector in .kxm/routes.yaml and ensure it is not disabled; this recommendation does not choose a model or change routing.`),
|
|
195
|
+
...(writeStep ? [
|
|
196
|
+
`Keep each writer step at assignments.maximum: 1 and set limits.maxConcurrentRuns: 1 in .kxm/project.yaml; the live ${selected.id} writer profile requires a lone writer in the checkout.`,
|
|
197
|
+
`If .kxm/roster.yaml exists, it must be readable kxm.developer-roster.v1 with a lineup.writer route matching harness: ${selected.id} and the selected writer model (model or vendor/model), status: admitted, and permissions containing edit.`,
|
|
198
|
+
"Configure the test gate in .kxm/gates.yaml with kind: command and argv for this repository's actual verification command. A scaffold/example command or simulation is not evidence that the implementation works.",
|
|
199
|
+
] : []),
|
|
200
|
+
`Validate the installed workflow with kxm gate validate --file .kxm/workflows/${bestPattern.id}.yaml and the project configuration with kxm init --dry-run before creating a run.`,
|
|
201
|
+
],
|
|
202
|
+
shell,
|
|
203
|
+
createCommand: `kxm run ${bestPattern.id} -- ${quotedPrompt}`,
|
|
204
|
+
driveCommand: "kxm runs drive <runId> --wait",
|
|
205
|
+
statusCommand: "kxm runs status <runId>",
|
|
206
|
+
receiptCommand: "kxm runs receipt <runId>",
|
|
153
207
|
},
|
|
154
|
-
suggestedCommand: bestPattern.defaultCommand(prompt.replace(/"/g, '\\"')),
|
|
155
208
|
};
|
|
156
209
|
}
|
|
@@ -5,9 +5,28 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { stringify } from "yaml";
|
|
10
10
|
import { repoConfigDirectory, userConfigDirectory } from "./config.ts";
|
|
11
|
+
import { compileKxmWorkflow } from "./engine-compile.ts";
|
|
12
|
+
import { KxmConfigError, KxmSchemaRegistry, kxmResourceIdentifier, parseRestrictedYaml } from "./project-config.ts";
|
|
13
|
+
|
|
14
|
+
function assertWorkflowId(workflowId: string): void {
|
|
15
|
+
if (!kxmResourceIdentifier(workflowId)) {
|
|
16
|
+
throw new Error(`workflow_id_invalid: '${workflowId}' must be a flat lowercase slug of at most 64 characters, starting with a letter and using letters, digits, single hyphens or underscores; paths and platform-reserved names are not allowed (use 'bug-fix', not 'software-engineering/bug-fix')`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let workflowSchemaRegistry: KxmSchemaRegistry | undefined;
|
|
21
|
+
|
|
22
|
+
function workflowPayload(workflowId: string, content: Record<string, unknown> | string, filePath: string): string {
|
|
23
|
+
const payload = typeof content === "string" ? content : stringify(content);
|
|
24
|
+
const value = parseRestrictedYaml(payload, filePath);
|
|
25
|
+
const issues = (workflowSchemaRegistry ??= new KxmSchemaRegistry()).validate("workflow", value, filePath);
|
|
26
|
+
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
27
|
+
compileKxmWorkflow({ id: workflowId, value, logicalPath: filePath });
|
|
28
|
+
return payload;
|
|
29
|
+
}
|
|
11
30
|
|
|
12
31
|
export interface WorkflowDefSummary {
|
|
13
32
|
id: string;
|
|
@@ -197,9 +216,7 @@ export function parseWorkflowFile(filePath: string): Record<string, unknown> | u
|
|
|
197
216
|
if (!existsSync(filePath)) return undefined;
|
|
198
217
|
try {
|
|
199
218
|
const raw = readFileSync(filePath, "utf8");
|
|
200
|
-
|
|
201
|
-
if (!parsed || typeof parsed !== "object") return undefined;
|
|
202
|
-
return parsed;
|
|
219
|
+
return parseRestrictedYaml(raw, filePath);
|
|
203
220
|
} catch {
|
|
204
221
|
return undefined;
|
|
205
222
|
}
|
|
@@ -218,11 +235,11 @@ export function listWorkflowDefinitions(options: {
|
|
|
218
235
|
const localDefs = new Map<string, { def: Record<string, unknown>; filePath: string }>();
|
|
219
236
|
if (scopeFilter !== "global" && existsSync(localDir)) {
|
|
220
237
|
for (const entry of readdirSync(localDir)) {
|
|
221
|
-
|
|
238
|
+
const id = entry.endsWith(".yaml") ? entry.slice(0, -5) : entry.endsWith(".yml") ? entry.slice(0, -4) : undefined;
|
|
239
|
+
if (id && kxmResourceIdentifier(id)) {
|
|
222
240
|
const filePath = join(localDir, entry);
|
|
223
241
|
const def = parseWorkflowFile(filePath);
|
|
224
242
|
if (def) {
|
|
225
|
-
const id = (def.id as string) || entry.replace(/\.ya?ml$/i, "");
|
|
226
243
|
localDefs.set(id, { def, filePath });
|
|
227
244
|
}
|
|
228
245
|
}
|
|
@@ -232,11 +249,11 @@ export function listWorkflowDefinitions(options: {
|
|
|
232
249
|
const globalDefs = new Map<string, { def: Record<string, unknown>; filePath: string }>();
|
|
233
250
|
if (scopeFilter !== "local" && existsSync(globalDir)) {
|
|
234
251
|
for (const entry of readdirSync(globalDir)) {
|
|
235
|
-
|
|
252
|
+
const id = entry.endsWith(".yaml") ? entry.slice(0, -5) : entry.endsWith(".yml") ? entry.slice(0, -4) : undefined;
|
|
253
|
+
if (id && kxmResourceIdentifier(id)) {
|
|
236
254
|
const filePath = join(globalDir, entry);
|
|
237
255
|
const def = parseWorkflowFile(filePath);
|
|
238
256
|
if (def) {
|
|
239
|
-
const id = (def.id as string) || entry.replace(/\.ya?ml$/i, "");
|
|
240
257
|
globalDefs.set(id, { def, filePath });
|
|
241
258
|
}
|
|
242
259
|
}
|
|
@@ -248,7 +265,7 @@ export function listWorkflowDefinitions(options: {
|
|
|
248
265
|
const steps = Array.isArray(def.steps) ? def.steps : [];
|
|
249
266
|
for (const step of steps) {
|
|
250
267
|
if (step && typeof step === "object") {
|
|
251
|
-
const role = (step as Record<string, unknown>).
|
|
268
|
+
const role = (step as Record<string, unknown>).agent;
|
|
252
269
|
if (typeof role === "string" && !roles.includes(role)) {
|
|
253
270
|
roles.push(role);
|
|
254
271
|
}
|
|
@@ -298,6 +315,7 @@ export function getWorkflowDefinition(
|
|
|
298
315
|
userConfigDir?: string | undefined;
|
|
299
316
|
} = {},
|
|
300
317
|
): { workflow: Record<string, unknown>; scope: "global" | "local"; filePath: string } | undefined {
|
|
318
|
+
assertWorkflowId(workflowId);
|
|
301
319
|
const scope = options.scope ?? "all";
|
|
302
320
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
303
321
|
|
|
@@ -331,19 +349,21 @@ export function addWorkflowDefinition(
|
|
|
331
349
|
dryRun?: boolean | undefined;
|
|
332
350
|
} = {},
|
|
333
351
|
): { id: string; filePath: string; scope: "global" | "local" } {
|
|
352
|
+
assertWorkflowId(workflowId);
|
|
334
353
|
const scope = options.scope ?? "local";
|
|
335
354
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
336
|
-
const dir = options.
|
|
337
|
-
? workflowsDirectory(scope, repoRoot, options.userConfigDir)
|
|
338
|
-
: ensureWorkflowsDirectory(scope, repoRoot, options.userConfigDir);
|
|
355
|
+
const dir = workflowsDirectory(scope, repoRoot, options.userConfigDir);
|
|
339
356
|
const filePath = join(dir, `${workflowId}.yaml`);
|
|
340
357
|
|
|
341
358
|
if (existsSync(filePath) && !options.overwrite) {
|
|
342
359
|
throw new Error(`workflow_already_exists: workflow '${workflowId}' already exists at ${filePath}`);
|
|
343
360
|
}
|
|
344
361
|
|
|
345
|
-
const payload =
|
|
346
|
-
if (!options.dryRun)
|
|
362
|
+
const payload = workflowPayload(workflowId, content, filePath);
|
|
363
|
+
if (!options.dryRun) {
|
|
364
|
+
ensureWorkflowsDirectory(scope, repoRoot, options.userConfigDir);
|
|
365
|
+
writeFileSync(filePath, payload, "utf8");
|
|
366
|
+
}
|
|
347
367
|
return { id: workflowId, filePath, scope };
|
|
348
368
|
}
|
|
349
369
|
|
|
@@ -356,6 +376,7 @@ export function removeWorkflowDefinition(
|
|
|
356
376
|
dryRun?: boolean | undefined;
|
|
357
377
|
} = {},
|
|
358
378
|
): { id: string; removed: boolean; filePath: string; scope: "global" | "local" } {
|
|
379
|
+
assertWorkflowId(workflowId);
|
|
359
380
|
const scope = options.scope ?? "local";
|
|
360
381
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
361
382
|
const dir = workflowsDirectory(scope, repoRoot, options.userConfigDir);
|
|
@@ -392,11 +413,13 @@ export function modifyWorkflowDefinition(
|
|
|
392
413
|
|
|
393
414
|
const scope = options.scope ?? target.scope;
|
|
394
415
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
395
|
-
const dir = options.
|
|
396
|
-
? workflowsDirectory(scope, repoRoot, options.userConfigDir)
|
|
397
|
-
: ensureWorkflowsDirectory(scope, repoRoot, options.userConfigDir);
|
|
416
|
+
const dir = workflowsDirectory(scope, repoRoot, options.userConfigDir);
|
|
398
417
|
const filePath = join(dir, `${workflowId}.yaml`);
|
|
399
418
|
|
|
400
|
-
|
|
419
|
+
const payload = workflowPayload(workflowId, updated, filePath);
|
|
420
|
+
if (!options.dryRun) {
|
|
421
|
+
ensureWorkflowsDirectory(scope, repoRoot, options.userConfigDir);
|
|
422
|
+
writeFileSync(filePath, payload, "utf8");
|
|
423
|
+
}
|
|
401
424
|
return { id: workflowId, workflow: updated, filePath, scope };
|
|
402
425
|
}
|