@kontextmind/kxm 0.7.121 → 0.7.123

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.
@@ -17313,7 +17313,7 @@ function sessionTokenFixHint(policy) {
17313
17313
  }
17314
17314
 
17315
17315
  // plugins/kxm/src/mcp-server.ts
17316
- var VERSION = "0.7.121";
17316
+ var VERSION = "0.7.123";
17317
17317
  var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
17318
17318
  var inbox = /* @__PURE__ */ new Map();
17319
17319
  var notifiedInbox = /* @__PURE__ */ new Set();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.121",
3
+ "version": "0.7.123",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -20,7 +20,7 @@ writers, or replace `.kxm/roster.yaml` trusted policy.
20
20
  | Delegate to or answer another agent | `kxm-peer` | `peer` (`peer await` is capped at 60 seconds) |
21
21
  | Work inside a durable workflow run, gates, signed callbacks | `kxm-workflow` | `workflow`, `gate` |
22
22
  | Role definitions, role hosts, model rosters | `kxm-definitions` | `role` |
23
- | Create, drive, inspect, or cancel a run, use a worktree lane, or land a branch | `kxm-runs` | `run`, `runs`, `lane`, `land` |
23
+ | Create, drive, inspect, or cancel a run, use a worktree lane, land a branch, run the developer assignment loop, or build and serve the docs site | `kxm-runs` | `run`, `runs`, `lane`, `land`, `assign`, `docs` |
24
24
  | What we know or decided, recall, context footprint, Git memory | `kxm-context-memory` | `context`, `memory`, `explain` |
25
25
  | Turn a repeated practice into a governed skill | `kxm-skill-lifecycle` | `skills` |
26
26
  | What KXM learned, repeated asks, recorded route spend | `kxm-routing-improve` | `routing`, `improve` |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: kxm-runs
3
- description: Create, drive, and inspect local KXM runs, manage worktree lanes, and land a branch. kxm runs drive with --simulated executes a run model-free and settles it with a verified receipt. Use when asked to start a workflow run, check its status, read its receipt, cancel it, smoke-test a workflow, create a lane, or land the current branch.
3
+ description: Create, drive, and inspect local KXM runs, manage worktree lanes, land a branch, and run the developer assignment loop. kxm runs drive with --simulated executes a run model-free and settles it with a verified receipt. Use when asked to start a workflow run, check its status, read its receipt, cancel it, smoke-test a workflow, create a lane, land the current branch, or call kxm assign (run, witness, plan-current, attribute, observe-cost, accept, change-report).
4
4
  ---
5
5
 
6
6
  # KXM runs
@@ -11,6 +11,11 @@ the Runtime supervisor. A created run stays `created` until
11
11
  an admitted model; `--simulated` is the model-free producer. Do not invent get,
12
12
  create, or logs verbs under `runs`.
13
13
 
14
+ `kxm docs build` regenerates the docs site from the roadmap state by running
15
+ `node plans/kxm-roadmap/update-dashboard.mjs` at the project root. `kxm docs
16
+ serve` serves that site with `python3 ops/docs-site/serve.py` and passes
17
+ `--port` through when it is set. Both require a KXM project.
18
+
14
19
  ## Commands
15
20
 
16
21
  | Command | Purpose | Options / arguments |
@@ -27,6 +32,15 @@ create, or logs verbs under `runs`.
27
32
  | `kxm lane drop <unit>` | Remove the worktree and the record. The branch is not deleted | `--force`, `--json` |
28
33
  | `kxm lane run <unit>` | Create the lane if needed, start a run from a brief, and drive it | `--brief <file>` (required), `--workflow <id>`, `--base <ref>`, `--wait`, `--timeout-ms <n>`, `--json` |
29
34
  | `kxm land` | Verify, regenerate docs, push, open or reuse a pull request, rebase, unblock, squash-merge, watch the release, and note a milestone | `--pr <n>`, `--stage <name>`, `--body-file <path>`, `--json`, `--dry-run` |
