karajan-code 2.10.1 → 2.11.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/README.md +5 -1
- package/package.json +1 -1
- package/src/audit/sonar-findings.js +12 -0
- package/src/cli/register-meta.js +4 -4
- package/src/cli/register-pipeline.js +3 -3
- package/src/cli/register-plan.js +1 -1
- package/src/commands/board.js +28 -4
- package/src/commands/init.js +270 -61
- package/src/config/loader.js +32 -1
- package/src/git/automation.js +94 -4
- package/src/git/hu-automation.js +35 -3
- package/src/orchestrator/config-init.js +17 -0
- package/src/orchestrator/drivers/post-loop.js +31 -2
- package/src/orchestrator/flow-runner.js +41 -0
- package/src/orchestrator/stages/sonar-stage.js +19 -0
- package/src/skills/addyosmani-catalog.js +29 -7
- package/src/sonar/project-key.js +37 -0
- package/src/sonar/token-bootstrap.js +182 -0
- package/src/types/session.js +2 -1
- package/src/utils/display/session-summary.js +12 -2
- package/src/utils/git.js +105 -1
|
@@ -26,9 +26,19 @@ function printSessionPlanner(planner) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function printSessionSonar(sonar) {
|
|
29
|
+
export function printSessionSonar(sonar) {
|
|
30
30
|
if (!sonar) return;
|
|
31
|
-
|
|
31
|
+
// Three buckets so the banner reads correctly:
|
|
32
|
+
// - OK \u2192 green (real pass).
|
|
33
|
+
// - SKIPPED / PENDING \u2192 gray (informational; sonar
|
|
34
|
+
// wasn't gated against, this is NOT a fail). Pre-2026-05-07 N4
|
|
35
|
+
// dogfooding these were rendered red, which made a clean run
|
|
36
|
+
// look like a sonar failure during the live demo.
|
|
37
|
+
// - everything else (ERROR, WARN, ...) \u2192 red (real fail).
|
|
38
|
+
let gateLabel;
|
|
39
|
+
if (sonar.gateStatus === "OK") gateLabel = ANSI.green;
|
|
40
|
+
else if (sonar.gateStatus === "SKIPPED" || sonar.gateStatus === "PENDING") gateLabel = ANSI.gray;
|
|
41
|
+
else gateLabel = ANSI.red;
|
|
32
42
|
console.log(` ${ANSI.dim}\ud83d\udd0d Sonar: ${gateLabel}${sonar.gateStatus}${ANSI.reset}${ANSI.dim} (${sonar.openIssues ?? 0} issues)${ANSI.reset}`);
|
|
33
43
|
if (typeof sonar.issuesInitial === "number" || typeof sonar.issuesResolved === "number") {
|
|
34
44
|
const issuesInitial = sonar.issuesInitial ?? sonar.openIssues ?? 0;
|
package/src/utils/git.js
CHANGED
|
@@ -65,6 +65,42 @@ export async function revParse(ref) {
|
|
|
65
65
|
return runGit(["rev-parse", ref]);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* List commits in the range `fromSha..toRef`. Used by the post-loop
|
|
70
|
+
* journal builder to enumerate every commit produced during a `kj run`
|
|
71
|
+
* — both the coder's own commits AND the post-loop scaffold commit —
|
|
72
|
+
* so the user-visible summary doesn't lose track of them. Pre-fix the
|
|
73
|
+
* summary only listed `gitResult.commits` (post-loop only), which made
|
|
74
|
+
* `kj run "..."` runs look like the coder hadn't committed.
|
|
75
|
+
*
|
|
76
|
+
* Returns oldest-first so the list reads in execution order.
|
|
77
|
+
*
|
|
78
|
+
* @param {string|null|undefined} fromSha The SHA right BEFORE the run started
|
|
79
|
+
* (typically `session.session_start_sha`). Empty/missing → returns [].
|
|
80
|
+
* @param {string} [toRef="HEAD"]
|
|
81
|
+
* @returns {Promise<Array<{hash: string, message: string}>>}
|
|
82
|
+
*/
|
|
83
|
+
export async function listCommitsBetween(fromSha, toRef = "HEAD") {
|
|
84
|
+
if (!fromSha) return [];
|
|
85
|
+
const result = await run(
|
|
86
|
+
"git",
|
|
87
|
+
["log", "--reverse", "--format=%H%x09%s", `${fromSha}..${toRef}`],
|
|
88
|
+
).catch(() => null);
|
|
89
|
+
if (!result || result.exitCode !== 0) return [];
|
|
90
|
+
return String(result.stdout || "")
|
|
91
|
+
.split("\n")
|
|
92
|
+
.map((line) => line.trimEnd())
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.map((line) => {
|
|
95
|
+
const tabIdx = line.indexOf("\t");
|
|
96
|
+
// Defensive: if git emitted no tab (shouldn't happen with our format)
|
|
97
|
+
// treat the whole line as the hash and leave the message empty rather
|
|
98
|
+
// than throw — caller can still render the row.
|
|
99
|
+
if (tabIdx === -1) return { hash: line, message: "" };
|
|
100
|
+
return { hash: line.slice(0, tabIdx), message: line.slice(tabIdx + 1) };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
68
104
|
export async function syncBaseBranch({ baseBranch, autoRebase }) {
|
|
69
105
|
const local = await revParse(baseBranch);
|
|
70
106
|
const remote = await revParse(`origin/${baseBranch}`);
|
|
@@ -109,11 +145,79 @@ export async function hasChanges() {
|
|
|
109
145
|
return status.length > 0;
|
|
110
146
|
}
|
|
111
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Return the relative paths of every uncommitted change (modified,
|
|
150
|
+
* untracked, staged, etc.) in the working tree. Each path is the
|
|
151
|
+
* filename as `git status --porcelain` reports it (after the 2-char
|
|
152
|
+
* status code + space). Returns an empty array when the tree is clean.
|
|
153
|
+
*
|
|
154
|
+
* Used by `finalizeGitAutomation` to decide whether the changes
|
|
155
|
+
* pending at the end of a pipeline are exclusively Karajan
|
|
156
|
+
* scaffolding (and thus deserve a `chore:` commit message instead
|
|
157
|
+
* of inheriting the user task's `feat:`).
|
|
158
|
+
*
|
|
159
|
+
* NOTE: bypasses `runGit()` because that helper trims stdout, which
|
|
160
|
+
* eats the leading space of porcelain lines whose first status char
|
|
161
|
+
* is " " (e.g. " M .gitignore" → "M .gitignore"). The leading space
|
|
162
|
+
* is structural here — without it the slice(3) below would crop one
|
|
163
|
+
* character off the first path.
|
|
164
|
+
*/
|
|
165
|
+
export async function listPendingPaths() {
|
|
166
|
+
const res = await run("git", ["status", "--porcelain"]);
|
|
167
|
+
if (res.exitCode !== 0) {
|
|
168
|
+
throw new Error(`git status --porcelain failed: ${res.stderr || res.stdout}`);
|
|
169
|
+
}
|
|
170
|
+
const stdout = res.stdout;
|
|
171
|
+
if (!stdout) return [];
|
|
172
|
+
const lines = stdout.split("\n").map((l) => l.replace(/\r$/, "")).filter(Boolean);
|
|
173
|
+
const out = [];
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
// Each line is "XY <path>" (XY = 2-char status, space, path).
|
|
176
|
+
// Renames look like "R old -> new"; we want the new path.
|
|
177
|
+
const after = line.slice(3);
|
|
178
|
+
const arrow = after.indexOf(" -> ");
|
|
179
|
+
out.push(arrow !== -1 ? after.slice(arrow + 4) : after);
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Locale-tolerant detection of "git commit refused because the working
|
|
185
|
+
// tree was already clean by the time we ran it". Observed in N4
|
|
186
|
+
// dogfooding (2026-05-07) — `hasChanges()` reported true after `add -A`
|
|
187
|
+
// but `git commit` then failed with the Spanish message "nada para hacer
|
|
188
|
+
// commit, el árbol de trabajo está limpio". The race window is real:
|
|
189
|
+
// post-loop journal writes can finish between hasChanges and commit, and
|
|
190
|
+
// some files come back from `add -A` with no diff vs HEAD even though
|
|
191
|
+
// they show up in `git status --porcelain`. We treat any of those as
|
|
192
|
+
// "no commit needed" rather than escalating to Solomon.
|
|
193
|
+
const NOTHING_TO_COMMIT_PATTERNS = [
|
|
194
|
+
/nothing to commit/i,
|
|
195
|
+
/nada para hacer commit/i, // git in es_ES locale
|
|
196
|
+
/no hay nada para confirmar/i, // git in es_ES alt phrasing
|
|
197
|
+
/aucune modification ajout[ée]e au commit/i, // fr_FR
|
|
198
|
+
/nichts zu committen/i, // de_DE
|
|
199
|
+
];
|
|
200
|
+
function isNothingToCommit(message) {
|
|
201
|
+
const m = String(message || "");
|
|
202
|
+
return NOTHING_TO_COMMIT_PATTERNS.some((re) => re.test(m));
|
|
203
|
+
}
|
|
204
|
+
|
|
112
205
|
export async function commitAll(message) {
|
|
113
206
|
await runGit(["add", "-A"]);
|
|
114
207
|
const changed = await hasChanges();
|
|
115
208
|
if (!changed) return { committed: false };
|
|
116
|
-
|
|
209
|
+
try {
|
|
210
|
+
await runGit(["commit", "-m", message]);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
// `git status --porcelain` and `git commit` disagreed about whether
|
|
213
|
+
// there was anything to commit. Don't escalate — the only outcome
|
|
214
|
+
// either way is "no new commit was created", which the caller
|
|
215
|
+
// already handles via committed=false.
|
|
216
|
+
if (isNothingToCommit(err?.message)) {
|
|
217
|
+
return { committed: false };
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
117
221
|
const raw = await runGit(["log", "-1", "--pretty=format:%H%x1f%s"]);
|
|
118
222
|
const [hash, commitMessage] = raw.split("\x1f");
|
|
119
223
|
return { committed: true, commit: { hash, message: commitMessage } };
|