karajan-code 4.44.0 → 4.45.0
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/package.json +1 -1
- package/src/environment/contract-commit.js +13 -3
- package/src/orchestrator/drivers/post-loop.js +19 -0
- package/src/orchestrator/drivers/run-hu-batch.js +20 -1
- package/src/orchestrator/stages/acceptance-coverage-stage.js +47 -0
- package/src/prompts/acceptance-coverage.js +32 -0
- package/src/roles/acceptance-coverage-role.js +78 -0
- package/src/utils/update-check.js +24 -1
package/package.json
CHANGED
|
@@ -40,6 +40,8 @@ const runner = (projectDir, env) => (args) => execFileSync("git", ["-C", project
|
|
|
40
40
|
// or under a stack root (a fullstack monorepo is hardened on every side).
|
|
41
41
|
const isContract = (file) =>
|
|
42
42
|
CONTRACT_PATHS.some((p) => (p.endsWith("/") || p.endsWith("-") ? file.startsWith(p) : file === p)) || GENERATED_CONFIG_FILES.has(basename(file));
|
|
43
|
+
/** The supervisor's files: the hooks and their provenance, sealed by the human (ADR 0009). */
|
|
44
|
+
const isSupervisor = (file) => file.startsWith(".karajan/hooks/") || file === ".karajan/supervisor-provenance.json";
|
|
43
45
|
|
|
44
46
|
/** The contract files git sees as changed, with their porcelain code (`??` untracked). */
|
|
45
47
|
function contractStatus(git) {
|
|
@@ -70,9 +72,14 @@ const branchOf = (git) => { try { return git(["symbolic-ref", "--short", "HEAD"]
|
|
|
70
72
|
*/
|
|
71
73
|
export function commitContract({ projectDir, before = new Set(), baseBranch = "main", env = process.env }) {
|
|
72
74
|
const git = runner(projectDir, env);
|
|
73
|
-
const files = contractStatus(git).map(([file]) => file).filter((file) => !before.has(file)).sort();
|
|
74
|
-
if (files.length === 0) return { committed: false, reason: "nothing of the contract to commit" };
|
|
75
75
|
const history = hasCommits(git);
|
|
76
|
+
// KJC-BUG-0302: a sealed supervisor regenerated (tracked hooks or provenance
|
|
77
|
+
// that changed) is the seal's, a human act (ADR 0009), never the contract
|
|
78
|
+
// commit's: regenerated together after a version bump, the gate refused the one
|
|
79
|
+
// and the seal the other, and the person had to unstage the hook by hand. The
|
|
80
|
+
// first install (untracked hooks) still rides in the contract commit.
|
|
81
|
+
const files = contractStatus(git).filter(([file, code]) => !before.has(file) && !(history && code !== "??" && isSupervisor(file))).map(([file]) => file).sort();
|
|
82
|
+
if (files.length === 0) return { committed: false, reason: "nothing of the contract to commit" };
|
|
76
83
|
if (history && branchOf(git) === baseBranch) {
|
|
77
84
|
// kj never commits on the base branch, and will not switch the person's branch
|
|
78
85
|
// for them: the files are named so the commit can be made where it belongs.
|
|
@@ -83,7 +90,10 @@ export function commitContract({ projectDir, before = new Set(), baseBranch = "m
|
|
|
83
90
|
// --only: these paths and nothing else. What the person had staged stays staged.
|
|
84
91
|
git(["commit", "--only", "-m", history ? REGEN_MESSAGE : FRESH_MESSAGE, "--", ...files]);
|
|
85
92
|
} catch (err) {
|
|
86
|
-
|
|
93
|
+
// KJC-BUG-0302: the files stay staged, and the reason names the way to commit them.
|
|
94
|
+
const why = String(err.stderr || err.message).trim().split("\n")[0];
|
|
95
|
+
const quoted = files.map((f) => "'" + f.replaceAll("'", String.raw`'\''`) + "'").join(" ");
|
|
96
|
+
return { committed: false, files, reason: `git could not commit the contract: ${why}. Commit these generated files on a branch yourself: git add -- ${quoted} && kj review --staged && git commit -m "chore(kj): el contrato del método, generado por kj"` };
|
|
87
97
|
}
|
|
88
98
|
return { committed: true, files };
|
|
89
99
|
}
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
} from "../../session/mutators.js";
|
|
33
33
|
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
34
34
|
import { runTesterStage, runSecurityStage, runFinalAuditStage } from "../post-loop-stages.js";
|
|
35
|
+
import { runAcceptanceCoverageStage } from "../stages/acceptance-coverage-stage.js";
|
|
36
|
+
import { criteriaOfCard } from "../../roles/acceptance-coverage-role.js";
|
|
35
37
|
import { invokeSolomon } from "../solomon-escalation.js";
|
|
36
38
|
import { tryCiComment } from "../ci-integration.js";
|
|
37
39
|
import { getIntegration } from "../integrations.js";
|
|
@@ -103,6 +105,23 @@ export async function handlePostLoopStages({ config, session, emitter, eventBase
|
|
|
103
105
|
return { action: "continue" };
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
// KJC-TSK-0987 (#1894): with a card, each acceptance criterion is read against
|
|
109
|
+
// the diff by a different AI before the run is approved. An uncovered one goes
|
|
110
|
+
// back to the coder; a judge with no verdict stops the approval, never grants it.
|
|
111
|
+
const coverage = await runAcceptanceCoverageStage({
|
|
112
|
+
config, logger, emitter, eventBase, trackBudget, iteration: i, task,
|
|
113
|
+
criteria: criteriaOfCard(session.pg_card), diff: postLoopDiff,
|
|
114
|
+
});
|
|
115
|
+
if (coverage.stageResult) stageResults.acceptanceCoverage = coverage.stageResult;
|
|
116
|
+
if (coverage.action === "retry") {
|
|
117
|
+
setReviewerFeedback(session, coverage.feedback);
|
|
118
|
+
await saveSession(session);
|
|
119
|
+
return { action: "continue" };
|
|
120
|
+
}
|
|
121
|
+
if (coverage.action === "fail") {
|
|
122
|
+
return { action: "return", result: { approved: false, sessionId: session.id, reason: "acceptance_coverage_unavailable", error: coverage.stageResult.summary } };
|
|
123
|
+
}
|
|
124
|
+
|
|
106
125
|
return { action: "proceed" };
|
|
107
126
|
}
|
|
108
127
|
|
|
@@ -32,6 +32,9 @@ import { runRepair } from "../repair/repair-runner.js";
|
|
|
32
32
|
import { writeHistoryRecord } from "./post-loop.js";
|
|
33
33
|
import { setReviewerFeedback } from "../../session/mutators.js";
|
|
34
34
|
import { createHuSnapshot } from "../../git/hu-snapshot.js";
|
|
35
|
+
import { generateDiff } from "../../review/diff-generator.js";
|
|
36
|
+
import { runAcceptanceCoverageStage } from "../stages/acceptance-coverage-stage.js";
|
|
37
|
+
import { criteriaOfCard } from "../../roles/acceptance-coverage-role.js";
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
40
|
* @param {object} args
|
|
@@ -422,12 +425,28 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
422
425
|
// the host reads, not only in a warn line of the log.
|
|
423
426
|
const warnings = collectGitWarnings(subPipelineResult.results);
|
|
424
427
|
for (const w of warnings) logger.warn(`git automation: ${w}`);
|
|
428
|
+
// KJC-TSK-0987 (#1894): the HUs approved themselves against their own stories;
|
|
429
|
+
// the card's criteria are read against the diff of the whole run before the
|
|
430
|
+
// run says approved. Uncovered: approved:false with the list, for the host.
|
|
431
|
+
let coverage = { action: "skip" };
|
|
432
|
+
if (subPipelineResult.approved) {
|
|
433
|
+
const diff = await generateDiff({ baseRef: ctx.session.head_at_start || ctx.session.session_start_sha });
|
|
434
|
+
coverage = await runAcceptanceCoverageStage({
|
|
435
|
+
config: ctx.config, logger, emitter, eventBase: ctx.eventBase, trackBudget: ctx.trackBudget,
|
|
436
|
+
iteration: 0, task, criteria: criteriaOfCard(ctx.session.pg_card), diff,
|
|
437
|
+
});
|
|
438
|
+
if (coverage.stageResult) ctx.stageResults.acceptanceCoverage = coverage.stageResult;
|
|
439
|
+
}
|
|
440
|
+
const uncovered = coverage.action === "retry" ? { reason: "acceptance_criteria_uncovered", uncovered: coverage.stageResult.uncovered } : {};
|
|
441
|
+
const unavailable = coverage.action === "fail" ? { reason: "acceptance_coverage_unavailable", error: coverage.stageResult.summary } : {};
|
|
425
442
|
const finalResult = {
|
|
426
|
-
approved: subPipelineResult.approved,
|
|
443
|
+
approved: subPipelineResult.approved && coverage.action !== "retry" && coverage.action !== "fail",
|
|
427
444
|
sessionId: ctx.session.id,
|
|
428
445
|
huResults: subPipelineResult.results,
|
|
429
446
|
blockedIds: subPipelineResult.blockedIds,
|
|
430
447
|
planId: ctx.session._planRef?.planId || null,
|
|
448
|
+
...uncovered,
|
|
449
|
+
...unavailable,
|
|
431
450
|
...(warnings.length > 0 ? { warnings } : {})
|
|
432
451
|
};
|
|
433
452
|
await writeHistoryRecord({ sessionId: ctx.session.id, task, result: finalResult, logger });
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// KJC-TSK-0987 (issue #1894): the acceptance-coverage stage. With a card whose
|
|
2
|
+
// criteria the run was meant to implement, a different AI reads each criterion
|
|
3
|
+
// against the diff before the run is approved. No card or no criteria: the
|
|
4
|
+
// stage does not run and costs nothing. An unreadable verdict is the judge's
|
|
5
|
+
// failure, never an approval.
|
|
6
|
+
import { AcceptanceCoverageRole } from "../../roles/acceptance-coverage-role.js";
|
|
7
|
+
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
8
|
+
|
|
9
|
+
const STAGE = "acceptance-coverage";
|
|
10
|
+
|
|
11
|
+
/** The feedback the coder gets: the criteria with no evidence in the diff. */
|
|
12
|
+
export const uncoveredFeedback = (uncovered) => {
|
|
13
|
+
const lines = uncovered.map((c) => "- [" + c.index + "] " + c.text + (c.evidence ? " (judge: " + c.evidence + ")" : ""));
|
|
14
|
+
return "Acceptance criteria NOT covered by the diff (implement them, with tests that exercise them):\n" + lines.join("\n");
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @returns {Promise<{action: "skip"|"ok"|"retry"|"fail", stageResult?: object, feedback?: string}>}
|
|
19
|
+
*/
|
|
20
|
+
export async function runAcceptanceCoverageStage({ config, logger, emitter, eventBase, trackBudget, iteration, task, criteria, diff, RoleClass = AcceptanceCoverageRole }) {
|
|
21
|
+
if (!Array.isArray(criteria) || criteria.length === 0) return { action: "skip" };
|
|
22
|
+
logger.setContext?.({ iteration, stage: STAGE });
|
|
23
|
+
emitProgress(emitter, makeEvent(`${STAGE}:start`, { ...eventBase, stage: STAGE }, {
|
|
24
|
+
message: `Acceptance coverage: ${criteria.length} criteria against the diff`,
|
|
25
|
+
detail: { criteria: criteria.length, executorType: "agent" },
|
|
26
|
+
}));
|
|
27
|
+
const role = new RoleClass({ config, logger, emitter });
|
|
28
|
+
await role.init({ task, iteration });
|
|
29
|
+
const started = Date.now();
|
|
30
|
+
const output = await role.run({ task, criteria, diff });
|
|
31
|
+
const provider = output.result?.provider || role.resolveProvider();
|
|
32
|
+
trackBudget?.({ role: STAGE, provider, model: config?.roles?.[STAGE]?.model || null, result: output, duration_ms: Date.now() - started });
|
|
33
|
+
|
|
34
|
+
if (!output.ok) {
|
|
35
|
+
const stageResult = { ok: false, summary: output.summary || "acceptance-coverage: the judge gave no verdict", provider, error: output.result?.error || null };
|
|
36
|
+
emitProgress(emitter, makeEvent(`${STAGE}:end`, { ...eventBase, stage: STAGE }, { status: "fail", message: stageResult.summary, detail: stageResult }));
|
|
37
|
+
return { action: "fail", stageResult };
|
|
38
|
+
}
|
|
39
|
+
const { criteria: verdicts, uncovered } = output.result;
|
|
40
|
+
const stageResult = { ok: uncovered.length === 0, summary: output.summary, provider, criteria: verdicts, uncovered };
|
|
41
|
+
emitProgress(emitter, makeEvent(`${STAGE}:end`, { ...eventBase, stage: STAGE }, {
|
|
42
|
+
status: stageResult.ok ? "ok" : "fail", message: output.summary, detail: stageResult,
|
|
43
|
+
}));
|
|
44
|
+
if (stageResult.ok) return { action: "ok", stageResult };
|
|
45
|
+
logger.warn(`${output.summary} — sending them back to the coder`);
|
|
46
|
+
return { action: "retry", stageResult, feedback: uncoveredFeedback(uncovered) };
|
|
47
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// KJC-TSK-0987 (issue #1894): the prompt of the acceptance-coverage judge. A run
|
|
2
|
+
// ended approved with none of the card's three criteria implemented; this judge
|
|
3
|
+
// reads each criterion against the diff and says, one by one, whether it is
|
|
4
|
+
// covered and where.
|
|
5
|
+
|
|
6
|
+
const PREAMBLE = "IMPORTANT: You are running as a Karajan sub-agent. "
|
|
7
|
+
+ "Do NOT use any MCP tools, do NOT mention Karajan. "
|
|
8
|
+
+ "Your job is to judge whether a diff covers a card's acceptance criteria.";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {{criteria: string[], diff: string, task?: string, instructions?: string|null}} opts
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function buildAcceptanceCoveragePrompt({ criteria, diff, task = "", instructions = null }) {
|
|
15
|
+
const list = criteria.map((c, i) => `${i + 1}. ${c}`).join("\n");
|
|
16
|
+
const body = [
|
|
17
|
+
"You are the AcceptanceCoverage judge for Karajan Code.",
|
|
18
|
+
"Input: (a) the acceptance criteria of the task's card, numbered, and (b) the diff the run produced.",
|
|
19
|
+
"For EACH criterion decide whether the diff implements it. Covered means the behavior the",
|
|
20
|
+
"criterion describes exists in the diff (code, config or tests that exercise it), not that",
|
|
21
|
+
"something related was touched. Point to the evidence: file and what it does. A criterion",
|
|
22
|
+
"with no evidence in the diff is NOT covered, whatever the task description says.",
|
|
23
|
+
"Return a single valid JSON object and nothing else.",
|
|
24
|
+
'Schema: {"criteria":[{"index":number,"covered":boolean,"evidence":string}]}',
|
|
25
|
+
"One entry per criterion, in order, index starting at 1. Do not add or drop criteria.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
const sections = [PREAMBLE];
|
|
28
|
+
if (instructions) sections.push(instructions);
|
|
29
|
+
if (task) sections.push(`## Task\n${task}`);
|
|
30
|
+
sections.push(body, `## Acceptance criteria (${criteria.length})\n${list}`, `## Diff\n${diff || "(empty diff)"}`);
|
|
31
|
+
return sections.join("\n\n");
|
|
32
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// KJC-TSK-0987 (issue #1894): AcceptanceCoverageRole, the judge that reads each
|
|
2
|
+
// acceptance criterion of the card against the diff before the run is approved.
|
|
3
|
+
// Judged by a different AI than the coder (the reviewer's provider by default).
|
|
4
|
+
// Role + helpers ship here; the stage wires it (same PR), the loops next.
|
|
5
|
+
|
|
6
|
+
import { AgentRole } from "./agent-role.js";
|
|
7
|
+
import { buildAcceptanceCoveragePrompt } from "../prompts/acceptance-coverage.js";
|
|
8
|
+
import { extractFirstJson } from "../utils/json-extract.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The card's acceptance criteria as plain sentences, one per criterion.
|
|
12
|
+
* Structured Given/When/Then first; else the free text, one per line or bullet.
|
|
13
|
+
* @param {object|null} card
|
|
14
|
+
* @returns {string[]}
|
|
15
|
+
*/
|
|
16
|
+
export function criteriaOfCard(card) {
|
|
17
|
+
const structured = Array.isArray(card?.acceptanceCriteriaStructured) ? card.acceptanceCriteriaStructured : [];
|
|
18
|
+
const fromStructured = structured
|
|
19
|
+
.map((ac) => (ac?.given && ac?.when && ac?.then ? `Given ${ac.given}, when ${ac.when}, then ${ac.then}` : String(ac?.raw || "").trim()))
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
if (fromStructured.length > 0) return fromStructured;
|
|
22
|
+
return String(card?.acceptanceCriteria || "")
|
|
23
|
+
.split("\n")
|
|
24
|
+
.map((line) => line.replace(/^\s*(?:[-*•]|\d+[.)])\s*/, "").trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const normalizeVerdicts = (parsed, criteria) => criteria.map((text, i) => {
|
|
29
|
+
const v = Array.isArray(parsed?.criteria) ? parsed.criteria.find((c) => Number(c?.index) === i + 1) : null;
|
|
30
|
+
return {
|
|
31
|
+
index: i + 1,
|
|
32
|
+
text,
|
|
33
|
+
// No entry for a criterion is no evidence for it: not covered, never assumed.
|
|
34
|
+
covered: v ? Boolean(v.covered) : false,
|
|
35
|
+
evidence: typeof v?.evidence === "string" ? v.evidence.trim() : "",
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export class AcceptanceCoverageRole extends AgentRole {
|
|
40
|
+
constructor(opts) {
|
|
41
|
+
super({ ...opts, name: "acceptance-coverage" });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A different AI than the coder: the reviewer's, unless the role has its own.
|
|
45
|
+
resolveProvider() {
|
|
46
|
+
return this.config?.roles?.["acceptance-coverage"]?.provider
|
|
47
|
+
|| this.config?.roles?.reviewer?.provider
|
|
48
|
+
|| this.config?.reviewer
|
|
49
|
+
|| "codex";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
extractInput(input) {
|
|
53
|
+
return { ...super.extractInput(input), criteria: Array.isArray(input?.criteria) ? input.criteria : [], diff: String(input?.diff || "") };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async buildPrompt({ task, criteria, diff }) {
|
|
57
|
+
this._criteria = criteria;
|
|
58
|
+
return { prompt: buildAcceptanceCoveragePrompt({ criteria, diff, task, instructions: this.instructions }) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
parseOutput(raw) { return extractFirstJson(raw); }
|
|
62
|
+
|
|
63
|
+
// A verdict with no `criteria` array is not a verdict: the reviewer's failure.
|
|
64
|
+
isSuccessful(parsed) { return Array.isArray(parsed?.criteria); }
|
|
65
|
+
|
|
66
|
+
buildSuccessResult(parsed, provider) {
|
|
67
|
+
const criteria = normalizeVerdicts(parsed, this._criteria || []);
|
|
68
|
+
const uncovered = criteria.filter((c) => !c.covered);
|
|
69
|
+
return { criteria, uncovered, provider, error: Array.isArray(parsed?.criteria) ? undefined : "no criteria array in the verdict" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
buildSummary(parsed) {
|
|
73
|
+
if (!Array.isArray(parsed?.criteria)) return "acceptance-coverage: unreadable verdict (no criteria array)";
|
|
74
|
+
const total = (this._criteria || []).length;
|
|
75
|
+
const uncovered = normalizeVerdicts(parsed, this._criteria || []).filter((c) => !c.covered).length;
|
|
76
|
+
return uncovered === 0 ? `acceptance-coverage: ${total}/${total} criteria covered` : `acceptance-coverage: ${uncovered} of ${total} criteria NOT covered`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -3,8 +3,22 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { isSea } from "node:sea";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { getKarajanHome } from "./paths.js";
|
|
7
8
|
|
|
9
|
+
// KJC-BUG-0301: `kj update` on a kj linked from a source tree (npm link) said
|
|
10
|
+
// "Updating 4.44.0 → 4.43.0", installed the registry's older version globally
|
|
11
|
+
// and replaced the development link. Two facts decide now, before any install.
|
|
12
|
+
/** Whether this kj runs from a source tree (npm link), not from a node_modules install. */
|
|
13
|
+
export const isSourceTree = (selfPath = fileURLToPath(import.meta.url)) => !selfPath.split(path.sep).includes("node_modules");
|
|
14
|
+
const numeric = (v) => String(v).split(/[.-]/, 3).map((n) => Number.parseInt(n, 10) || 0);
|
|
15
|
+
/** Whether version `a` is strictly newer than `b` (major.minor.patch; a prerelease tag is ignored). */
|
|
16
|
+
export const isNewerVersion = (a, b) => {
|
|
17
|
+
const [x, y] = [numeric(a), numeric(b)];
|
|
18
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i];
|
|
19
|
+
return false;
|
|
20
|
+
};
|
|
21
|
+
|
|
8
22
|
const CACHE_FILE = "update-check.json";
|
|
9
23
|
// KJC-TSK-0690: 6h — kj can ship several releases a day; a session should
|
|
10
24
|
// not work a full day blind. KJ_NO_UPDATE_CHECK=1 opts out (CI).
|
|
@@ -183,7 +197,7 @@ export async function printUpdateNotice(currentVersion) {
|
|
|
183
197
|
* @param {typeof fetch} [opts.fetchFn] - injectable fetch (registry lookup + installer download)
|
|
184
198
|
* @returns {Promise<{ ok: boolean, alreadyLatest?: boolean, latest?: string, manual?: boolean }>}
|
|
185
199
|
*/
|
|
186
|
-
export async function performSelfUpdate({ currentVersion, exec, logger = console, channel, fetchFn = fetch } = {}) {
|
|
200
|
+
export async function performSelfUpdate({ currentVersion, exec, logger = console, channel, fetchFn = fetch, sourceTree = isSourceTree() } = {}) {
|
|
187
201
|
const run = exec || (async (cmd, args) => (await import("execa")).execa(cmd, args));
|
|
188
202
|
const ch = channel || detectInstallChannel();
|
|
189
203
|
logger.log(`Current version: ${currentVersion}`);
|
|
@@ -207,6 +221,15 @@ export async function performSelfUpdate({ currentVersion, exec, logger = console
|
|
|
207
221
|
logger.log(`Already on the latest version (${currentVersion}).`);
|
|
208
222
|
return { ok: true, alreadyLatest: true, latest };
|
|
209
223
|
}
|
|
224
|
+
// KJC-BUG-0301: never a downgrade, and never over a development link.
|
|
225
|
+
if (isNewerVersion(currentVersion, latest)) {
|
|
226
|
+
logger.log(`This kj (${currentVersion}) is newer than the registry's latest (${latest}): nothing to update.`);
|
|
227
|
+
return { ok: true, newerThanRegistry: true, latest };
|
|
228
|
+
}
|
|
229
|
+
if (ch === "npm" && sourceTree) {
|
|
230
|
+
logger.log(`Update available: ${currentVersion} → ${latest}, but this kj runs from a source tree (npm link) and kj update will not replace it. Pull or rebuild that tree, or unlink it first: npm unlink -g ${packageName()}`);
|
|
231
|
+
return { ok: true, latest, manual: true };
|
|
232
|
+
}
|
|
210
233
|
|
|
211
234
|
if (ch === "sea" && process.platform === "win32") {
|
|
212
235
|
// Executing a downloaded .ps1 from inside the binary is not supported
|