35
+ | `kxm assign run` | Dispatch one assignment manifest. The runner performs every check | `--manifest <path>` |
36
+ | `kxm assign witness` | Run the fixed witness for an existing record | `--record-dir <path>` |
37
+ | `kxm assign plan-current` | Stamp or advance the current-plan pointer | `--task-dir <path>`, `--plan <path>`, `--sha256 <hex>`, `--base-commit <sha>`, `--expected-generation <n>` |
38
+ | `kxm assign attribute` | Attach a private note. It is not proof | `--task-dir <path>`, `--record-dir <path>`, `--class <class>`, `--explanation-file <path>` |
39
+ | `kxm assign observe-cost` | Import one cost-only observation | `--task-dir <path>`, `--input <path>` |
40
+ | `kxm assign accept` | Bind a witnessed commit and two critic records | `--task-dir <path>`, `--commit <sha>`, `--record-dir <path>`, `--critic <path>` twice, optional `--observed-pr <id>`, `--observed-ci <id>` |
41
+ | `kxm assign change-report` | Report attempts, rework, and spend | `--task-dir <path>` |
42
+ | `kxm docs build` | Regenerate the docs site from roadmap state | `--dry-run`, `--json` |
43
+ | `kxm docs serve` | Serve the built site on the tailnet address | `--port <port>`, `--dry-run`, `--json` |
30
44
 
31
45
  The run record and its events keep only the prompt's hash, but the full
