@kontextmind/kxm 0.7.123 → 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/.kxm/workflows/implement-only.yaml +20 -0
- package/.kxm/workflows/review-arch-only.yaml +20 -0
- package/.kxm/workflows/review-cli-only.yaml +20 -0
- package/CHANGELOG.md +33 -3
- package/docs/contributing/assignment-runner.md +15 -4
- package/docs/reference/cli-reference.md +10 -6
- package/docs/reference/config-reference.md +2 -1
- package/docs/reference/workflow-catalog.md +10 -0
- package/docs/reference/workflow-definitions.md +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +83 -13
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +209 -72
- package/plugins/kxm/dist/runtime.js +233 -94
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/land.ts +2 -0
- package/plugins/kxm/src/cli/project.ts +14 -2
- package/plugins/kxm/src/cli.ts +62 -8
- package/plugins/kxm/src/engine-fold.ts +15 -7
- package/plugins/kxm/src/engine-plan.ts +10 -1
- package/plugins/kxm/src/engine.ts +105 -2
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/oneshot-evidence.ts +44 -0
- package/plugins/kxm/src/oneshot-producer.ts +5 -5
- package/plugins/kxm/src/runtime-service.ts +10 -0
- package/plugins/kxm/src/runtime-store.ts +1 -0
- package/plugins/kxm/src/runtime-supervisor.ts +2 -0
- package/schemas/drive-receipt.schema.json +1 -0
- package/schemas/project.schema.json +2 -1
- package/scripts/pr-land.mjs +68 -19
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) {
|