@kontextmind/kxm 0.7.105 → 0.7.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +11 -0
- package/docs/contributing/test-matrix.md +2 -0
- package/docs/reference/cli-reference.md +3 -1
- package/docs/reference/config-reference.md +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +60 -51
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +11 -9
- package/plugins/kxm/dist/runtime.js +11 -9
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/roles.ts +47 -40
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/project-config.ts +33 -5
package/CHANGELOG.md
CHANGED
|
@@ -345,6 +345,17 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
345
345
|
picking a global definition wrote the one-step scaffold under its id and reported
|
|
346
346
|
success. It now writes the global definition's content, with `--description` replacing
|
|
347
347
|
its description, and the loader check refuses one the project cannot load.
|
|
348
|
+
- **`kxm role add --pick <global-id>` copies the global role.** In local scope, picking a
|
|
349
|
+
global role wrote an empty `Role <id>` with no skills or roster under its ID. It now
|
|
350
|
+
writes the global role's content, with `--description`, `--skills` and `--model`
|
|
351
|
+
replacing those fields the way they do for a built-in template.
|
|
352
|
+
- **`kxm role add` no longer leaves a project that refuses to load.** A local add now needs
|
|
353
|
+
a KXM project (`project_not_found` otherwise, and no stray `.kxm/` that would make
|
|
354
|
+
`kxm init` refuse), writes under the project root from any subdirectory, and is checked
|
|
355
|
+
by the project loader first with the new role in place of any file of that ID. A
|
|
356
|
+
`writer` role whose roster leaves out the `implementer` agent's model, including the
|
|
357
|
+
built-in `writer` template for such a project, is refused with `role_invalid`, exit 2,
|
|
358
|
+
and nothing is written, also under `--dry-run`.
|
|
348
359
|
- **Live `kxm runs drive` can author on an audited writer profile.** A write-repository
|
|
349
360
|
step on pi (`-a`, with extensions, skills, and the session off) or grok
|
|
350
361
|
(`--always-approve`, with subagents and web search off) runs against the checkout.
|
|
@@ -67,6 +67,8 @@ to run one file is in [Develop KXM](development.md#run-one-file-or-one-test).
|
|
|
67
67
|
| Explicit local YAML validates with runner schema/transitions; webhook environment sources retain JSON/secret checks | `gate-validation.test.ts` |
|
|
68
68
|
| `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") |
|
|
69
69
|
| `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") |
|
|
70
|
+
| `kxm role add --pick <global-id>` copies the global role into the project, not an empty role, with `--description`, `--skills` and `--model` applied over it | `role-and-workflow-manager.test.ts` ("role add --pick <global-id> copies that global role into the project, with --description, --skills and --model applied over it") |
|
|
71
|
+
| `kxm role add` writes a local role only at the project root and only if the project still loads, so a `writer` roster without the implementer's model is refused; outside a project it refuses | `role-and-workflow-manager.test.ts` ("role add writes a local role only at the project root, and only if the project loader accepts it") |
|
|
70
72
|
| `default.yaml` and the 13-step `fix.yaml` compile deterministically; back edges need budgets | `engine-compile.test.ts` |
|
|
71
73
|
| Artifact gate: non-empty regular files pass; missing, empty, non-file and escaping paths fail | `artifacts-exist.test.ts` |
|
|
72
74
|
| Vision gate: strict verdicts, admitted routes only, unreadable images fail closed | `vision-gate.test.ts` |
|
|
@@ -1197,7 +1197,7 @@ Not run with a configured role; in a fresh project it exits 1 with `kxm: role 'w
|
|
|
1197
1197
|
kxm role add [roleId] [--file <path>] [--description <text>] [--skills <skills>] [--harness <harness>] [--model <model>] [--scope global|local] [--overwrite] [--pick [selection]]
|
|
1198
1198
|
```
|
|
1199
1199
|
|
|
1200
|
-
Adds a role definition. Without a role ID, or with `--pick`, you choose from the built-in templates (`writer`, `planner`, `critic-arch`, `critic-cli`, `verifier`) and, for local scope, existing global roles. With `--file`, the YAML file is used and its `id` is replaced by the role ID.
|
|
1200
|
+
Adds a role definition. Without a role ID, or with `--pick`, you choose from the built-in templates (`writer`, `planner`, `critic-arch`, `critic-cli`, `verifier`) and, for local scope, existing global roles; a global role 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 role's file, with `--description`, `--skills`, and `--model` (with `--harness`) replacing its description, skills, and roster. With `--file`, the YAML file is used and its `id` is replaced by the role ID. Otherwise a role with only the given options is written.
|
|
1201
1201
|
|
|
1202
1202
|
| Option | Argument | Default | Description |
|
|
1203
1203
|
|---|---|---|---|
|
|
@@ -1211,6 +1211,8 @@ Adds a role definition. Without a role ID, or with `--pick`, you choose from the
|
|
|
1211
1211
|
| `--pick` | `[selection]` | none | Pick from available role templates (index or id) |
|
|
1212
1212
|
|
|
1213
1213
|
- Writes `<scope dir>/roles/<id>.yaml`. `--dry-run` plans the write and writes nothing.
|
|
1214
|
+
- Local scope belongs to a KXM project: the file lands in the project root's `.kxm/roles/` 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 role in place of any file of that ID. The loader reads only `writer.yaml`, whose enabled roster must include the `implementer` agent's model (see [Roles](config-reference.md#kxmrolesroleyaml-kxmrolev1)). If the project would not load, the command refuses with `role_invalid`, lists each issue and writes nothing, also under `--dry-run`, and `--overwrite` replaces a `writer.yaml` the loader refuses. Global scope is not checked, because no loader reads it.
|
|
1215
|
+
- Refusals exit 2 and honor `--json`: `project_not_found` and `role_invalid` (with `issues`, each `{phase, code, file, message}`). An existing role without `--overwrite` exits 1 with a plain `role add failed: role_already_exists: ...` line, also under `--dry-run`.
|
|
1214
1216
|
- JSON keys: `roleId`, `id`, `filePath`, `scope`.
|
|
1215
1217
|
|
|
1216
1218
|
> [!WARNING]
|
|
@@ -965,7 +965,7 @@ read different fields:
|
|
|
965
965
|
|---|---|---|---|
|
|
966
966
|
| Project loader (`validateBundle`) | `.kxm/roles/writer.yaml` only | `roster[].model`, `roster[].enabled` | Cross-checks the writer roster against the `implementer` agent's model (see below). Parsed as restricted YAML. |
|
|
967
967
|
| Runtime route check (`listRoleBindings` in `plugins/kxm/src/routes.ts`) | `.kxm/roles/<role>.yaml`, role = agent ID, `writer` for `implementer` | `roster[].model` | The agent's `provider/model` must appear exactly. `enabled` is ignored, so a disabled entry still admits. The role name comes from the filename. |
|
|
968
|
-
| `kxm role` commands (`plugins/kxm/src/role.ts`) | `.kxm/roles/*.yaml` and `~/.config/kxm/roles/*.yaml` | Everything below | Listing and editing only. A file without `schema: kxm.role.v1` is silently skipped. A local file overrides a global one with the same ID. |
|
|
968
|
+
| `kxm role` commands (`plugins/kxm/src/role.ts`) | `.kxm/roles/*.yaml` and `~/.config/kxm/roles/*.yaml` | Everything below | Listing and editing only. A file without `schema: kxm.role.v1` is silently skipped. A local file overrides a global one with the same ID. A local `kxm role add` runs the project loader check above first and refuses with `role_invalid` a role the loader would reject. |
|
|
969
969
|
|
|
970
970
|
The loader check applies when the agent `implementer` (or else `writer`)
|
|
971
971
|
declares a model and the roster has at least one enabled entry. One enabled
|
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.106",
|
|
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
|
@@ -23246,7 +23246,7 @@ function validateModelReferences(models, issues) {
|
|
|
23246
23246
|
};
|
|
23247
23247
|
for (const id of models.keys()) walk(id, []);
|
|
23248
23248
|
}
|
|
23249
|
-
function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot) {
|
|
23249
|
+
function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot, writerRole) {
|
|
23250
23250
|
const issues = [];
|
|
23251
23251
|
const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate) => Boolean(candidate));
|
|
23252
23252
|
const repositoryIds = /* @__PURE__ */ new Set();
|
|
@@ -23317,9 +23317,9 @@ function validateBundle(project, repositories, agents, models, workflows, enviro
|
|
|
23317
23317
|
if (projectRoot) {
|
|
23318
23318
|
const writerRolePath = join6(projectRoot, ".kxm", "roles", "writer.yaml");
|
|
23319
23319
|
const implementerAgent = agents.get("implementer") ?? agents.get("writer");
|
|
23320
|
-
if (existsSync6(writerRolePath) && implementerAgent) {
|
|
23320
|
+
if ((writerRole !== void 0 || existsSync6(writerRolePath)) && implementerAgent) {
|
|
23321
23321
|
try {
|
|
23322
|
-
const rawRole = parseRestrictedYaml2(readFileSync5(writerRolePath, "utf8"));
|
|
23322
|
+
const rawRole = parseRestrictedYaml2(writerRole ?? readFileSync5(writerRolePath, "utf8"));
|
|
23323
23323
|
const roleObj = objectValue(rawRole);
|
|
23324
23324
|
const rosterEntries = valuesOf(roleObj ?? {}, "roster").map((candidate) => objectValue(candidate)).filter((entry) => Boolean(entry));
|
|
23325
23325
|
const enabledRosterModels = rosterEntries.filter((entry) => entry.enabled !== false).map((entry) => stringValue(entry.model)).filter((m2) => Boolean(m2));
|
|
@@ -23390,15 +23390,23 @@ function loadKxmProject(projectRoot, options = {}) {
|
|
|
23390
23390
|
return loadProjectBundle(projectRoot, options);
|
|
23391
23391
|
}
|
|
23392
23392
|
function kxmWorkflowWriteIssues(projectRoot, workflowId, document, options = {}) {
|
|
23393
|
+
return candidateIssues(projectRoot, options, { kind: "workflow", id: workflowId, document });
|
|
23394
|
+
}
|
|
23395
|
+
function kxmRoleWriteIssues(projectRoot, roleId, document, options = {}) {
|
|
23396
|
+
return candidateIssues(projectRoot, options, { kind: "role", id: roleId, document });
|
|
23397
|
+
}
|
|
23398
|
+
function candidateIssues(projectRoot, options, candidate) {
|
|
23393
23399
|
try {
|
|
23394
|
-
loadProjectBundle(projectRoot, options,
|
|
23400
|
+
loadProjectBundle(projectRoot, options, candidate);
|
|
23395
23401
|
return [];
|
|
23396
23402
|
} catch (error) {
|
|
23397
23403
|
if (error instanceof KxmConfigError) return error.issues;
|
|
23398
23404
|
throw error;
|
|
23399
23405
|
}
|
|
23400
23406
|
}
|
|
23401
|
-
function loadProjectBundle(projectRoot, options,
|
|
23407
|
+
function loadProjectBundle(projectRoot, options, candidate) {
|
|
23408
|
+
const workflowCandidate = candidate?.kind === "workflow" ? candidate : void 0;
|
|
23409
|
+
const writerRole = candidate?.kind === "role" && candidate.id.toLocaleLowerCase("en-US") === "writer" ? candidate.document : void 0;
|
|
23402
23410
|
assertNoRegisteredGates(options);
|
|
23403
23411
|
const root = resolve3(projectRoot);
|
|
23404
23412
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -23421,7 +23429,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
23421
23429
|
earlyIssues.push(issue2("semantic", "template_provenance_project_mismatch", ".kxm/template-provenance.yaml", "template provenance belongs to a different project identity"));
|
|
23422
23430
|
}
|
|
23423
23431
|
}
|
|
23424
|
-
const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories").map((
|
|
23432
|
+
const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories").map((candidate2) => stringValue(objectValue(candidate2)?.id)).filter((candidate2) => candidate2 !== void 0));
|
|
23425
23433
|
for (const repositoryId of Object.keys(options.repositoryBindings ?? {}).sort(compareCodeUnits3)) {
|
|
23426
23434
|
if (!resourceIdentifier(repositoryId)) {
|
|
23427
23435
|
earlyIssues.push(issue2("path", "repository_binding_id_invalid", ".kxm/project.yaml", `host-local binding identity ${repositoryId} is invalid`));
|
|
@@ -23437,7 +23445,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
23437
23445
|
const { id, document } = workflowCandidate;
|
|
23438
23446
|
const logicalPath = `.kxm/workflows/${id}.yaml`;
|
|
23439
23447
|
if (!resourceIdentifier(id)) fail2("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
|
|
23440
|
-
const collision = [...workflows.keys()].find((
|
|
23448
|
+
const collision = [...workflows.keys()].find((candidate2) => candidate2.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
23441
23449
|
if (collision) fail2("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
|
|
23442
23450
|
workflows.set(id, { kind: "workflow", id, file: join6(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
|
|
23443
23451
|
}
|
|
@@ -23456,8 +23464,8 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
23456
23464
|
}
|
|
23457
23465
|
}
|
|
23458
23466
|
const seenBindings = /* @__PURE__ */ new Map();
|
|
23459
|
-
for (const
|
|
23460
|
-
const entry = objectValue(
|
|
23467
|
+
for (const candidate2 of valuesOf(project.value, "repositories")) {
|
|
23468
|
+
const entry = objectValue(candidate2);
|
|
23461
23469
|
const repositoryId = entry && stringValue(entry.id);
|
|
23462
23470
|
const pathHint = entry && stringValue(entry.pathHint);
|
|
23463
23471
|
const role = entry && stringValue(entry.role);
|
|
@@ -23554,7 +23562,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
23554
23562
|
}
|
|
23555
23563
|
}
|
|
23556
23564
|
if (loadIssues.length > 0) throw new KxmConfigError(loadIssues);
|
|
23557
|
-
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root);
|
|
23565
|
+
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, writerRole);
|
|
23558
23566
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
23559
23567
|
const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...workflows.values(), ...environments, ...gateRegistry ? [gateRegistry] : []].sort((left, right) => compareCodeUnits3(left.logicalPath, right.logicalPath));
|
|
23560
23568
|
return {
|
|
@@ -29074,6 +29082,7 @@ async function cmdRoleGet(runtime, roleId, options) {
|
|
|
29074
29082
|
}
|
|
29075
29083
|
async function cmdRoleAdd(runtime, roleId, options) {
|
|
29076
29084
|
const scope = options.scope ?? "local";
|
|
29085
|
+
let base;
|
|
29077
29086
|
if (!roleId || options.pick) {
|
|
29078
29087
|
const candidates = Object.values(DEFAULT_ROLES).map((r) => ({
|
|
29079
29088
|
id: r.id,
|
|
@@ -29084,8 +29093,9 @@ async function cmdRoleAdd(runtime, roleId, options) {
|
|
|
29084
29093
|
if (scope === "local") {
|
|
29085
29094
|
const globalRoles = listRoles({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
29086
29095
|
for (const gr of globalRoles) {
|
|
29087
|
-
|
|
29088
|
-
|
|
29096
|
+
const definition = parseRoleFile(gr.filePath);
|
|
29097
|
+
if (definition && !candidates.some((c) => c.id === gr.id)) {
|
|
29098
|
+
candidates.push({ id: gr.id, description: gr.description, label: "global", payload: definition });
|
|
29089
29099
|
}
|
|
29090
29100
|
}
|
|
29091
29101
|
}
|
|
@@ -29097,63 +29107,62 @@ async function cmdRoleAdd(runtime, roleId, options) {
|
|
|
29097
29107
|
}
|
|
29098
29108
|
} else {
|
|
29099
29109
|
roleId = picked.id;
|
|
29100
|
-
|
|
29101
|
-
const base = DEFAULT_ROLES[picked.id];
|
|
29102
|
-
const roleDef2 = {
|
|
29103
|
-
...base,
|
|
29104
|
-
description: options.description || base.description,
|
|
29105
|
-
skills: options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : base.skills,
|
|
29106
|
-
roster: options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : base.roster
|
|
29107
|
-
};
|
|
29108
|
-
try {
|
|
29109
|
-
const res = addRole(roleDef2, {
|
|
29110
|
-
scope,
|
|
29111
|
-
repoRoot: runtime.cwd,
|
|
29112
|
-
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
29113
|
-
overwrite: options.overwrite,
|
|
29114
|
-
dryRun: runtime.dryRun
|
|
29115
|
-
});
|
|
29116
|
-
if (runtime.dryRun) {
|
|
29117
|
-
printPlan(runtime, { command: "role add", roleId, ...res }, [{ action: "write", target: res.filePath }], `add role '${roleId}' to ${res.scope}`);
|
|
29118
|
-
return 0;
|
|
29119
|
-
}
|
|
29120
|
-
print(
|
|
29121
|
-
runtime.io,
|
|
29122
|
-
runtime.json,
|
|
29123
|
-
{ ok: true, command: "role add", roleId, ...res },
|
|
29124
|
-
`Added role '${roleId}' to ${res.scope} (${res.filePath})
|
|
29125
|
-
`
|
|
29126
|
-
);
|
|
29127
|
-
return 0;
|
|
29128
|
-
} catch (err) {
|
|
29129
|
-
runtime.io.stderr(`role add failed: ${err.message}
|
|
29130
|
-
`);
|
|
29131
|
-
return 1;
|
|
29132
|
-
}
|
|
29133
|
-
}
|
|
29110
|
+
base = picked.payload;
|
|
29134
29111
|
}
|
|
29135
29112
|
}
|
|
29113
|
+
const skills = options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
29114
|
+
const roster = options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : void 0;
|
|
29136
29115
|
let roleDef;
|
|
29137
29116
|
if (options.file) {
|
|
29138
29117
|
const filePath = resolve15(runtime.cwd, options.file);
|
|
29139
29118
|
const content = readFileSync19(filePath, "utf8");
|
|
29140
29119
|
roleDef = (0, import_yaml10.parse)(content);
|
|
29141
29120
|
roleDef.id = roleId;
|
|
29121
|
+
} else if (base) {
|
|
29122
|
+
roleDef = {
|
|
29123
|
+
...base,
|
|
29124
|
+
description: options.description || base.description,
|
|
29125
|
+
skills: skills ?? base.skills,
|
|
29126
|
+
roster: roster ?? base.roster
|
|
29127
|
+
};
|
|
29142
29128
|
} else {
|
|
29143
|
-
const skills = options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
29144
|
-
const roster = options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : [];
|
|
29145
29129
|
roleDef = {
|
|
29146
29130
|
schema: "kxm.role.v1",
|
|
29147
29131
|
id: roleId,
|
|
29148
29132
|
description: options.description || `Role ${roleId}`,
|
|
29149
|
-
skills,
|
|
29150
|
-
roster
|
|
29133
|
+
skills: skills ?? [],
|
|
29134
|
+
roster: roster ?? []
|
|
29151
29135
|
};
|
|
29152
29136
|
}
|
|
29137
|
+
let repoRoot = runtime.cwd;
|
|
29138
|
+
if (scope === "local") {
|
|
29139
|
+
const projectRoot = discoverKxmProjectRoot(runtime.cwd);
|
|
29140
|
+
if (!projectRoot) {
|
|
29141
|
+
print(
|
|
29142
|
+
runtime.io,
|
|
29143
|
+
runtime.json,
|
|
29144
|
+
{ ok: false, command: "role add", error: "project_not_found" },
|
|
29145
|
+
"role add failed: local roles belong to a KXM project; run kxm init at the repository root, or pass --scope global"
|
|
29146
|
+
);
|
|
29147
|
+
return 2;
|
|
29148
|
+
}
|
|
29149
|
+
const issues = kxmRoleWriteIssues(projectRoot, roleDef.id, (0, import_yaml10.stringify)(roleDef));
|
|
29150
|
+
if (issues.length > 0) {
|
|
29151
|
+
print(
|
|
29152
|
+
runtime.io,
|
|
29153
|
+
runtime.json,
|
|
29154
|
+
{ ok: false, command: "role add", error: "role_invalid", issues },
|
|
29155
|
+
`role add failed: with this role the project would not load, so nothing was written
|
|
29156
|
+
${issues.map((entry) => ` ${entry.file}: ${entry.code}: ${entry.message}`).join("\n")}`
|
|
29157
|
+
);
|
|
29158
|
+
return 2;
|
|
29159
|
+
}
|
|
29160
|
+
repoRoot = projectRoot;
|
|
29161
|
+
}
|
|
29153
29162
|
try {
|
|
29154
29163
|
const res = addRole(roleDef, {
|
|
29155
29164
|
scope,
|
|
29156
|
-
repoRoot
|
|
29165
|
+
repoRoot,
|
|
29157
29166
|
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
29158
29167
|
overwrite: options.overwrite,
|
|
29159
29168
|
dryRun: runtime.dryRun
|
|
@@ -17313,7 +17313,7 @@ function sessionTokenFixHint(policy) {
|
|
|
17313
17313
|
}
|
|
17314
17314
|
|
|
17315
17315
|
// plugins/kxm/src/mcp-server.ts
|
|
17316
|
-
var VERSION = "0.7.
|
|
17316
|
+
var VERSION = "0.7.106";
|
|
17317
17317
|
var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
17318
17318
|
var inbox = /* @__PURE__ */ new Map();
|
|
17319
17319
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
@@ -17151,7 +17151,7 @@ function validateModelReferences(models, issues) {
|
|
|
17151
17151
|
};
|
|
17152
17152
|
for (const id of models.keys()) walk(id, []);
|
|
17153
17153
|
}
|
|
17154
|
-
function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot) {
|
|
17154
|
+
function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot, writerRole) {
|
|
17155
17155
|
const issues = [];
|
|
17156
17156
|
const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate) => Boolean(candidate));
|
|
17157
17157
|
const repositoryIds = /* @__PURE__ */ new Set();
|
|
@@ -17222,9 +17222,9 @@ function validateBundle(project, repositories, agents, models, workflows, enviro
|
|
|
17222
17222
|
if (projectRoot) {
|
|
17223
17223
|
const writerRolePath = join2(projectRoot, ".kxm", "roles", "writer.yaml");
|
|
17224
17224
|
const implementerAgent = agents.get("implementer") ?? agents.get("writer");
|
|
17225
|
-
if (existsSync3(writerRolePath) && implementerAgent) {
|
|
17225
|
+
if ((writerRole !== void 0 || existsSync3(writerRolePath)) && implementerAgent) {
|
|
17226
17226
|
try {
|
|
17227
|
-
const rawRole = parseRestrictedYaml2(readFileSync(writerRolePath, "utf8"));
|
|
17227
|
+
const rawRole = parseRestrictedYaml2(writerRole ?? readFileSync(writerRolePath, "utf8"));
|
|
17228
17228
|
const roleObj = objectValue(rawRole);
|
|
17229
17229
|
const rosterEntries = valuesOf(roleObj ?? {}, "roster").map((candidate) => objectValue(candidate)).filter((entry) => Boolean(entry));
|
|
17230
17230
|
const enabledRosterModels = rosterEntries.filter((entry) => entry.enabled !== false).map((entry) => stringValue(entry.model)).filter((m) => Boolean(m));
|
|
@@ -17290,7 +17290,9 @@ function assertNoRegisteredGates(options) {
|
|
|
17290
17290
|
function loadKxmProject(projectRoot, options = {}) {
|
|
17291
17291
|
return loadProjectBundle(projectRoot, options);
|
|
17292
17292
|
}
|
|
17293
|
-
function loadProjectBundle(projectRoot, options,
|
|
17293
|
+
function loadProjectBundle(projectRoot, options, candidate) {
|
|
17294
|
+
const workflowCandidate = candidate?.kind === "workflow" ? candidate : void 0;
|
|
17295
|
+
const writerRole = candidate?.kind === "role" && candidate.id.toLocaleLowerCase("en-US") === "writer" ? candidate.document : void 0;
|
|
17294
17296
|
assertNoRegisteredGates(options);
|
|
17295
17297
|
const root = resolve(projectRoot);
|
|
17296
17298
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -17313,7 +17315,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17313
17315
|
earlyIssues.push(issue2("semantic", "template_provenance_project_mismatch", ".kxm/template-provenance.yaml", "template provenance belongs to a different project identity"));
|
|
17314
17316
|
}
|
|
17315
17317
|
}
|
|
17316
|
-
const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories").map((
|
|
17318
|
+
const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories").map((candidate2) => stringValue(objectValue(candidate2)?.id)).filter((candidate2) => candidate2 !== void 0));
|
|
17317
17319
|
for (const repositoryId of Object.keys(options.repositoryBindings ?? {}).sort(compareCodeUnits3)) {
|
|
17318
17320
|
if (!resourceIdentifier(repositoryId)) {
|
|
17319
17321
|
earlyIssues.push(issue2("path", "repository_binding_id_invalid", ".kxm/project.yaml", `host-local binding identity ${repositoryId} is invalid`));
|
|
@@ -17329,7 +17331,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17329
17331
|
const { id, document } = workflowCandidate;
|
|
17330
17332
|
const logicalPath = `.kxm/workflows/${id}.yaml`;
|
|
17331
17333
|
if (!resourceIdentifier(id)) fail2("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
|
|
17332
|
-
const collision = [...workflows.keys()].find((
|
|
17334
|
+
const collision = [...workflows.keys()].find((candidate2) => candidate2.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
17333
17335
|
if (collision) fail2("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
|
|
17334
17336
|
workflows.set(id, { kind: "workflow", id, file: join2(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
|
|
17335
17337
|
}
|
|
@@ -17348,8 +17350,8 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17348
17350
|
}
|
|
17349
17351
|
}
|
|
17350
17352
|
const seenBindings = /* @__PURE__ */ new Map();
|
|
17351
|
-
for (const
|
|
17352
|
-
const entry = objectValue(
|
|
17353
|
+
for (const candidate2 of valuesOf(project.value, "repositories")) {
|
|
17354
|
+
const entry = objectValue(candidate2);
|
|
17353
17355
|
const repositoryId = entry && stringValue(entry.id);
|
|
17354
17356
|
const pathHint = entry && stringValue(entry.pathHint);
|
|
17355
17357
|
const role = entry && stringValue(entry.role);
|
|
@@ -17446,7 +17448,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17446
17448
|
}
|
|
17447
17449
|
}
|
|
17448
17450
|
if (loadIssues.length > 0) throw new KxmConfigError(loadIssues);
|
|
17449
|
-
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root);
|
|
17451
|
+
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, writerRole);
|
|
17450
17452
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
17451
17453
|
const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...workflows.values(), ...environments, ...gateRegistry ? [gateRegistry] : []].sort((left, right) => compareCodeUnits3(left.logicalPath, right.logicalPath));
|
|
17452
17454
|
return {
|
|
@@ -17321,7 +17321,7 @@ function validateModelReferences(models, issues) {
|
|
|
17321
17321
|
};
|
|
17322
17322
|
for (const id of models.keys()) walk(id, []);
|
|
17323
17323
|
}
|
|
17324
|
-
function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot) {
|
|
17324
|
+
function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot, writerRole) {
|
|
17325
17325
|
const issues = [];
|
|
17326
17326
|
const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate) => Boolean(candidate));
|
|
17327
17327
|
const repositoryIds = /* @__PURE__ */ new Set();
|
|
@@ -17392,9 +17392,9 @@ function validateBundle(project, repositories, agents, models, workflows, enviro
|
|
|
17392
17392
|
if (projectRoot) {
|
|
17393
17393
|
const writerRolePath = join2(projectRoot, ".kxm", "roles", "writer.yaml");
|
|
17394
17394
|
const implementerAgent = agents.get("implementer") ?? agents.get("writer");
|
|
17395
|
-
if (existsSync3(writerRolePath) && implementerAgent) {
|
|
17395
|
+
if ((writerRole !== void 0 || existsSync3(writerRolePath)) && implementerAgent) {
|
|
17396
17396
|
try {
|
|
17397
|
-
const rawRole = parseRestrictedYaml2(readFileSync(writerRolePath, "utf8"));
|
|
17397
|
+
const rawRole = parseRestrictedYaml2(writerRole ?? readFileSync(writerRolePath, "utf8"));
|
|
17398
17398
|
const roleObj = objectValue(rawRole);
|
|
17399
17399
|
const rosterEntries = valuesOf(roleObj ?? {}, "roster").map((candidate) => objectValue(candidate)).filter((entry) => Boolean(entry));
|
|
17400
17400
|
const enabledRosterModels = rosterEntries.filter((entry) => entry.enabled !== false).map((entry) => stringValue(entry.model)).filter((m) => Boolean(m));
|
|
@@ -17460,7 +17460,9 @@ function assertNoRegisteredGates(options) {
|
|
|
17460
17460
|
function loadKxmProject(projectRoot, options = {}) {
|
|
17461
17461
|
return loadProjectBundle(projectRoot, options);
|
|
17462
17462
|
}
|
|
17463
|
-
function loadProjectBundle(projectRoot, options,
|
|
17463
|
+
function loadProjectBundle(projectRoot, options, candidate) {
|
|
17464
|
+
const workflowCandidate = candidate?.kind === "workflow" ? candidate : void 0;
|
|
17465
|
+
const writerRole = candidate?.kind === "role" && candidate.id.toLocaleLowerCase("en-US") === "writer" ? candidate.document : void 0;
|
|
17464
17466
|
assertNoRegisteredGates(options);
|
|
17465
17467
|
const root = resolve(projectRoot);
|
|
17466
17468
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -17483,7 +17485,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17483
17485
|
earlyIssues.push(issue2("semantic", "template_provenance_project_mismatch", ".kxm/template-provenance.yaml", "template provenance belongs to a different project identity"));
|
|
17484
17486
|
}
|
|
17485
17487
|
}
|
|
17486
|
-
const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories").map((
|
|
17488
|
+
const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories").map((candidate2) => stringValue(objectValue(candidate2)?.id)).filter((candidate2) => candidate2 !== void 0));
|
|
17487
17489
|
for (const repositoryId of Object.keys(options.repositoryBindings ?? {}).sort(compareCodeUnits3)) {
|
|
17488
17490
|
if (!resourceIdentifier(repositoryId)) {
|
|
17489
17491
|
earlyIssues.push(issue2("path", "repository_binding_id_invalid", ".kxm/project.yaml", `host-local binding identity ${repositoryId} is invalid`));
|
|
@@ -17499,7 +17501,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17499
17501
|
const { id, document } = workflowCandidate;
|
|
17500
17502
|
const logicalPath = `.kxm/workflows/${id}.yaml`;
|
|
17501
17503
|
if (!resourceIdentifier(id)) fail2("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
|
|
17502
|
-
const collision = [...workflows.keys()].find((
|
|
17504
|
+
const collision = [...workflows.keys()].find((candidate2) => candidate2.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
17503
17505
|
if (collision) fail2("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
|
|
17504
17506
|
workflows.set(id, { kind: "workflow", id, file: join2(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
|
|
17505
17507
|
}
|
|
@@ -17518,8 +17520,8 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17518
17520
|
}
|
|
17519
17521
|
}
|
|
17520
17522
|
const seenBindings = /* @__PURE__ */ new Map();
|
|
17521
|
-
for (const
|
|
17522
|
-
const entry = objectValue(
|
|
17523
|
+
for (const candidate2 of valuesOf(project.value, "repositories")) {
|
|
17524
|
+
const entry = objectValue(candidate2);
|
|
17523
17525
|
const repositoryId = entry && stringValue(entry.id);
|
|
17524
17526
|
const pathHint = entry && stringValue(entry.pathHint);
|
|
17525
17527
|
const role = entry && stringValue(entry.role);
|
|
@@ -17616,7 +17618,7 @@ function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
|
17616
17618
|
}
|
|
17617
17619
|
}
|
|
17618
17620
|
if (loadIssues.length > 0) throw new KxmConfigError(loadIssues);
|
|
17619
|
-
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root);
|
|
17621
|
+
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, writerRole);
|
|
17620
17622
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
17621
17623
|
const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...workflows.values(), ...environments, ...gateRegistry ? [gateRegistry] : []].sort((left, right) => compareCodeUnits3(left.logicalPath, right.logicalPath));
|
|
17622
17624
|
return {
|
package/plugins/kxm/package.json
CHANGED
|
@@ -15,9 +15,10 @@ import {
|
|
|
15
15
|
loadRoleHostsConfig,
|
|
16
16
|
setRoleSeatHost,
|
|
17
17
|
resolveRoleSeat,
|
|
18
|
+
parseRoleFile,
|
|
18
19
|
type KxmRoleDefinition,
|
|
19
20
|
} from "../role.ts";
|
|
20
|
-
import { discoverKxmProjectRoot } from "../project-config.ts";
|
|
21
|
+
import { discoverKxmProjectRoot, kxmRoleWriteIssues } from "../project-config.ts";
|
|
21
22
|
import { ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
|
|
22
23
|
import { projectRuntimeOwnsRun } from "../runtime-store.ts";
|
|
23
24
|
import { resumeWorkflowFromRuling, type WorkflowRun } from "../workflow.ts";
|
|
@@ -156,6 +157,7 @@ export async function cmdRoleAdd(
|
|
|
156
157
|
},
|
|
157
158
|
): Promise<number> {
|
|
158
159
|
const scope = options.scope ?? "local";
|
|
160
|
+
let base: KxmRoleDefinition | undefined;
|
|
159
161
|
if (!roleId || options.pick) {
|
|
160
162
|
const candidates: PickCandidate[] = Object.values(DEFAULT_ROLES).map((r) => ({
|
|
161
163
|
id: r.id,
|
|
@@ -166,8 +168,10 @@ export async function cmdRoleAdd(
|
|
|
166
168
|
if (scope === "local") {
|
|
167
169
|
const globalRoles = listRoles({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
168
170
|
for (const gr of globalRoles) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
// The listing is a summary; the copy needs the definition, read from the listed file (it may be `.yml`).
|
|
172
|
+
const definition = parseRoleFile(gr.filePath);
|
|
173
|
+
if (definition && !candidates.some((c) => c.id === gr.id)) {
|
|
174
|
+
candidates.push({ id: gr.id, description: gr.description, label: "global", payload: definition });
|
|
171
175
|
}
|
|
172
176
|
}
|
|
173
177
|
}
|
|
@@ -179,63 +183,66 @@ export async function cmdRoleAdd(
|
|
|
179
183
|
}
|
|
180
184
|
} else {
|
|
181
185
|
roleId = picked.id;
|
|
182
|
-
|
|
183
|
-
const base = DEFAULT_ROLES[picked.id]!;
|
|
184
|
-
const roleDef: KxmRoleDefinition = {
|
|
185
|
-
...base,
|
|
186
|
-
description: options.description || base.description,
|
|
187
|
-
skills: options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : base.skills,
|
|
188
|
-
roster: options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : base.roster,
|
|
189
|
-
};
|
|
190
|
-
try {
|
|
191
|
-
const res = addRole(roleDef, {
|
|
192
|
-
scope,
|
|
193
|
-
repoRoot: runtime.cwd,
|
|
194
|
-
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
195
|
-
overwrite: options.overwrite,
|
|
196
|
-
dryRun: runtime.dryRun,
|
|
197
|
-
});
|
|
198
|
-
if (runtime.dryRun) {
|
|
199
|
-
printPlan(runtime, { command: "role add", roleId, ...res }, [{ action: "write", target: res.filePath }], `add role '${roleId}' to ${res.scope}`);
|
|
200
|
-
return 0;
|
|
201
|
-
}
|
|
202
|
-
print(
|
|
203
|
-
runtime.io,
|
|
204
|
-
runtime.json,
|
|
205
|
-
{ ok: true, command: "role add", roleId, ...res },
|
|
206
|
-
`Added role '${roleId}' to ${res.scope} (${res.filePath})\n`,
|
|
207
|
-
);
|
|
208
|
-
return 0;
|
|
209
|
-
} catch (err: unknown) {
|
|
210
|
-
runtime.io.stderr(`role add failed: ${(err as Error).message}\n`);
|
|
211
|
-
return 1;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
186
|
+
base = picked.payload as KxmRoleDefinition;
|
|
214
187
|
}
|
|
215
188
|
}
|
|
216
189
|
|
|
190
|
+
const skills = options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
191
|
+
const roster = options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : undefined;
|
|
217
192
|
let roleDef: KxmRoleDefinition;
|
|
218
193
|
if (options.file) {
|
|
219
194
|
const filePath = resolve(runtime.cwd, options.file);
|
|
220
195
|
const content = readFileSync(filePath, "utf8");
|
|
221
196
|
roleDef = parseYaml(content) as KxmRoleDefinition;
|
|
222
197
|
roleDef.id = roleId!;
|
|
198
|
+
} else if (base) {
|
|
199
|
+
roleDef = {
|
|
200
|
+
...base,
|
|
201
|
+
description: options.description || base.description,
|
|
202
|
+
skills: skills ?? base.skills,
|
|
203
|
+
roster: roster ?? base.roster,
|
|
204
|
+
};
|
|
223
205
|
} else {
|
|
224
|
-
const skills = options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
225
|
-
const roster = options.model ? [{ harness: options.harness ?? "pi", model: options.model }] : [];
|
|
226
206
|
roleDef = {
|
|
227
207
|
schema: "kxm.role.v1",
|
|
228
208
|
id: roleId!,
|
|
229
209
|
description: options.description || `Role ${roleId}`,
|
|
230
|
-
skills,
|
|
231
|
-
roster,
|
|
210
|
+
skills: skills ?? [],
|
|
211
|
+
roster: roster ?? [],
|
|
232
212
|
};
|
|
233
213
|
}
|
|
234
214
|
|
|
215
|
+
// A local role file sits in the project the loader reads, and the loader refuses the
|
|
216
|
+
// whole project over a writer roster it rejects, so the role is checked before it lands.
|
|
217
|
+
let repoRoot = runtime.cwd;
|
|
218
|
+
if (scope === "local") {
|
|
219
|
+
const projectRoot = discoverKxmProjectRoot(runtime.cwd);
|
|
220
|
+
if (!projectRoot) {
|
|
221
|
+
print(
|
|
222
|
+
runtime.io,
|
|
223
|
+
runtime.json,
|
|
224
|
+
{ ok: false, command: "role add", error: "project_not_found" },
|
|
225
|
+
"role add failed: local roles belong to a KXM project; run kxm init at the repository root, or pass --scope global",
|
|
226
|
+
);
|
|
227
|
+
return 2;
|
|
228
|
+
}
|
|
229
|
+
const issues = kxmRoleWriteIssues(projectRoot, roleDef.id, stringifyYaml(roleDef));
|
|
230
|
+
if (issues.length > 0) {
|
|
231
|
+
print(
|
|
232
|
+
runtime.io,
|
|
233
|
+
runtime.json,
|
|
234
|
+
{ ok: false, command: "role add", error: "role_invalid", issues },
|
|
235
|
+
`role add failed: with this role the project would not load, so nothing was written\n${issues.map((entry) => ` ${entry.file}: ${entry.code}: ${entry.message}`).join("\n")}`,
|
|
236
|
+
);
|
|
237
|
+
return 2;
|
|
238
|
+
}
|
|
239
|
+
repoRoot = projectRoot;
|
|
240
|
+
}
|
|
241
|
+
|
|
235
242
|
try {
|
|
236
243
|
const res = addRole(roleDef, {
|
|
237
244
|
scope,
|
|
238
|
-
repoRoot
|
|
245
|
+
repoRoot,
|
|
239
246
|
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
240
247
|
overwrite: options.overwrite,
|
|
241
248
|
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.106";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -1124,6 +1124,7 @@ function validateBundle(
|
|
|
1124
1124
|
options: KxmConfigOptions,
|
|
1125
1125
|
gateRegistry?: KxmResource,
|
|
1126
1126
|
projectRoot?: string,
|
|
1127
|
+
writerRole?: string,
|
|
1127
1128
|
): KxmConfigIssue[] {
|
|
1128
1129
|
const issues: KxmConfigIssue[] = [];
|
|
1129
1130
|
const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate): candidate is JsonObject => Boolean(candidate));
|
|
@@ -1200,9 +1201,9 @@ function validateBundle(
|
|
|
1200
1201
|
if (projectRoot) {
|
|
1201
1202
|
const writerRolePath = join(projectRoot, ".kxm", "roles", "writer.yaml");
|
|
1202
1203
|
const implementerAgent = agents.get("implementer") ?? agents.get("writer");
|
|
1203
|
-
if (existsSync(writerRolePath) && implementerAgent) {
|
|
1204
|
+
if ((writerRole !== undefined || existsSync(writerRolePath)) && implementerAgent) {
|
|
1204
1205
|
try {
|
|
1205
|
-
const rawRole = parseRestrictedYaml(readFileSync(writerRolePath, "utf8"));
|
|
1206
|
+
const rawRole = parseRestrictedYaml(writerRole ?? readFileSync(writerRolePath, "utf8"));
|
|
1206
1207
|
const roleObj = objectValue(rawRole);
|
|
1207
1208
|
const rosterEntries = valuesOf(roleObj ?? {}, "roster")
|
|
1208
1209
|
.map((candidate) => objectValue(candidate))
|
|
@@ -1304,8 +1305,32 @@ export function kxmWorkflowWriteIssues(
|
|
|
1304
1305
|
document: string,
|
|
1305
1306
|
options: KxmConfigOptions = {},
|
|
1306
1307
|
): readonly KxmConfigIssue[] {
|
|
1308
|
+
return candidateIssues(projectRoot, options, { kind: "workflow", id: workflowId, document });
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* What `loadKxmProject` would refuse once `.kxm/roles/<roleId>.yaml` held `document`,
|
|
1313
|
+
* found without writing it. The loader reads only the writer role, so for any other
|
|
1314
|
+
* id this is the project's load as it stands.
|
|
1315
|
+
*/
|
|
1316
|
+
export function kxmRoleWriteIssues(
|
|
1317
|
+
projectRoot: string,
|
|
1318
|
+
roleId: string,
|
|
1319
|
+
document: string,
|
|
1320
|
+
options: KxmConfigOptions = {},
|
|
1321
|
+
): readonly KxmConfigIssue[] {
|
|
1322
|
+
return candidateIssues(projectRoot, options, { kind: "role", id: roleId, document });
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
interface WriteCandidate {
|
|
1326
|
+
kind: "workflow" | "role";
|
|
1327
|
+
id: string;
|
|
1328
|
+
document: string;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function candidateIssues(projectRoot: string, options: KxmConfigOptions, candidate: WriteCandidate): readonly KxmConfigIssue[] {
|
|
1307
1332
|
try {
|
|
1308
|
-
loadProjectBundle(projectRoot, options,
|
|
1333
|
+
loadProjectBundle(projectRoot, options, candidate);
|
|
1309
1334
|
return [];
|
|
1310
1335
|
} catch (error) {
|
|
1311
1336
|
if (error instanceof KxmConfigError) return error.issues;
|
|
@@ -1316,8 +1341,11 @@ export function kxmWorkflowWriteIssues(
|
|
|
1316
1341
|
function loadProjectBundle(
|
|
1317
1342
|
projectRoot: string,
|
|
1318
1343
|
options: KxmConfigOptions,
|
|
1319
|
-
|
|
1344
|
+
candidate?: WriteCandidate,
|
|
1320
1345
|
): KxmProjectBundle {
|
|
1346
|
+
const workflowCandidate = candidate?.kind === "workflow" ? candidate : undefined;
|
|
1347
|
+
// A case-insensitive filesystem serves `Writer.yaml` to the loader's `writer.yaml` lookup.
|
|
1348
|
+
const writerRole = candidate?.kind === "role" && candidate.id.toLocaleLowerCase("en-US") === "writer" ? candidate.document : undefined;
|
|
1321
1349
|
assertNoRegisteredGates(options);
|
|
1322
1350
|
const root = resolve(projectRoot);
|
|
1323
1351
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -1484,7 +1512,7 @@ function loadProjectBundle(
|
|
|
1484
1512
|
}
|
|
1485
1513
|
if (loadIssues.length > 0) throw new KxmConfigError(loadIssues);
|
|
1486
1514
|
|
|
1487
|
-
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root);
|
|
1515
|
+
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, writerRole);
|
|
1488
1516
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
1489
1517
|
const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...workflows.values(), ...environments, ...(gateRegistry ? [gateRegistry] : [])]
|
|
1490
1518
|
.sort((left, right) => compareCodeUnits(left.logicalPath, right.logicalPath));
|