32
46
  prompt text is kept in a local `run-events.db.run-prompts.json` file (mode
@@ -80,6 +94,7 @@ squash-merges. `release` waits for the tag and the npm publish. `milestone`
80
94
  reports `deep_review_required` when a phase flips to done or the body contains
81
95
  a `Milestone:` line, and does not run the review. `--dry-run` prints each
82
96
  stage's plan and does not mutate.
97
+ `kxm assign` spawns `node scripts/assignment-run.mjs` from the project root and returns the child's exit code. Paths are passed through as given. `--dry-run` prints that argv and does not spawn. `--json` formats this command's own output and is not forwarded. The group does not load a working-directory `.env`. A project without `scripts/assignment-run.mjs` is refused with `assign_runner_missing`. Outside a KXM project the refusal is `project_required`. Witness, roster, and acceptance checks belong to the runner, documented in `docs/contributing/assignment-runner.md`.
83
98
 
84
99
  ## Refusals
85
100
 
@@ -0,0 +1,144 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { discoverKxmProjectRoot } from "../project-config.ts";
5
+ import { CLI_RESULT_SCHEMA, print, type Runtime } from "./types.ts";
6
+
7
+ export interface AssignSpawnOptions {
8
+ cwd: string;
9
+ stdio: "inherit";
10
+ env: NodeJS.ProcessEnv;
11
+ }
12
+
13
+ export interface AssignSpawnResult {
14
+ status: number | null;
15
+ error?: Error;
16
+ }
17
+
18
+ /** Test seam. Production leaves `spawn` unset and uses `spawnSync`. */
19
+ export const kxmAssignCliSeams: {
20
+ spawn?: (command: string, args: readonly string[], options: AssignSpawnOptions) => AssignSpawnResult;
21
+ } = {};
22
+
23
+ export type AssignVerb =
24
+ | "run"
25
+ | "witness"
26
+ | "plan-current"
27
+ | "attribute"
28
+ | "observe-cost"
29
+ | "accept"
30
+ | "change-report";
31
+
32
+ export interface AssignArgs {
33
+ manifest?: string;
34
+ recordDir?: string;
35
+ taskDir?: string;
36
+ plan?: string;
37
+ sha256?: string;
38
+ baseCommit?: string;
39
+ expectedGeneration?: string;
40
+ classification?: string;
41
+ explanationFile?: string;
42
+ input?: string;
43
+ commit?: string;
44
+ critics?: readonly string[];
45
+ observedPr?: string;
46
+ observedCi?: string;
47
+ }
48
+
49
+ const RUNNER_REL = "scripts/assignment-run.mjs";
50
+
51
+ function refuse(runtime: Runtime, command: string, error: string, text: string): number {
52
+ print(runtime.io, runtime.json, { ok: false, command, error }, text);
53
+ return 1;
54
+ }
55
+
56
+ function flag(value: string | undefined): string {
57
+ if (value === undefined) throw new Error("assign invocation missing a parsed flag");
58
+ return value;
59
+ }
60
+
61
+ /** Argv after `node scripts/assignment-run.mjs`, in the recipe's flag order.
62
+ * The runner's usage text documents no `--json` flag. `accept` prints JSON
63
+ * and takes none, so the global `--json` flag stays on this command. */
64
+ function runnerArgv(subcommand: AssignVerb, args: AssignArgs): string[] {
65
+ switch (subcommand) {
66
+ case "run":
67
+ return ["run", "--manifest", flag(args.manifest)];
68
+ case "witness":
69
+ return ["witness", "--record-dir", flag(args.recordDir)];
70
+ case "plan-current":
71
+ return [
72
+ "plan-current",
73
+ "--task-dir", flag(args.taskDir),
74
+ "--plan", flag(args.plan),
75
+ "--sha256", flag(args.sha256),
76
+ "--base-commit", flag(args.baseCommit),
77
+ "--expected-generation", flag(args.expectedGeneration),
78
+ ];
79
+ case "attribute":
80
+ return [
81
+ "attribute",
82
+ "--task-dir", flag(args.taskDir),
83
+ "--record-dir", flag(args.recordDir),
84
+ "--class", flag(args.classification),
85
+ "--explanation-file", flag(args.explanationFile),
86
+ ];
87
+ case "observe-cost":
88
+ return ["observe-cost", "--task-dir", flag(args.taskDir), "--input", flag(args.input)];
89
+ case "accept": {
90
+ const argv = [
91
+ "accept",
92
+ "--task-dir", flag(args.taskDir),
93
+ "--commit", flag(args.commit),
94
+ "--record-dir", flag(args.recordDir),
95
+ ];
96
+ for (const critic of args.critics ?? []) argv.push("--critic", critic);
97
+ if (args.observedPr !== undefined) argv.push("--observed-pr", args.observedPr);
98
+ if (args.observedCi !== undefined) argv.push("--observed-ci", args.observedCi);
99
+ return argv;
100
+ }
101
+ case "change-report":
102
+ return ["change-report", "--task-dir", flag(args.taskDir)];
103
+ default: {
104
+ const unreachable: never = subcommand;
105
+ throw new Error(`unknown assign verb ${String(unreachable)}`);
106
+ }
107
+ }
108
+ }
109
+
110
+ export async function cmdAssign(runtime: Runtime, subcommand: AssignVerb, args: AssignArgs): Promise<number> {
111
+ const command = `assign ${subcommand}`;
112
+ const root = discoverKxmProjectRoot(runtime.cwd);
113
+ if (!root) return refuse(runtime, command, "project_required", `${command} requires a KXM project (run kxm init first)`);
114
+ if (!existsSync(join(root, "scripts", "assignment-run.mjs"))) {
115
+ return refuse(runtime, command, "assign_runner_missing", `${command} requires scripts/assignment-run.mjs in the project root`);
116
+ }
117
+ const argv = ["node", RUNNER_REL, ...runnerArgv(subcommand, args)];
118
+ if (runtime.dryRun) {
119
+ // print() redacts every 64-hex string. plan-current passes a plan sha256
120
+ // through unchanged, so the dry-run argv is written directly.
121
+ const payload = { schema: CLI_RESULT_SCHEMA, ok: true, command, dryRun: true, argv };
122
+ runtime.io.stdout(runtime.json ? `${JSON.stringify(payload)}\n` : `${argv.join(" ")}\n`);
123
+ return 0;
124
+ }
125
+ // Never load a `.env` from the working directory. The justfile keeps
126
+ // `set dotenv-load` off for the same reason: a gitignored `.env` could set
127
+ // `NODE_OPTIONS`, and that value runs before the runner validates identity,
128
+ // tree, or roster. Pass the process environment through unchanged.
129
+ const spawn = kxmAssignCliSeams.spawn ?? ((commandName: string, commandArgs: readonly string[], options: AssignSpawnOptions): AssignSpawnResult => {
130
+ const result = spawnSync(commandName, commandArgs, {
131
+ cwd: options.cwd,
132
+ stdio: options.stdio,
133
+ env: options.env,
134
+ windowsHide: true,
135
+ });
136
+ return { status: result.status, ...(result.error ? { error: result.error } : {}) };
137
+ });
138
+ const result = spawn("node", argv.slice(1), { cwd: root, stdio: "inherit", env: runtime.env });
139
+ if (result.status === null) {
140
+ const detail = result.error?.message ?? "the assignment runner could not be started";
141
+ return refuse(runtime, command, "assign_spawn_failed", detail);
142
+ }
143
+ return result.status;
144
+ }
@@ -0,0 +1,78 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { discoverKxmProjectRoot } from "../project-config.ts";
5
+ import { print, type Runtime } from "./types.ts";
6
+
7
+ const GENERATOR = "plans/kxm-roadmap/update-dashboard.mjs";
8
+ const SERVER = "ops/docs-site/serve.py";
9
+
10
+ function requireProject(runtime: Runtime, command: string): string | number {
11
+ const root = discoverKxmProjectRoot(runtime.cwd);
12
+ if (!root) {
13
+ print(
14
+ runtime.io,
15
+ runtime.json,
16
+ { ok: false, command, error: "project_required" },
17
+ `kxm ${command} requires a KXM project (run kxm init first)`,
18
+ );
19
+ return 1;
20
+ }
21
+ return root;
22
+ }
23
+
24
+ function requireFile(runtime: Runtime, command: string, root: string, relative: string, error: string): string | number {
25
+ const file = join(root, relative);
26
+ if (existsSync(file)) return file;
27
+ print(
28
+ runtime.io,
29
+ runtime.json,
30
+ { ok: false, command, error, path: relative },
31
+ `kxm ${command} requires ${relative}`,
32
+ );
33
+ return 1;
34
+ }
35
+
36
+ function printCommand(runtime: Runtime, command: string, line: string): number {
37
+ print(
38
+ runtime.io,
39
+ runtime.json,
40
+ { ok: true, command, dryRun: true, detail: line },
41
+ line,
42
+ );
43
+ return 0;
44
+ }
45
+
46
+ function runInherited(command: string, args: readonly string[], cwd: string): Promise<number> {
47
+ return new Promise((resolveExit) => {
48
+ const child = spawn(command, [...args], { cwd, stdio: "inherit" });
49
+ child.once("error", () => resolveExit(1));
50
+ child.once("exit", (code) => resolveExit(code ?? 1));
51
+ });
52
+ }
53
+
54
+ /** Regenerate the docs site from the roadmap state. */
55
+ export async function cmdDocsBuild(runtime: Runtime): Promise<number> {
56
+ const command = "docs build";
57
+ const root = requireProject(runtime, command);
58
+ if (typeof root === "number") return root;
59
+ const file = requireFile(runtime, command, root, GENERATOR, "docs_generator_missing");
60
+ if (typeof file === "number") return file;
61
+ const line = `node ${GENERATOR}`;
62
+ if (runtime.dryRun) return printCommand(runtime, command, line);
63
+ return runInherited(process.execPath, [file], root);
64
+ }
65
+
66
+ /** Serve the built site. `--port` is passed through when set. */
67
+ export async function cmdDocsServe(runtime: Runtime, options: { port?: string } = {}): Promise<number> {
68
+ const command = "docs serve";
69
+ const root = requireProject(runtime, command);
70
+ if (typeof root === "number") return root;
71
+ const file = requireFile(runtime, command, root, SERVER, "docs_server_missing");
72
+ if (typeof file === "number") return file;
73
+ const args = [file];
74
+ if (options.port !== undefined && options.port !== "") args.push("--port", options.port);
75
+ const line = ["python3", SERVER, ...args.slice(1)].join(" ");
76
+ if (runtime.dryRun) return printCommand(runtime, command, line);
77
+ return runInherited("python3", args, root);
78
+ }
@@ -120,6 +120,7 @@ cmdBackup,
120
120
  } from "./cli/project.ts";
