karajan-code 4.66.0 → 4.67.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/commands/harden.js +1 -1
- package/src/git/automation.js +25 -4
- package/src/harden/harness-hooks.js +16 -13
- package/src/harden/hook-commands.js +30 -3
- package/src/orchestrator/drivers/init-context.js +2 -2
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +4 -0
- package/src/orchestrator/flow-runner.js +4 -3
- package/src/orchestrator/stages/format-stage.js +61 -0
- package/src/review/pr-requirements.js +33 -4
- package/src/session/mutators.js +5 -0
package/package.json
CHANGED
package/src/commands/harden.js
CHANGED
|
@@ -44,7 +44,7 @@ const JS_TEST_CMD = {
|
|
|
44
44
|
*/
|
|
45
45
|
export async function resolveCmds(projectDir, language) {
|
|
46
46
|
const lang = language ?? detectStackRoots(projectDir)[0]?.language ?? null;
|
|
47
|
-
const cmds =
|
|
47
|
+
const cmds = commandsForLanguage(lang, projectDir);
|
|
48
48
|
if (lang !== "javascript" && lang !== "typescript") return cmds;
|
|
49
49
|
|
|
50
50
|
const pkgPath = join(projectDir, "package.json");
|
package/src/git/automation.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { addCheckpoint } from "../session/store.js";
|
|
7
|
+
import { setGitPreexisting } from "../session/mutators.js";
|
|
7
8
|
import { pipelineSonarBlock, stampStagedVerdict } from "../review/verdict-store.js";
|
|
8
9
|
import { branchSize } from "../commands/pr-size.js";
|
|
9
10
|
import { pushRemedy } from "./hu-automation.js";
|
|
@@ -80,7 +81,11 @@ function prefixForTaskType(taskType) {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
export function commitMessageFromTask(task, taskType = null) {
|
|
83
|
-
|
|
84
|
+
// KJC-BUG-0413 (#2565): the header comes from the first paragraph, without
|
|
85
|
+
// its leading Markdown marks; the whole task made "feat: # Roadmap ... Context: ...".
|
|
86
|
+
const firstParagraph = String(task || "").trim().split(/\n[ \t]*\r?\n/)[0];
|
|
87
|
+
const clean = firstParagraph
|
|
88
|
+
.replace(/^[#>*\-\s]+/, "")
|
|
84
89
|
.replaceAll(/\s+/g, " ")
|
|
85
90
|
.trim();
|
|
86
91
|
const prefix = prefixForTaskType(taskType);
|
|
@@ -119,9 +124,22 @@ async function checkoutRunBranch(created) {
|
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
export async function prepareGitAutomation({ config, task, logger, session }) {
|
|
127
|
+
export async function prepareGitAutomation({ config, task, logger, session, preexisting: resumedBaseline = null }) {
|
|
123
128
|
const enabled = config.git.auto_commit || config.git.auto_push || config.git.auto_pr;
|
|
124
|
-
if (!enabled)
|
|
129
|
+
if (!enabled) {
|
|
130
|
+
// KJC-TSK-1237: the format stage needs the baseline also when kj does not
|
|
131
|
+
// commit; outside git there is none, and that stage says so.
|
|
132
|
+
let baseline = resumedBaseline;
|
|
133
|
+
if (!baseline) {
|
|
134
|
+
try {
|
|
135
|
+
baseline = await listPendingPaths();
|
|
136
|
+
} catch {
|
|
137
|
+
baseline = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (session && Array.isArray(baseline)) setGitPreexisting(session, baseline);
|
|
141
|
+
return { enabled: false };
|
|
142
|
+
}
|
|
125
143
|
|
|
126
144
|
if (!(await ensureGitRepo())) {
|
|
127
145
|
throw new Error("Git automation requested but current directory is not a git repository");
|
|
@@ -147,7 +165,10 @@ export async function prepareGitAutomation({ config, task, logger, session }) {
|
|
|
147
165
|
// pipeline commits only what it changes. Both names of a pending rename are
|
|
148
166
|
// kept. Fails closed: with no answer from git the run stops here rather than
|
|
149
167
|
// commit the person's pending changes later.
|
|
150
|
-
|
|
168
|
+
// KJC-BUG-0412 (#2564): a resumed run keeps the baseline of the session it
|
|
169
|
+
// resumes; the tree now also holds that session's own work.
|
|
170
|
+
const preexisting = resumedBaseline ?? await listPendingPaths();
|
|
171
|
+
if (session) setGitPreexisting(session, preexisting);
|
|
151
172
|
const repoHasCommits = await hasCommits();
|
|
152
173
|
|
|
153
174
|
// New repo without commits: create branch directly (no fetch/sync possible)
|
|
@@ -50,15 +50,16 @@ export function guardCommand(guard, { blocking = false, viaKj = isSea() || !kjHo
|
|
|
50
50
|
return `if command -v ${bin} >/dev/null 2>&1; then ${run}; else ${missing}${blocking ? "; exit 2" : ""}; fi`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
53
|
+
/**
|
|
54
|
+
* Drop the commands that ran this guard some other way: node on the project's copy
|
|
55
|
+
* (before ADR 0030) or the other runner (KJC-BUG-0403: switching runner must not
|
|
56
|
+
* leave the guard wired twice).
|
|
57
|
+
*/
|
|
58
|
+
const withoutStale = (list, stale) => list.flatMap((e) => {
|
|
59
|
+
if (!Array.isArray(e?.hooks) || !e.hooks.some((h) => stale.includes(h?.command))) return [e];
|
|
60
|
+
const hooks = e.hooks.filter((h) => !stale.includes(h?.command));
|
|
61
|
+
return hooks.length ? [{ ...e, hooks }] : [];
|
|
62
|
+
});
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
65
|
* KJC-TSK-1206 (ADR 0030): the wiring is what decides which guard runs, so it is
|
|
@@ -98,7 +99,7 @@ export function wiringState({ projectDir, entries }) {
|
|
|
98
99
|
* basename; an entry is present when that script already appears under the
|
|
99
100
|
* same event (and matcher, when given).
|
|
100
101
|
*/
|
|
101
|
-
export function mergeClaudeHooks({ projectDir, logger = console, entries }) {
|
|
102
|
+
export function mergeClaudeHooks({ projectDir, logger = console, entries, viaKj = isSea() || !kjHookOnPath() }) {
|
|
102
103
|
const settingsPath = join(projectDir, ".claude", "settings.json");
|
|
103
104
|
let settings = {};
|
|
104
105
|
if (existsSync(settingsPath)) {
|
|
@@ -116,10 +117,12 @@ export function mergeClaudeHooks({ projectDir, logger = console, entries }) {
|
|
|
116
117
|
return { wired: false };
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
|
-
const viaKj = isSea() || !kjHookOnPath();
|
|
120
120
|
for (const { event, matcher, script } of entries) {
|
|
121
|
-
const
|
|
122
|
-
const
|
|
121
|
+
const guard = script.replace(/\.mjs$/, "");
|
|
122
|
+
const blocking = event === "PreToolUse";
|
|
123
|
+
const stale = [`node ${join(".karajan", "harness", script)}`, guardCommand(guard, { blocking, viaKj: !viaKj })];
|
|
124
|
+
const list = withoutStale(Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [], stale);
|
|
125
|
+
const command = guardCommand(guard, { blocking, viaKj });
|
|
123
126
|
const present = list.some(
|
|
124
127
|
(e) => (e?.hooks || []).some((h) => h?.command === command) && (matcher === undefined || e?.matcher === matcher),
|
|
125
128
|
);
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* commands depend on what the project defines). Unknown language ⇒ no commands,
|
|
9
9
|
* leaving only the universal POSIX-shell checks.
|
|
10
10
|
*/
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
// KJC-BUG-0411 (#2563): uv and poetry keep pytest and ruff in the project's
|
|
15
|
+
// venv, where a bare command exits 127; their manager runs them.
|
|
16
|
+
const PYTHON_RUNNERS = [["uv.lock", "uv run"], ["poetry.lock", "poetry run"]];
|
|
11
17
|
|
|
12
18
|
export const COMMANDS_BY_LANGUAGE = {
|
|
13
19
|
python: { lint: "ruff check .", format: "ruff format --check .", test: "pytest" },
|
|
@@ -19,7 +25,28 @@ export const COMMANDS_BY_LANGUAGE = {
|
|
|
19
25
|
},
|
|
20
26
|
};
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
// KJC-TSK-1237 (#2565): the formatter in write mode, run on the files of a session.
|
|
29
|
+
const FORMAT_WRITE_BY_LANGUAGE = {
|
|
30
|
+
python: { command: "ruff format", extensions: [".py", ".pyi"] },
|
|
31
|
+
go: { command: "gofmt -w", extensions: [".go"] },
|
|
32
|
+
php: { command: "vendor/bin/php-cs-fixer fix", extensions: [".php"] },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const runnerFor = (language, projectDir) => (language === "python" && projectDir
|
|
36
|
+
? PYTHON_RUNNERS.find(([lock]) => existsSync(join(projectDir, lock)))?.[1] ?? null
|
|
37
|
+
: null);
|
|
38
|
+
|
|
39
|
+
/** Native commands for a language, or {} when JS/TS or unknown; python ones through uv/poetry when the project uses them. */
|
|
40
|
+
export function commandsForLanguage(language, projectDir = null) {
|
|
41
|
+
const cmds = { ...COMMANDS_BY_LANGUAGE[language] };
|
|
42
|
+
const runner = runnerFor(language, projectDir);
|
|
43
|
+
if (runner) for (const [step, command] of Object.entries(cmds)) cmds[step] = `${runner} ${command}`;
|
|
44
|
+
return cmds;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The write-mode formatter of a language and the extensions it takes, or null. */
|
|
48
|
+
export function formatWriteFor(language, projectDir = null) {
|
|
49
|
+
const entry = FORMAT_WRITE_BY_LANGUAGE[language];
|
|
50
|
+
if (!entry) return null;
|
|
51
|
+
return { ...entry, command: [runnerFor(language, projectDir), entry.command].filter(Boolean).join(" ") };
|
|
25
52
|
}
|
|
@@ -47,7 +47,7 @@ export function adoptPreLoopConfig(ctx, updatedConfig) {
|
|
|
47
47
|
ctx.coderRoleInstance.config = updatedConfig;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
export async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
|
|
50
|
+
export async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags, preexisting = null }) {
|
|
51
51
|
// Auto-init .karajan/ if missing (copies coder-rules, review-rules, role templates)
|
|
52
52
|
const initProjectDir = config.projectDir || process.cwd();
|
|
53
53
|
await autoInit(initProjectDir, logger);
|
|
@@ -229,7 +229,7 @@ export async function initFlowContext({ task, config, logger, emitter, askQuesti
|
|
|
229
229
|
ctx.journalDecisions = [];
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session });
|
|
232
|
+
ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session, preexisting });
|
|
233
233
|
const projectDir = ctx.config.projectDir || process.cwd();
|
|
234
234
|
ctx.reviewRules = (await resolveReviewProfile({ mode: ctx.config.review_mode, projectDir })).rules;
|
|
235
235
|
await ctx.coderRoleInstance.init();
|
|
@@ -23,9 +23,13 @@ import { runPerfStage } from "../../stages/perf-stage.js";
|
|
|
23
23
|
import { runTddDisciplineStage } from "../../stages/tdd-discipline-stage.js";
|
|
24
24
|
import { runSuiteGreenStage, suiteGreenRequired } from "../../stages/suite-green-stage.js";
|
|
25
25
|
import { runToolJudgeStage } from "../../stages/tool-judge-stage.js";
|
|
26
|
+
import { runFormatStage } from "../../stages/format-stage.js";
|
|
26
27
|
import { tryCiComment } from "../../ci-integration.js";
|
|
27
28
|
|
|
28
29
|
export async function runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults, coderRole, pipelineFlags, brainCtx }) {
|
|
30
|
+
// KJC-TSK-1237 (#2565): the coder's files reach every gate formatted.
|
|
31
|
+
await runFormatStage({ config, logger, emitter, eventBase, session });
|
|
32
|
+
|
|
29
33
|
const tddResult = await runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration: i, askQuestion, task, brainCtx });
|
|
30
34
|
if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
|
|
31
35
|
if (tddResult.action === "continue") return { action: "continue" };
|
|
@@ -112,7 +112,7 @@ export async function runFlow(opts) {
|
|
|
112
112
|
return withRunContext({}, () => _runFlowInner(opts));
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
async function _runFlowInner({ task, config, logger, flags = {}, emitter = null, askQuestion = null, pgTaskId = null, pgProject = null }) {
|
|
115
|
+
async function _runFlowInner({ task, config, logger, flags = {}, emitter = null, askQuestion = null, pgTaskId = null, pgProject = null, preexisting = null }) {
|
|
116
116
|
// Defensive test-harness resolution for callers that bypass loadConfig()
|
|
117
117
|
// (orchestrator unit tests that build raw config objects). Production
|
|
118
118
|
// callers go through src/config/loader.js → loadConfig which already
|
|
@@ -153,7 +153,7 @@ async function _runFlowInner({ task, config, logger, flags = {}, emitter = null,
|
|
|
153
153
|
|
|
154
154
|
let ctx;
|
|
155
155
|
try {
|
|
156
|
-
ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
|
|
156
|
+
ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags, preexisting });
|
|
157
157
|
} catch (initError) {
|
|
158
158
|
// Pre-loop / preflight failures are CONFIG problems, not pipeline
|
|
159
159
|
// disputes. Pre-v2.7.5 we escalated them to Solomon, which:
|
|
@@ -356,7 +356,8 @@ export async function resumeFlow({ sessionId, answer, config, logger, flags = {}
|
|
|
356
356
|
|
|
357
357
|
// Re-run the flow with the existing session context
|
|
358
358
|
try {
|
|
359
|
-
|
|
359
|
+
// KJC-BUG-0412: what was pending is what was pending when THIS session started.
|
|
360
|
+
return await runFlow({ task, config: sessionConfig, logger, flags, emitter, askQuestion, preexisting: session.git_preexisting ?? null });
|
|
360
361
|
} catch (err) {
|
|
361
362
|
await markSessionStatus(session, "failed");
|
|
362
363
|
throw err;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-1237 (#2565): no stage formatted the coder's files, so a pre-commit
|
|
3
|
+
* format check aborted the final commit after every gate was green, and a
|
|
4
|
+
* one-command fix reached Solomon. This stage runs the project's formatter in
|
|
5
|
+
* write mode (the one kj harden knows) on the files of this session only:
|
|
6
|
+
* pending now and not pending when the session started. It is not a gate: a
|
|
7
|
+
* failing or missing formatter is said and the next stages judge the files.
|
|
8
|
+
* Opt-out: `development.format_after_coder: false`.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
11
|
+
import { extname, join } from "node:path";
|
|
12
|
+
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
13
|
+
import { runCommand } from "../../utils/process.js";
|
|
14
|
+
import { listPendingPaths } from "../../utils/git.js";
|
|
15
|
+
import { detectStackRoots } from "../../harden/stack-roots.js";
|
|
16
|
+
import { formatWriteFor } from "../../harden/hook-commands.js";
|
|
17
|
+
|
|
18
|
+
const TIMEOUT_MS = 2 * 60 * 1000;
|
|
19
|
+
const primaryLanguage = (projectDir) => detectStackRoots(projectDir)[0]?.language ?? null;
|
|
20
|
+
|
|
21
|
+
const expand = (projectDir, p) => (p.endsWith("/")
|
|
22
|
+
? readdirSync(join(projectDir, p), { recursive: true }).map((f) => join(p, f))
|
|
23
|
+
: [p]);
|
|
24
|
+
|
|
25
|
+
/** Files of the session the formatter takes; git names a new directory, so its files are listed. */
|
|
26
|
+
function filesToFormat({ pending, preexisting, extensions, projectDir }) {
|
|
27
|
+
const before = new Set(preexisting);
|
|
28
|
+
return pending.filter((p) => !before.has(p) && existsSync(join(projectDir, p)))
|
|
29
|
+
.flatMap((p) => expand(projectDir, p))
|
|
30
|
+
.filter((p) => extensions.includes(extname(p)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @returns {Promise<{ran: boolean, ok?: boolean, files?: string[]}>} */
|
|
34
|
+
export async function runFormatStage({ config, logger, emitter, eventBase, session, detectLanguageImpl = primaryLanguage, listPendingImpl = (dir) => listPendingPaths(dir), runImpl = async (...args) => runCommand(...args) }) {
|
|
35
|
+
if (config?.development?.format_after_coder === false) return { ran: false };
|
|
36
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
37
|
+
const formatter = formatWriteFor(detectLanguageImpl(projectDir), projectDir);
|
|
38
|
+
if (!formatter) return { ran: false };
|
|
39
|
+
const preexisting = session?.git_preexisting;
|
|
40
|
+
if (!Array.isArray(preexisting)) {
|
|
41
|
+
logger?.warn?.("format: no baseline of what was pending when the session started: nothing formatted");
|
|
42
|
+
return { ran: false };
|
|
43
|
+
}
|
|
44
|
+
let files;
|
|
45
|
+
try {
|
|
46
|
+
files = filesToFormat({ pending: await listPendingImpl(projectDir), preexisting, extensions: formatter.extensions, projectDir });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
logger?.warn?.(`format: the session's files could not be listed (${err.message}): nothing formatted`);
|
|
49
|
+
return { ran: false };
|
|
50
|
+
}
|
|
51
|
+
if (files.length === 0) return { ran: false };
|
|
52
|
+
const [bin, ...args] = formatter.command.split(" ");
|
|
53
|
+
const res = await runImpl(bin, [...args, "--", ...files], { cwd: projectDir, timeout: TIMEOUT_MS }).catch(() => ({ exitCode: null }));
|
|
54
|
+
const ok = res.exitCode === 0;
|
|
55
|
+
const message = ok
|
|
56
|
+
? `format: ${formatter.command} on ${files.length} file(s)`
|
|
57
|
+
: `format: ${formatter.command} failed (exit ${res.exitCode ?? "none"}); the next stages judge the files as they are`;
|
|
58
|
+
logger?.[ok ? "info" : "warn"]?.(message);
|
|
59
|
+
emitProgress(emitter, makeEvent("format:result", { ...eventBase, stage: "format" }, { status: ok ? "ok" : "warn", message, detail: { command: formatter.command, files } }));
|
|
60
|
+
return { ran: true, ok, files };
|
|
61
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* The rules are the own-work ones: the same budget, Conventional Commits and
|
|
5
5
|
* attribution patterns, and tests-with-code.
|
|
6
6
|
*/
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
7
9
|
import { AI_ATTRIBUTION, CONVENTIONAL_TYPES } from "../harden/hook-templates.js";
|
|
8
10
|
import { loadPolicy } from "../policy/engine.js";
|
|
9
11
|
import { runCommand } from "../utils/process.js";
|
|
@@ -59,12 +61,35 @@ export function commitChecks(commits) {
|
|
|
59
61
|
];
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
// KJC-TSK-1238 (#2263): a convention the reviewed repo declares (commitlint, kj's commit-msg hook).
|
|
65
|
+
const CONVENTION_FILES = [
|
|
66
|
+
"commitlint.config.js", "commitlint.config.cjs", "commitlint.config.mjs", "commitlint.config.ts",
|
|
67
|
+
".commitlintrc", ".commitlintrc.json", ".commitlintrc.yml", ".commitlintrc.yaml", ".commitlintrc.js", ".commitlintrc.cjs",
|
|
68
|
+
".karajan/hooks/commit-msg",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
export function declaresCommitConvention(projectDir) {
|
|
72
|
+
if (CONVENTION_FILES.some((f) => existsSync(join(projectDir, f)))) return true;
|
|
73
|
+
try {
|
|
74
|
+
return Boolean(JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8")).commitlint);
|
|
75
|
+
} catch {
|
|
76
|
+
return false; // no package.json, or not JSON: it declares nothing
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Source changed with no test changed (a delete-only change needs none). On
|
|
82
|
+
* someone else's PR test-first is kj's rule, not theirs (KJC-TSK-1238): a
|
|
83
|
+
* change with no business logic needs no test, so it is a question, not a failure.
|
|
84
|
+
*/
|
|
63
85
|
export function testsCheck({ config, numstat }) {
|
|
64
86
|
const rows = rowsOf(numstat);
|
|
65
87
|
const res = checkTestsWithCode({ config, stagedFiles: rows.map((r) => r.file), numstat: rows });
|
|
66
|
-
|
|
67
|
-
return {
|
|
88
|
+
if (res.ok && res.mode !== "warn") return { id: "tests", ok: true, detail: `tests: ${res.mode}` };
|
|
89
|
+
return {
|
|
90
|
+
id: "tests", ok: null, detail: res.reason || "source changed with no test changed",
|
|
91
|
+
question: "No test changed: does the change touch business logic, behaviour a unit, integration or E2E test could observe? A plain index, configuration or infrastructure needs none.",
|
|
92
|
+
};
|
|
68
93
|
}
|
|
69
94
|
|
|
70
95
|
const git = async (projectDir, args) => {
|
|
@@ -83,5 +108,9 @@ export async function prRequirementChecks({ range, projectDir, config, run = git
|
|
|
83
108
|
const [sha, subject = "", body = ""] = r.split("\x1f");
|
|
84
109
|
return { sha, subject, body };
|
|
85
110
|
});
|
|
86
|
-
|
|
111
|
+
const [conventional, attribution] = commitChecks(commits);
|
|
112
|
+
const commitFormat = declaresCommitConvention(projectDir)
|
|
113
|
+
? conventional
|
|
114
|
+
: { id: "conventional-commits", ok: null, skipped: true, detail: "the repo declares no commit convention (no commitlint config, no kj commit-msg hook): not checked" };
|
|
115
|
+
return [sizeCheck({ numstat, limit: prSizeLimit({ projectDir, config }).limit }), commitFormat, attribution, testsCheck({ config, numstat })];
|
|
87
116
|
}
|
package/src/session/mutators.js
CHANGED
|
@@ -227,6 +227,11 @@ export function setPgTaskId(session, id) {
|
|
|
227
227
|
session.pg_task_id = id;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/** KJC-BUG-0412: what was pending in the tree when the session started. */
|
|
231
|
+
export function setGitPreexisting(session, paths) {
|
|
232
|
+
session.git_preexisting = paths;
|
|
233
|
+
}
|
|
234
|
+
|
|
230
235
|
export function appendPgCommits(session, commits) {
|
|
231
236
|
session.pg_commits = [...(session.pg_commits ?? []), ...commits];
|
|
232
237
|
}
|