karajan-code 4.6.1 → 4.6.3
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
CHANGED
|
@@ -113,14 +113,26 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
113
113
|
// single test change warn (block via method_gates.tests_with_code).
|
|
114
114
|
const changedFiles = (await rawDiff(flags.range, ["--name-only"])).split("\n").map((f) => f.trim()).filter(Boolean);
|
|
115
115
|
|
|
116
|
-
// KJC-TSK-0688 (MG-C): oversized-diff
|
|
117
|
-
//
|
|
116
|
+
// KJC-TSK-0688 (MG-C) + KJC-TSK-0691 (MG-E): oversized-diff policy.
|
|
117
|
+
// Field case: a 522-line PR sailed past the warning with a
|
|
118
|
+
// rationalization — where the project wants teeth, pr_size: "block"
|
|
119
|
+
// rejects deterministically with a NAMED escape. Default stays warn.
|
|
118
120
|
const sizeWarn = config?.method_gates?.pr_size_warn ?? 150;
|
|
119
121
|
if (sizeWarn > 0) {
|
|
120
122
|
const numstat = await rawDiff(flags.range, ["--numstat"]);
|
|
121
123
|
const added = numstat.split("\n").reduce((acc, l) => acc + (Number(l.split("\t")[0]) || 0), 0);
|
|
122
124
|
if (added > sizeWarn) {
|
|
123
|
-
|
|
125
|
+
const sizePolicy = config?.method_gates?.pr_size || "warn";
|
|
126
|
+
if (process.env.KJ_ALLOW_LARGE_PR === "1") {
|
|
127
|
+
console.log(`⚠ pr-size exempt: ${added} lines added — KJ_ALLOW_LARGE_PR=1 (explicit escape hatch)`);
|
|
128
|
+
} else if (sizePolicy === "block") {
|
|
129
|
+
const reason = `${added} lines added exceeds the ${sizeWarn}-line budget (method_gates.pr_size: block) — partition the work, or get your user's explicit OK and re-run with KJ_ALLOW_LARGE_PR=1`;
|
|
130
|
+
console.log(`✗ pr-size gate: ${reason}`);
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
return { verdict: "rejected", reviewer: "pr-size", issues: [{ severity: "high", description: reason }] };
|
|
133
|
+
} else {
|
|
134
|
+
console.log(`⚠ pr-size: ${added} lines added (guideline ~${sizeWarn}) — an oversized warning is not an opinion: partition, or ask your user (method_gates.pr_size: block to harden)`);
|
|
135
|
+
}
|
|
124
136
|
}
|
|
125
137
|
}
|
|
126
138
|
const tests = checkTestsWithCode({ config, stagedFiles: changedFiles });
|
|
@@ -66,6 +66,10 @@ Invariants (the git gates enforce these — they are not suggestions):
|
|
|
66
66
|
and it must be reviewed again. Disagree with a rejection? \`kj solomon\`.
|
|
67
67
|
- Security findings are never overridable — not even by arbitration. You
|
|
68
68
|
absorb the security role: \`kj brief security\` states what must be true.
|
|
69
|
+
- A design decision the card's AC don't cover belongs to the USER: record
|
|
70
|
+
it as a proposed ADR (\`kj adr add\`) and ask — never bury it in a PR
|
|
71
|
+
bullet. An oversized-diff warning is not an opinion either: partition,
|
|
72
|
+
or get an explicit OK.
|
|
69
73
|
- Branch first: never commit on the base branch — every change reaches it
|
|
70
74
|
through an atomic PR (~150 net lines, Conventional Commits). More than one
|
|
71
75
|
task, or the base tree must stay untouched? \`kj worktree start <slug>\`
|
|
@@ -16,8 +16,8 @@ export const SHEBANG = "#!/usr/bin/env sh";
|
|
|
16
16
|
/** Hooks enabled per profile. */
|
|
17
17
|
export const PROFILE_HOOKS = {
|
|
18
18
|
minimal: ["commit-msg"],
|
|
19
|
-
standard: ["pre-commit", "commit-msg", "pre-push", "post-merge"],
|
|
20
|
-
strict: ["pre-commit", "commit-msg", "pre-push", "post-merge"],
|
|
19
|
+
standard: ["pre-commit", "prepare-commit-msg", "commit-msg", "pre-push", "post-merge"],
|
|
20
|
+
strict: ["pre-commit", "prepare-commit-msg", "commit-msg", "pre-push", "post-merge"],
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
/** Build the managed body for a single hook. */
|
|
@@ -99,6 +99,37 @@ export function hookBody(hook, cmds = {}, { globalHooksDir = null, baseBranch =
|
|
|
99
99
|
else lines.push("# (no test command detected for this stack)");
|
|
100
100
|
return [...lines, ...chain].join("\n");
|
|
101
101
|
}
|
|
102
|
+
case "prepare-commit-msg":
|
|
103
|
+
// KJC-TSK-0692 (MG-F): the branch already carries the card ref
|
|
104
|
+
// (validated by the card-first gate) — the subject inherits it
|
|
105
|
+
// automatically. A convention that enforces itself needs no adoption.
|
|
106
|
+
return [
|
|
107
|
+
'msg_file="$1"',
|
|
108
|
+
'branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")',
|
|
109
|
+
"# POSIX-safe extraction (no grep -o): the ref is the leading",
|
|
110
|
+
"# card-shaped token of the branch's last segment (feat/REF-desc).",
|
|
111
|
+
"seg=$(printf '%s' \"$branch\" | tr '[:upper:]' '[:lower:]'); seg=\"${seg##*/}\"",
|
|
112
|
+
"ref=$(printf '%s' \"$seg\" | sed -nE 's/^([a-z][a-z0-9]{1,9}(-[a-z][a-z0-9]{1,9})*-[0-9]{1,6})(-.*)?$/\\1/p')",
|
|
113
|
+
'header=$(head -n1 "$msg_file")',
|
|
114
|
+
'if [ -n "$ref" ] && [ -n "$header" ]; then',
|
|
115
|
+
' case "$header" in',
|
|
116
|
+
" Merge*|fixup!*|squash!*) ;;",
|
|
117
|
+
" *)",
|
|
118
|
+
" # Boundary match — a header mentioning KJW-TSK-00011 must not",
|
|
119
|
+
" # suppress the KJW-TSK-0001 stamp. $ref is [a-z0-9-] only by",
|
|
120
|
+
" # construction (sed extraction above), safe inside the ERE.",
|
|
121
|
+
' if ! printf \'%s\' "$header" | grep -qiE "(^|[^a-zA-Z0-9-])$ref([^a-zA-Z0-9-]|$)"; then',
|
|
122
|
+
" stamp=$(printf '%s' \"$ref\" | tr '[:lower:]' '[:upper:]')",
|
|
123
|
+
' new_header="$header ($stamp)"',
|
|
124
|
+
' if [ "${#new_header}" -le 100 ]; then',
|
|
125
|
+
" { printf '%s\\n' \"$new_header\"; tail -n +2 \"$msg_file\"; } > \"$msg_file.kjtmp\" && mv \"$msg_file.kjtmp\" \"$msg_file\"",
|
|
126
|
+
" fi",
|
|
127
|
+
" fi",
|
|
128
|
+
" ;;",
|
|
129
|
+
" esac",
|
|
130
|
+
"fi",
|
|
131
|
+
...chain,
|
|
132
|
+
].join("\n");
|
|
102
133
|
case "post-merge":
|
|
103
134
|
return [
|
|
104
135
|
"# Refresh the local RAG index so retrieval never serves stale code.",
|
|
@@ -27,6 +27,13 @@ export function isPublishableNpm(projectDir) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Lockfile-detected package manager (KJC-BUG-0131): npm ci kills pnpm/yarn CI. */
|
|
31
|
+
export function detectPackageManager(projectDir) {
|
|
32
|
+
if (existsSync(join(projectDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
33
|
+
if (existsSync(join(projectDir, "yarn.lock"))) return "yarn";
|
|
34
|
+
return "npm";
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
export function installWorkflows({
|
|
31
38
|
projectDir = process.cwd(),
|
|
32
39
|
language = null,
|
|
@@ -35,9 +42,10 @@ export function installWorkflows({
|
|
|
35
42
|
dryRun = false,
|
|
36
43
|
} = {}) {
|
|
37
44
|
const dir = join(projectDir, WORKFLOWS_DIR);
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
45
|
+
const pm = detectPackageManager(projectDir);
|
|
46
|
+
const quality = qualityWorkflowFor(language, pm);
|
|
47
|
+
const extras = extraWorkflowsFor({ profile, publishable: isPublishableNpm(projectDir), pm });
|
|
48
|
+
const mut = mutation ? mutationWorkflowFor(language, pm) : null;
|
|
41
49
|
const all = [...WORKFLOWS, ...(quality ? [quality] : []), ...extras, ...(mut ? [mut] : [])];
|
|
42
50
|
const results = [];
|
|
43
51
|
for (const wf of all) {
|
|
@@ -54,15 +54,37 @@ const header = (steps) =>
|
|
|
54
54
|
...steps,
|
|
55
55
|
].join("\n");
|
|
56
56
|
|
|
57
|
+
// Per-package-manager commands (KJC-BUG-0131, issue #1330): `npm ci` in a
|
|
58
|
+
// pnpm/yarn repo dies with EUSAGE (no package-lock.json). corepack ships with
|
|
59
|
+
// Node 22 and resolves the right pnpm/yarn from `packageManager` — no
|
|
60
|
+
// third-party action. pnpm passes flags AFTER the script name to the script
|
|
61
|
+
// itself, so --if-present must go BEFORE it; yarn has no --if-present at all,
|
|
62
|
+
// so lint runs through `npm run` (scripts don't touch the lockfile).
|
|
63
|
+
const PM_COMMANDS = {
|
|
64
|
+
npm: { setup: [], install: "npm ci", lint: "npm run -s lint --if-present", test: "npm test" },
|
|
65
|
+
pnpm: {
|
|
66
|
+
setup: [" - run: corepack enable"],
|
|
67
|
+
install: "pnpm install --frozen-lockfile",
|
|
68
|
+
lint: "pnpm run --if-present -s lint",
|
|
69
|
+
test: "pnpm test",
|
|
70
|
+
},
|
|
71
|
+
yarn: {
|
|
72
|
+
setup: [" - run: corepack enable"],
|
|
73
|
+
install: "yarn install --frozen-lockfile",
|
|
74
|
+
lint: "npm run -s lint --if-present",
|
|
75
|
+
test: "yarn test",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const nodeSetupSteps = (pm) => [
|
|
80
|
+
" - uses: actions/setup-node@v4",
|
|
81
|
+
" with:",
|
|
82
|
+
" node-version: 22",
|
|
83
|
+
...(PM_COMMANDS[pm] || PM_COMMANDS.npm).setup,
|
|
84
|
+
` - run: ${(PM_COMMANDS[pm] || PM_COMMANDS.npm).install}`,
|
|
85
|
+
];
|
|
86
|
+
|
|
57
87
|
const QUALITY_BY_LANGUAGE = {
|
|
58
|
-
javascript: header([
|
|
59
|
-
" - uses: actions/setup-node@v4",
|
|
60
|
-
" with:",
|
|
61
|
-
" node-version: 22",
|
|
62
|
-
" - run: npm ci",
|
|
63
|
-
" - run: npm run -s lint --if-present",
|
|
64
|
-
" - run: npm test",
|
|
65
|
-
]),
|
|
66
88
|
python: header([
|
|
67
89
|
" - uses: actions/setup-python@v5",
|
|
68
90
|
" with:",
|
|
@@ -87,11 +109,13 @@ const QUALITY_BY_LANGUAGE = {
|
|
|
87
109
|
" - run: vendor/bin/phpunit",
|
|
88
110
|
]),
|
|
89
111
|
};
|
|
90
|
-
QUALITY_BY_LANGUAGE.typescript = QUALITY_BY_LANGUAGE.javascript;
|
|
91
|
-
|
|
92
112
|
/** Stack-aware Quality workflow entry, or null for an unknown language. */
|
|
93
|
-
export function qualityWorkflowFor(language) {
|
|
94
|
-
const
|
|
113
|
+
export function qualityWorkflowFor(language, pm = "npm") {
|
|
114
|
+
const c = PM_COMMANDS[pm] || PM_COMMANDS.npm;
|
|
115
|
+
const body =
|
|
116
|
+
language === "javascript" || language === "typescript"
|
|
117
|
+
? header([...nodeSetupSteps(pm), ` - run: ${c.lint}`, ` - run: ${c.test}`])
|
|
118
|
+
: QUALITY_BY_LANGUAGE[language];
|
|
95
119
|
return body ? { file: "kj-quality.yml", blockId: "wf-quality", body } : null;
|
|
96
120
|
}
|
|
97
121
|
|
|
@@ -123,29 +147,29 @@ export const SHRINK_BUDGET_WORKFLOW = [
|
|
|
123
147
|
].join("\n");
|
|
124
148
|
|
|
125
149
|
// Packs the tarball and installs it clean — only for publishable npm packages.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
].join("\n");
|
|
150
|
+
// `npm pack`/`npm install <tarball>` need no lockfile, so only the dependency
|
|
151
|
+
// install step is package-manager-aware.
|
|
152
|
+
export const packSmokeWorkflow = (pm = "npm") =>
|
|
153
|
+
[
|
|
154
|
+
"name: Pack smoke",
|
|
155
|
+
"on:",
|
|
156
|
+
" pull_request:",
|
|
157
|
+
" branches: [main]",
|
|
158
|
+
"permissions:",
|
|
159
|
+
" contents: read",
|
|
160
|
+
"jobs:",
|
|
161
|
+
" pack-smoke:",
|
|
162
|
+
" runs-on: ubuntu-latest",
|
|
163
|
+
" steps:",
|
|
164
|
+
" - uses: actions/checkout@v4",
|
|
165
|
+
...nodeSetupSteps(pm),
|
|
166
|
+
" - name: Pack and install the tarball clean",
|
|
167
|
+
" run: |",
|
|
168
|
+
" tgz=$(npm pack --silent)",
|
|
169
|
+
" dir=$(mktemp -d)",
|
|
170
|
+
' npm install --prefix "$dir" "$PWD/$tgz" --no-audit --no-fund',
|
|
171
|
+
' echo "Tarball installs clean: $tgz"',
|
|
172
|
+
].join("\n");
|
|
149
173
|
|
|
150
174
|
// Opt-in nightly/manual mutation audit (KJC-TSK-0589). NEVER a PR gate: it runs
|
|
151
175
|
// on a weekly schedule + manual dispatch, and the mutate step is non-blocking
|
|
@@ -153,7 +177,7 @@ export const PACK_SMOKE_WORKFLOW = [
|
|
|
153
177
|
// yields a JSON report (stryker/mutmut/infection) get a workflow — no silent
|
|
154
178
|
// scaffold for tools that can't report yet.
|
|
155
179
|
const MUTATION_TOOLCHAIN = {
|
|
156
|
-
javascript:
|
|
180
|
+
javascript: "node", // resolved per package manager in mutationWorkflowFor
|
|
157
181
|
python: [
|
|
158
182
|
" - uses: actions/setup-python@v5",
|
|
159
183
|
" with:",
|
|
@@ -170,9 +194,11 @@ const MUTATION_TOOLCHAIN = {
|
|
|
170
194
|
MUTATION_TOOLCHAIN.typescript = MUTATION_TOOLCHAIN.javascript;
|
|
171
195
|
|
|
172
196
|
/** Opt-in mutation workflow entry, or null for a stack without a JSON report. */
|
|
173
|
-
export function mutationWorkflowFor(language) {
|
|
174
|
-
const
|
|
175
|
-
if (!
|
|
197
|
+
export function mutationWorkflowFor(language, pm = "npm") {
|
|
198
|
+
const entry = MUTATION_TOOLCHAIN[language];
|
|
199
|
+
if (!entry) return null;
|
|
200
|
+
const c = PM_COMMANDS[pm] || PM_COMMANDS.npm;
|
|
201
|
+
const toolchain = entry === "node" ? [...c.setup, ` - run: ${c.install}`] : entry;
|
|
176
202
|
const body = [
|
|
177
203
|
"name: Mutation (nightly)",
|
|
178
204
|
"on:",
|
|
@@ -200,13 +226,13 @@ export function mutationWorkflowFor(language) {
|
|
|
200
226
|
}
|
|
201
227
|
|
|
202
228
|
/** Conditional workflows: shrink-budget on strict, pack-smoke when publishable. */
|
|
203
|
-
export function extraWorkflowsFor({ profile = "standard", publishable = false } = {}) {
|
|
229
|
+
export function extraWorkflowsFor({ profile = "standard", publishable = false, pm = "npm" } = {}) {
|
|
204
230
|
const extras = [];
|
|
205
231
|
if (profile === "strict") {
|
|
206
232
|
extras.push({ file: "kj-shrink-budget.yml", blockId: "wf-shrink", body: SHRINK_BUDGET_WORKFLOW });
|
|
207
233
|
}
|
|
208
234
|
if (publishable) {
|
|
209
|
-
extras.push({ file: "kj-pack-smoke.yml", blockId: "wf-pack-smoke", body:
|
|
235
|
+
extras.push({ file: "kj-pack-smoke.yml", blockId: "wf-pack-smoke", body: packSmokeWorkflow(pm) });
|
|
210
236
|
}
|
|
211
237
|
return extras;
|
|
212
238
|
}
|
package/src/roles/coder-role.js
CHANGED
|
@@ -17,9 +17,17 @@ export class CoderRole extends AgentRole {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
createAgentInstance(provider) {
|
|
20
|
+
// KJC-BUG-0129 (issues #1301/#1303): running INSIDE an agent does not
|
|
21
|
+
// make it "the host executor" — delegating sent the full instruction
|
|
22
|
+
// prompt through elicitInput as a hanging board "question". A CLI
|
|
23
|
+
// provider spawns as a subprocess ALWAYS; host delegation is explicit
|
|
24
|
+
// opt-in (roles.coder.host_delegation: true) for MCP-only setups.
|
|
20
25
|
if (this._askHost && isHostAgent(provider)) {
|
|
21
|
-
this.
|
|
22
|
-
|
|
26
|
+
if (this.config?.roles?.coder?.host_delegation === true) {
|
|
27
|
+
this.logger.info(`Host-as-coder: delegating to host AI (roles.coder.host_delegation, skipping ${provider} subprocess)`);
|
|
28
|
+
return new HostAgent(this.config, this.logger, { askHost: this._askHost });
|
|
29
|
+
}
|
|
30
|
+
this.logger.info(`Running inside ${provider} — spawning it as a subprocess anyway (set roles.coder.host_delegation: true to delegate to the host)`);
|
|
23
31
|
}
|
|
24
32
|
return this._createAgent(provider, this.config, this.logger);
|
|
25
33
|
}
|