121
121
 
122
122
  import { cmdPluginInstall } from "./cli/plugins.ts";
123
+ import { cmdDocsBuild, cmdDocsServe } from "./cli/docs.ts";
123
124
 
124
125
  import {
125
126
  applyLane,
@@ -132,6 +133,7 @@ import {
132
133
  } from "./cli/lanes.ts";
133
134
 
134
135
  import { cmdLand } from "./cli/land.ts";
136
+ import { cmdAssign } from "./cli/assign.ts";
135
137
 
136
138
  import {
137
139
  cmdStatus,
@@ -200,6 +202,8 @@ const DRY_RUN_COMMANDS: ReadonlySet<string> = new Set([
200
202
  "init", "backup", "restore", "run", "explain", "suggest", "update", "dash", "completion", "completion install",
201
203
  "lane create", "lane list", "lane status", "lane drop", "lane run",
202
204
  "land",
205
+ "assign run", "assign witness", "assign plan-current", "assign attribute",
206
+ "assign observe-cost", "assign accept", "assign change-report",
203
207
  "plugin install", "plugin",
204
208
  "runs status", "runs drive", "runs receipt", "runs cancel", "runs list",
205
209
  "tenant status", "models inventory-refresh", "harness list", "auth token",
@@ -224,6 +228,7 @@ const DRY_RUN_COMMANDS: ReadonlySet<string> = new Set([
224
228
  "goal create", "goal list",
225
229
  "task create", "task list", "task get", "task run", "task sync",
226
230
  "studio layout", "studio serve",
231
+ "docs build", "docs serve",
227
232
  ]);
228
233
 
229
234
  class DryRunRefused extends Error {
@@ -499,6 +504,74 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
499
504
  ...(options.bodyFile !== undefined ? { bodyFile: options.bodyFile } : {}),
500
505
  });
501
506
  });
507
+ const assignCmd = addGlobalOptions(program.command("assign").description("Run this repository's developer assignment runner"));
508
+ assignCmd.helpCommand("help", "Show assign help");
509
+ addGlobalOptions(assignCmd.command("run").description("Dispatch a bound assignment manifest"))
510
+ .requiredOption("--manifest <path>", "Absolute path to the assignment manifest")
511
+ .action(async function assignRunAction(this: Command, options: { manifest: string }) {
512
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "run", { manifest: options.manifest });
513
+ });
514
+ addGlobalOptions(assignCmd.command("witness").description("Run the fixed verification witness for an existing assignment"))
515
+ .requiredOption("--record-dir <path>", "Absolute path to the assignment record directory")
516
+ .action(async function assignWitnessAction(this: Command, options: { recordDir: string }) {
517
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "witness", { recordDir: options.recordDir });
518
+ });
519
+ addGlobalOptions(assignCmd.command("plan-current").description("Stamp or advance the current-plan pointer"))
520
+ .requiredOption("--task-dir <path>", "Absolute path to the task directory")
521
+ .requiredOption("--plan <path>", "Absolute path to the plan file")
522
+ .requiredOption("--sha256 <hex>", "SHA-256 of the plan file")
523
+ .requiredOption("--base-commit <sha>", "Base commit the pointer records")
524
+ .requiredOption("--expected-generation <n>", "Generation the pointer must currently have")
525
+ .action(async function assignPlanCurrentAction(this: Command, options: { taskDir: string; plan: string; sha256: string; baseCommit: string; expectedGeneration: string }) {
526
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "plan-current", {
527
+ taskDir: options.taskDir,
528
+ plan: options.plan,
529
+ sha256: options.sha256,
530
+ baseCommit: options.baseCommit,
531
+ expectedGeneration: options.expectedGeneration,
532
+ });
533
+ });
534
+ addGlobalOptions(assignCmd.command("attribute").description("Attach a private attribution note"))
535
+ .requiredOption("--task-dir <path>", "Absolute path to the task directory")
536
+ .requiredOption("--record-dir <path>", "Absolute path to the assignment record directory")
537
+ .requiredOption("--class <class>", "orchestration, model, environment, or unclassified")
538
+ .requiredOption("--explanation-file <path>", "Absolute path to the note file")
539
+ .action(async function assignAttributeAction(this: Command, options: { taskDir: string; recordDir: string; class: string; explanationFile: string }) {
540
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "attribute", {
541
+ taskDir: options.taskDir,
542
+ recordDir: options.recordDir,
543
+ classification: options.class,
544
+ explanationFile: options.explanationFile,
545
+ });
546
+ });
547
+ addGlobalOptions(assignCmd.command("observe-cost").description("Import one historical cost observation as cost-only"))
548
+ .requiredOption("--task-dir <path>", "Absolute path to the task directory")
549
+ .requiredOption("--input <path>", "Absolute path to the observation file")
550
+ .action(async function assignObserveCostAction(this: Command, options: { taskDir: string; input: string }) {
551
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "observe-cost", { taskDir: options.taskDir, input: options.input });
552
+ });
553
+ addGlobalOptions(assignCmd.command("accept").description("Bind an exact witnessed commit and two critic PASS records"))
554
+ .requiredOption("--task-dir <path>", "Absolute path to the task directory")
555
+ .requiredOption("--commit <sha>", "Commit to accept")
556
+ .requiredOption("--record-dir <path>", "Absolute path to the writer record directory")
557
+ .requiredOption("--critic <path>", "Absolute path to a critic record directory; pass twice", (value: string, previous: string[]) => previous.concat(value), [] as string[])
558
+ .option("--observed-pr <id>", "Observed pull request id")
559
+ .option("--observed-ci <id>", "Observed CI run id")
560
+ .action(async function assignAcceptAction(this: Command, options: { taskDir: string; commit: string; recordDir: string; critic: string[]; observedPr?: string; observedCi?: string }) {
561
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "accept", {
562
+ taskDir: options.taskDir,
563
+ commit: options.commit,
564
+ recordDir: options.recordDir,
565
+ critics: options.critic,
566
+ ...(options.observedPr !== undefined ? { observedPr: options.observedPr } : {}),
567
+ ...(options.observedCi !== undefined ? { observedCi: options.observedCi } : {}),
568
+ });
569
+ });
570
+ addGlobalOptions(assignCmd.command("change-report").description("Report attempts, rework, costs, and retained verification history"))
571
+ .requiredOption("--task-dir <path>", "Absolute path to the task directory")
572
+ .action(async function assignChangeReportAction(this: Command, options: { taskDir: string }) {
573
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "change-report", { taskDir: options.taskDir });
574
+ });
502
575
 
