@kontextmind/kxm 0.7.99 → 0.7.100
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 +2 -0
- package/docs/reference/cli-reference.md +4 -3
- 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 +101 -55
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +19 -4
- package/plugins/kxm/dist/runtime.js +19 -4
- package/plugins/kxm/package.json +1 -1
- 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/CHANGELOG.md
CHANGED
|
@@ -319,6 +319,18 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
319
319
|
|
|
320
320
|
### Fixed
|
|
321
321
|
|
|
322
|
+
- **A local `kxm workflow add` writes only what the project loader accepts, where it reads
|
|
323
|
+
it.** Local scope needs a KXM project (`project_not_found` outside one, creating nothing)
|
|
324
|
+
and writes to the project root's `.kxm/workflows/` from any subdirectory. Before writing,
|
|
325
|
+
also under `--dry-run`, the project loader checks the project with the new document; if
|
|
326
|
+
it would not load, the command exits 2 with `workflow_invalid`, lists the issues and
|
|
327
|
+
writes nothing. A `--file` in the shape written through 0.7.92 is refused, and
|
|
328
|
+
`--overwrite` repairs a file left in that shape. See
|
|
329
|
+
`docs/reference/cli-reference.md#kxm-workflow-add`.
|
|
330
|
+
- **`kxm workflow add --pick <global-id>` copies the global definition.** In local scope,
|
|
331
|
+
picking a global definition wrote the one-step scaffold under its id and reported
|
|
332
|
+
success. It now writes the global definition's content, with `--description` replacing
|
|
333
|
+
its description, and the loader check refuses one the project cannot load.
|
|
322
334
|
- **Live `kxm runs drive` can author on an audited writer profile.** A write-repository
|
|
323
335
|
step on pi (`-a`, with extensions, skills, and the session off) or grok
|
|
324
336
|
(`--always-approve`, with subagents and web search off) runs against the checkout.
|
|
@@ -61,6 +61,8 @@ to run one file is in [Develop KXM](development.md#run-one-file-or-one-test).
|
|
|
61
61
|
| Dispatch context: only committed, pinned memory and hash-verified promoted skills reach an agent; the rest is a `dispatch_context_*` gap | `engine.test.ts` ("dispatch context: agents receive only committed, pinned memory and verified skills; anything else is withheld with a gap and the step still completes") |
|
|
62
62
|
| `kxm run` prints the simulated drive command for the new run | `cli.test.ts` ("kxm run prints the simulated drive command for the created run") |
|
|
63
63
|
| `kxm workflow add --template` writes workflows that validate and plan; an impossible gate outcome is refused | `cli-experience.test.ts` ("workflow add templates validate and plan a run, and a gate outcome the step can never produce is refused") |
|
|
64
|
+
| `kxm workflow add` writes a local workflow only where the loader reads it and only if the project still loads; outside a project it refuses | `role-and-workflow-manager.test.ts` ("workflow add writes only what the project loader accepts, at the project root, and loadKxmProject still loads") |
|
|
65
|
+
| `kxm workflow add --pick <global-id>` copies the global definition into the project, not the scaffold, and the loader check refuses one that does not fit | `role-and-workflow-manager.test.ts` ("workflow add --pick <global-id> copies that global definition into the project, and refuses one the project loader rejects") |
|
|
64
66
|
| `default.yaml` and the 13-step `fix.yaml` compile deterministically; back edges need budgets | `engine-compile.test.ts` |
|
|
65
67
|
| Artifact gate: non-empty regular files pass; missing, empty, non-file and escaping paths fail | `artifacts-exist.test.ts` |
|
|
66
68
|
| Vision gate: strict verdicts, admitted routes only, unreadable images fail closed | `vision-gate.test.ts` |
|
|
@@ -91,7 +91,7 @@ kxm -V
|
|
|
91
91
|
- The `command` field is not always the words you typed: `hub stop` and `session stop` report `stop`, `session token` reports `auth token`, `routes admit` and `routes disable` report `routes admitted` and `routes disabled`, `models inventory-refresh` reports `models inventory refresh`, `workflow export` reports `retrospective export`, `gate degrade` reports `workflow degrade`, `gate signal` and `workflow signal` report `signal`, `gate github watch` reports `github watch`, `agent worker` reports `worker`, and `improve report` reports `improve`.
|
|
92
92
|
- A result with `ok: false` is written to stderr in both text and JSON mode; everything else goes to stdout. `runtime status` is the exception: when the supervisor is down it prints `ok: true, running: false` on stdout and exits 1.
|
|
93
93
|
- `peer` subcommands and `workflow checkpoint|record|wait` print the hub's result object as returned, tagged with `schema` but without `ok` or `command`. Their failures print `{"ok":false,"error":"command_failed","detail":"..."}`. Text mode prints the same JSON.
|
|
94
|
-
- Several error paths ignore `--json` and print one plain line on stderr: argument checks in `agent worker`, `session start`, `workflow start`, `gate degrade`, `gate signal`, and `gate github watch`; every error from `config`, `role`, `workflow definitions|add|remove|modify` (except the
|
|
94
|
+
- Several error paths ignore `--json` and print one plain line on stderr: argument checks in `agent worker`, `session start`, `workflow start`, `gate degrade`, `gate signal`, and `gate github watch`; every error from `config`, `role`, `workflow definitions|add|remove|modify` (except the `workflow add` refusals listed under it, which honor `--json`), `goal`, `task`, `memory`, `skills` (other than `skills create --dry-run`), `suggest`, `studio`, and `completion`. Check the exit code before parsing stdout.
|
|
95
95
|
- Every `context` subcommand exits 1 with a Node.js stack trace and no JSON when the hub cannot be reached.
|
|
96
96
|
- Output is redacted. Values of environment variables whose names contain `TOKEN`, `SECRET`, `KEY`, or `PASSWORD` are replaced with `[redacted]`, and 64-character hex strings are replaced unless they appear in a known digest field such as `configRevision` or `sha256`. `session brief` and `auth token` print the session token itself; treat their output as a credential.
|
|
97
97
|
|
|
@@ -1961,7 +1961,7 @@ WORKFLOW DEFINITIONS:
|
|
|
1961
1961
|
kxm workflow add [workflowId] [--template <name> | --file <path> | --pick [selection]] [--description <text>] [--scope global|local] [--overwrite]
|
|
1962
1962
|
```
|
|
1963
1963
|
|
|
1964
|
-
Add a workflow definition to global or local configuration. With `--template <name>`, the named built-in template is written under the workflow ID. Without a workflow ID, or with `--pick`, you choose from the built-in templates and, for local scope, existing global definitions. With `--file`, the YAML file is copied as-is. Otherwise a one-step scaffold is written: one `implementer` agent step with write access to `control` that ends the run `completed` on `passed` and `failed` on `failed`.
|
|
1964
|
+
Add a workflow definition to global or local configuration. With `--template <name>`, the named built-in template is written under the workflow ID. Without a workflow ID, or with `--pick`, you choose from the built-in templates and, for local scope, existing global definitions; a global definition with a template's ID is not offered. The choice is written under its own ID with its content: the template, or a copy of the global definition's file, with `--description` replacing its description. With `--file`, the YAML file is copied as-is once it passes the local check below. Otherwise a one-step scaffold is written: one `implementer` agent step with write access to `control` that ends the run `completed` on `passed` and `failed` on `failed`.
|
|
1965
1965
|
|
|
1966
1966
|
| Option | Argument | Default | Description |
|
|
1967
1967
|
|---|---|---|---|
|
|
@@ -1975,7 +1975,8 @@ Add a workflow definition to global or local configuration. With `--template <na
|
|
|
1975
1975
|
- Templates: `implement-and-verify` runs the `implementer` agent, then the project's `test` gate, and a failing gate (`implementation-failure`) sends the work back to `implement` at most twice. `dual-critic-review` adds two review steps between them, both run as the `coordinator` agent with read access; point `review-arch` and `review-cli` at your own agents for independent critics. `spec-and-plan` plans and then reviews the plan, both as `coordinator`, reading the repository only.
|
|
1976
1976
|
- The templates and the scaffold are valid `kxm.workflow.v1` definitions that use only what `kxm init` creates: the `coordinator` and `implementer` agents, the `control` repository, and the `test` gate. Each was checked with `kxm init --json` and `kxm run <id> --dry-run` for this page. A new file under `.kxm/workflows/` is a permission expansion that `kxm trust check` asks you to review before you commit it.
|
|
1977
1977
|
- Writes `<scope dir>/workflows/<id>.yaml`. `kxm run` loads only `.kxm/workflows/`, so a `--scope global` definition is not runnable until it is copied into a project. `--dry-run` plans the write and writes nothing.
|
|
1978
|
-
-
|
|
1978
|
+
- Local scope belongs to a KXM project: the file lands in the project root's `.kxm/workflows/` from any subdirectory, and outside a project the command refuses with `project_not_found` and creates nothing. Before writing, the project loader checks the project with the new document in place of any file of that ID, using the parser, schema and bundle rules `kxm run` uses. If the project would not load, the command refuses with `workflow_invalid`, lists each issue and writes nothing, also under `--dry-run`. So `--file` or a picked global definition with a top-level `id` or a `role:` step, the shape `workflow add` wrote through 0.7.92, is refused, and `--overwrite` replaces a file left in that shape. Global scope is not checked, because no loader reads it.
|
|
1979
|
+
- Refusals exit 2 and honor `--json`: `workflow_template_unknown` (the text names the three templates), `workflow_id_required` (no workflow ID), `workflow_add_conflict` (`--template` combined with `--file` or `--pick`), `project_not_found`, and `workflow_invalid` (with `issues`, each `{phase, code, file, message}`). An existing definition without `--overwrite` exits 1 with a plain `workflow add failed: workflow_already_exists: ...` line, also under `--dry-run`.
|
|
1979
1980
|
- JSON keys: `workflowId`, `id`, `filePath`, `scope`.
|
|
1980
1981
|
|
|
1981
1982
|
Start a first workflow from a template:
|
|
@@ -889,8 +889,10 @@ compiles and pins the plan, then drives it live, or without models with
|
|
|
889
889
|
reads). Both are valid `kxm.workflow.v1` definitions that use only what
|
|
890
890
|
`kxm init` creates: the `coordinator` and `implementer` agents, the `control`
|
|
891
891
|
repository, and the `test` gate. The templates route gate failures on
|
|
892
|
-
`implementation-failure`.
|
|
893
|
-
|
|
892
|
+
`implementation-failure`. A local add is checked by the project loader first:
|
|
893
|
+
if the project would not load with the new file, it is refused with
|
|
894
|
+
`workflow_invalid` and nothing is written. Review the new file with
|
|
895
|
+
`kxm trust check` before committing it.
|
|
894
896
|
|
|
895
897
|
## `.kxm/gates.yaml` (`kxm.gate-registry.v1`)
|
|
896
898
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "kxm",
|
|
4
4
|
"displayName": "KXM",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.100",
|
|
6
6
|
"description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "KontextMind",
|
package/plugins/kxm/dist/cli.js
CHANGED
|
@@ -164,10 +164,10 @@ var require_code = __commonJS({
|
|
|
164
164
|
function interpolate(x2) {
|
|
165
165
|
return typeof x2 == "number" || typeof x2 == "boolean" || x2 === null ? x2 : safeStringify(Array.isArray(x2) ? x2.join(",") : x2);
|
|
166
166
|
}
|
|
167
|
-
function
|
|
167
|
+
function stringify12(x2) {
|
|
168
168
|
return new _Code(safeStringify(x2));
|
|
169
169
|
}
|
|
170
|
-
exports.stringify =
|
|
170
|
+
exports.stringify = stringify12;
|
|
171
171
|
function safeStringify(x2) {
|
|
172
172
|
return JSON.stringify(x2).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
173
173
|
}
|
|
@@ -8918,7 +8918,7 @@ var require_stringify = __commonJS({
|
|
|
8918
8918
|
props.push(doc.directives.tagString(tag));
|
|
8919
8919
|
return props.join(" ");
|
|
8920
8920
|
}
|
|
8921
|
-
function
|
|
8921
|
+
function stringify12(item, ctx, onComment, onChompKeep) {
|
|
8922
8922
|
if (identity.isPair(item))
|
|
8923
8923
|
return item.toString(ctx, onComment, onChompKeep);
|
|
8924
8924
|
if (identity.isAlias(item)) {
|
|
@@ -8947,7 +8947,7 @@ var require_stringify = __commonJS({
|
|
|
8947
8947
|
${ctx.indent}${str}`;
|
|
8948
8948
|
}
|
|
8949
8949
|
exports.createStringifyContext = createStringifyContext;
|
|
8950
|
-
exports.stringify =
|
|
8950
|
+
exports.stringify = stringify12;
|
|
8951
8951
|
}
|
|
8952
8952
|
});
|
|
8953
8953
|
|
|
@@ -8957,7 +8957,7 @@ var require_stringifyPair = __commonJS({
|
|
|
8957
8957
|
"use strict";
|
|
8958
8958
|
var identity = require_identity();
|
|
8959
8959
|
var Scalar = require_Scalar();
|
|
8960
|
-
var
|
|
8960
|
+
var stringify12 = require_stringify();
|
|
8961
8961
|
var stringifyComment = require_stringifyComment();
|
|
8962
8962
|
function stringifyPair({ key, value }, ctx, onComment, onChompKeep) {
|
|
8963
8963
|
const { allNullValues, doc, indent, indentStep, options: { commentString, indentSeq, simpleKeys } } = ctx;
|
|
@@ -8979,7 +8979,7 @@ var require_stringifyPair = __commonJS({
|
|
|
8979
8979
|
});
|
|
8980
8980
|
let keyCommentDone = false;
|
|
8981
8981
|
let chompKeep = false;
|
|
8982
|
-
let str =
|
|
8982
|
+
let str = stringify12.stringify(key, ctx, () => keyCommentDone = true, () => chompKeep = true);
|
|
8983
8983
|
if (!explicitKey && !ctx.inFlow && str.length > 1024) {
|
|
8984
8984
|
if (simpleKeys)
|
|
8985
8985
|
throw new Error("With simple keys, single line scalar must not span more than 1024 characters");
|
|
@@ -9031,7 +9031,7 @@ ${indent}:`;
|
|
|
9031
9031
|
ctx.indent = ctx.indent.substring(2);
|
|
9032
9032
|
}
|
|
9033
9033
|
let valueCommentDone = false;
|
|
9034
|
-
const valueStr =
|
|
9034
|
+
const valueStr = stringify12.stringify(value, ctx, () => valueCommentDone = true, () => chompKeep = true);
|
|
9035
9035
|
let ws = " ";
|
|
9036
9036
|
if (keyComment || vsb || vcb) {
|
|
9037
9037
|
ws = vsb ? "\n" : "";
|
|
@@ -9172,7 +9172,7 @@ var require_addPairToJSMap = __commonJS({
|
|
|
9172
9172
|
"use strict";
|
|
9173
9173
|
var log = require_log();
|
|
9174
9174
|
var merge = require_merge();
|
|
9175
|
-
var
|
|
9175
|
+
var stringify12 = require_stringify();
|
|
9176
9176
|
var identity = require_identity();
|
|
9177
9177
|
var toJS = require_toJS();
|
|
9178
9178
|
function addPairToJSMap(ctx, map, { key, value }) {
|
|
@@ -9208,7 +9208,7 @@ var require_addPairToJSMap = __commonJS({
|
|
|
9208
9208
|
if (typeof jsKey !== "object")
|
|
9209
9209
|
return String(jsKey);
|
|
9210
9210
|
if (identity.isNode(key) && ctx?.doc) {
|
|
9211
|
-
const strCtx =
|
|
9211
|
+
const strCtx = stringify12.createStringifyContext(ctx.doc, {});
|
|
9212
9212
|
strCtx.anchors = /* @__PURE__ */ new Set();
|
|
9213
9213
|
for (const node of ctx.anchors.keys())
|
|
9214
9214
|
strCtx.anchors.add(node.anchor);
|
|
@@ -9275,12 +9275,12 @@ var require_stringifyCollection = __commonJS({
|
|
|
9275
9275
|
"node_modules/yaml/dist/stringify/stringifyCollection.js"(exports) {
|
|
9276
9276
|
"use strict";
|
|
9277
9277
|
var identity = require_identity();
|
|
9278
|
-
var
|
|
9278
|
+
var stringify12 = require_stringify();
|
|
9279
9279
|
var stringifyComment = require_stringifyComment();
|
|
9280
9280
|
function stringifyCollection(collection, ctx, options) {
|
|
9281
9281
|
const flow = ctx.inFlow ?? collection.flow;
|
|
9282
|
-
const
|
|
9283
|
-
return
|
|
9282
|
+
const stringify13 = flow ? stringifyFlowCollection : stringifyBlockCollection;
|
|
9283
|
+
return stringify13(collection, ctx, options);
|
|
9284
9284
|
}
|
|
9285
9285
|
function stringifyBlockCollection({ comment, items }, ctx, { blockItemPrefix, flowChars, itemIndent, onChompKeep, onComment }) {
|
|
9286
9286
|
const { indent, options: { commentString } } = ctx;
|
|
@@ -9305,7 +9305,7 @@ var require_stringifyCollection = __commonJS({
|
|
|
9305
9305
|
}
|
|
9306
9306
|
}
|
|
9307
9307
|
chompKeep = false;
|
|
9308
|
-
let str2 =
|
|
9308
|
+
let str2 = stringify12.stringify(item, itemCtx, () => comment2 = null, () => chompKeep = true);
|
|
9309
9309
|
if (comment2)
|
|
9310
9310
|
str2 += stringifyComment.lineComment(str2, itemIndent, commentString(comment2));
|
|
9311
9311
|
if (chompKeep && comment2)
|
|
@@ -9372,7 +9372,7 @@ ${indent}${line}` : "\n";
|
|
|
9372
9372
|
}
|
|
9373
9373
|
if (comment)
|
|
9374
9374
|
reqNewline = true;
|
|
9375
|
-
let str =
|
|
9375
|
+
let str = stringify12.stringify(item, itemCtx, () => comment = null);
|
|
9376
9376
|
reqNewline || (reqNewline = lines.length > linesAtValue || str.includes("\n"));
|
|
9377
9377
|
if (i < items.length - 1) {
|
|
9378
9378
|
str += ",";
|
|
@@ -10733,7 +10733,7 @@ var require_stringifyDocument = __commonJS({
|
|
|
10733
10733
|
"node_modules/yaml/dist/stringify/stringifyDocument.js"(exports) {
|
|
10734
10734
|
"use strict";
|
|
10735
10735
|
var identity = require_identity();
|
|
10736
|
-
var
|
|
10736
|
+
var stringify12 = require_stringify();
|
|
10737
10737
|
var stringifyComment = require_stringifyComment();
|
|
10738
10738
|
function stringifyDocument(doc, options) {
|
|
10739
10739
|
const lines = [];
|
|
@@ -10748,7 +10748,7 @@ var require_stringifyDocument = __commonJS({
|
|
|
10748
10748
|
}
|
|
10749
10749
|
if (hasDirectives)
|
|
10750
10750
|
lines.push("---");
|
|
10751
|
-
const ctx =
|
|
10751
|
+
const ctx = stringify12.createStringifyContext(doc, options);
|
|
10752
10752
|
const { commentString } = ctx.options;
|
|
10753
10753
|
if (doc.commentBefore) {
|
|
10754
10754
|
if (lines.length !== 1)
|
|
@@ -10770,7 +10770,7 @@ var require_stringifyDocument = __commonJS({
|
|
|
10770
10770
|
contentComment = doc.contents.comment;
|
|
10771
10771
|
}
|
|
10772
10772
|
const onChompKeep = contentComment ? void 0 : () => chompKeep = true;
|
|
10773
|
-
let body =
|
|
10773
|
+
let body = stringify12.stringify(doc.contents, ctx, () => contentComment = null, onChompKeep);
|
|
10774
10774
|
if (contentComment)
|
|
10775
10775
|
body += stringifyComment.lineComment(body, "", commentString(contentComment));
|
|
10776
10776
|
if ((body[0] === "|" || body[0] === ">") && lines[lines.length - 1] === "---") {
|
|
@@ -10778,7 +10778,7 @@ var require_stringifyDocument = __commonJS({
|
|
|
10778
10778
|
} else
|
|
10779
10779
|
lines.push(body);
|
|
10780
10780
|
} else {
|
|
10781
|
-
lines.push(
|
|
10781
|
+
lines.push(stringify12.stringify(doc.contents, ctx));
|
|
10782
10782
|
}
|
|
10783
10783
|
if (doc.directives?.docEnd) {
|
|
10784
10784
|
if (doc.comment) {
|
|
@@ -12914,7 +12914,7 @@ var require_cst_scalar = __commonJS({
|
|
|
12914
12914
|
var require_cst_stringify = __commonJS({
|
|
12915
12915
|
"node_modules/yaml/dist/parse/cst-stringify.js"(exports) {
|
|
12916
12916
|
"use strict";
|
|
12917
|
-
var
|
|
12917
|
+
var stringify12 = (cst) => "type" in cst ? stringifyToken(cst) : stringifyItem(cst);
|
|
12918
12918
|
function stringifyToken(token) {
|
|
12919
12919
|
switch (token.type) {
|
|
12920
12920
|
case "block-scalar": {
|
|
@@ -12967,7 +12967,7 @@ var require_cst_stringify = __commonJS({
|
|
|
12967
12967
|
res += stringifyToken(value);
|
|
12968
12968
|
return res;
|
|
12969
12969
|
}
|
|
12970
|
-
exports.stringify =
|
|
12970
|
+
exports.stringify = stringify12;
|
|
12971
12971
|
}
|
|
12972
12972
|
});
|
|
12973
12973
|
|
|
@@ -14697,7 +14697,7 @@ var require_public_api = __commonJS({
|
|
|
14697
14697
|
}
|
|
14698
14698
|
return doc.toJS(Object.assign({ reviver: _reviver }, options));
|
|
14699
14699
|
}
|
|
14700
|
-
function
|
|
14700
|
+
function stringify12(value, replacer, options) {
|
|
14701
14701
|
let _replacer = null;
|
|
14702
14702
|
if (typeof replacer === "function" || Array.isArray(replacer)) {
|
|
14703
14703
|
_replacer = replacer;
|
|
@@ -14722,7 +14722,7 @@ var require_public_api = __commonJS({
|
|
|
14722
14722
|
exports.parse = parse12;
|
|
14723
14723
|
exports.parseAllDocuments = parseAllDocuments;
|
|
14724
14724
|
exports.parseDocument = parseDocument2;
|
|
14725
|
-
exports.stringify =
|
|
14725
|
+
exports.stringify = stringify12;
|
|
14726
14726
|
}
|
|
14727
14727
|
});
|
|
14728
14728
|
|
|
@@ -22700,10 +22700,13 @@ function readResource(registry, root, file, logicalPath, kind, id, containmentRo
|
|
|
22700
22700
|
parent = dirname4(parent);
|
|
22701
22701
|
}
|
|
22702
22702
|
if (!stat.isFile()) fail2("path", "resource_not_file", label, "configuration resource must be a regular file");
|
|
22703
|
-
|
|
22703
|
+
return { kind, ...id === void 0 ? {} : { id }, file, logicalPath, value: resourceValue(registry, readFileSync5(file), label, kind) };
|
|
22704
|
+
}
|
|
22705
|
+
function resourceValue(registry, input, label, kind) {
|
|
22706
|
+
const value = parseRestrictedYaml2(input, label);
|
|
22704
22707
|
const issues = registry.validate(kind, value, label);
|
|
22705
22708
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
22706
|
-
return
|
|
22709
|
+
return value;
|
|
22707
22710
|
}
|
|
22708
22711
|
function readTemplateProvenance(registry, root) {
|
|
22709
22712
|
const file = join6(root, ".kxm", "template-provenance.yaml");
|
|
@@ -22752,7 +22755,7 @@ function readTemplateProvenance(registry, root) {
|
|
|
22752
22755
|
}
|
|
22753
22756
|
return value;
|
|
22754
22757
|
}
|
|
22755
|
-
function listNamedResources(registry, root, directory, logicalDirectory, kind) {
|
|
22758
|
+
function listNamedResources(registry, root, directory, logicalDirectory, kind, replacedId) {
|
|
22756
22759
|
const resources = /* @__PURE__ */ new Map();
|
|
22757
22760
|
if (!existsSync6(directory)) return resources;
|
|
22758
22761
|
const stat = lstatSync(directory);
|
|
@@ -22773,6 +22776,7 @@ function listNamedResources(registry, root, directory, logicalDirectory, kind) {
|
|
|
22773
22776
|
}
|
|
22774
22777
|
const id = basename2(entry.name, ".yaml");
|
|
22775
22778
|
if (kind === "model" && id === "inventory") continue;
|
|
22779
|
+
if (id === replacedId) continue;
|
|
22776
22780
|
if (!resourceIdentifier(id)) {
|
|
22777
22781
|
issues.push(issue2("path", "resource_id_invalid", displayPath(root, join6(directory, entry.name)), `filename-derived identity ${id} is invalid or platform-reserved`));
|
|
22778
22782
|
continue;
|
|
@@ -23377,6 +23381,18 @@ function assertNoRegisteredGates(options) {
|
|
|
23377
23381
|
}
|
|
23378
23382
|
}
|
|
23379
23383
|
function loadKxmProject(projectRoot, options = {}) {
|
|
23384
|
+
return loadProjectBundle(projectRoot, options);
|
|
23385
|
+
}
|
|
23386
|
+
function kxmWorkflowWriteIssues(projectRoot, workflowId, document, options = {}) {
|
|
23387
|
+
try {
|
|
23388
|
+
loadProjectBundle(projectRoot, options, { id: workflowId, document });
|
|
23389
|
+
return [];
|
|
23390
|
+
} catch (error) {
|
|
23391
|
+
if (error instanceof KxmConfigError) return error.issues;
|
|
23392
|
+
throw error;
|
|
23393
|
+
}
|
|
23394
|
+
}
|
|
23395
|
+
function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
23380
23396
|
assertNoRegisteredGates(options);
|
|
23381
23397
|
const root = resolve3(projectRoot);
|
|
23382
23398
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -23410,7 +23426,15 @@ function loadKxmProject(projectRoot, options = {}) {
|
|
|
23410
23426
|
if (earlyIssues.length > 0) throw new KxmConfigError(earlyIssues);
|
|
23411
23427
|
const agents = listNamedResources(registry, root, join6(root, ".kxm", "agents"), ".kxm/agents", "agent");
|
|
23412
23428
|
const models = listNamedResources(registry, root, join6(root, ".kxm", "models"), ".kxm/models", "model");
|
|
23413
|
-
const workflows = listNamedResources(registry, root, join6(root, ".kxm", "workflows"), ".kxm/workflows", "workflow");
|
|
23429
|
+
const workflows = listNamedResources(registry, root, join6(root, ".kxm", "workflows"), ".kxm/workflows", "workflow", workflowCandidate?.id);
|
|
23430
|
+
if (workflowCandidate) {
|
|
23431
|
+
const { id, document } = workflowCandidate;
|
|
23432
|
+
const logicalPath = `.kxm/workflows/${id}.yaml`;
|
|
23433
|
+
if (!resourceIdentifier(id)) fail2("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
|
|
23434
|
+
const collision = [...workflows.keys()].find((candidate) => candidate.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
23435
|
+
if (collision) fail2("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
|
|
23436
|
+
workflows.set(id, { kind: "workflow", id, file: join6(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
|
|
23437
|
+
}
|
|
23414
23438
|
const gatePath = join6(root, ".kxm", "gates.yaml");
|
|
23415
23439
|
const gateRegistry = existsSync6(gatePath) ? readResource(registry, root, gatePath, ".kxm/gates.yaml", "gate-registry") : void 0;
|
|
23416
23440
|
const environments = [];
|
|
@@ -28919,6 +28943,7 @@ async function cmdRoleResume(runtime, runId, ruling) {
|
|
|
28919
28943
|
}
|
|
28920
28944
|
|
|
28921
28945
|
// plugins/kxm/src/cli/workflows.ts
|
|
28946
|
+
var import_yaml12 = __toESM(require_dist(), 1);
|
|
28922
28947
|
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
28923
28948
|
import { existsSync as existsSync21, readFileSync as readFileSync20 } from "node:fs";
|
|
28924
28949
|
import { join as join22, resolve as resolve17 } from "node:path";
|
|
@@ -29770,12 +29795,12 @@ async function cmdWorkflowDefinitions(runtime, options) {
|
|
|
29770
29795
|
}
|
|
29771
29796
|
async function cmdWorkflowAdd(runtime, workflowId, options) {
|
|
29772
29797
|
const scope = options.scope ?? "local";
|
|
29798
|
+
const refuse = (error, text) => {
|
|
29799
|
+
print(runtime.io, runtime.json, { ok: false, command: "workflow add", error }, `workflow add failed: ${text}`);
|
|
29800
|
+
return 2;
|
|
29801
|
+
};
|
|
29773
29802
|
let content;
|
|
29774
29803
|
if (options.template !== void 0) {
|
|
29775
|
-
const refuse = (error, text) => {
|
|
29776
|
-
print(runtime.io, runtime.json, { ok: false, command: "workflow add", error }, `workflow add failed: ${text}`);
|
|
29777
|
-
return 2;
|
|
29778
|
-
};
|
|
29779
29804
|
if (options.file !== void 0 || options.pick !== void 0) {
|
|
29780
29805
|
return refuse("workflow_add_conflict", "--template cannot be combined with --file or --pick");
|
|
29781
29806
|
}
|
|
@@ -29795,8 +29820,9 @@ async function cmdWorkflowAdd(runtime, workflowId, options) {
|
|
|
29795
29820
|
if (scope === "local") {
|
|
29796
29821
|
const globalDefs = listWorkflowDefinitions({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
29797
29822
|
for (const gd of globalDefs) {
|
|
29798
|
-
|
|
29799
|
-
|
|
29823
|
+
const definition = parseWorkflowFile(gd.filePath);
|
|
29824
|
+
if (definition && !candidates.some((c) => c.id === gd.id)) {
|
|
29825
|
+
candidates.push({ id: gd.id, description: gd.description, label: "global", payload: definition });
|
|
29800
29826
|
}
|
|
29801
29827
|
}
|
|
29802
29828
|
}
|
|
@@ -29823,9 +29849,29 @@ async function cmdWorkflowAdd(runtime, workflowId, options) {
|
|
|
29823
29849
|
content = scaffoldWorkflowDefinition(options.description || `Workflow ${workflowId}`);
|
|
29824
29850
|
}
|
|
29825
29851
|
try {
|
|
29826
|
-
const
|
|
29852
|
+
const document = typeof content === "string" ? content : (0, import_yaml12.stringify)(content);
|
|
29853
|
+
let repoRoot = runtime.cwd;
|
|
29854
|
+
if (scope === "local") {
|
|
29855
|
+
const projectRoot = discoverKxmProjectRoot(runtime.cwd);
|
|
29856
|
+
if (!projectRoot) {
|
|
29857
|
+
return refuse("project_not_found", "local workflows belong to a KXM project; run kxm init at the repository root, or pass --scope global");
|
|
29858
|
+
}
|
|
29859
|
+
const issues = kxmWorkflowWriteIssues(projectRoot, workflowId, document);
|
|
29860
|
+
if (issues.length > 0) {
|
|
29861
|
+
print(
|
|
29862
|
+
runtime.io,
|
|
29863
|
+
runtime.json,
|
|
29864
|
+
{ ok: false, command: "workflow add", error: "workflow_invalid", issues },
|
|
29865
|
+
`workflow add failed: with this workflow the project would not load, so nothing was written
|
|
29866
|
+
${issues.map((entry) => ` ${entry.file}: ${entry.code}: ${entry.message}`).join("\n")}`
|
|
29867
|
+
);
|
|
29868
|
+
return 2;
|
|
29869
|
+
}
|
|
29870
|
+
repoRoot = projectRoot;
|
|
29871
|
+
}
|
|
29872
|
+
const res = addWorkflowDefinition(workflowId, document, {
|
|
29827
29873
|
scope,
|
|
29828
|
-
repoRoot
|
|
29874
|
+
repoRoot,
|
|
29829
29875
|
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
29830
29876
|
overwrite: options.overwrite,
|
|
29831
29877
|
dryRun: runtime.dryRun
|
|
@@ -30249,7 +30295,7 @@ async function cmdRetrospectiveExport(runtime, runId, options) {
|
|
|
30249
30295
|
}
|
|
30250
30296
|
|
|
30251
30297
|
// plugins/kxm/src/cli/tasks.ts
|
|
30252
|
-
var
|
|
30298
|
+
var import_yaml15 = __toESM(require_dist(), 1);
|
|
30253
30299
|
import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync10 } from "node:fs";
|
|
30254
30300
|
import { basename as basename7, join as join29, resolve as resolve22 } from "node:path";
|
|
30255
30301
|
|
|
@@ -30360,7 +30406,7 @@ function suggestWorkflowAndRoles(prompt, options = {}) {
|
|
|
30360
30406
|
}
|
|
30361
30407
|
|
|
30362
30408
|
// plugins/kxm/src/task-manager.ts
|
|
30363
|
-
var
|
|
30409
|
+
var import_yaml13 = __toESM(require_dist(), 1);
|
|
30364
30410
|
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
30365
30411
|
import { existsSync as existsSync22, mkdirSync as mkdirSync17, readFileSync as readFileSync21, readdirSync as readdirSync8, writeFileSync as writeFileSync17 } from "node:fs";
|
|
30366
30412
|
import { join as join23, resolve as resolve18 } from "node:path";
|
|
@@ -30394,7 +30440,7 @@ function createGoal(repoRoot, input, options = {}) {
|
|
|
30394
30440
|
};
|
|
30395
30441
|
if (options.dryRun) return record;
|
|
30396
30442
|
mkdirSync17(goalsDirectory(repoRoot), { recursive: true });
|
|
30397
|
-
writeFileSync17(goalFilePath(repoRoot, id), (0,
|
|
30443
|
+
writeFileSync17(goalFilePath(repoRoot, id), (0, import_yaml13.stringify)(record).trim() + "\n", "utf8");
|
|
30398
30444
|
return record;
|
|
30399
30445
|
}
|
|
30400
30446
|
function listGoals(repoRoot) {
|
|
@@ -30406,7 +30452,7 @@ function listGoals(repoRoot) {
|
|
|
30406
30452
|
if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
|
|
30407
30453
|
try {
|
|
30408
30454
|
const text = readFileSync21(join23(dir, entry.name), "utf8");
|
|
30409
|
-
const doc = (0,
|
|
30455
|
+
const doc = (0, import_yaml13.parse)(text);
|
|
30410
30456
|
if (doc && doc.schema === GOAL_SCHEMA) goals.push(doc);
|
|
30411
30457
|
} catch {
|
|
30412
30458
|
}
|
|
@@ -30438,7 +30484,7 @@ function createTask(repoRoot, input, options = {}) {
|
|
|
30438
30484
|
};
|
|
30439
30485
|
if (options.dryRun) return record;
|
|
30440
30486
|
mkdirSync17(tasksDirectory(repoRoot), { recursive: true });
|
|
30441
|
-
writeFileSync17(taskFilePath(repoRoot, id), (0,
|
|
30487
|
+
writeFileSync17(taskFilePath(repoRoot, id), (0, import_yaml13.stringify)(record).trim() + "\n", "utf8");
|
|
30442
30488
|
return record;
|
|
30443
30489
|
}
|
|
30444
30490
|
function listTasks(repoRoot, options = {}) {
|
|
@@ -30450,7 +30496,7 @@ function listTasks(repoRoot, options = {}) {
|
|
|
30450
30496
|
if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
|
|
30451
30497
|
try {
|
|
30452
30498
|
const text = readFileSync21(join23(dir, entry.name), "utf8");
|
|
30453
|
-
const doc = (0,
|
|
30499
|
+
const doc = (0, import_yaml13.parse)(text);
|
|
30454
30500
|
if (doc && doc.schema === TASK_SCHEMA) {
|
|
30455
30501
|
if (options.goalId && doc.goalId !== options.goalId) continue;
|
|
30456
30502
|
if (options.status && doc.status !== options.status) continue;
|
|
@@ -30468,7 +30514,7 @@ function getTask(repoRoot, taskId) {
|
|
|
30468
30514
|
if (!existsSync22(filePath)) return void 0;
|
|
30469
30515
|
try {
|
|
30470
30516
|
const text = readFileSync21(filePath, "utf8");
|
|
30471
|
-
const doc = (0,
|
|
30517
|
+
const doc = (0, import_yaml13.parse)(text);
|
|
30472
30518
|
return doc && doc.schema === TASK_SCHEMA ? doc : void 0;
|
|
30473
30519
|
} catch {
|
|
30474
30520
|
return void 0;
|
|
@@ -30482,7 +30528,7 @@ function updateTaskStatus(repoRoot, taskId, status, options = {}) {
|
|
|
30482
30528
|
task.status = status;
|
|
30483
30529
|
if (options.workflowRunId) task.workflowRunId = options.workflowRunId;
|
|
30484
30530
|
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
30485
|
-
if (!options.dryRun) writeFileSync17(taskFilePath(repoRoot, taskId), (0,
|
|
30531
|
+
if (!options.dryRun) writeFileSync17(taskFilePath(repoRoot, taskId), (0, import_yaml13.stringify)(task).trim() + "\n", "utf8");
|
|
30486
30532
|
return task;
|
|
30487
30533
|
}
|
|
30488
30534
|
function syncTaskWithTracker(repoRoot, taskId, options = {}) {
|
|
@@ -30497,7 +30543,7 @@ function syncTaskWithTracker(repoRoot, taskId, options = {}) {
|
|
|
30497
30543
|
task.trackerSync.syncStatus = "synced";
|
|
30498
30544
|
task.trackerSync.lastSyncedAt = now;
|
|
30499
30545
|
task.updatedAt = now;
|
|
30500
|
-
if (!options.dryRun) writeFileSync17(taskFilePath(repoRoot, taskId), (0,
|
|
30546
|
+
if (!options.dryRun) writeFileSync17(taskFilePath(repoRoot, taskId), (0, import_yaml13.stringify)(task).trim() + "\n", "utf8");
|
|
30501
30547
|
return task;
|
|
30502
30548
|
}
|
|
30503
30549
|
|
|
@@ -31269,7 +31315,7 @@ import { join as join28, resolve as resolve21 } from "node:path";
|
|
|
31269
31315
|
import { createInterface as createInterface2 } from "node:readline";
|
|
31270
31316
|
|
|
31271
31317
|
// plugins/kxm/src/model-inventory.ts
|
|
31272
|
-
var
|
|
31318
|
+
var import_yaml14 = __toESM(require_dist(), 1);
|
|
31273
31319
|
import { mkdirSync as mkdirSync18, writeFileSync as writeFileSync18 } from "node:fs";
|
|
31274
31320
|
import { join as join24 } from "node:path";
|
|
31275
31321
|
var OR_URL = "https://openrouter.ai/api/v1/models";
|
|
@@ -31358,7 +31404,7 @@ async function refreshModelInventory(options) {
|
|
|
31358
31404
|
for (const item of nous.data) add(item, "nous", "discount");
|
|
31359
31405
|
const inventory = { schema: "kxm.model-inventory.v1", fetchedAt, currency: "USD", sources, models: [...byId.values()].sort((a, b2) => a.id.localeCompare(b2.id)) };
|
|
31360
31406
|
mkdirSync18(join24(options.outputRoot, ".kxm", "models"), { recursive: true });
|
|
31361
|
-
writeFileSync18(join24(options.outputRoot, ".kxm", "models", "inventory.yaml"), (0,
|
|
31407
|
+
writeFileSync18(join24(options.outputRoot, ".kxm", "models", "inventory.yaml"), (0, import_yaml14.stringify)(inventory), "utf8");
|
|
31362
31408
|
return inventory;
|
|
31363
31409
|
}
|
|
31364
31410
|
|
|
@@ -34539,7 +34585,7 @@ steps:
|
|
|
34539
34585
|
maxTransitions: 2
|
|
34540
34586
|
`;
|
|
34541
34587
|
}
|
|
34542
|
-
const parsedYaml = (0,
|
|
34588
|
+
const parsedYaml = (0, import_yaml15.parse)(yamlContent);
|
|
34543
34589
|
const plan = compileKxmWorkflow({ id: workflowId, value: parsedYaml });
|
|
34544
34590
|
const layout = generateStudioLayout(plan);
|
|
34545
34591
|
print(runtime.io, runtime.json, { ok: true, command: "studio layout", layout }, JSON.stringify(layout, null, 2));
|
|
@@ -34581,7 +34627,7 @@ async function cmdStudioServe(runtime, options) {
|
|
|
34581
34627
|
}
|
|
34582
34628
|
if (targetPath && existsSync27(targetPath)) {
|
|
34583
34629
|
const raw = readFileSync25(targetPath, "utf8");
|
|
34584
|
-
const parsed = (0,
|
|
34630
|
+
const parsed = (0, import_yaml15.parse)(raw);
|
|
34585
34631
|
const wfId = basename7(targetPath).replace(/\.(yaml|yml)$/, "");
|
|
34586
34632
|
return compileKxmWorkflow({ id: wfId, value: parsed });
|
|
34587
34633
|
}
|
|
@@ -45227,7 +45273,7 @@ function resolveInstallKind(probe, npmGlobalRoot) {
|
|
|
45227
45273
|
}
|
|
45228
45274
|
|
|
45229
45275
|
// plugins/kxm/src/kxm-update-config.ts
|
|
45230
|
-
var
|
|
45276
|
+
var import_yaml16 = __toESM(require_dist(), 1);
|
|
45231
45277
|
import { existsSync as existsSync31, readFileSync as readFileSync30 } from "node:fs";
|
|
45232
45278
|
import { join as join39 } from "node:path";
|
|
45233
45279
|
function loadKxmUpdateConfig(env = process.env) {
|
|
@@ -45235,7 +45281,7 @@ function loadKxmUpdateConfig(env = process.env) {
|
|
|
45235
45281
|
if (!existsSync31(path4)) return { schema: KXM_UPDATE_SCHEMA, auto: false, source: "github" };
|
|
45236
45282
|
let parsed;
|
|
45237
45283
|
try {
|
|
45238
|
-
parsed = (0,
|
|
45284
|
+
parsed = (0, import_yaml16.parse)(readFileSync30(path4, "utf8"));
|
|
45239
45285
|
} catch {
|
|
45240
45286
|
throw new KxmUpdateConfigError("update.yaml is not valid YAML");
|
|
45241
45287
|
}
|
|
@@ -46602,7 +46648,7 @@ function loadRoutingSources(options) {
|
|
|
46602
46648
|
}
|
|
46603
46649
|
|
|
46604
46650
|
// plugins/kxm/src/modes.ts
|
|
46605
|
-
var
|
|
46651
|
+
var import_yaml17 = __toESM(require_dist(), 1);
|
|
46606
46652
|
import { existsSync as existsSync36, readFileSync as readFileSync34 } from "node:fs";
|
|
46607
46653
|
import { join as join43 } from "node:path";
|
|
46608
46654
|
var DEFAULT_MODES_CONFIG = Object.freeze({
|
|
@@ -46673,7 +46719,7 @@ function loadModesConfig(projectRoot) {
|
|
|
46673
46719
|
}
|
|
46674
46720
|
try {
|
|
46675
46721
|
const raw = readFileSync34(modesPath, "utf8");
|
|
46676
|
-
const parsed = (0,
|
|
46722
|
+
const parsed = (0, import_yaml17.parse)(raw);
|
|
46677
46723
|
if (parsed && parsed.schema === "kxm.modes.v1" && parsed.majorModes) {
|
|
46678
46724
|
return {
|
|
46679
46725
|
schema: "kxm.modes.v1",
|
|
@@ -47661,7 +47707,7 @@ ${line}
|
|
|
47661
47707
|
}
|
|
47662
47708
|
|
|
47663
47709
|
// plugins/kxm/src/init-guide-setup.ts
|
|
47664
|
-
var
|
|
47710
|
+
var import_yaml18 = __toESM(require_dist(), 1);
|
|
47665
47711
|
import { existsSync as existsSync39, mkdirSync as mkdirSync30, writeFileSync as writeFileSync27 } from "node:fs";
|
|
47666
47712
|
import { dirname as dirname24, join as join46 } from "node:path";
|
|
47667
47713
|
var ADMITTED_GUIDE_BINDINGS = Object.freeze({
|
|
@@ -48057,13 +48103,13 @@ function renderGuideSetupFiles(projectRoot, plan) {
|
|
|
48057
48103
|
if (!stage) continue;
|
|
48058
48104
|
files.push({
|
|
48059
48105
|
path: join46(projectRoot, ".kxm", "agents", `${role}.yaml`),
|
|
48060
|
-
content: (0,
|
|
48106
|
+
content: (0, import_yaml18.stringify)(agentDocument(role, stage, binding))
|
|
48061
48107
|
});
|
|
48062
48108
|
}
|
|
48063
48109
|
for (const workflow of plan.workflows) {
|
|
48064
48110
|
files.push({
|
|
48065
48111
|
path: join46(projectRoot, ".kxm", "workflows", `${workflow.slug}.yaml`),
|
|
48066
|
-
content: (0,
|
|
48112
|
+
content: (0, import_yaml18.stringify)(workflowDocument(workflow))
|
|
48067
48113
|
});
|
|
48068
48114
|
}
|
|
48069
48115
|
return files;
|
|
@@ -48081,7 +48127,7 @@ function mergeGuideRouteAdmission(projectRoot, plan) {
|
|
|
48081
48127
|
policy.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
48082
48128
|
const path4 = join46(projectRoot, ".kxm", "routes.yaml");
|
|
48083
48129
|
mkdirSync30(dirname24(path4), { recursive: true });
|
|
48084
|
-
writeFileSync27(path4, (0,
|
|
48130
|
+
writeFileSync27(path4, (0, import_yaml18.stringify)(policy), "utf8");
|
|
48085
48131
|
return added;
|
|
48086
48132
|
}
|
|
48087
48133
|
function writeGuideSetupFiles(files) {
|
|
@@ -17308,7 +17308,7 @@ function sessionTokenFixHint(policy) {
|
|
|
17308
17308
|
}
|
|
17309
17309
|
|
|
17310
17310
|
// plugins/kxm/src/mcp-server.ts
|
|
17311
|
-
var VERSION = "0.7.
|
|
17311
|
+
var VERSION = "0.7.100";
|
|
17312
17312
|
var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
17313
17313
|
var inbox = /* @__PURE__ */ new Map();
|
|
17314
17314
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
@@ -16611,10 +16611,13 @@ function readResource(registry, root, file, logicalPath, kind, id, containmentRo
|
|
|
16611
16611
|
parent = dirname2(parent);
|
|
16612
16612
|
}
|
|
16613
16613
|
if (!stat.isFile()) fail2("path", "resource_not_file", label, "configuration resource must be a regular file");
|
|
16614
|
-
|
|
16614
|
+
return { kind, ...id === void 0 ? {} : { id }, file, logicalPath, value: resourceValue(registry, readFileSync(file), label, kind) };
|
|
16615
|
+
}
|
|
16616
|
+
function resourceValue(registry, input, label, kind) {
|
|
16617
|
+
const value = parseRestrictedYaml2(input, label);
|
|
16615
16618
|
const issues = registry.validate(kind, value, label);
|
|
16616
16619
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
16617
|
-
return
|
|
16620
|
+
return value;
|
|
16618
16621
|
}
|
|
16619
16622
|
function readTemplateProvenance(registry, root) {
|
|
16620
16623
|
const file = join2(root, ".kxm", "template-provenance.yaml");
|
|
@@ -16663,7 +16666,7 @@ function readTemplateProvenance(registry, root) {
|
|
|
16663
16666
|
}
|
|
16664
16667
|
return value;
|
|
16665
16668
|
}
|
|
16666
|
-
function listNamedResources(registry, root, directory, logicalDirectory, kind) {
|
|
16669
|
+
function listNamedResources(registry, root, directory, logicalDirectory, kind, replacedId) {
|
|
16667
16670
|
const resources = /* @__PURE__ */ new Map();
|
|
16668
16671
|
if (!existsSync3(directory)) return resources;
|
|
16669
16672
|
const stat = lstatSync(directory);
|
|
@@ -16684,6 +16687,7 @@ function listNamedResources(registry, root, directory, logicalDirectory, kind) {
|
|
|
16684
16687
|
}
|
|
16685
16688
|
const id = basename(entry.name, ".yaml");
|
|
16686
16689
|
if (kind === "model" && id === "inventory") continue;
|
|
16690
|
+
if (id === replacedId) continue;
|
|
16687
16691
|
if (!resourceIdentifier(id)) {
|
|
16688
16692
|
issues.push(issue2("path", "resource_id_invalid", displayPath(root, join2(directory, entry.name)), `filename-derived identity ${id} is invalid or platform-reserved`));
|
|
16689
16693
|
continue;
|
|
@@ -17284,6 +17288,9 @@ function assertNoRegisteredGates(options) {
|
|
|
17284
17288
|
}
|
|
17285
17289
|
}
|
|
17286
17290
|
function loadKxmProject(projectRoot, options = {}) {
|
|
17291
|
+
return loadProjectBundle(projectRoot, options);
|
|
17292
|
+
}
|
|
17293
|
+
function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
17287
17294
|
assertNoRegisteredGates(options);
|
|
17288
17295
|
const root = resolve(projectRoot);
|
|
17289
17296
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -17317,7 +17324,15 @@ function loadKxmProject(projectRoot, options = {}) {
|
|
|
17317
17324
|
if (earlyIssues.length > 0) throw new KxmConfigError(earlyIssues);
|
|
17318
17325
|
const agents = listNamedResources(registry, root, join2(root, ".kxm", "agents"), ".kxm/agents", "agent");
|
|
17319
17326
|
const models = listNamedResources(registry, root, join2(root, ".kxm", "models"), ".kxm/models", "model");
|
|
17320
|
-
const workflows = listNamedResources(registry, root, join2(root, ".kxm", "workflows"), ".kxm/workflows", "workflow");
|
|
17327
|
+
const workflows = listNamedResources(registry, root, join2(root, ".kxm", "workflows"), ".kxm/workflows", "workflow", workflowCandidate?.id);
|
|
17328
|
+
if (workflowCandidate) {
|
|
17329
|
+
const { id, document } = workflowCandidate;
|
|
17330
|
+
const logicalPath = `.kxm/workflows/${id}.yaml`;
|
|
17331
|
+
if (!resourceIdentifier(id)) fail2("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
|
|
17332
|
+
const collision = [...workflows.keys()].find((candidate) => candidate.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
17333
|
+
if (collision) fail2("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
|
|
17334
|
+
workflows.set(id, { kind: "workflow", id, file: join2(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
|
|
17335
|
+
}
|
|
17321
17336
|
const gatePath = join2(root, ".kxm", "gates.yaml");
|
|
17322
17337
|
const gateRegistry = existsSync3(gatePath) ? readResource(registry, root, gatePath, ".kxm/gates.yaml", "gate-registry") : void 0;
|
|
17323
17338
|
const environments = [];
|
|
@@ -16781,10 +16781,13 @@ function readResource(registry, root, file, logicalPath, kind, id, containmentRo
|
|
|
16781
16781
|
parent = dirname2(parent);
|
|
16782
16782
|
}
|
|
16783
16783
|
if (!stat.isFile()) fail2("path", "resource_not_file", label, "configuration resource must be a regular file");
|
|
16784
|
-
|
|
16784
|
+
return { kind, ...id === void 0 ? {} : { id }, file, logicalPath, value: resourceValue(registry, readFileSync(file), label, kind) };
|
|
16785
|
+
}
|
|
16786
|
+
function resourceValue(registry, input, label, kind) {
|
|
16787
|
+
const value = parseRestrictedYaml2(input, label);
|
|
16785
16788
|
const issues = registry.validate(kind, value, label);
|
|
16786
16789
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
16787
|
-
return
|
|
16790
|
+
return value;
|
|
16788
16791
|
}
|
|
16789
16792
|
function readTemplateProvenance(registry, root) {
|
|
16790
16793
|
const file = join2(root, ".kxm", "template-provenance.yaml");
|
|
@@ -16833,7 +16836,7 @@ function readTemplateProvenance(registry, root) {
|
|
|
16833
16836
|
}
|
|
16834
16837
|
return value;
|
|
16835
16838
|
}
|
|
16836
|
-
function listNamedResources(registry, root, directory, logicalDirectory, kind) {
|
|
16839
|
+
function listNamedResources(registry, root, directory, logicalDirectory, kind, replacedId) {
|
|
16837
16840
|
const resources = /* @__PURE__ */ new Map();
|
|
16838
16841
|
if (!existsSync3(directory)) return resources;
|
|
16839
16842
|
const stat = lstatSync(directory);
|
|
@@ -16854,6 +16857,7 @@ function listNamedResources(registry, root, directory, logicalDirectory, kind) {
|
|
|
16854
16857
|
}
|
|
16855
16858
|
const id = basename(entry.name, ".yaml");
|
|
16856
16859
|
if (kind === "model" && id === "inventory") continue;
|
|
16860
|
+
if (id === replacedId) continue;
|
|
16857
16861
|
if (!resourceIdentifier(id)) {
|
|
16858
16862
|
issues.push(issue2("path", "resource_id_invalid", displayPath(root, join2(directory, entry.name)), `filename-derived identity ${id} is invalid or platform-reserved`));
|
|
16859
16863
|
continue;
|
|
@@ -17454,6 +17458,9 @@ function assertNoRegisteredGates(options) {
|
|
|
17454
17458
|
}
|
|
17455
17459
|
}
|
|
17456
17460
|
function loadKxmProject(projectRoot, options = {}) {
|
|
17461
|
+
return loadProjectBundle(projectRoot, options);
|
|
17462
|
+
}
|
|
17463
|
+
function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
17457
17464
|
assertNoRegisteredGates(options);
|
|
17458
17465
|
const root = resolve(projectRoot);
|
|
17459
17466
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -17487,7 +17494,15 @@ function loadKxmProject(projectRoot, options = {}) {
|
|
|
17487
17494
|
if (earlyIssues.length > 0) throw new KxmConfigError(earlyIssues);
|
|
17488
17495
|
const agents = listNamedResources(registry, root, join2(root, ".kxm", "agents"), ".kxm/agents", "agent");
|
|
17489
17496
|
const models = listNamedResources(registry, root, join2(root, ".kxm", "models"), ".kxm/models", "model");
|
|
17490
|
-
const workflows = listNamedResources(registry, root, join2(root, ".kxm", "workflows"), ".kxm/workflows", "workflow");
|
|
17497
|
+
const workflows = listNamedResources(registry, root, join2(root, ".kxm", "workflows"), ".kxm/workflows", "workflow", workflowCandidate?.id);
|
|
17498
|
+
if (workflowCandidate) {
|
|
17499
|
+
const { id, document } = workflowCandidate;
|
|
17500
|
+
const logicalPath = `.kxm/workflows/${id}.yaml`;
|
|
17501
|
+
if (!resourceIdentifier(id)) fail2("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
|
|
17502
|
+
const collision = [...workflows.keys()].find((candidate) => candidate.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
17503
|
+
if (collision) fail2("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
|
|
17504
|
+
workflows.set(id, { kind: "workflow", id, file: join2(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
|
|
17505
|
+
}
|
|
17491
17506
|
const gatePath = join2(root, ".kxm", "gates.yaml");
|
|
17492
17507
|
const gateRegistry = existsSync3(gatePath) ? readResource(registry, root, gatePath, ".kxm/gates.yaml", "gate-registry") : void 0;
|
|
17493
17508
|
const environments = [];
|
package/plugins/kxm/package.json
CHANGED
|
@@ -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.100";
|
|
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[] = [];
|