@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
|
@@ -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
|
}
|