503
576
  addGlobalOptions(program.command("run").description("Create a KXM run without executing steps; follow its prerequisites, then kxm runs drive <runId> --wait")
504
577
  .argument("[workflow]", "Workflow id to run")
@@ -554,6 +627,18 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
554
627
  result.code = await cmdKxmRunList(runtimeFrom(ctx, this));
555
628
  });
556
629
 
630
+ const docsCmd = addGlobalOptions(program.command("docs").description("Build and serve the tailnet docs site"));
631
+ docsCmd.helpCommand("help", "Show docs help");
632
+ addGlobalOptions(docsCmd.command("build").description("Regenerate the docs site from the roadmap state"))
633
+ .action(async function docsBuildAction(this: Command) {
634
+ result.code = await cmdDocsBuild(runtimeFrom(ctx, this));
635
+ });
636
+ addGlobalOptions(docsCmd.command("serve").description("Serve the built docs site on this machine's tailnet address"))
637
+ .option("--port <port>", "Port passed through to the docs server")
638
+ .action(async function docsServeAction(this: Command, options: { port?: string }) {
639
+ result.code = await cmdDocsServe(runtimeFrom(ctx, this), options);
640
+ });
641
+
557
642
  const tenantCmd = addGlobalOptions(program.command("tenant").description("Composed tenant reads for machine clients (portal)"));
