@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.
@@ -11,7 +11,7 @@
11
11
  "name": "kxm",
12
12
  "source": "./plugins/kxm",
13
13
  "description": "Durable workflows, peer agents, and kxm tui",
14
- "version": "0.7.99",
14
+ "version": "0.7.100",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
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 three `workflow add --template` refusals, which honor `--json`), `goal`, `task`, `memory`, `skills` (other than `skills create --dry-run`), `suggest`, `studio`, and `completion`. Check the exit code before parsing stdout.
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
- - `--template` refusals exit 2 and honor `--json`: `workflow_template_unknown` (the text names the three templates), `workflow_id_required` (no workflow ID), and `workflow_add_conflict` (combined with `--file` or `--pick`). An existing definition without `--overwrite` exits 1 with a plain `workflow add failed: workflow_already_exists: ...` line, also under `--dry-run`.
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`. Review the new file with `kxm trust check` before
893
- committing it.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.99",
3
+ "version": "0.7.100",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -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.99",
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",
@@ -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 stringify11(x2) {
167
+ function stringify12(x2) {
168
168
  return new _Code(safeStringify(x2));
169
169
  }
170
- exports.stringify = stringify11;
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 stringify11(item, ctx, onComment, onChompKeep) {
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 = stringify11;
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 stringify11 = require_stringify();
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 = stringify11.stringify(key, ctx, () => keyCommentDone = true, () => chompKeep = true);
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 = stringify11.stringify(value, ctx, () => valueCommentDone = true, () => chompKeep = true);
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 stringify11 = require_stringify();
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 = stringify11.createStringifyContext(ctx.doc, {});
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 stringify11 = require_stringify();
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 stringify12 = flow ? stringifyFlowCollection : stringifyBlockCollection;
9283
- return stringify12(collection, ctx, options);
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 = stringify11.stringify(item, itemCtx, () => comment2 = null, () => chompKeep = true);
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 = stringify11.stringify(item, itemCtx, () => comment = null);
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 stringify11 = require_stringify();
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 = stringify11.createStringifyContext(doc, options);
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 = stringify11.stringify(doc.contents, ctx, () => contentComment = null, onChompKeep);
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(stringify11.stringify(doc.contents, ctx));
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 stringify11 = (cst) => "type" in cst ? stringifyToken(cst) : stringifyItem(cst);
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 = stringify11;
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 stringify11(value, replacer, options) {
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 = stringify11;
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
- const value = parseRestrictedYaml2(readFileSync5(file), label);
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 { kind, ...id === void 0 ? {} : { id }, file, logicalPath, value };
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
- if (!candidates.some((c) => c.id === gd.id)) {
29799
- candidates.push({ id: gd.id, description: gd.description, label: "global" });
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 res = addWorkflowDefinition(workflowId, content, {
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: runtime.cwd,
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 import_yaml14 = __toESM(require_dist(), 1);
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 import_yaml12 = __toESM(require_dist(), 1);
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, import_yaml12.stringify)(record).trim() + "\n", "utf8");
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, import_yaml12.parse)(text);
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, import_yaml12.stringify)(record).trim() + "\n", "utf8");
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, import_yaml12.parse)(text);
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, import_yaml12.parse)(text);
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, import_yaml12.stringify)(task).trim() + "\n", "utf8");
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, import_yaml12.stringify)(task).trim() + "\n", "utf8");
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 import_yaml13 = __toESM(require_dist(), 1);
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, import_yaml13.stringify)(inventory), "utf8");
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, import_yaml14.parse)(yamlContent);
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, import_yaml14.parse)(raw);
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 import_yaml15 = __toESM(require_dist(), 1);
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, import_yaml15.parse)(readFileSync30(path4, "utf8"));
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 import_yaml16 = __toESM(require_dist(), 1);
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, import_yaml16.parse)(raw);
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 import_yaml17 = __toESM(require_dist(), 1);
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, import_yaml17.stringify)(agentDocument(role, stage, binding))
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, import_yaml17.stringify)(workflowDocument(workflow))
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, import_yaml17.stringify)(policy), "utf8");
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.99";
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
- const value = parseRestrictedYaml2(readFileSync(file), label);
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 { kind, ...id === void 0 ? {} : { id }, file, logicalPath, value };
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
- const value = parseRestrictedYaml2(readFileSync(file), label);
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 { kind, ...id === void 0 ? {} : { id }, file, logicalPath, value };
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 = [];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.99",
3
+ "version": "0.7.100",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -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
- if (!candidates.some((c) => c.id === gd.id)) {
242
- candidates.push({ id: gd.id, description: gd.description, label: "global" });
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 res = addWorkflowDefinition(workflowId!, content, {
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: runtime.cwd,
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.99";
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
- const value = parseRestrictedYaml(readFileSync(file), label);
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 { kind, ...(id === undefined ? {} : { id }), file, logicalPath, value };
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[] = [];