@kontextmind/kxm 0.7.124 → 0.7.125
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 +13 -0
- package/docs/reference/cli-reference.md +9 -5
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +59 -8
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/land.ts +2 -0
- package/plugins/kxm/src/cli.ts +62 -8
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/scripts/pr-land.mjs +68 -19
package/CHANGELOG.md
CHANGED
|
@@ -138,6 +138,11 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
138
138
|
|
|
139
139
|
### Changed
|
|
140
140
|
|
|
141
|
+
- **Usage errors under `--json` print a `usage_error` envelope and exit 2.**
|
|
142
|
+
A missing required option, unknown command, or other Commander usage error
|
|
143
|
+
writes `kxm.cli-result.v1` to stdout with `command`, `error`, and `detail`.
|
|
144
|
+
Text mode still prints the Commander line on stderr.
|
|
145
|
+
|
|
141
146
|
- **Windows Validate legs run again on GitHub-hosted `windows-latest`.** The PR gate
|
|
142
147
|
still requires only `Validate (linux, Node 22.19.0)` and `Validate (linux, Node 24)`
|
|
143
148
|
on ARC `kontextmind-doks`. Windows Node 22.19.0 and 24 report `validate:pr` but are
|
|
@@ -403,6 +408,14 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
403
408
|
|
|
404
409
|
### Fixed
|
|
405
410
|
|
|
411
|
+
- **`kxm land` names the pull request from the first commit subject and matches the Release run by time.**
|
|
412
|
+
`--title` sets the title; otherwise the subject of the first commit on the
|
|
413
|
+
branch is used, and a missing subject refuses `land_pr_title_missing`. The
|
|
414
|
+
title is never the branch name. The squash commit title stays `<title> (#n)`.
|
|
415
|
+
After Auto-Release succeeds, the Release run is the first `release.yml` run
|
|
416
|
+
created after that Auto-Release run, with no title filter. Both run ids are
|
|
417
|
+
recorded and printed, and each wait logs one JSON line every two minutes.
|
|
418
|
+
|
|
406
419
|
- **A local `kxm workflow add` writes only what the project loader accepts, where it reads
|
|
407
420
|
it.** Local scope needs a KXM project (`project_not_found` outside one, creating nothing)
|
|
408
421
|
and writes to the project root's `.kxm/workflows/` from any subdirectory. Before writing,
|
|
@@ -86,6 +86,7 @@ kxm -V
|
|
|
86
86
|
### JSON results
|
|
87
87
|
|
|
88
88
|
- Every JSON result carries a `schema` field. Most commands print `kxm.cli-result.v1` with `ok` and `command` fields plus command-specific keys.
|
|
89
|
+
- A usage error under `--json` (unknown command or option, missing argument, or missing required option) prints one `kxm.cli-result.v1` object on stdout and exits 2. `ok` is false, `error` is `usage_error`, `command` is the command path, and `detail` is the Commander message. Text mode still prints that message on stderr.
|
|
89
90
|
- Gate-style commands print a `kxm.worker-result.v1` envelope, which adds `worker`, `createdAt`, `outcome` (`passed`, `warning`, or `failed`), and `summary`: `gate validate`, `gate artifacts-exist`, `gate degrade`, `gate signal`, `workflow signal`, `gate github watch`, and `agent worker --dry-run`. Outside `--dry-run`, the `gate` subcommands and `workflow signal` also append the envelope to `.kxm/logs/telemetry.jsonl`.
|
|
90
91
|
- `session brief` prints `kxm.session-brief.v1` and `tenant status` prints `kxm.tenant-status.v1`.
|
|
91
92
|
- The `command` field is not always the words you typed: `hub stop` and `session stop` report `stop`, `session token` reports `auth token`, `routes admit` and `routes disable` report `routes admitted` and `routes disabled`, `models inventory-refresh` reports `models inventory refresh`, `workflow export` reports `retrospective export`, `gate degrade` reports `workflow degrade`, `gate signal` and `workflow signal` report `signal`, `gate github watch` reports `github watch`, `agent worker` reports `worker`, and `improve report` reports `improve`.
|
|
@@ -135,7 +136,7 @@ For this page, 63 `--dry-run` invocations (every command in the first list, the
|
|
|
135
136
|
|
|
136
137
|
Exit 1 covers an `ok: false` result, an unreachable hub for `hub view`, permission expansions for `trust check`, a stopped supervisor for `runtime status`, missing local state, and a planning-only `init`.
|
|
137
138
|
|
|
138
|
-
Exit 2 covers an unknown command or option, a missing argument or required option, a value KXM rejects before acting, `--workspace` where unsupported, conflicting flags, a group run without a subcommand, a removed command (`removed_command`), and `--dry-run` on a command that cannot plan (`dry_run_unsupported`).
|
|
139
|
+
Exit 2 covers an unknown command or option, a missing argument or required option, a value KXM rejects before acting, `--workspace` where unsupported, conflicting flags, a group run without a subcommand, a removed command (`removed_command`), and `--dry-run` on a command that cannot plan (`dry_run_unsupported`). With `--json`, Commander usage errors are `usage_error` envelopes on stdout and exit 2.
|
|
139
140
|
|
|
140
141
|
## Where commands read and write
|
|
141
142
|
|
|
@@ -1440,7 +1441,7 @@ Refusals (exit 1): `brief_unreadable`, `lane_unit_invalid`, `lane_exists`, `lane
|
|
|
1440
1441
|
Land the current branch. `npm run verify` is the first stage and is not replaced. The command spawns `scripts/pr-land.mjs` from the project root and streams one JSON line per stage. With no `--stage`, the stages run in order and stop at the first refusal. `--dry-run` prints each stage's plan and does not mutate. Squash is the only merge. A required review is reported and not bypassed.
|
|
1441
1442
|
|
|
1442
1443
|
```text
|
|
1443
|
-
kxm land [--pr <n>] [--stage <name>] [--body-file <path>]
|
|
1444
|
+
kxm land [--pr <n>] [--stage <name>] [--title <text>] [--body-file <path>]
|
|
1444
1445
|
```
|
|
1445
1446
|
|
|
1446
1447
|
```bash
|
|
@@ -1451,6 +1452,7 @@ kxm land --stage verify --dry-run
|
|
|
1451
1452
|
|---|---|---|---|
|
|
1452
1453
|
| `--pr` | `<n>` | the open pull request for this branch | Pull request number to reuse |
|
|
1453
1454
|
| `--stage` | `<name>` | all stages | One of `verify`, `docs`, `push`, `pr`, `rebase`, `unblock`, `merge`, `release`, `milestone` |
|
|
1455
|
+
| `--title` | `<text>` | subject of the first commit on the branch | Pull request title passed to `gh pr create`. Never the branch name |
|
|
1454
1456
|
| `--body-file` | `<path>` | none | Body file for `gh pr create`. Required when creating a pull request |
|
|
1455
1457
|
|
|
1456
1458
|
Outside a KXM project the command refuses `project_required` (exit 1). Unknown arguments and an unknown stage exit 2.
|
|
@@ -1462,14 +1464,16 @@ Outside a KXM project the command refuses `project_required` (exit 1). Unknown a
|
|
|
1462
1464
|
| `verify` | Refuses `land_dirty_tree` unless `git status --porcelain` is empty. Skips when `.kxm/logs/land-verify-<tree>.json` for `HEAD^{tree}` is younger than 30 minutes. Otherwise runs `npm run verify` and writes that receipt. |
|
|
1463
1465
|
| `docs` | Runs `plans/kxm-roadmap/update-dashboard.mjs` when that file exists. Commits changes under `docs/roadmap/`, `plans/kxm-roadmap/`, or `docs/architecture/` as `docs(roadmap): regenerate after verify`. Any other path, including `state.json`, is `land_docs_failed`. When the generator is absent the stage passes with `docs: skipped (generator absent)`. |
|
|
1464
1466
|
| `push` | `git push -u origin <branch>`. After a rebase in this run, the push uses `--force-with-lease`. |
|
|
1465
|
-
| `pr` | Reuses the branch's open pull request, or creates one with `gh pr create --body-file`. |
|
|
1467
|
+
| `pr` | Reuses the branch's open pull request, or creates one with `gh pr create --body-file`. The title is `--title` when that option is set; otherwise it is the subject of the first commit (`git log --reverse --format=%s origin/main..HEAD`). `--dry-run` prints that title. |
|
|
1466
1468
|
| `rebase` | When `mergeStateStatus` is `BEHIND` or `DIRTY`: fetch, rebase onto `origin/main`, and resolve only three conflicts (take `plugins/kxm/dist` from main and rebuild; union CHANGELOG Unreleased bullets, ours first; union the tracker "Landed in this tree" list, newest first). Runs `docs` again when the tree changed, re-verifies unless `git merge-tree --write-tree origin/main HEAD` was clean, and pushes with the lease. Five rounds, then `land_conflict_manual`. |
|
|
1467
1469
|
| `unblock` | Reads `statusCheckRollup` and `reviewDecision`. Reruns one failed check with `gh run rerun --failed`. A second failure or `REVIEW_REQUIRED` is `land_blocked`. |
|
|
1468
1470
|
| `merge` | Enables auto-merge with `enablePullRequestAutoMerge` and `mergeMethod: SQUASH`. When the response contains "clean status", squash-merges with `PUT /repos/{owner}/{repo}/pulls/{n}/merge` and commit title `<title> (#n)`. Polls `gh pr view --json state` every 30 seconds for up to 20 minutes. |
|
|
1469
|
-
| `release` | Records the newest `v*` tag before the merge, waits for the Auto-Release run whose title contains the pull request title, requires a newer tag from `git ls-remote --tags origin`, waits for the
|
|
1471
|
+
| `release` | Records the newest `v*` tag before the merge, waits for the Auto-Release run whose title contains the pull request title, requires a newer tag from `git ls-remote --tags origin`, then waits for the first `release.yml` run whose `createdAt` is after that Auto-Release run, with no title filter. Both run ids are written to `.kxm/logs/land-release-context.json` and printed in the stage detail (`PUBLISHED <version> auto-release <id> release <id>`). Each wait prints one JSON line every 2 minutes (`waiting auto-release.yml 4m`). The npm poll is `npm view @kontextmind/kxm@<version> version` every 30 seconds for 10 minutes. |
|
|
1470
1472
|
| `milestone` | Compares `plans/kxm-roadmap/state.json` from before and after `docs`. When a phase goes from an open task to all tasks `done`, or the pull request body contains a `Milestone:` line, prints `deep_review_required: true` and exits 0. The review is the `/reanalyze-roadmap` skill, not this command. An absent state file passes with skipped. |
|
|
1471
1473
|
|
|
1472
|
-
Refusals (exit 1): `project_required`, `land_dirty_tree`, `land_verify_failed`, `land_docs_failed`, `land_push_rejected`, `land_pr_body_missing`, `land_conflict_manual`, `land_blocked`, `land_merge_failed`, `land_release_failed`, `land_publish_timeout`, `land_milestone_failed`.
|
|
1474
|
+
Refusals (exit 1): `project_required`, `land_dirty_tree`, `land_verify_failed`, `land_docs_failed`, `land_push_rejected`, `land_pr_body_missing`, `land_pr_title_missing`, `land_conflict_manual`, `land_blocked`, `land_merge_failed`, `land_release_failed`, `land_publish_timeout`, `land_milestone_failed`.
|
|
1475
|
+
|
|
1476
|
+
`land_pr_title_missing`: the `pr` stage was not given `--title`, and the first commit subject on `origin/main..HEAD` is empty.
|
|
1473
1477
|
|
|
1474
1478
|
## `kxm assign`
|
|
1475
1479
|
|
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.125",
|
|
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
|
@@ -37185,6 +37185,7 @@ function cmdLand(runtime, options = {}) {
|
|
|
37185
37185
|
const args = [script];
|
|
37186
37186
|
if (options.pr) args.push("--pr", options.pr);
|
|
37187
37187
|
if (options.stage) args.push("--stage", options.stage);
|
|
37188
|
+
if (options.title) args.push("--title", options.title);
|
|
37188
37189
|
if (options.bodyFile) args.push("--body-file", options.bodyFile);
|
|
37189
37190
|
if (runtime.json) args.push("--json");
|
|
37190
37191
|
if (runtime.dryRun) args.push("--dry-run");
|
|
@@ -51581,7 +51582,7 @@ async function dispatchAgentCliCommand(runtime, toolName, rawArgs) {
|
|
|
51581
51582
|
}
|
|
51582
51583
|
}
|
|
51583
51584
|
}
|
|
51584
|
-
function createProgram(ctx, result) {
|
|
51585
|
+
function createProgram(ctx, result, argv) {
|
|
51585
51586
|
const bind = (action) => {
|
|
51586
51587
|
return async function commandAction(...args) {
|
|
51587
51588
|
const command = args.at(-1) instanceof Command ? args.at(-1) : this;
|
|
@@ -51591,7 +51592,11 @@ function createProgram(ctx, result) {
|
|
|
51591
51592
|
const program2 = new Command(CLI_NAME2);
|
|
51592
51593
|
program2.description("KXM local-first orchestration CLI").version(readInstalledKxmVersion(findKxmRepoRoot(import.meta.url)), "-V, --version", "Print the installed kxm version").exitOverride().configureOutput({
|
|
51593
51594
|
writeOut: (text) => ctx.io.stdout(text),
|
|
51594
|
-
|
|
51595
|
+
// Subcommands inherit this object by reference (copyInheritedSettings).
|
|
51596
|
+
writeErr: (text) => {
|
|
51597
|
+
if (hasJsonFlag(argv)) return;
|
|
51598
|
+
ctx.io.stderr(text);
|
|
51599
|
+
}
|
|
51595
51600
|
}).helpCommand("help", "Show help");
|
|
51596
51601
|
addGlobalOptions(program2);
|
|
51597
51602
|
program2.hook("preAction", (_program, actionCommand) => {
|
|
@@ -51636,10 +51641,11 @@ function createProgram(ctx, result) {
|
|
|
51636
51641
|
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {}
|
|
51637
51642
|
});
|
|
51638
51643
|
});
|
|
51639
|
-
addGlobalOptions(program2.command("land").description("Land the current branch: verify, regenerate docs, push, pull request, rebase, unblock, squash-merge, release, and milestone")).option("--pr <n>", "Existing pull request number").option("--stage <name>", "Run one stage: verify, docs, push, pr, rebase, unblock, merge, release, or milestone").option("--body-file <path>", "Pull request body file used when creating a pull request").action(async function landAction(options) {
|
|
51644
|
+
addGlobalOptions(program2.command("land").description("Land the current branch: verify, regenerate docs, push, pull request, rebase, unblock, squash-merge, release, and milestone")).option("--pr <n>", "Existing pull request number").option("--stage <name>", "Run one stage: verify, docs, push, pr, rebase, unblock, merge, release, or milestone").option("--title <text>", "Pull request title. Default is the subject of the first commit on the branch").option("--body-file <path>", "Pull request body file used when creating a pull request").action(async function landAction(options) {
|
|
51640
51645
|
result.code = await cmdLand(runtimeFrom(ctx, this), {
|
|
51641
51646
|
...options.pr !== void 0 ? { pr: options.pr } : {},
|
|
51642
51647
|
...options.stage !== void 0 ? { stage: options.stage } : {},
|
|
51648
|
+
...options.title !== void 0 ? { title: options.title } : {},
|
|
51643
51649
|
...options.bodyFile !== void 0 ? { bodyFile: options.bodyFile } : {}
|
|
51644
51650
|
});
|
|
51645
51651
|
});
|
|
@@ -52116,10 +52122,55 @@ function createProgram(ctx, result) {
|
|
|
52116
52122
|
});
|
|
52117
52123
|
return program2;
|
|
52118
52124
|
}
|
|
52119
|
-
function
|
|
52125
|
+
function hasJsonFlag(argv) {
|
|
52126
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
52127
|
+
const arg = argv[index];
|
|
52128
|
+
if (arg === "--") break;
|
|
52129
|
+
if (arg === "--json") return true;
|
|
52130
|
+
}
|
|
52131
|
+
return false;
|
|
52132
|
+
}
|
|
52133
|
+
function commandWords(argv) {
|
|
52134
|
+
const words = [];
|
|
52135
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
52136
|
+
const arg = argv[index];
|
|
52137
|
+
if (arg === "--") break;
|
|
52138
|
+
if (!arg.startsWith("-")) {
|
|
52139
|
+
words.push(arg);
|
|
52140
|
+
continue;
|
|
52141
|
+
}
|
|
52142
|
+
if (arg.includes("=") || arg === "--json" || arg === "--dry-run" || arg === "-h" || arg === "--help" || arg === "-V" || arg === "--version") continue;
|
|
52143
|
+
const next = argv[index + 1];
|
|
52144
|
+
if (next !== void 0 && !next.startsWith("-")) index += 1;
|
|
52145
|
+
}
|
|
52146
|
+
return words;
|
|
52147
|
+
}
|
|
52148
|
+
function commandPathFromArgv(program2, argv) {
|
|
52149
|
+
const names3 = [];
|
|
52150
|
+
let current = program2;
|
|
52151
|
+
for (const word of commandWords(argv)) {
|
|
52152
|
+
const next = current.commands.find((command) => command.name() === word || command.aliases().includes(word));
|
|
52153
|
+
if (!next) break;
|
|
52154
|
+
names3.push(next.name());
|
|
52155
|
+
current = next;
|
|
52156
|
+
}
|
|
52157
|
+
return names3.join(" ");
|
|
52158
|
+
}
|
|
52159
|
+
function mapCommanderError(error, argv, program2, io) {
|
|
52120
52160
|
if (error.exitCode === 0) return 0;
|
|
52121
|
-
|
|
52122
|
-
|
|
52161
|
+
const code = USAGE_ERROR_CODES.has(error.code) ? 2 : error.exitCode || 1;
|
|
52162
|
+
if (code === 2 && hasJsonFlag(argv)) {
|
|
52163
|
+
const command = commandPathFromArgv(program2, argv);
|
|
52164
|
+
io.stdout(`${JSON.stringify({
|
|
52165
|
+
schema: CLI_RESULT_SCHEMA,
|
|
52166
|
+
ok: false,
|
|
52167
|
+
command: command || "kxm",
|
|
52168
|
+
error: "usage_error",
|
|
52169
|
+
detail: error.message
|
|
52170
|
+
})}
|
|
52171
|
+
`);
|
|
52172
|
+
}
|
|
52173
|
+
return code;
|
|
52123
52174
|
}
|
|
52124
52175
|
var MESH_REMOVED_TEXT = "kxm mesh was removed. Use kxm init, kxm hub start|view|stop, and node scripts/smoke-multi-pi.mjs (KXM_SMOKE=1).";
|
|
52125
52176
|
function removedMeshInvocation(argv) {
|
|
@@ -52160,12 +52211,12 @@ async function runCli(argv, env = process.env, io = { stdout: (text) => process.
|
|
|
52160
52211
|
return 2;
|
|
52161
52212
|
}
|
|
52162
52213
|
const result = { code: 0 };
|
|
52163
|
-
const program2 = createProgram({ env, io, cwd }, result);
|
|
52214
|
+
const program2 = createProgram({ env, io, cwd }, result, argv);
|
|
52164
52215
|
try {
|
|
52165
52216
|
await program2.parseAsync(argv, { from: "user" });
|
|
52166
52217
|
return result.code;
|
|
52167
52218
|
} catch (error) {
|
|
52168
|
-
if (error instanceof CommanderError) return mapCommanderError(error);
|
|
52219
|
+
if (error instanceof CommanderError) return mapCommanderError(error, argv, program2, io);
|
|
52169
52220
|
if (error instanceof DryRunRefused) return error.code;
|
|
52170
52221
|
throw error;
|
|
52171
52222
|
}
|
|
@@ -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.125";
|
|
17317
17317
|
var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
17318
17318
|
var inbox = /* @__PURE__ */ new Map();
|
|
17319
17319
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
package/plugins/kxm/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import { print, type Runtime } from "./types.ts";
|
|
|
7
7
|
export interface LandOptions {
|
|
8
8
|
pr?: string;
|
|
9
9
|
stage?: string;
|
|
10
|
+
title?: string;
|
|
10
11
|
bodyFile?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -35,6 +36,7 @@ export function cmdLand(runtime: Runtime, options: LandOptions = {}): Promise<nu
|
|
|
35
36
|
const args = [script];
|
|
36
37
|
if (options.pr) args.push("--pr", options.pr);
|
|
37
38
|
if (options.stage) args.push("--stage", options.stage);
|
|
39
|
+
if (options.title) args.push("--title", options.title);
|
|
38
40
|
if (options.bodyFile) args.push("--body-file", options.bodyFile);
|
|
39
41
|
if (runtime.json) args.push("--json");
|
|
40
42
|
if (runtime.dryRun) args.push("--dry-run");
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { projectRuntimeOwnsRun } from "./runtime-store.ts";
|
|
|
31
31
|
|
|
32
32
|
// Submodule imports
|
|
33
33
|
import {
|
|
34
|
+
CLI_RESULT_SCHEMA,
|
|
34
35
|
print,
|
|
35
36
|
refuseDryRun,
|
|
36
37
|
runtimeFrom,
|
|
@@ -400,7 +401,7 @@ async function dispatchAgentCliCommand(
|
|
|
400
401
|
}
|
|
401
402
|
}
|
|
402
403
|
|
|
403
|
-
function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
404
|
+
function createProgram(ctx: CliContext, result: { code: number }, argv: readonly string[]): Command {
|
|
404
405
|
const bind = (action: (runtime: Runtime, ...args: never[]) => Promise<number>) => {
|
|
405
406
|
return async function commandAction(this: Command, ...args: unknown[]) {
|
|
406
407
|
const command = args.at(-1) instanceof Command ? args.at(-1) as Command : this;
|
|
@@ -415,7 +416,11 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
415
416
|
.exitOverride()
|
|
416
417
|
.configureOutput({
|
|
417
418
|
writeOut: (text) => ctx.io.stdout(text),
|
|
418
|
-
|
|
419
|
+
// Subcommands inherit this object by reference (copyInheritedSettings).
|
|
420
|
+
writeErr: (text) => {
|
|
421
|
+
if (hasJsonFlag(argv)) return;
|
|
422
|
+
ctx.io.stderr(text);
|
|
423
|
+
},
|
|
419
424
|
})
|
|
420
425
|
.helpCommand("help", "Show help");
|
|
421
426
|
addGlobalOptions(program);
|
|
@@ -496,11 +501,13 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
496
501
|
addGlobalOptions(program.command("land").description("Land the current branch: verify, regenerate docs, push, pull request, rebase, unblock, squash-merge, release, and milestone"))
|
|
497
502
|
.option("--pr <n>", "Existing pull request number")
|
|
498
503
|
.option("--stage <name>", "Run one stage: verify, docs, push, pr, rebase, unblock, merge, release, or milestone")
|
|
504
|
+
.option("--title <text>", "Pull request title. Default is the subject of the first commit on the branch")
|
|
499
505
|
.option("--body-file <path>", "Pull request body file used when creating a pull request")
|
|
500
|
-
.action(async function landAction(this: Command, options: { pr?: string; stage?: string; bodyFile?: string }) {
|
|
506
|
+
.action(async function landAction(this: Command, options: { pr?: string; stage?: string; title?: string; bodyFile?: string }) {
|
|
501
507
|
result.code = await cmdLand(runtimeFrom(ctx, this), {
|
|
502
508
|
...(options.pr !== undefined ? { pr: options.pr } : {}),
|
|
503
509
|
...(options.stage !== undefined ? { stage: options.stage } : {}),
|
|
510
|
+
...(options.title !== undefined ? { title: options.title } : {}),
|
|
504
511
|
...(options.bodyFile !== undefined ? { bodyFile: options.bodyFile } : {}),
|
|
505
512
|
});
|
|
506
513
|
});
|
|
@@ -1434,10 +1441,57 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
1434
1441
|
return program;
|
|
1435
1442
|
}
|
|
1436
1443
|
|
|
1437
|
-
function
|
|
1444
|
+
function hasJsonFlag(argv: readonly string[]): boolean {
|
|
1445
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1446
|
+
const arg = argv[index]!;
|
|
1447
|
+
if (arg === "--") break;
|
|
1448
|
+
if (arg === "--json") return true;
|
|
1449
|
+
}
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function commandWords(argv: readonly string[]): string[] {
|
|
1454
|
+
const words: string[] = [];
|
|
1455
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1456
|
+
const arg = argv[index]!;
|
|
1457
|
+
if (arg === "--") break;
|
|
1458
|
+
if (!arg.startsWith("-")) {
|
|
1459
|
+
words.push(arg);
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
if (arg.includes("=") || arg === "--json" || arg === "--dry-run" || arg === "-h" || arg === "--help" || arg === "-V" || arg === "--version") continue;
|
|
1463
|
+
const next = argv[index + 1];
|
|
1464
|
+
if (next !== undefined && !next.startsWith("-")) index += 1;
|
|
1465
|
+
}
|
|
1466
|
+
return words;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function commandPathFromArgv(program: Command, argv: readonly string[]): string {
|
|
1470
|
+
const names: string[] = [];
|
|
1471
|
+
let current: Command = program;
|
|
1472
|
+
for (const word of commandWords(argv)) {
|
|
1473
|
+
const next = current.commands.find((command) => command.name() === word || command.aliases().includes(word));
|
|
1474
|
+
if (!next) break;
|
|
1475
|
+
names.push(next.name());
|
|
1476
|
+
current = next;
|
|
1477
|
+
}
|
|
1478
|
+
return names.join(" ");
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function mapCommanderError(error: CommanderError, argv: readonly string[], program: Command, io: CliIo): number {
|
|
1438
1482
|
if (error.exitCode === 0) return 0;
|
|
1439
|
-
|
|
1440
|
-
|
|
1483
|
+
const code = USAGE_ERROR_CODES.has(error.code) ? 2 : (error.exitCode || 1);
|
|
1484
|
+
if (code === 2 && hasJsonFlag(argv)) {
|
|
1485
|
+
const command = commandPathFromArgv(program, argv);
|
|
1486
|
+
io.stdout(`${JSON.stringify({
|
|
1487
|
+
schema: CLI_RESULT_SCHEMA,
|
|
1488
|
+
ok: false,
|
|
1489
|
+
command: command || "kxm",
|
|
1490
|
+
error: "usage_error",
|
|
1491
|
+
detail: error.message,
|
|
1492
|
+
})}\n`);
|
|
1493
|
+
}
|
|
1494
|
+
return code;
|
|
1441
1495
|
}
|
|
1442
1496
|
|
|
1443
1497
|
const MESH_REMOVED_TEXT = "kxm mesh was removed. Use kxm init, kxm hub start|view|stop, and node scripts/smoke-multi-pi.mjs (KXM_SMOKE=1).";
|
|
@@ -1486,12 +1540,12 @@ export async function runCli(
|
|
|
1486
1540
|
return 2;
|
|
1487
1541
|
}
|
|
1488
1542
|
const result = { code: 0 };
|
|
1489
|
-
const program = createProgram({ env, io, cwd }, result);
|
|
1543
|
+
const program = createProgram({ env, io, cwd }, result, argv);
|
|
1490
1544
|
try {
|
|
1491
1545
|
await program.parseAsync(argv, { from: "user" });
|
|
1492
1546
|
return result.code;
|
|
1493
1547
|
} catch (error) {
|
|
1494
|
-
if (error instanceof CommanderError) return mapCommanderError(error);
|
|
1548
|
+
if (error instanceof CommanderError) return mapCommanderError(error, argv, program, io);
|
|
1495
1549
|
if (error instanceof DryRunRefused) return error.code;
|
|
1496
1550
|
throw error;
|
|
1497
1551
|
}
|
|
@@ -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.125";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
package/scripts/pr-land.mjs
CHANGED
|
@@ -28,6 +28,7 @@ const REBASE_ROUNDS = 5;
|
|
|
28
28
|
const POLL_MS = positiveInt(process.env.KXM_LAND_POLL_MS, 30_000);
|
|
29
29
|
const MERGE_WAIT_MS = 20 * 60 * 1000;
|
|
30
30
|
const RELEASE_WAIT_MS = 20 * 60 * 1000;
|
|
31
|
+
const RELEASE_PROGRESS_MS = 2 * 60 * 1000;
|
|
31
32
|
const PUBLISH_WAIT_MS = 10 * 60 * 1000;
|
|
32
33
|
const DOCS_COMMIT = "docs(roadmap): regenerate after verify";
|
|
33
34
|
const GENERATOR = "plans/kxm-roadmap/update-dashboard.mjs";
|
|
@@ -40,6 +41,7 @@ const UNRELEASED_HEADING = "## Unreleased";
|
|
|
40
41
|
const root = process.cwd();
|
|
41
42
|
let dryRun = false;
|
|
42
43
|
let bodyFile;
|
|
44
|
+
let requestedTitle;
|
|
43
45
|
let prNumber;
|
|
44
46
|
let forceLease = false;
|
|
45
47
|
let phasesSnapshotted = false;
|
|
@@ -198,13 +200,14 @@ function parseArgs(argv) {
|
|
|
198
200
|
options.help = true;
|
|
199
201
|
continue;
|
|
200
202
|
}
|
|
201
|
-
if (arg === "--pr" || arg === "--body-file" || arg === "--stage") {
|
|
203
|
+
if (arg === "--pr" || arg === "--body-file" || arg === "--stage" || arg === "--title") {
|
|
202
204
|
const value = argv[index + 1];
|
|
203
205
|
if (value === undefined || value.startsWith("--")) return { error: `${arg} requires a value` };
|
|
204
206
|
index += 1;
|
|
205
207
|
if (arg === "--pr") options.pr = value;
|
|
206
208
|
if (arg === "--body-file") options.bodyFile = value;
|
|
207
209
|
if (arg === "--stage") options.stage = value;
|
|
210
|
+
if (arg === "--title") options.title = value;
|
|
208
211
|
continue;
|
|
209
212
|
}
|
|
210
213
|
return { error: `unknown argument ${arg}` };
|
|
@@ -324,12 +327,24 @@ function pushStage() {
|
|
|
324
327
|
return pass("push", { detail: forceLease ? "pushed with lease" : "pushed" });
|
|
325
328
|
}
|
|
326
329
|
|
|
330
|
+
function pullRequestTitle() {
|
|
331
|
+
const explicit = String(requestedTitle ?? "").trim();
|
|
332
|
+
if (explicit) return { ok: true, text: explicit };
|
|
333
|
+
const logged = run("git", ["log", "--reverse", "--format=%s", "origin/main..HEAD"]);
|
|
334
|
+
if (logged.status !== 0) return { ok: false, detail: "no commit subject on origin/main..HEAD" };
|
|
335
|
+
const subject = logged.stdout.split("\n").map((line) => line.trim()).find(Boolean) ?? "";
|
|
336
|
+
if (!subject) return { ok: false, detail: "no commit subject on origin/main..HEAD" };
|
|
337
|
+
return { ok: true, text: subject };
|
|
338
|
+
}
|
|
339
|
+
|
|
327
340
|
function prStage() {
|
|
341
|
+
const titled = pullRequestTitle();
|
|
328
342
|
if (dryRun) {
|
|
343
|
+
if (!titled.ok) return refuse("pr", "land_pr_title_missing", titled.detail);
|
|
329
344
|
return plan("pr", [
|
|
330
345
|
"gh pr list --head <branch> --state open --json number,title",
|
|
331
|
-
|
|
332
|
-
]);
|
|
346
|
+
`gh pr create --title ${titled.text} --body-file <path>`,
|
|
347
|
+
], titled.text);
|
|
333
348
|
}
|
|
334
349
|
const found = ensurePr();
|
|
335
350
|
if (!found.ok) return refuse("pr", "land_pr_body_missing", found.detail);
|
|
@@ -337,12 +352,13 @@ function prStage() {
|
|
|
337
352
|
prNumber = String(found.pr.number);
|
|
338
353
|
return pass("pr", { detail: `reused #${prNumber}` });
|
|
339
354
|
}
|
|
355
|
+
if (!titled.ok) return refuse("pr", "land_pr_title_missing", titled.detail);
|
|
340
356
|
if (!bodyFile) return refuse("pr", "land_pr_body_missing", "creating a pull request requires --body-file");
|
|
341
357
|
if (!existsSync(bodyFile.startsWith("/") ? bodyFile : join(root, bodyFile))) {
|
|
342
358
|
return refuse("pr", "land_pr_body_missing", `body file not found: ${bodyFile}`);
|
|
343
359
|
}
|
|
344
360
|
const branch = found.branch || branchName();
|
|
345
|
-
const created = gh(["pr", "create", "--head", branch, "--title",
|
|
361
|
+
const created = gh(["pr", "create", "--head", branch, "--title", titled.text, "--body-file", bodyFile]);
|
|
346
362
|
if (created.status !== 0) return refuse("pr", "land_pr_body_missing", created.stderr || created.stdout);
|
|
347
363
|
const again = ensurePr();
|
|
348
364
|
if (!again.ok || !again.pr?.number) return refuse("pr", "land_pr_body_missing", "pull request was not created");
|
|
@@ -782,27 +798,54 @@ function mergeStage() {
|
|
|
782
798
|
return pass("merge", { detail: `squashed #${prNumber}` });
|
|
783
799
|
}
|
|
784
800
|
|
|
785
|
-
function selectRun(runs, title, after) {
|
|
801
|
+
function selectRun(runs, title, after, earliest = false) {
|
|
786
802
|
if (!Array.isArray(runs)) return undefined;
|
|
787
803
|
const matches = runs.filter((runRow) => {
|
|
788
804
|
if (!runRow || typeof runRow !== "object") return false;
|
|
789
805
|
const display = String(runRow.displayTitle ?? "");
|
|
790
|
-
if (title && !display.includes(title)) return false;
|
|
806
|
+
if (!earliest && title && !display.includes(title)) return false;
|
|
791
807
|
const created = Date.parse(String(runRow.createdAt ?? ""));
|
|
792
|
-
if (
|
|
808
|
+
if (earliest) {
|
|
809
|
+
if (!Number.isFinite(after) || !Number.isFinite(created) || created <= after) return false;
|
|
810
|
+
} else if (Number.isFinite(after) && Number.isFinite(created) && created < after) {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
793
813
|
return true;
|
|
794
814
|
});
|
|
795
|
-
matches.sort((left, right) =>
|
|
815
|
+
matches.sort((left, right) => {
|
|
816
|
+
const delta = Date.parse(String(left.createdAt ?? "")) - Date.parse(String(right.createdAt ?? ""));
|
|
817
|
+
return earliest ? delta : -delta;
|
|
818
|
+
});
|
|
796
819
|
return matches[0];
|
|
797
820
|
}
|
|
798
821
|
|
|
799
|
-
function
|
|
822
|
+
function workflowRunId(run) {
|
|
823
|
+
if (!run || typeof run !== "object" || run.databaseId === undefined || run.databaseId === null) return "";
|
|
824
|
+
return String(run.databaseId);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function noteReleaseWait(workflow, started, logged) {
|
|
828
|
+
const minutes = Math.floor((Date.now() - started) / RELEASE_PROGRESS_MS) * 2;
|
|
829
|
+
if (minutes < 2 || minutes <= logged.minutes) return;
|
|
830
|
+
logged.minutes = minutes;
|
|
831
|
+
emit({ stage: "release", ok: true, detail: `waiting ${workflow} ${minutes}m` });
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function waitForWorkflow(workflow, title, after, earliest = false) {
|
|
835
|
+
const started = Date.now();
|
|
836
|
+
const logged = { minutes: 0 };
|
|
800
837
|
let failure = "";
|
|
801
838
|
const polled = poll(RELEASE_WAIT_MS, () => {
|
|
802
|
-
const listed = ghJson(["run", "list", `--workflow=${workflow}`, "--json", "status,conclusion,displayTitle,createdAt"]);
|
|
803
|
-
if (!listed.ok)
|
|
804
|
-
|
|
805
|
-
|
|
839
|
+
const listed = ghJson(["run", "list", `--workflow=${workflow}`, "--json", "databaseId,status,conclusion,displayTitle,createdAt"]);
|
|
840
|
+
if (!listed.ok) {
|
|
841
|
+
noteReleaseWait(workflow, started, logged);
|
|
842
|
+
return { done: false, detail: listed.result.stderr || listed.result.stdout };
|
|
843
|
+
}
|
|
844
|
+
const match = selectRun(listed.value, title, after, earliest);
|
|
845
|
+
if (!match) {
|
|
846
|
+
noteReleaseWait(workflow, started, logged);
|
|
847
|
+
return { done: false };
|
|
848
|
+
}
|
|
806
849
|
const conclusion = String(match.conclusion ?? "").toLowerCase();
|
|
807
850
|
const status = String(match.status ?? "").toLowerCase();
|
|
808
851
|
if (conclusion === "success") return { done: true, run: match };
|
|
@@ -810,6 +853,7 @@ function waitForWorkflow(workflow, title, after) {
|
|
|
810
853
|
failure = `${workflow} concluded ${conclusion}`;
|
|
811
854
|
return { done: true, failed: true };
|
|
812
855
|
}
|
|
856
|
+
noteReleaseWait(workflow, started, logged);
|
|
813
857
|
return { done: false };
|
|
814
858
|
});
|
|
815
859
|
if (polled.failed) return { ok: false, detail: failure };
|
|
@@ -820,11 +864,11 @@ function waitForWorkflow(workflow, title, after) {
|
|
|
820
864
|
function releaseStage() {
|
|
821
865
|
if (dryRun) {
|
|
822
866
|
return plan("release", [
|
|
823
|
-
"gh run list --workflow=auto-release.yml --json status,conclusion,displayTitle,createdAt",
|
|
867
|
+
"gh run list --workflow=auto-release.yml --json databaseId,status,conclusion,displayTitle,createdAt",
|
|
824
868
|
"git ls-remote --tags origin",
|
|
825
|
-
"gh run list --workflow=release.yml --json status,conclusion,displayTitle,createdAt",
|
|
869
|
+
"gh run list --workflow=release.yml --json databaseId,status,conclusion,displayTitle,createdAt",
|
|
826
870
|
"npm view @kontextmind/kxm@<version> version",
|
|
827
|
-
], "npm poll is 10 minutes");
|
|
871
|
+
], "npm poll is 10 minutes; Release is matched by time after Auto-Release");
|
|
828
872
|
}
|
|
829
873
|
const context = readReleaseContext();
|
|
830
874
|
const found = ensurePr();
|
|
@@ -838,8 +882,12 @@ function releaseStage() {
|
|
|
838
882
|
if (!tags.tag || (previous && versionCompare(tags.tag, previous) <= 0)) {
|
|
839
883
|
return refuse("release", "land_release_failed", `tag ${tags.tag || "(none)"} is not newer than ${previous || "(none)"}`);
|
|
840
884
|
}
|
|
841
|
-
const
|
|
885
|
+
const autoCreated = Date.parse(String(auto.run?.createdAt ?? ""));
|
|
886
|
+
const release = waitForWorkflow("release.yml", "", autoCreated, true);
|
|
842
887
|
if (!release.ok) return refuse("release", "land_release_failed", release.detail);
|
|
888
|
+
const autoReleaseRunId = workflowRunId(auto.run);
|
|
889
|
+
const releaseRunId = workflowRunId(release.run);
|
|
890
|
+
writeReleaseContext({ autoReleaseRunId, releaseRunId });
|
|
843
891
|
const version = tags.tag.replace(/^v/, "");
|
|
844
892
|
const published = poll(PUBLISH_WAIT_MS, () => {
|
|
845
893
|
const viewed = run("npm", ["view", `@kontextmind/kxm@${version}`, "version"]);
|
|
@@ -847,7 +895,7 @@ function releaseStage() {
|
|
|
847
895
|
return { done: false };
|
|
848
896
|
});
|
|
849
897
|
if (!published.version) return refuse("release", "land_publish_timeout", `npm view @kontextmind/kxm@${version} version`);
|
|
850
|
-
return pass("release", { detail: `PUBLISHED ${published.version}` });
|
|
898
|
+
return pass("release", { detail: `PUBLISHED ${published.version} auto-release ${autoReleaseRunId} release ${releaseRunId}` });
|
|
851
899
|
}
|
|
852
900
|
|
|
853
901
|
function taskStatuses(tasks) {
|
|
@@ -950,12 +998,13 @@ function main() {
|
|
|
950
998
|
emit({
|
|
951
999
|
stage: "usage",
|
|
952
1000
|
ok: true,
|
|
953
|
-
detail: "node scripts/pr-land.mjs [--pr <n>] [--body-file <path>] [--stage <name>] [--json] [--dry-run]",
|
|
1001
|
+
detail: "node scripts/pr-land.mjs [--pr <n>] [--title <text>] [--body-file <path>] [--stage <name>] [--json] [--dry-run]",
|
|
954
1002
|
});
|
|
955
1003
|
return 0;
|
|
956
1004
|
}
|
|
957
1005
|
dryRun = parsed.options.dryRun;
|
|
958
1006
|
bodyFile = parsed.options.bodyFile;
|
|
1007
|
+
requestedTitle = parsed.options.title;
|
|
959
1008
|
prNumber = parsed.options.pr;
|
|
960
1009
|
const names = parsed.options.stage ? [parsed.options.stage] : STAGES;
|
|
961
1010
|
for (const name of names) {
|