@kontextmind/kxm 0.7.99 → 0.7.101
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 +15 -2
- package/docs/contributing/test-matrix.md +2 -0
- package/docs/reference/cli-reference.md +8 -5
- package/docs/reference/config-reference.md +4 -2
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +103 -57
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +19 -4
- package/plugins/kxm/dist/runtime.js +155 -131
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +4 -4
- package/plugins/kxm/src/cli/roles.ts +3 -2
- package/plugins/kxm/src/cli/workflows.ts +34 -9
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/project-config.ts +45 -3
- package/plugins/kxm/src/runtime-store.ts +4 -4
package/plugins/kxm/package.json
CHANGED
|
@@ -81,10 +81,10 @@ kxm workflow record run_12345 decision "Serialize the fixture setup" --stage-id
|
|
|
81
81
|
does not read project `.kxm/workflows/*.yaml`; validate those with
|
|
82
82
|
`kxm init --dry-run --json`.
|
|
83
83
|
|
|
84
|
-
`kxm gate signal` on a
|
|
85
|
-
Runtime and passes `--recovery-action` through; on a hub webhook run
|
|
86
|
-
|
|
87
|
-
secret. Reuse `--delivery-id` to retry one unchanged callback.
|
|
84
|
+
`kxm gate signal` on a run this project's Runtime store holds goes to the
|
|
85
|
+
Runtime and passes `--recovery-action` through; on a hub webhook run, including
|
|
86
|
+
one with the same `run_` + 32-hex shape, it posts a signed callback that needs
|
|
87
|
+
`KXM_WORKFLOW_ID` and the definition's signal secret. Reuse `--delivery-id` to retry one unchanged callback.
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
90
|
kxm gate validate --file workflows.json --json
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "../role.ts";
|
|
20
20
|
import { discoverKxmProjectRoot } from "../project-config.ts";
|
|
21
21
|
import { ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
|
|
22
|
+
import { projectRuntimeOwnsRun } from "../runtime-store.ts";
|
|
22
23
|
import { resumeWorkflowFromRuling, type WorkflowRun } from "../workflow.ts";
|
|
23
24
|
import { print, printPlan, type CliIo, type Runtime } from "./types.ts";
|
|
24
25
|
|
|
@@ -547,9 +548,9 @@ export async function cmdRoleResume(
|
|
|
547
548
|
|
|
548
549
|
const effectiveRuling = ruling?.trim() || "operator_ruling: waived and resumed";
|
|
549
550
|
|
|
550
|
-
//
|
|
551
|
+
// A run this project's Runtime owns; a hub workflow run of the same id shape falls through.
|
|
551
552
|
const projectRoot = discoverKxmProjectRoot(runtime.cwd);
|
|
552
|
-
if (projectRoot &&
|
|
553
|
+
if (projectRoot && projectRuntimeOwnsRun(projectRoot, runId, runtime.env)) {
|
|
553
554
|
if (runtime.dryRun) {
|
|
554
555
|
printPlan(
|
|
555
556
|
runtime,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { stringify } from "yaml";
|
|
4
5
|
import { openReadOnlyDatabase } from "../sqlite.ts";
|
|
5
6
|
import { buildRetrospective, writeRetrospective } from "../retrospective.ts";
|
|
6
7
|
import { redactSecrets } from "../redact.ts";
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
addWorkflowDefinition,
|
|
11
12
|
removeWorkflowDefinition,
|
|
12
13
|
modifyWorkflowDefinition,
|
|
14
|
+
parseWorkflowFile,
|
|
13
15
|
scaffoldWorkflowDefinition,
|
|
14
16
|
WORKFLOW_TEMPLATES,
|
|
15
17
|
} from "../workflow-manager.ts";
|
|
@@ -21,7 +23,7 @@ import {
|
|
|
21
23
|
type WorkflowJournalEntry,
|
|
22
24
|
type WorkflowRun,
|
|
23
25
|
} from "../workflow.ts";
|
|
24
|
-
import { discoverKxmProjectRoot } from "../project-config.ts";
|
|
26
|
+
import { discoverKxmProjectRoot, kxmWorkflowWriteIssues } from "../project-config.ts";
|
|
25
27
|
import { ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
|
|
26
28
|
import { projectRuntimeOwnsRun } from "../runtime-store.ts";
|
|
27
29
|
import type { WorkerOutcome } from "../envelope.ts";
|
|
@@ -213,12 +215,12 @@ export async function cmdWorkflowAdd(
|
|
|
213
215
|
},
|
|
214
216
|
): Promise<number> {
|
|
215
217
|
const scope = options.scope ?? "local";
|
|
218
|
+
const refuse = (error: string, text: string): number => {
|
|
219
|
+
print(runtime.io, runtime.json, { ok: false, command: "workflow add", error }, `workflow add failed: ${text}`);
|
|
220
|
+
return 2;
|
|
221
|
+
};
|
|
216
222
|
let content: Record<string, unknown> | string | undefined;
|
|
217
223
|
if (options.template !== undefined) {
|
|
218
|
-
const refuse = (error: string, text: string): number => {
|
|
219
|
-
print(runtime.io, runtime.json, { ok: false, command: "workflow add", error }, `workflow add failed: ${text}`);
|
|
220
|
-
return 2;
|
|
221
|
-
};
|
|
222
224
|
if (options.file !== undefined || options.pick !== undefined) {
|
|
223
225
|
return refuse("workflow_add_conflict", "--template cannot be combined with --file or --pick");
|
|
224
226
|
}
|
|
@@ -238,8 +240,10 @@ export async function cmdWorkflowAdd(
|
|
|
238
240
|
if (scope === "local") {
|
|
239
241
|
const globalDefs = listWorkflowDefinitions({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
240
242
|
for (const gd of globalDefs) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
// Read the listed file itself: it may be `.yml`, which a lookup by `<id>.yaml` misses.
|
|
244
|
+
const definition = parseWorkflowFile(gd.filePath);
|
|
245
|
+
if (definition && !candidates.some((c) => c.id === gd.id)) {
|
|
246
|
+
candidates.push({ id: gd.id, description: gd.description, label: "global", payload: definition });
|
|
243
247
|
}
|
|
244
248
|
}
|
|
245
249
|
}
|
|
@@ -268,9 +272,30 @@ export async function cmdWorkflowAdd(
|
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
try {
|
|
271
|
-
const
|
|
275
|
+
const document = typeof content === "string" ? content : stringify(content);
|
|
276
|
+
// A local workflow is read by the project loader, which refuses the whole
|
|
277
|
+
// project over one bad file, so it is checked by that loader before it lands.
|
|
278
|
+
let repoRoot = runtime.cwd;
|
|
279
|
+
if (scope === "local") {
|
|
280
|
+
const projectRoot = discoverKxmProjectRoot(runtime.cwd);
|
|
281
|
+
if (!projectRoot) {
|
|
282
|
+
return refuse("project_not_found", "local workflows belong to a KXM project; run kxm init at the repository root, or pass --scope global");
|
|
283
|
+
}
|
|
284
|
+
const issues = kxmWorkflowWriteIssues(projectRoot, workflowId!, document);
|
|
285
|
+
if (issues.length > 0) {
|
|
286
|
+
print(
|
|
287
|
+
runtime.io,
|
|
288
|
+
runtime.json,
|
|
289
|
+
{ ok: false, command: "workflow add", error: "workflow_invalid", issues },
|
|
290
|
+
`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
|
+
);
|
|
292
|
+
return 2;
|
|
293
|
+
}
|
|
294
|
+
repoRoot = projectRoot;
|
|
295
|
+
}
|
|
296
|
+
const res = addWorkflowDefinition(workflowId!, document, {
|
|
272
297
|
scope,
|
|
273
|
-
repoRoot
|
|
298
|
+
repoRoot,
|
|
274
299
|
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
275
300
|
overwrite: options.overwrite,
|
|
276
301
|
dryRun: runtime.dryRun,
|
|
@@ -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.101";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -453,10 +453,14 @@ function readResource(
|
|
|
453
453
|
parent = dirname(parent);
|
|
454
454
|
}
|
|
455
455
|
if (!stat.isFile()) fail("path", "resource_not_file", label, "configuration resource must be a regular file");
|
|
456
|
-
|
|
456
|
+
return { kind, ...(id === undefined ? {} : { id }), file, logicalPath, value: resourceValue(registry, readFileSync(file), label, kind) };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function resourceValue(registry: KxmSchemaRegistry, input: string | Uint8Array, label: string, kind: KxmResourceKind): JsonObject {
|
|
460
|
+
const value = parseRestrictedYaml(input, label);
|
|
457
461
|
const issues = registry.validate(kind, value, label);
|
|
458
462
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
459
|
-
return
|
|
463
|
+
return value;
|
|
460
464
|
}
|
|
461
465
|
|
|
462
466
|
function readTemplateProvenance(registry: KxmSchemaRegistry, root: string): JsonObject | undefined {
|
|
@@ -513,6 +517,7 @@ function listNamedResources(
|
|
|
513
517
|
directory: string,
|
|
514
518
|
logicalDirectory: string,
|
|
515
519
|
kind: "agent" | "model" | "workflow",
|
|
520
|
+
replacedId?: string,
|
|
516
521
|
): Map<string, KxmResource> {
|
|
517
522
|
const resources = new Map<string, KxmResource>();
|
|
518
523
|
if (!existsSync(directory)) return resources;
|
|
@@ -534,6 +539,7 @@ function listNamedResources(
|
|
|
534
539
|
}
|
|
535
540
|
const id = basename(entry.name, ".yaml");
|
|
536
541
|
if (kind === "model" && id === "inventory") continue;
|
|
542
|
+
if (id === replacedId) continue;
|
|
537
543
|
if (!resourceIdentifier(id)) {
|
|
538
544
|
issues.push(issue("path", "resource_id_invalid", displayPath(root, join(directory, entry.name)), `filename-derived identity ${id} is invalid or platform-reserved`));
|
|
539
545
|
continue;
|
|
@@ -1284,6 +1290,34 @@ export function assertNoRegisteredGates(options: object): void {
|
|
|
1284
1290
|
|
|
1285
1291
|
/** Load and semantically validate one complete, path-derived KXM configuration bundle. */
|
|
1286
1292
|
export function loadKxmProject(projectRoot: string, options: KxmConfigOptions = {}): KxmProjectBundle {
|
|
1293
|
+
return loadProjectBundle(projectRoot, options);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* What `loadKxmProject` would refuse once `.kxm/workflows/<workflowId>.yaml` held
|
|
1298
|
+
* `document`, found without writing it: the same parser, schema and bundle rules,
|
|
1299
|
+
* with `document` standing in for any file of that name.
|
|
1300
|
+
*/
|
|
1301
|
+
export function kxmWorkflowWriteIssues(
|
|
1302
|
+
projectRoot: string,
|
|
1303
|
+
workflowId: string,
|
|
1304
|
+
document: string,
|
|
1305
|
+
options: KxmConfigOptions = {},
|
|
1306
|
+
): readonly KxmConfigIssue[] {
|
|
1307
|
+
try {
|
|
1308
|
+
loadProjectBundle(projectRoot, options, { id: workflowId, document });
|
|
1309
|
+
return [];
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
if (error instanceof KxmConfigError) return error.issues;
|
|
1312
|
+
throw error;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function loadProjectBundle(
|
|
1317
|
+
projectRoot: string,
|
|
1318
|
+
options: KxmConfigOptions,
|
|
1319
|
+
workflowCandidate?: { id: string; document: string },
|
|
1320
|
+
): KxmProjectBundle {
|
|
1287
1321
|
assertNoRegisteredGates(options);
|
|
1288
1322
|
const root = resolve(projectRoot);
|
|
1289
1323
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -1323,7 +1357,15 @@ export function loadKxmProject(projectRoot: string, options: KxmConfigOptions =
|
|
|
1323
1357
|
|
|
1324
1358
|
const agents = listNamedResources(registry, root, join(root, ".kxm", "agents"), ".kxm/agents", "agent");
|
|
1325
1359
|
const models = listNamedResources(registry, root, join(root, ".kxm", "models"), ".kxm/models", "model");
|
|
1326
|
-
const workflows = listNamedResources(registry, root, join(root, ".kxm", "workflows"), ".kxm/workflows", "workflow");
|
|
1360
|
+
const workflows = listNamedResources(registry, root, join(root, ".kxm", "workflows"), ".kxm/workflows", "workflow", workflowCandidate?.id);
|
|
1361
|
+
if (workflowCandidate) {
|
|
1362
|
+
const { id, document } = workflowCandidate;
|
|
1363
|
+
const logicalPath = `.kxm/workflows/${id}.yaml`;
|
|
1364
|
+
if (!resourceIdentifier(id)) fail("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
|
|
1365
|
+
const collision = [...workflows.keys()].find((candidate) => candidate.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
1366
|
+
if (collision) fail("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
|
|
1367
|
+
workflows.set(id, { kind: "workflow", id, file: join(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
|
|
1368
|
+
}
|
|
1327
1369
|
const gatePath = join(root, ".kxm", "gates.yaml");
|
|
1328
1370
|
const gateRegistry = existsSync(gatePath) ? readResource(registry, root, gatePath, ".kxm/gates.yaml", "gate-registry") : undefined;
|
|
1329
1371
|
const environments: KxmResource[] = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { DatabaseSync } from "./sqlite.ts";
|
|
4
|
+
import { DatabaseSync, openReadOnlyDatabase } from "./sqlite.ts";
|
|
5
5
|
import { KxmConfigError, validateCoordinator, validateDriveReceipt, validateIntakeMessage, validateRunEvent, kxmCanonicalJson, type JsonValue, type KxmConfigIssue, type KxmConfigOptions } from "./project-config.ts";
|
|
6
6
|
import { kxmUserStateRoot } from "./bindings.ts";
|
|
7
7
|
import { deriveKxmSyncEvent, kxmSyncEventBytes, KxmSyncRedactor } from "./sync-transform.ts";
|
|
@@ -59,13 +59,13 @@ export function kxmProjectRunEventsPath(projectRoot: string, env: NodeJS.Process
|
|
|
59
59
|
/**
|
|
60
60
|
* Whether this project's Runtime event store holds `runId`. Hub workflow runs and Runtime
|
|
61
61
|
* runs share the `run_<32 hex>` shape, so a command addressed by run id asks the owning
|
|
62
|
-
* store instead of guessing from the id. Read-only
|
|
63
|
-
* owns no runs.
|
|
62
|
+
* store instead of guessing from the id. Read-only and leaves no sidecars, so a dry run
|
|
63
|
+
* can ask; a project whose Runtime never ran owns no runs.
|
|
64
64
|
*/
|
|
65
65
|
export function projectRuntimeOwnsRun(projectRoot: string, runId: string, env: NodeJS.ProcessEnv): boolean {
|
|
66
66
|
const path = kxmProjectRunEventsPath(projectRoot, env);
|
|
67
67
|
if (!existsSync(path)) return false;
|
|
68
|
-
const database =
|
|
68
|
+
const database = openReadOnlyDatabase(path);
|
|
69
69
|
try {
|
|
70
70
|
database.exec("PRAGMA busy_timeout = 5000");
|
|
71
71
|
return database.prepare("SELECT 1 FROM runs WHERE run_id = ?").get(runId) !== undefined;
|