@kontextmind/kxm 0.7.120 → 0.7.122

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.120";
17316
+ var VERSION = "0.7.122";
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.120",
3
+ "version": "0.7.122",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -20,7 +20,8 @@ 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, or use a worktree lane | `kxm-runs` | `run`, `runs`, `lane` |
23
+ | Create, drive, inspect, or cancel a run, use a worktree lane, or land a branch | `kxm-runs` | `run`, `runs`, `lane`, `land` |
24
+ | Create, drive, inspect, or cancel a run, use a worktree lane, or run the developer assignment loop | `kxm-runs` | `run`, `runs`, `lane`, `assign` |
24
25
  | What we know or decided, recall, context footprint, Git memory | `kxm-context-memory` | `context`, `memory`, `explain` |
25
26
  | Turn a repeated practice into a governed skill | `kxm-skill-lifecycle` | `skills` |
26
27
  | 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, and manage worktree lanes. 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, or create a lane.
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
@@ -26,6 +26,14 @@ create, or logs verbs under `runs`.
26
26
  | `kxm lane status <unit>` | One lane plus its last run status, or unknown when the supervisor is not answering | `--json` |
27
27
  | `kxm lane drop <unit>` | Remove the worktree and the record. The branch is not deleted | `--force`, `--json` |
28
28
  | `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
+ | `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` |
30
+ | `kxm assign run` | Dispatch one assignment manifest. The runner performs every check | `--manifest <path>` |
31
+ | `kxm assign witness` | Run the fixed witness for an existing record | `--record-dir <path>` |
32
+ | `kxm assign plan-current` | Stamp or advance the current-plan pointer | `--task-dir <path>`, `--plan <path>`, `--sha256 <hex>`, `--base-commit <sha>`, `--expected-generation <n>` |
33
+ | `kxm assign attribute` | Attach a private note. It is not proof | `--task-dir <path>`, `--record-dir <path>`, `--class <class>`, `--explanation-file <path>` |
34
+ | `kxm assign observe-cost` | Import one cost-only observation | `--task-dir <path>`, `--input <path>` |
35
+ | `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>` |
36
+ | `kxm assign change-report` | Report attempts, rework, and spend | `--task-dir <path>` |
29
37
 
30
38
  The run record and its events keep only the prompt's hash, but the full
31
39
  prompt text is kept in a local `run-events.db.run-prompts.json` file (mode
@@ -65,6 +73,22 @@ file. `kxm run --lane <unit>` and `kxm runs status|drive|receipt|cancel --lane`
65
73
  discover the project from the lane worktree. `kxm lane run <unit> --brief <file>`
66
74
  creates the lane when it is missing, starts the run, and drives it.
67
75
 
76
+ `kxm land` lands the current branch. With no `--stage` it runs these stages in
77
+ order: `verify`, `docs`, `push`, `pr`, `rebase`, `unblock`, `merge`,
78
+ `release`, `milestone`. `verify` runs `npm run verify` on a clean tree and is
79
+ not replaced by the later stages. `docs` regenerates the roadmap when
80
+ `plans/kxm-roadmap/update-dashboard.mjs` is present and commits only those
81
+ generated pages. `push` publishes the branch. `pr` reuses the open pull
82
+ request or creates one from `--body-file`. `rebase` rebases onto
83
+ `origin/main` for at most five rounds, resolving only the dist rebuild, the
84
+ CHANGELOG Unreleased union, and the tracker "Landed in this tree" union.
85
+ `unblock` reruns one failed check and reports a required review. `merge`
86
+ squash-merges. `release` waits for the tag and the npm publish. `milestone`
87
+ reports `deep_review_required` when a phase flips to done or the body contains
88
+ a `Milestone:` line, and does not run the review. `--dry-run` prints each
89
+ stage's plan and does not mutate.
90
+ `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`.
91
+
68
92
  ## Refusals
69
93
 
70
94
  - `run_workflow_unknown`: the workflow ID is not a project workflow. Run only
@@ -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,48 @@
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { discoverKxmProjectRoot } from "../project-config.ts";
4
+ import { findKxmRepoRoot } from "../repo-root.ts";
5
+ import { print, type Runtime } from "./types.ts";
6
+
7
+ export interface LandOptions {
8
+ pr?: string;
9
+ stage?: string;
10
+ bodyFile?: string;
11
+ }
12
+
13
+ function childEnv(): NodeJS.ProcessEnv {
14
+ const env: NodeJS.ProcessEnv = {};
15
+ for (const [name, value] of Object.entries(process.env)) {
16
+ if (value === undefined) continue;
17
+ if (name.toUpperCase().startsWith("GIT_")) continue;
18
+ env[name] = value;
19
+ }
20
+ return env;
21
+ }
22
+
23
+ function refuse(runtime: Runtime, error: string, text: string): number {
24
+ print(runtime.io, runtime.json, { ok: false, command: "land", error }, text);
25
+ return 1;
26
+ }
27
+
28
+ /** Spawn scripts/pr-land.mjs and stream its JSON lines. The exit code is the script's. */
29
+ export function cmdLand(runtime: Runtime, options: LandOptions = {}): Promise<number> {
30
+ const root = discoverKxmProjectRoot(runtime.cwd);
31
+ if (!root) {
32
+ return Promise.resolve(refuse(runtime, "project_required", "land requires a KXM project (run kxm init first)"));
33
+ }
34
+ const script = join(findKxmRepoRoot(import.meta.url), "scripts", "pr-land.mjs");
35
+ const args = [script];
36
+ if (options.pr) args.push("--pr", options.pr);
37
+ if (options.stage) args.push("--stage", options.stage);
38
+ if (options.bodyFile) args.push("--body-file", options.bodyFile);
39
+ if (runtime.json) args.push("--json");
40
+ if (runtime.dryRun) args.push("--dry-run");
41
+ return new Promise((resolveExit) => {
42
+ const child = spawn(process.execPath, args, { cwd: root, env: childEnv(), windowsHide: true });
43
+ child.stdout.on("data", (chunk: Buffer) => runtime.io.stdout(chunk.toString("utf8")));
44
+ child.stderr.on("data", (chunk: Buffer) => runtime.io.stderr(chunk.toString("utf8")));
45
+ child.once("error", () => resolveExit(1));
46
+ child.once("close", (code) => resolveExit(code ?? 1));
47
+ });
48
+ }
@@ -131,6 +131,9 @@ import {
131
131
  cmdLaneStatus,
132
132
  } from "./cli/lanes.ts";
133
133
 
134
+ import { cmdLand } from "./cli/land.ts";
135
+ import { cmdAssign } from "./cli/assign.ts";
136
+
134
137
  import {
135
138
  cmdStatus,
136
139
  cmdDash,
@@ -197,6 +200,9 @@ const USAGE_ERROR_CODES = new Set([
197
200
  const DRY_RUN_COMMANDS: ReadonlySet<string> = new Set([
198
201
  "init", "backup", "restore", "run", "explain", "suggest", "update", "dash", "completion", "completion install",
199
202
  "lane create", "lane list", "lane status", "lane drop", "lane run",
203
+ "land",
204
+ "assign run", "assign witness", "assign plan-current", "assign attribute",
205
+ "assign observe-cost", "assign accept", "assign change-report",
200
206
  "plugin install", "plugin",
201
207
  "runs status", "runs drive", "runs receipt", "runs cancel", "runs list",
202
208
  "tenant status", "models inventory-refresh", "harness list", "auth token",
@@ -485,6 +491,86 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
485
491
  });
486
492
  });
487
493
 
494
+ addGlobalOptions(program.command("land").description("Land the current branch: verify, regenerate docs, push, pull request, rebase, unblock, squash-merge, release, and milestone"))
495
+ .option("--pr <n>", "Existing pull request number")
496
+ .option("--stage <name>", "Run one stage: verify, docs, push, pr, rebase, unblock, merge, release, or milestone")
497
+ .option("--body-file <path>", "Pull request body file used when creating a pull request")
498
+ .action(async function landAction(this: Command, options: { pr?: string; stage?: string; bodyFile?: string }) {
499
+ result.code = await cmdLand(runtimeFrom(ctx, this), {
500
+ ...(options.pr !== undefined ? { pr: options.pr } : {}),
501
+ ...(options.stage !== undefined ? { stage: options.stage } : {}),
502
+ ...(options.bodyFile !== undefined ? { bodyFile: options.bodyFile } : {}),
503
+ });
504
+ });
505
+ const assignCmd = addGlobalOptions(program.command("assign").description("Run this repository's developer assignment runner"));
506
+ assignCmd.helpCommand("help", "Show assign help");
507
+ addGlobalOptions(assignCmd.command("run").description("Dispatch a bound assignment manifest"))
508
+ .requiredOption("--manifest <path>", "Absolute path to the assignment manifest")
509
+ .action(async function assignRunAction(this: Command, options: { manifest: string }) {
510
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "run", { manifest: options.manifest });
511
+ });
512
+ addGlobalOptions(assignCmd.command("witness").description("Run the fixed verification witness for an existing assignment"))
513
+ .requiredOption("--record-dir <path>", "Absolute path to the assignment record directory")
514
+ .action(async function assignWitnessAction(this: Command, options: { recordDir: string }) {
515
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "witness", { recordDir: options.recordDir });
516
+ });
517
+ addGlobalOptions(assignCmd.command("plan-current").description("Stamp or advance the current-plan pointer"))
518
+ .requiredOption("--task-dir <path>", "Absolute path to the task directory")
519
+ .requiredOption("--plan <path>", "Absolute path to the plan file")
520
+ .requiredOption("--sha256 <hex>", "SHA-256 of the plan file")
521
+ .requiredOption("--base-commit <sha>", "Base commit the pointer records")
522
+ .requiredOption("--expected-generation <n>", "Generation the pointer must currently have")
523
+ .action(async function assignPlanCurrentAction(this: Command, options: { taskDir: string; plan: string; sha256: string; baseCommit: string; expectedGeneration: string }) {
524
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "plan-current", {
525
+ taskDir: options.taskDir,
526
+ plan: options.plan,
527
+ sha256: options.sha256,
528
+ baseCommit: options.baseCommit,
529
+ expectedGeneration: options.expectedGeneration,
530
+ });
531
+ });
532
+ addGlobalOptions(assignCmd.command("attribute").description("Attach a private attribution note"))
533
+ .requiredOption("--task-dir <path>", "Absolute path to the task directory")
534
+ .requiredOption("--record-dir <path>", "Absolute path to the assignment record directory")
535
+ .requiredOption("--class <class>", "orchestration, model, environment, or unclassified")
536
+ .requiredOption("--explanation-file <path>", "Absolute path to the note file")
537
+ .action(async function assignAttributeAction(this: Command, options: { taskDir: string; recordDir: string; class: string; explanationFile: string }) {
538
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "attribute", {
539
+ taskDir: options.taskDir,
540
+ recordDir: options.recordDir,
541
+ classification: options.class,
542
+ explanationFile: options.explanationFile,
543
+ });
544
+ });
545
+ addGlobalOptions(assignCmd.command("observe-cost").description("Import one historical cost observation as cost-only"))
546
+ .requiredOption("--task-dir <path>", "Absolute path to the task directory")
547
+ .requiredOption("--input <path>", "Absolute path to the observation file")
548
+ .action(async function assignObserveCostAction(this: Command, options: { taskDir: string; input: string }) {
549
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "observe-cost", { taskDir: options.taskDir, input: options.input });
550
+ });
551
+ addGlobalOptions(assignCmd.command("accept").description("Bind an exact witnessed commit and two critic PASS records"))
552
+ .requiredOption("--task-dir <path>", "Absolute path to the task directory")
553
+ .requiredOption("--commit <sha>", "Commit to accept")
554
+ .requiredOption("--record-dir <path>", "Absolute path to the writer record directory")
555
+ .requiredOption("--critic <path>", "Absolute path to a critic record directory; pass twice", (value: string, previous: string[]) => previous.concat(value), [] as string[])
556
+ .option("--observed-pr <id>", "Observed pull request id")
557
+ .option("--observed-ci <id>", "Observed CI run id")
558
+ .action(async function assignAcceptAction(this: Command, options: { taskDir: string; commit: string; recordDir: string; critic: string[]; observedPr?: string; observedCi?: string }) {
559
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "accept", {
560
+ taskDir: options.taskDir,
561
+ commit: options.commit,
562
+ recordDir: options.recordDir,
563
+ critics: options.critic,
564
+ ...(options.observedPr !== undefined ? { observedPr: options.observedPr } : {}),
565
+ ...(options.observedCi !== undefined ? { observedCi: options.observedCi } : {}),
566
+ });
567
+ });
568
+ addGlobalOptions(assignCmd.command("change-report").description("Report attempts, rework, costs, and retained verification history"))
569
+ .requiredOption("--task-dir <path>", "Absolute path to the task directory")
570
+ .action(async function assignChangeReportAction(this: Command, options: { taskDir: string }) {
571
+ result.code = await cmdAssign(runtimeFrom(ctx, this), "change-report", { taskDir: options.taskDir });
572
+ });
573
+
488
574
  addGlobalOptions(program.command("run").description("Create a KXM run without executing steps; follow its prerequisites, then kxm runs drive <runId> --wait")
489
575
  .argument("[workflow]", "Workflow id to run")
490
576
  .argument("[prompt...]", "Run prompt (events keep its hash; the full text is kept in a local 0600 sidecar file)")
@@ -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.120";
14
+ const VERSION = "0.7.122";
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,2 @@
1
+ export function unionChangelogUnreleased(ours: string, theirs: string): string;
2
+ export function unionLandedTracker(ours: string, theirs: string): string;