@kontextmind/kxm 0.7.124 → 0.7.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@
11
11
  "name": "kxm",
12
12
  "source": "./plugins/kxm",
13
13
  "description": "Durable workflows, peer agents, and kxm tui",
14
- "version": "0.7.124",
14
+ "version": "0.7.126",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
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,
package/docs/README.md CHANGED
@@ -114,6 +114,8 @@ KXM connects coding agents through a durable, authenticated [hub](glossary.md#hu
114
114
  | [Packages and workspaces](contributing/packages.md) | Maintainers | Workspace layout, Nx targets, Bun task running and the layer gate |
115
115
  | [KXM terminal components](contributing/tui-components.md) | Maintainers, integrators | The panel kit behind `kxm dash` |
116
116
  | [Repository work delivery skill](contributing/repo-work-delivery.md) | Contributors | The repository-local skill for delivering a change |
117
+ | [Operating rules](contributing/operating-rules.md) | Agents, maintainers | The operator's standing instructions, dated, and where each is enforced |
118
+ | [Learnings](contributing/learnings.md) | Agents, maintainers | Durable lessons from running the writer, landing, and docs loops |
117
119
  | [Artifact templates](templates/README.md) | Workflow authors | Document templates and where workflows use them |
118
120
 
119
121
  The templates: [feature](templates/feature.md) · [bug fix](templates/bug-fix.md) · [ADR](templates/adr.md) · [architecture](templates/architecture.md) · [research](templates/research.md) · [review](templates/review.md) · [test plan](templates/test-plan.md) · [test report](templates/test-report.md) · [runbook](templates/runbook.md) · [postmortem](templates/postmortem.md) · [handoff](templates/handoff.md).
@@ -23,6 +23,10 @@ Read from `plugins/kxm/src/cli.ts`, `scripts/kxm-hub.mjs`, `plugins/kxm/src/hub.
23
23
  | Workspace logs | `workspaceDirs` | `.kxm/logs`, or `KXM_LOGS_DIR` | log files | `plugins/kxm/src/cli/types.ts` |
24
24
  | Workspace assets | `workspaceDirs` | `.kxm/assets`, or `KXM_ASSETS_DIR` | asset files | `plugins/kxm/src/cli/types.ts` |
25
25
  | Workspace state | `workspaceDirs` | `.kxm/state`, or `KXM_STATE_DIR` | `kxm.db` when the hub uses this directory | `plugins/kxm/src/cli/types.ts` |
26
+ | Lane registry | `kxm lane create` | no listener | `lanes.json` under the workspace state dir | `plugins/kxm/src/cli/lanes.ts` |
27
+ | Landing | `kxm land` via `scripts/pr-land.mjs` | no listener | `land-release-context.json` and `land-phases-before.json` under `.kxm/logs` | `scripts/pr-land.mjs` |
28
+ | Assignment runner entry | `kxm assign <verb>` | no listener | none; the runner writes task records | `plugins/kxm/src/cli/assign.ts` |
29
+ | Docs site | `kxm docs serve` via `ops/docs-site/serve.py` | the tailnet IPv4 from `tailscale ip -4` and one port; refuses any other address | none; serves the built `site/` | `ops/docs-site/serve.py` |
26
30
 
27
31
  On macOS the user state root is `Library/Application Support/KXM` under the
28
32
  home directory (`kxmUserStateRoot` in `plugins/kxm/src/bindings.ts`). Windows
@@ -0,0 +1,105 @@
1
+ ---
2
+ title: "Learnings"
3
+ description: "Durable lessons from running the writer, critic, and landing loop on this repository. One entry per lesson, with the evidence and where it applies. Pruned when a lesson stops being true."
4
+ audience: "agents and maintainers"
5
+ updated: "2026-09-26"
6
+ ---
7
+
8
+ # Learnings
9
+
10
+ A lesson earns a line here when it would save a future session more than a
11
+ few minutes. Each entry: the lesson, the evidence, where it applies. An
12
+ entry whose fix has landed is deleted, not archived.
13
+
14
+ ## Dispatch and lanes
15
+
16
+ - **Branch every lane from the current `origin/main`, and rebase before
17
+ review, not after.** Three branches cut before #330 and #331 landed each
18
+ needed a hand rebase with the same additive conflicts (`cli.ts`
19
+ registrations, skill ownership lists, skill mirrors, changelog, dist).
20
+ Evidence: kxm-assign, docs-site, run-driver-timeouts on 2026-09-26.
21
+ Applies to: every brief; `kxm land`'s rebase stage only knows three
22
+ conflict unions.
23
+ - **After an additive rebase, check the skill frontmatter and heading
24
+ spacing.** Keeping both sides doubled a `description:` key in a SKILL.md
25
+ (strict-YAML test) and butted two `##` headings together (MD022).
26
+ Evidence: kxm-assign verify failures, 2026-09-26. Applies to: any
27
+ hand-resolved conflict in `plugins/kxm/skills/` or the CLI reference.
28
+ - **Never run more than two `npm run verify` at once on this machine.**
29
+ Six concurrent runs stretched a seven-minute gate past thirty minutes and
30
+ starved a landing. Evidence: 2026-09-26, 02:10 to 02:40. Applies to: the
31
+ supervisor tick and any batch of writers finishing together.
32
+ - **The transport's stdin read can fail with `EAGAIN` under concurrent
33
+ detached dispatch.** Retry once; the request itself is fine. Evidence:
34
+ two occurrences on 2026-09-26 (backlog S18). Applies to: `just impl-bg`
35
+ and `just review-cli` until they retire.
36
+ - **Do not launch a writer under the Bash tool's ten-minute background
37
+ cap.** It kills the harness mid-run. Use the detached recipe or
38
+ `kxm lane run`. Evidence: the first lane-cli dispatch, 2026-09-26.
39
+ - **Do not run a standalone `npm run verify` on a branch that `kxm land`
40
+ will land.** The `verify` stage runs it again, so the standalone run
41
+ only spends one of the two verify slots twice. Run the critics on the
42
+ writer's own verify evidence, then go straight to `kxm land`. Evidence:
43
+ docs-site, 2026-09-26. Applies to: every lane after its writer finishes.
44
+
45
+ ## Engine and runtime
46
+
47
+ - **The improvement loop is blind while writers bypass `kxm run`.** Every
48
+ writer this week ran through the harness runner (`just impl-bg` and the
49
+ critic recipes), which records no engine events, so `kxm improve report`
50
+ and `kxm routing report` both return zero records and no candidates.
51
+ Evidence: both reports on 2026-09-26 at 08:14 UTC list the engine store as
52
+ absent and telemetry as empty. Applies to: dispatch. Once the one-step
53
+ workflows land, dispatch through `kxm lane run --workflow implement-only`
54
+ so attempts land in the run-events store; until then the sixth-tick
55
+ improvement loop reports nothing by design.
56
+
57
+ - **A live agent step had a hard 120 second timeout with no configuration
58
+ path.** The supervisor built the one-shot producer without a timeout.
59
+ Fixed on the run-driver-timeouts branch (step `timeoutMs`, project
60
+ `limits.agentStepTimeoutMs`, default one hour). Delete this entry when
61
+ that lands.
62
+ - **A run with an unreconciled executing attempt could not be cancelled or
63
+ driven again**, and that survived a supervisor restart; deleting the
64
+ lane's Runtime project store was the only recovery. Fixed on the same
65
+ branch (`executing_unrecorded`, admission released on a handoff receipt).
66
+ Delete when it lands.
67
+ - **The engine sends `--reasoning-effort low` on a first attempt regardless
68
+ of the role's effort.** `engine.ts` hard-codes it. Open; covered by the
69
+ role alignment plan P2 and P6.
70
+ - **Template provenance is refused when the installed kxm moves ahead of
71
+ the stamped revision, and a project without the file validates as
72
+ ready.** Deleting the file is the sanctioned state until `kxm init` can
73
+ re-stamp. Evidence: every fresh lane on 2026-09-26 until #330 removed it.
74
+
75
+ ## `kxm land`
76
+
77
+ - **Auto-merge cannot be enabled on a PR that is already `CLEAN`**; the
78
+ mutation answers "clean status" and the REST squash merge is the path.
79
+ While CI is paused every PR is clean, so this is the normal path.
80
+ - **The Release workflow's run is titled `Release`, never the PR title.**
81
+ Match it by time after the Auto-Release run. Fixed on kxm-land-followup;
82
+ delete when it lands.
83
+ - **A verify failure inside `kxm land` shows only the child's last output
84
+ line.** Re-run verify by hand to see the cause until backlog S19 lands.
85
+
86
+ ## Reviews
87
+
88
+ - **A CLI critic reviewing a branch cut from an older base will report
89
+ main's later additions as deletions.** Say the base commit in the brief
90
+ and tell the critic to review against it. Evidence: docs-site review,
91
+ two of five findings were base artifacts.
92
+ - **Usage errors are Commander prose even under `--json` in every group.**
93
+ Repo-wide, one fix in `mapCommanderError`; on kxm-land-followup. Delete
94
+ when it lands.
95
+
96
+ ## omp research, kept for the alignment plan
97
+
98
+ - **omp's roster is `modelRoles` plus `retry.fallbackChains`**, walked on
99
+ provider errors with a revert policy; KXM's roster is a membership test
100
+ and never rotates. The passive `kxm.role.v2` draft already has the
101
+ better shape (routes with status and fallbacks); activate it rather than
102
+ invent a new v2. Source: `plans/research-omp-config-schema.md`.
103
+ - **omp has no workflow file.** Its `workflowz` is a prompt contract over
104
+ an eval kernel. Nothing to port; KXM's workflow file is the stronger
105
+ model.
@@ -0,0 +1,107 @@
1
+ ---
2
+ title: "Operating rules"
3
+ description: "Standing instructions from the operator that every agent session on this repository follows, with the date each was given. Read before planning, dispatching, landing, or editing the roadmap."
4
+ audience: "agents and maintainers"
5
+ updated: "2026-09-26"
6
+ ---
7
+
8
+ # Operating rules
9
+
10
+ These are the operator's standing instructions, recorded so a session does
11
+ not have to be told twice. Each rule names the date it was given and where
12
+ it is enforced. A rule that is retired is deleted, not kept beside its
13
+ replacement.
14
+
15
+ ## Roles and routes
16
+
17
+ - **The planner does not write product code.** Claude plans, reviews, and
18
+ lands. The default writer is the native Grok CLI (`grok-4.7`), a starting
19
+ rotation rather than a sole writer. If Grok is logged out, use a relief
20
+ route the tracker admits or stop and say what was hit. (`CLAUDE.md`,
21
+ standing.)
22
+ - **One writer per checkout.** Each unit of work runs in its own lane
23
+ worktree (`kxm lane create <unit>`). Two writers in one tree clobber each
24
+ other. (2026-09-25.)
25
+
26
+ ## Landing
27
+
28
+ - **Every PR is landed without asking.** Once a PR is open: monitor it,
29
+ rebase onto `main` when it falls behind, resolve conflicts and blockers,
30
+ merge, and follow through to the auto-release tag and the npm publish.
31
+ Report the outcome; do not stop to ask for the merge. Enforced by
32
+ `kxm land` (verify, docs, push, pr, rebase, unblock, merge, release,
33
+ milestone) and, until every stage is proven, by hand. (2026-09-26.)
34
+ - **Documentation is regenerated after a green pipeline and before the
35
+ merge.** The `docs` stage of `kxm land` runs the roadmap generator and
36
+ commits the regenerated pages on the branch. (2026-09-26.)
37
+ - **After every phase or milestone, the intense pass runs, not the
38
+ refresh.** `/reanalyze-roadmap` (the `kxm-roadmap-review` skill with its
39
+ read-only critic), triggered by the `milestone` stage reporting
40
+ `deep_review_required`. (2026-09-26.)
41
+ - **`npm run verify` stays the pre-push gate.** It is the first stage of
42
+ landing and is never replaced by it. CI's validate leg runs the same
43
+ script. (2026-09-26.)
44
+ - **After every publish, the workflow runs on the improvements it just
45
+ landed.** `kxm update --kxm`, then `kxm update --extensions`, then
46
+ `kxm plugin install --all`, so the CLI and the kxm plugin in pi, omp, and
47
+ Claude Code are current; the Claude Code session then needs
48
+ `/reload-plugins`, which only the operator can run. The supervisor tick
49
+ does this after any `PUBLISHED` line. (2026-09-26.)
50
+
51
+ ## Entry points
52
+
53
+ - **No new just recipes.** The repository is migrating off `just`. A needed
54
+ entry point is a `kxm` command: a group under `plugins/kxm/src/cli/`,
55
+ registered in `cli.ts`, owned by a bundled skill so `check:generated`
56
+ passes, documented in the CLI reference. Existing recipes retire as their
57
+ `kxm` verbs land; the transport recipes (`impl`, `plan`, `review-*`,
58
+ `impl-bg`) retire last, after one real unit has run through the one-step
59
+ workflows. (2026-09-26.)
60
+
61
+ ## Decisions and debt
62
+
63
+ - **Every option comes with pros and cons and two recommendations**, the
64
+ solid-product answer and the interim answer. (2026-09-26.)
65
+ - **Every shortcut goes on the backlog the same turn it is taken**, as an
66
+ item in `plans/backlog-shortcuts.md` with what was skipped and the proper
67
+ fix. (2026-09-26.)
68
+
69
+ ## Roadmap and plan content
70
+
71
+ - **No stale data.** A fact that no longer holds is replaced, never kept
72
+ beside its replacement.
73
+ - **History is brief.** One short line per material change, capped by the
74
+ generator; never a running narrative.
75
+ - **No stale contacts.** A contact carries a role and a confirmation date;
76
+ unconfirmed past 90 days is dropped. Empty is fine.
77
+ - **Present and upcoming only.** Superseded material leaves the roadmap.
78
+ - **Progressive detail.** A far-off phase or task is brief but complete
79
+ (goal, scope, done criterion, evidence needed) and gains detail as it
80
+ nears implementation. A task cannot be `ready` without a template and a
81
+ source.
82
+ - **Tasks use the artifact templates** under `docs/templates/`.
83
+
84
+ All six enforced by the `kxm.roadmap.v1` schema, the generator's semantic
85
+ refusals, and the two roadmap skills. (2026-09-26.)
86
+
87
+ ## Monitoring
88
+
89
+ - **Background monitors emit progress, not only results**, in this line
90
+ form: `[{lane}/{agent}]: {phrase}. {No action|Review needed}. -
91
+ {E}e|{D}d ({M}m{S}s)`. Routine lines are not echoed back in chat.
92
+ (2026-09-26, formatter at `~/.claude/scripts/evt-monitor.sh`.)
93
+ - **A supervisor tick runs every five minutes** while a session is open:
94
+ sweep the lanes, act on "Review needed", cap concurrent verifies at two,
95
+ regenerate the roadmap after a merge, and run the improvement loop every
96
+ sixth tick. Its prompt is checked in at
97
+ `plans/kxm-roadmap/supervisor-prompt.md`; a tick that finds the schedule
98
+ missing or expiring recreates it from that file in the same chat session,
99
+ never a new one, so the loop continues under the same task. (2026-09-26.)
100
+
101
+ ## Where the same rules live for agents
102
+
103
+ The session memory under `~/.claude/projects/…/memory/` mirrors these as
104
+ `pr-landing-autonomy`, `pipeline-docs-then-merge`, `no-just-prefer-kxm-cli`,
105
+ `decisions-pros-cons-backlog`, `roadmap-editorial-rules`, and
106
+ `monitor-progress-events`. This page is the checked-in copy; when the two
107
+ disagree, this page is updated and the memory follows.
@@ -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 Release run created after the merge, then polls `npm view @kontextmind/kxm@<version> version` every 30 seconds for 10 minutes. The success detail is `PUBLISHED <version>`. |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.124",
3
+ "version": "0.7.126",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "kxm",
4
4
  "displayName": "KXM",
5
- "version": "0.7.124",
5
+ "version": "0.7.126",
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",
@@ -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
- writeErr: (text) => ctx.io.stderr(text)
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 mapCommanderError(error) {
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
- if (USAGE_ERROR_CODES.has(error.code)) return 2;
52122
- return error.exitCode || 1;
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.124";
17316
+ var VERSION = "0.7.126";
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.124",
3
+ "version": "0.7.126",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -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");
@@ -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
- writeErr: (text) => ctx.io.stderr(text),
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 mapCommanderError(error: CommanderError): number {
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
- if (USAGE_ERROR_CODES.has(error.code)) return 2;
1440
- return error.exitCode || 1;
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.124";
14
+ const VERSION = "0.7.126";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();
@@ -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
- "gh pr create --body-file <path>",
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", branch, "--body-file", bodyFile]);
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 (Number.isFinite(after) && Number.isFinite(created) && created < after) return false;
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) => Date.parse(String(right.createdAt ?? "")) - Date.parse(String(left.createdAt ?? "")));
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 waitForWorkflow(workflow, title, after) {
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) return { done: false, detail: listed.result.stderr || listed.result.stdout };
804
- const match = selectRun(listed.value, title, after);
805
- if (!match) return { done: false };
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 release = waitForWorkflow("release.yml", title, after);
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) {