558
643
  tenantCmd.helpCommand("help", "Show tenant help");
559
644
  addGlobalOptions(tenantCmd.command("status").description("Read hub metadata and authoritative Runtime run state as one labeled view"))
@@ -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.121";
14
+ const VERSION = "0.7.123";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();
@@ -0,0 +1,147 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.kxm.dev/roadmap-state.schema.json",
4
+ "title": "KXM roadmap state",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema", "updated", "goal", "phases"],
8
+ "properties": {
9
+ "schema": { "const": "kxm.roadmap.v1" },
10
+ "updated": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
11
+ "goal": { "type": "string", "minLength": 1 },
12
+ "constraints": {
13
+ "type": "array",
14
+ "items": { "type": "string" }
15
+ },
16
+ "contacts": {
17
+ "type": "array",
18
+ "items": { "$ref": "#/$defs/contact" }
19
+ },
20
+ "contacts_comment": { "type": "string" },
21
+ "history": {
22
+ "type": "array",
23
+ "description": "Oldest first. The generator keeps the newest 12 lines and drops the older ones (history_over_cap). That prune is not a schema refusal.",
24
+ "items": { "$ref": "#/$defs/historyLine" }
25
+ },
26
+ "architecture": { "$ref": "#/$defs/architecture" },
27
+ "improvements": {
28
+ "type": "array",
29
+ "items": { "type": "string" }
30
+ },
31
+ "phases": {
32
+ "type": "array",
33
+ "items": { "$ref": "#/$defs/phase" }
34
+ }
35
+ },
36
+ "$defs": {
37
+ "date": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
38
+ "historyLine": {
39
+ "type": "object",
40
+ "additionalProperties": false,
41
+ "required": ["date", "line"],
42
+ "properties": {
43
+ "date": { "$ref": "#/$defs/date" },
44
+ "line": { "type": "string", "minLength": 1, "maxLength": 240 }
45
+ }
46
+ },
47
+ "contact": {
48
+ "type": "object",
49
+ "additionalProperties": false,
50
+ "required": ["role", "confirmed"],
51
+ "properties": {
52
+ "role": { "type": "string", "minLength": 1 },
53
+ "name": { "type": "string", "minLength": 1 },
54
+ "confirmed": { "$ref": "#/$defs/date" }
55
+ }
56
+ },
57
+ "architecture": {
58
+ "type": "object",
59
+ "additionalProperties": false,
60
+ "properties": {
61
+ "verified": {
62
+ "anyOf": [
63
+ { "type": "null" },
64
+ { "$ref": "#/$defs/date" }
65
+ ]
66
+ },
67
+ "docs": {
68
+ "type": "array",
69
+ "items": { "type": "string" }
70
+ },
71
+ "drift": {
72
+ "type": "array",
73
+ "items": {
74
+ "type": "object",
75
+ "additionalProperties": false,
76
+ "required": ["claim", "page", "status"],
77
+ "properties": {
78
+ "claim": { "type": "string" },
79
+ "page": { "type": "string" },
80
+ "status": { "type": "string" }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ },
86
+ "phase": {
87
+ "type": "object",
88
+ "additionalProperties": false,
89
+ "required": ["id", "slug", "title", "goal", "source", "tasks"],
90
+ "properties": {
91
+ "id": { "type": "string", "minLength": 1 },
92
+ "slug": { "type": "string", "minLength": 1 },
93
+ "title": { "type": "string", "minLength": 1 },
94
+ "goal": { "type": "string", "minLength": 1 },
95
+ "source": { "type": "string", "minLength": 1 },
96
+ "priority": { "const": "later" },
97
+ "horizon": {
98
+ "description": "Derived by the generator from phase order and priority (next, soon, or later). Not hand-set. A stored value is ignored.",
99
+ "enum": ["next", "soon", "later"]
100
+ },
101
+ "blockers": {
102
+ "type": "array",
103
+ "items": { "type": "string" }
104
+ },
105
+ "questions": {
106
+ "type": "array",
107
+ "items": { "type": "string" }
108
+ },
109
+ "tasks": {
110
+ "type": "array",
111
+ "items": { "$ref": "#/$defs/task" }
112
+ }
113
+ }
114
+ },
115
+ "task": {
116
+ "type": "object",
117
+ "additionalProperties": false,
118
+ "required": ["id", "title", "status"],
119
+ "properties": {
120
+ "id": { "type": "string", "minLength": 1 },
121
+ "title": { "type": "string", "minLength": 1 },
122
+ "status": { "enum": ["open", "done"] },
123
+ "evidence": { "type": "string" },
124
+ "planSection": { "type": "string", "minLength": 1 },
125
+ "doneCriterion": { "type": "string", "minLength": 1 },
126
+ "evidenceNeeded": { "type": "string", "minLength": 1 },
127
+ "template": {
128
+ "enum": [
129
+ "feature",
130
+ "research",
131
+ "bug-fix",
132
+ "architecture",
133
+ "adr",
134
+ "test-plan",
135
+ "test-report",
136
+ "review",
137
+ "handoff",
138
+ "runbook",
139
+ "postmortem",
140
+ "none"
141
+ ]
142
+ },
143
+ "detail": { "enum": ["brief", "scoped", "ready"] }
144
+ }
145
+ }
146
+ }
147
+ }