karajan-code 1.57.0 → 1.58.1

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.
Files changed (38) hide show
  1. package/README.md +19 -7
  2. package/docs/README.es.md +19 -7
  3. package/package.json +1 -1
  4. package/scripts/ai-attribution-guard.yml +77 -0
  5. package/scripts/esbuild-sea.config.mjs +83 -15
  6. package/scripts/install-guard-all-repos.sh +85 -0
  7. package/scripts/protect-all-repos.sh +88 -0
  8. package/src/bootstrap.js +8 -4
  9. package/src/cli.js +7 -0
  10. package/src/commands/init.js +30 -0
  11. package/src/config.js +19 -6
  12. package/src/domains/domain-loader.js +164 -0
  13. package/src/domains/domain-registry.js +140 -0
  14. package/src/domains/domain-synthesizer.js +129 -0
  15. package/src/mcp/shared-helpers.js +132 -4
  16. package/src/orchestrator/pipeline-context.js +3 -0
  17. package/src/orchestrator/preflight-checks.js +16 -5
  18. package/src/orchestrator/stages/domain-curator-stage.js +82 -0
  19. package/src/orchestrator/stages/triage-stage.js +4 -1
  20. package/src/orchestrator.js +18 -0
  21. package/src/prompts/architect.js +5 -1
  22. package/src/prompts/coder.js +5 -1
  23. package/src/prompts/hu-reviewer.js +5 -1
  24. package/src/prompts/planner.js +5 -1
  25. package/src/prompts/reviewer.js +5 -1
  26. package/src/roles/architect-role.js +1 -1
  27. package/src/roles/coder-role.js +2 -1
  28. package/src/roles/domain-curator-role.js +141 -0
  29. package/src/roles/hu-reviewer-role.js +1 -1
  30. package/src/roles/planner-role.js +6 -2
  31. package/src/roles/reviewer-role.js +7 -2
  32. package/src/roles/triage-role.js +10 -0
  33. package/src/skills/skill-loader.js +35 -4
  34. package/src/utils/process.js +4 -0
  35. package/src/utils/welcome.js +52 -0
  36. package/templates/domains/example-dental.md +57 -0
  37. package/templates/roles/domain-curator.md +52 -0
  38. package/templates/roles/triage.md +15 -1
package/README.md CHANGED
@@ -60,8 +60,19 @@ npm install -g karajan-code
60
60
 
61
61
  **Homebrew** (macOS):
62
62
  ```bash
63
- brew tap manufosela/tap
64
- brew install karajan-code
63
+ brew install manufosela/tap/karajan-code
64
+ ```
65
+
66
+ **Standalone binary** (no Node.js needed):
67
+ ```bash
68
+ # macOS (Apple Silicon)
69
+ curl -L https://github.com/manufosela/karajan-code/releases/latest/download/kj-darwin-arm64 -o kj && chmod +x kj
70
+
71
+ # Linux x64
72
+ curl -L https://github.com/manufosela/karajan-code/releases/latest/download/kj-linux-x64 -o kj && chmod +x kj
73
+
74
+ # Windows
75
+ curl -L https://github.com/manufosela/karajan-code/releases/latest/download/kj-win-x64.exe -o kj.exe
65
76
  ```
66
77
 
67
78
  **One-liner** (detects OS, installs via npm):
@@ -69,7 +80,7 @@ brew install karajan-code
69
80
  curl -fsSL https://raw.githubusercontent.com/manufosela/karajan-code/main/scripts/install-kj.sh | sh
70
81
  ```
71
82
 
72
- **Docker** (no Node.js needed):
83
+ **Docker**:
73
84
  ```bash
74
85
  docker run --rm -v $(pwd):/workspace karajan-code kj --version
75
86
  ```
@@ -157,15 +168,16 @@ $ claude $ kj-tail
157
168
  ## The pipeline
158
169
 
159
170
  ```
160
- hu-reviewer? → triage → discover? → architect? → planner? → coder → sonar? → impeccable? → reviewer → tester? → security? → solomon → commiter?
171
+ hu-reviewer? → triage → domain-curator? → discover? → architect? → planner? → coder → sonar? → impeccable? → reviewer → tester? → security? → solomon → commiter?
161
172
  ```
162
173
 
163
- **15 roles**, each executed by the AI agent you choose:
174
+ **16 roles**, each executed by the AI agent you choose:
164
175
 
165
176
  | Role | What it does | Default |
166
177
  |------|-------------|---------|
167
178
  | **hu-reviewer** | Certifies user stories before coding (6 dimensions, 7 antipatterns) | Auto (medium/complex) |
168
- | **triage** | Classifies complexity, activates roles, auto-simplifies for trivial tasks | **On** |
179
+ | **triage** | Classifies complexity, activates roles, detects domain hints | **On** |
180
+ | **domain-curator** | Discovers, proposes and synthesizes business-domain knowledge for the pipeline | Auto (when domains exist) |
169
181
  | **discover** | Detects gaps in requirements (Mom Test, Wendel, JTBD) | Off |
170
182
  | **architect** | Designs solution architecture before planning | Off |
171
183
  | **planner** | Generates structured implementation plans | Off |
@@ -229,7 +241,7 @@ Karajan auto-detects and auto-configures everything it can:
229
241
  - **TDD**: Detects test framework for 12 languages (vitest, jest, JUnit, pytest, go test, cargo test, and more). Auto-enables TDD for code tasks, skips for doc/infra
230
242
  - **Bootstrap gate**: Validates all prerequisites (git repo, remote, config, agents, SonarQube) before any tool runs. Fails hard with actionable fix instructions, never silently degrades
231
243
  - **Injection guard**: Scans diffs for prompt injection before AI review. Detects directive overrides, invisible Unicode, oversized comment payloads. Also runs as a GitHub Action on every PR
232
- - **SonarQube**: Auto-starts Docker container, generates config if missing
244
+ - **SonarQube**: Auto-starts Docker container, waits up to 60s for startup, generates config if missing
233
245
  - **Pipeline complexity**: Triage classifies task → trivial tasks skip reviewer loop
234
246
  - **Provider outages**: Retries on 500/502/503/504 with backoff (same as rate limits)
235
247
  - **Coverage**: Coverage-only quality gate failures treated as advisory
package/docs/README.es.md CHANGED
@@ -60,8 +60,19 @@ npm install -g karajan-code
60
60
 
61
61
  **Homebrew** (macOS):
62
62
  ```bash
63
- brew tap manufosela/tap
64
- brew install karajan-code
63
+ brew install manufosela/tap/karajan-code
64
+ ```
65
+
66
+ **Binario standalone** (sin Node.js):
67
+ ```bash
68
+ # macOS (Apple Silicon)
69
+ curl -L https://github.com/manufosela/karajan-code/releases/latest/download/kj-darwin-arm64 -o kj && chmod +x kj
70
+
71
+ # Linux x64
72
+ curl -L https://github.com/manufosela/karajan-code/releases/latest/download/kj-linux-x64 -o kj && chmod +x kj
73
+
74
+ # Windows
75
+ curl -L https://github.com/manufosela/karajan-code/releases/latest/download/kj-win-x64.exe -o kj.exe
65
76
  ```
66
77
 
67
78
  **One-liner** (detecta SO, instala via npm):
@@ -69,7 +80,7 @@ brew install karajan-code
69
80
  curl -fsSL https://raw.githubusercontent.com/manufosela/karajan-code/main/scripts/install-kj.sh | sh
70
81
  ```
71
82
 
72
- **Docker** (sin Node.js):
83
+ **Docker**:
73
84
  ```bash
74
85
  docker run --rm -v $(pwd):/workspace karajan-code kj --version
75
86
  ```
@@ -130,15 +141,16 @@ kj-tail --help # Todas las opciones
130
141
  ## El pipeline
131
142
 
132
143
  ```
133
- hu-reviewer? → triage → discover? → architect? → planner? → coder → sonar? → impeccable? → reviewer → tester? → security? → solomon → commiter?
144
+ hu-reviewer? → triage → domain-curator? → discover? → architect? → planner? → coder → sonar? → impeccable? → reviewer → tester? → security? → solomon → commiter?
134
145
  ```
135
146
 
136
- **15 roles**, cada uno ejecutado por el agente de IA que elijas:
147
+ **16 roles**, cada uno ejecutado por el agente de IA que elijas:
137
148
 
138
149
  | Rol | Que hace | Por defecto |
139
150
  |-----|----------|-------------|
140
151
  | **hu-reviewer** | Certifica historias de usuario antes de codificar (6 dimensiones, 7 antipatrones) | Auto (media/compleja) |
141
- | **triage** | Clasifica complejidad, activa roles, auto-simplifica para tareas triviales | **On** |
152
+ | **triage** | Clasifica complejidad, activa roles, detecta domain hints | **On** |
153
+ | **domain-curator** | Descubre, propone y sintetiza conocimiento de dominio de negocio para el pipeline | Auto (cuando existen dominios) |
142
154
  | **discover** | Detecta huecos en requisitos (Mom Test, Wendel, JTBD) | Off |
143
155
  | **architect** | Diseña la arquitectura de la solucion antes de planificar | Off |
144
156
  | **planner** | Genera planes de implementacion estructurados | Off |
@@ -202,7 +214,7 @@ Karajan auto-detecta y auto-configura todo lo que puede:
202
214
  - **TDD**: Detecta framework de tests para 12 lenguajes (vitest, jest, JUnit, pytest, go test, cargo test, y mas). Auto-activa TDD para tareas de codigo, lo salta para doc/infra
203
215
  - **Bootstrap gate**: Valida todos los prerequisitos (repo git, remote, config, agentes, SonarQube) antes de ejecutar. Falla con instrucciones claras, nunca degrada silenciosamente
204
216
  - **Injection guard**: Escanea diffs en busca de prompt injection antes del review de IA. Detecta directivas de override, Unicode invisible, payloads en comentarios sobredimensionados. Tambien como GitHub Action en cada PR
205
- - **SonarQube**: Auto-arranca contenedor Docker, genera config si falta
217
+ - **SonarQube**: Auto-arranca contenedor Docker, espera hasta 60s al arranque, genera config si falta
206
218
  - **Complejidad del pipeline**: Triage clasifica la tarea, las triviales saltan el loop del reviewer
207
219
  - **Caidas de proveedor**: Reintentos en 500/502/503/504 con backoff (igual que rate limits)
208
220
  - **Cobertura**: Fallos de quality gate solo por cobertura se tratan como advisory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.57.0",
3
+ "version": "1.58.1",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -0,0 +1,77 @@
1
+ # AI Attribution Guard — GitHub Action
2
+ #
3
+ # Copy this file to .github/workflows/ai-attribution-guard.yml in any repo.
4
+ # Blocks PRs that contain AI attribution comments or Co-Authored-By lines.
5
+ # No dependencies, no Node.js required. Pure bash + grep.
6
+
7
+ name: AI Attribution Guard
8
+
9
+ on:
10
+ pull_request:
11
+ types: [opened, synchronize, reopened]
12
+
13
+ jobs:
14
+ scan:
15
+ name: Scan for AI attribution
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0
21
+
22
+ - name: Check PR diff for AI attribution
23
+ run: |
24
+ DIFF=$(git diff origin/${{ github.base_ref }}...HEAD \
25
+ -- ':!*.png' ':!*.jpg' ':!*.svg' ':!*.ico' \
26
+ ':!*.lock' ':!package-lock.json' ':!yarn.lock' \
27
+ ':!*.min.js' ':!*.min.css' \
28
+ ':!.githooks/*' \
29
+ | grep '^+' \
30
+ | grep -v '^+++' \
31
+ || true)
32
+
33
+ [ -z "$DIFF" ] && echo "No diff to scan" && exit 0
34
+
35
+ # Attribution patterns (not functional references like provider: "claude")
36
+ ATTR='(Generated|Created|Written|Made|Built|Authored|Assisted|Powered|Produced) (by|with|using|via) .*(Claude|Anthropic|OpenAI|ChatGPT|GPT-[0-9]|Copilot|Gemini|Cursor|Windsurf|Codeium)'
37
+ COAUTH='Co-Authored-By:.*\b(Claude|Anthropic|OpenAI|ChatGPT|Copilot|Gemini|Cursor|Windsurf|Codeium)\b'
38
+ COMMENT='(//|#|/\*|\*|<!--|--)\s*.*(Claude|Anthropic|OpenAI|ChatGPT|Copilot|Gemini).*(generated|assisted|helped|created|wrote|built|made)'
39
+
40
+ FOUND=""
41
+ for PAT in "$ATTR" "$COAUTH" "$COMMENT"; do
42
+ MATCH=$(echo "$DIFF" | grep -i -E "$PAT" || true)
43
+ [ -n "$MATCH" ] && FOUND="${FOUND}${MATCH}\n"
44
+ done
45
+
46
+ if [ -n "$FOUND" ]; then
47
+ echo "::error::AI attribution detected in PR diff"
48
+ echo -e "$FOUND"
49
+ echo ""
50
+ echo "Remove AI attribution comments/lines and push again."
51
+ exit 1
52
+ fi
53
+
54
+ echo "AI attribution guard: clean"
55
+
56
+ - name: Check commit messages
57
+ run: |
58
+ COMMITS=$(git log --format='%s%n%b' origin/${{ github.base_ref }}...HEAD)
59
+
60
+ COAUTH='Co-Authored-By:.*\b(Claude|Anthropic|OpenAI|ChatGPT|Copilot|Gemini|Cursor|Windsurf|Codeium)\b'
61
+ ATTR='(Generated|Created|Written|Built|Authored) (by|with|using) .*(Claude|Anthropic|OpenAI|ChatGPT|Copilot|Gemini|Cursor|Windsurf|Codeium)'
62
+
63
+ FOUND=""
64
+ for PAT in "$COAUTH" "$ATTR"; do
65
+ MATCH=$(echo "$COMMITS" | grep -i -E "$PAT" || true)
66
+ [ -n "$MATCH" ] && FOUND="${FOUND}${MATCH}\n"
67
+ done
68
+
69
+ if [ -n "$FOUND" ]; then
70
+ echo "::error::AI attribution detected in commit messages"
71
+ echo -e "$FOUND"
72
+ echo ""
73
+ echo "Amend commits to remove AI attribution and force-push."
74
+ exit 1
75
+ fi
76
+
77
+ echo "Commit messages: clean"
@@ -2,9 +2,90 @@
2
2
  * esbuild configuration for Node.js SEA (Single Executable Application) bundle.
3
3
  *
4
4
  * Bundles the entire CLI into a single CJS file suitable for SEA injection.
5
- * SEA requires CJS format — ESM entry points are transpiled by esbuild.
5
+ * A plugin handles:
6
+ * 1. import.meta.url / import.meta.dirname -> CJS equivalents
7
+ * 2. Top-level await in cli.js -> async IIFE wrapper
8
+ * 3. Runtime package.json reads -> inlined version string
6
9
  */
7
10
 
11
+ import path from "node:path";
12
+ import fs from "node:fs/promises";
13
+ import { readFileSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
+ const PKG_VERSION = JSON.parse(readFileSync(path.join(ROOT, "package.json"), "utf8")).version;
18
+
19
+ const seaTransformPlugin = {
20
+ name: "sea-transform",
21
+ setup(build) {
22
+ build.onLoad({ filter: /\.m?js$/ }, async (args) => {
23
+ let contents = await fs.readFile(args.path, "utf8");
24
+ let modified = false;
25
+
26
+ // Replace import.meta.dirname with __dirname
27
+ if (contents.includes("import.meta.dirname")) {
28
+ contents = contents.replaceAll("import.meta.dirname", "__dirname");
29
+ modified = true;
30
+ }
31
+
32
+ // Replace import.meta.url with CJS equivalent
33
+ if (contents.includes("import.meta.url")) {
34
+ contents = contents.replaceAll(
35
+ "import.meta.url",
36
+ 'require("node:url").pathToFileURL(__filename).href'
37
+ );
38
+ modified = true;
39
+ }
40
+
41
+ // Inline version: replace any pattern that reads ../../package.json or ../package.json
42
+ // to extract the version, with the hardcoded version string.
43
+ // Covers cli.js, display.js, doctor.js, bootstrap.js, init.js, server.js
44
+ if (contents.includes("package.json") && contents.match(/["']\.\.\/.*package\.json/)) {
45
+ // Pattern: const X = path.resolve(..., "../../package.json");
46
+ // const Y = JSON.parse(readFileSync(X, "utf8")).version;
47
+ // Replace both lines with: const Y = "1.57.0";
48
+ contents = contents.replace(
49
+ /const (\w+) = (?:path\.resolve|resolve)\(.*?["']\.\.\/.*?package\.json["']\);\s*\n\s*(?:const|return) (\w+) = JSON\.parse\(.*?readFileSync\(\1.*?\).*?\.version;/g,
50
+ `const $2 = ${JSON.stringify(PKG_VERSION)};`
51
+ );
52
+ // Single-line pattern: return JSON.parse(readFileSync(pkgPath...)).version
53
+ contents = contents.replace(
54
+ /const (\w+) = (?:path\.resolve|resolve)\(.*?["']\.\.\/.*?package\.json["']\);\s*\n\s*return JSON\.parse\(.*?readFileSync\(\1.*?\).*?\.version;/g,
55
+ `return ${JSON.stringify(PKG_VERSION)};`
56
+ );
57
+ // Pattern in cli.js: const PKG_PATH = ...; const PKG_VERSION = ...;
58
+ contents = contents.replace(
59
+ /const PKG_PATH = .*?package\.json.*?;\s*\nconst PKG_VERSION = .*?\.version;/,
60
+ `const PKG_VERSION = ${JSON.stringify(PKG_VERSION)};`
61
+ );
62
+ // Pattern in display.js: const DISPLAY_PKG_PATH = ...; const DISPLAY_VERSION = ...;
63
+ contents = contents.replace(
64
+ /const DISPLAY_PKG_PATH = .*?package\.json.*?;\s*\nconst DISPLAY_VERSION = .*?\.version;/,
65
+ `const DISPLAY_VERSION = ${JSON.stringify(PKG_VERSION)};`
66
+ );
67
+ // Pattern in server.js: const PKG_PATH = ...; (used later to read version)
68
+ contents = contents.replace(
69
+ /const PKG_PATH = .*?package\.json.*?;\s*\nconst PKG = JSON\.parse\(readFileSync\(PKG_PATH.*?\)\);/,
70
+ `const PKG = { version: ${JSON.stringify(PKG_VERSION)}, name: "karajan-code" };`
71
+ );
72
+ modified = true;
73
+ }
74
+
75
+ // Wrap top-level await in cli.js inside an async IIFE
76
+ if (args.path.endsWith("src/cli.js") || args.path.endsWith("src\\cli.js")) {
77
+ contents = contents.replace(
78
+ /^(try\s*\{[\s\S]*?await\s+program\.parseAsync\(\)[\s\S]*?^\})\s*$/m,
79
+ "(async () => {\n$1\n})();"
80
+ );
81
+ modified = true;
82
+ }
83
+
84
+ return modified ? { contents, loader: "js" } : undefined;
85
+ });
86
+ }
87
+ };
88
+
8
89
  /** @type {import('esbuild').BuildOptions} */
9
90
  export const seaBuildOptions = {
10
91
  entryPoints: ["src/cli.js"],
@@ -14,20 +95,7 @@ export const seaBuildOptions = {
14
95
  target: "node20",
15
96
  bundle: true,
16
97
  minify: false,
17
- // Bundle everything — no externals. The binary must be fully self-contained.
18
98
  external: [],
19
- // Shim for CJS compatibility in the SEA context.
20
- banner: {
21
- js: [
22
- "/* Karajan Code — SEA bundle */",
23
- "/* eslint-disable */",
24
- 'const __importMetaUrl = require("node:url").pathToFileURL(__filename).href;',
25
- ].join("\n"),
26
- },
27
- // Replace import.meta.url references with the CJS-compatible shim variable.
28
- define: {
29
- "import.meta.url": "__importMetaUrl",
30
- },
31
- // Log level for debugging build issues.
99
+ plugins: [seaTransformPlugin],
32
100
  logLevel: "info",
33
101
  };
@@ -0,0 +1,85 @@
1
+ #!/bin/bash
2
+ #
3
+ # Installs the AI Attribution Guard workflow in all repos of a GitHub user.
4
+ # Requires: gh CLI authenticated with admin/push permissions.
5
+ #
6
+ # Usage:
7
+ # ./scripts/install-guard-all-repos.sh # dry-run
8
+ # ./scripts/install-guard-all-repos.sh --apply # install in all repos
9
+ #
10
+ # What it does:
11
+ # - Creates .github/workflows/ai-attribution-guard.yml in each repo
12
+ # - Skips repos that already have the file
13
+ # - Skips archived repos
14
+
15
+ set -euo pipefail
16
+
17
+ OWNER="${GH_OWNER:-manufosela}"
18
+ APPLY=false
19
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
20
+ WORKFLOW_FILE="$SCRIPT_DIR/ai-attribution-guard.yml"
21
+
22
+ if [[ "${1:-}" == "--apply" ]]; then
23
+ APPLY=true
24
+ fi
25
+
26
+ if [[ ! -f "$WORKFLOW_FILE" ]]; then
27
+ echo "ERROR: $WORKFLOW_FILE not found"
28
+ exit 1
29
+ fi
30
+
31
+ CONTENT=$(base64 -w 0 "$WORKFLOW_FILE")
32
+
33
+ echo "=== Install AI Attribution Guard in all repos of $OWNER ==="
34
+ echo ""
35
+
36
+ if [[ "$APPLY" == false ]]; then
37
+ echo " DRY RUN mode. Use --apply to actually install."
38
+ echo ""
39
+ fi
40
+
41
+ REPOS=$(gh repo list "$OWNER" --limit 500 --json name,isArchived \
42
+ --jq '.[] | select(.isArchived == false) | .name')
43
+
44
+ TOTAL=0
45
+ INSTALLED=0
46
+ SKIPPED=0
47
+ FAILED=0
48
+
49
+ while read -r REPO; do
50
+ [ -z "$REPO" ] && continue
51
+ TOTAL=$((TOTAL + 1))
52
+
53
+ # Check if workflow already exists
54
+ EXISTS=$(gh api "repos/$OWNER/$REPO/contents/.github/workflows/ai-attribution-guard.yml" --jq '.sha' 2>/dev/null || echo "")
55
+
56
+ if [[ -n "$EXISTS" ]]; then
57
+ echo " SKIP $REPO — workflow already exists"
58
+ SKIPPED=$((SKIPPED + 1))
59
+ continue
60
+ fi
61
+
62
+ if [[ "$APPLY" == false ]]; then
63
+ echo " WOULD $REPO"
64
+ continue
65
+ fi
66
+
67
+ if gh api "repos/$OWNER/$REPO/contents/.github/workflows/ai-attribution-guard.yml" -X PUT \
68
+ -f message="chore: add AI attribution guard workflow" \
69
+ -f content="$CONTENT" \
70
+ --jq '.commit.sha' 2>/dev/null; then
71
+ echo " OK $REPO"
72
+ INSTALLED=$((INSTALLED + 1))
73
+ else
74
+ echo " FAIL $REPO — check permissions"
75
+ FAILED=$((FAILED + 1))
76
+ fi
77
+
78
+ done <<< "$REPOS"
79
+
80
+ echo ""
81
+ echo "=== Summary ==="
82
+ echo " Total: $TOTAL"
83
+ echo " Installed: $INSTALLED"
84
+ echo " Skipped: $SKIPPED"
85
+ echo " Failed: $FAILED"
@@ -0,0 +1,88 @@
1
+ #!/bin/bash
2
+ #
3
+ # Configures branch protection on all repos of a GitHub user.
4
+ # Requires: gh CLI authenticated with admin permissions.
5
+ #
6
+ # Usage:
7
+ # ./scripts/protect-all-repos.sh # dry-run (shows what it would do)
8
+ # ./scripts/protect-all-repos.sh --apply # apply protection rules
9
+ #
10
+ # What it does:
11
+ # - Require PRs before merging (no direct push to default branch)
12
+ # - Block force push
13
+ # - Block branch deletion
14
+ #
15
+ # Skips repos where branch protection is already configured.
16
+
17
+ set -euo pipefail
18
+
19
+ OWNER="${GH_OWNER:-manufosela}"
20
+ APPLY=false
21
+
22
+ if [[ "${1:-}" == "--apply" ]]; then
23
+ APPLY=true
24
+ fi
25
+
26
+ echo "=== Branch Protection for all repos of $OWNER ==="
27
+ echo ""
28
+
29
+ if [[ "$APPLY" == false ]]; then
30
+ echo " DRY RUN mode. Use --apply to actually configure."
31
+ echo ""
32
+ fi
33
+
34
+ REPOS=$(gh repo list "$OWNER" --limit 500 --json name,defaultBranchRef,isArchived \
35
+ --jq '.[] | select(.isArchived == false) | "\(.name)\t\(.defaultBranchRef.name // "main")"')
36
+
37
+ TOTAL=0
38
+ PROTECTED=0
39
+ SKIPPED=0
40
+ FAILED=0
41
+
42
+ while IFS=$'\t' read -r REPO BRANCH; do
43
+ TOTAL=$((TOTAL + 1))
44
+
45
+ # Check if already protected
46
+ STATUS=$(gh api "repos/$OWNER/$REPO/branches/$BRANCH/protection" --jq '.required_pull_request_reviews.required_approving_review_count // 0' 2>/dev/null || echo "none")
47
+
48
+ if [[ "$STATUS" != "none" ]]; then
49
+ echo " SKIP $REPO ($BRANCH) — already protected"
50
+ SKIPPED=$((SKIPPED + 1))
51
+ continue
52
+ fi
53
+
54
+ if [[ "$APPLY" == false ]]; then
55
+ echo " WOULD $REPO ($BRANCH)"
56
+ continue
57
+ fi
58
+
59
+ # Apply protection
60
+ if gh api "repos/$OWNER/$REPO/branches/$BRANCH/protection" -X PUT \
61
+ --input - <<EOF 2>/dev/null; then
62
+ {
63
+ "required_pull_request_reviews": {
64
+ "required_approving_review_count": 0,
65
+ "dismiss_stale_reviews": false
66
+ },
67
+ "enforce_admins": false,
68
+ "required_status_checks": null,
69
+ "restrictions": null,
70
+ "allow_force_pushes": false,
71
+ "allow_deletions": false
72
+ }
73
+ EOF
74
+ echo " OK $REPO ($BRANCH)"
75
+ PROTECTED=$((PROTECTED + 1))
76
+ else
77
+ echo " FAIL $REPO ($BRANCH) — check permissions"
78
+ FAILED=$((FAILED + 1))
79
+ fi
80
+
81
+ done <<< "$REPOS"
82
+
83
+ echo ""
84
+ echo "=== Summary ==="
85
+ echo " Total: $TOTAL"
86
+ echo " Protected: $PROTECTED"
87
+ echo " Skipped: $SKIPPED"
88
+ echo " Failed: $FAILED"
package/src/bootstrap.js CHANGED
@@ -130,12 +130,16 @@ async function checkSonarQubeReady(config) {
130
130
  return { name: "sonarqube", ok: true, detail: `Reachable at ${host}`, fix: null };
131
131
  }
132
132
 
133
- // Auto-remediation: try to start
133
+ // Auto-remediation: start and wait up to 60s for SonarQube to be ready
134
134
  try {
135
135
  await sonarUp(host);
136
- reachable = await isSonarReachable(host);
137
- if (reachable) {
138
- return { name: "sonarqube", ok: true, detail: `Started and reachable at ${host}`, fix: null };
136
+ const deadline = Date.now() + 60000;
137
+ while (Date.now() < deadline) {
138
+ reachable = await isSonarReachable(host);
139
+ if (reachable) {
140
+ return { name: "sonarqube", ok: true, detail: `Started and reachable at ${host}`, fix: null };
141
+ }
142
+ await new Promise(r => setTimeout(r, 5000));
139
143
  }
140
144
  } catch { /* fall through */ }
141
145
 
package/src/cli.js CHANGED
@@ -28,6 +28,7 @@ import { undoCommand } from "./commands/undo.js";
28
28
  import { statusCommand } from "./commands/status.js";
29
29
 
30
30
  import { printUpdateNotice } from "./utils/update-check.js";
31
+ import { printWelcomeScreen } from "./utils/welcome.js";
31
32
 
32
33
  const PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
33
34
  const PKG_VERSION = JSON.parse(readFileSync(PKG_PATH, "utf8")).version;
@@ -369,6 +370,12 @@ sonar
369
370
  console.log(`Opened ${result.url}`);
370
371
  });
371
372
 
373
+ // Show welcome screen when invoked with no subcommand
374
+ program.action(async () => {
375
+ const { config } = await loadConfig().catch(() => ({ config: null }));
376
+ printWelcomeScreen({ version: PKG_VERSION, config });
377
+ });
378
+
372
379
  try {
373
380
  await program.parseAsync();
374
381
  } catch (error) {
@@ -305,6 +305,35 @@ async function installSkills(logger, interactive) {
305
305
  }
306
306
  }
307
307
 
308
+ const GITIGNORE_ENTRIES = [
309
+ { pattern: ".kj/", comment: "Karajan runtime (logs, worktrees)" },
310
+ { pattern: ".agent/", comment: "Agent skills (OpenSkills)" },
311
+ { pattern: ".scannerwork/", comment: "SonarQube scanner temp" },
312
+ ];
313
+
314
+ async function ensureGitignoreEntries(projectDir, logger) {
315
+ const gitignorePath = path.join(projectDir, ".gitignore");
316
+ let content;
317
+ try {
318
+ content = await fs.readFile(gitignorePath, "utf8");
319
+ } catch {
320
+ // No .gitignore yet — will create one
321
+ }
322
+ if (!content) content = "";
323
+
324
+ const missing = GITIGNORE_ENTRIES.filter(e => !content.includes(e.pattern));
325
+ if (missing.length === 0) return;
326
+
327
+ const block = [
328
+ "",
329
+ "# Karajan Code",
330
+ ...missing.map(e => e.pattern)
331
+ ].join("\n") + "\n";
332
+
333
+ await fs.appendFile(gitignorePath, block, "utf8");
334
+ logger.info(`Added to .gitignore: ${missing.map(e => e.pattern).join(", ")}`);
335
+ }
336
+
308
337
  export async function initCommand({ logger, flags = {} }) {
309
338
  const karajanHome = getKarajanHome();
310
339
  await ensureDir(karajanHome);
@@ -329,6 +358,7 @@ export async function initCommand({ logger, flags = {} }) {
329
358
  await handleConfigSetup({ config, configExists, interactive, configPath, logger });
330
359
  await ensureReviewRules(reviewRulesPath, logger);
331
360
  await ensureCoderRules(coderRulesPath, logger);
361
+ await ensureGitignoreEntries(process.cwd(), logger);
332
362
  await installSkills(logger, interactive);
333
363
 
334
364
  // Auto-detect project stack and preconfigure
package/src/config.js CHANGED
@@ -221,7 +221,7 @@ export async function loadProjectConfig(projectDir = process.cwd()) {
221
221
  return null;
222
222
  }
223
223
  const raw = await fs.readFile(projectConfigPath, "utf8");
224
- return yaml.load(raw) || {};
224
+ return yaml.load(raw, { json: true }) || {};
225
225
  }
226
226
 
227
227
  async function loadProjectPricingOverrides(projectDir = process.cwd()) {
@@ -231,7 +231,7 @@ async function loadProjectPricingOverrides(projectDir = process.cwd()) {
231
231
  }
232
232
 
233
233
  const raw = await fs.readFile(projectConfigPath, "utf8");
234
- const parsed = yaml.load(raw) || {};
234
+ const parsed = yaml.load(raw, { json: true }) || {};
235
235
  const pricing = parsed?.budget?.pricing;
236
236
  if (!pricing || typeof pricing !== "object") {
237
237
  return null;
@@ -249,7 +249,7 @@ export async function loadConfig(projectDir) {
249
249
  const globalExists = await exists(configPath);
250
250
  if (globalExists) {
251
251
  const raw = await fs.readFile(configPath, "utf8");
252
- globalConfig = yaml.load(raw) || {};
252
+ globalConfig = yaml.load(raw, { json: true }) || {};
253
253
  }
254
254
 
255
255
  // Load project config (.karajan/kj.config.yml)
@@ -469,6 +469,13 @@ const CODER_INHERITED_ROLES = new Set([
469
469
 
470
470
  function resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer) {
471
471
  if (roleConfig.provider) return roleConfig.provider;
472
+
473
+ // If model has "provider/model" format (e.g. "gemini/pro"), extract the provider
474
+ if (roleConfig.model && roleConfig.model.includes("/")) {
475
+ const inferredProvider = roleConfig.model.split("/")[0].toLowerCase();
476
+ if (AGENT_MODEL_SIGNATURES[inferredProvider]) return inferredProvider;
477
+ }
478
+
472
479
  if (role === "coder") return legacyCoder;
473
480
  if (role === "reviewer") return legacyReviewer;
474
481
  if (CODER_INHERITED_ROLES.has(role)) return roles.coder?.provider || legacyCoder;
@@ -476,7 +483,13 @@ function resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer) {
476
483
  }
477
484
 
478
485
  function resolveModel(roleConfig, role, config) {
479
- if (roleConfig.model) return { model: roleConfig.model, inherited: false };
486
+ if (roleConfig.model) {
487
+ // Strip "provider/" prefix from models like "gemini/pro" → "pro"
488
+ const model = roleConfig.model.includes("/")
489
+ ? roleConfig.model.split("/").slice(1).join("/")
490
+ : roleConfig.model;
491
+ return { model, inherited: false };
492
+ }
480
493
  if (role === "coder") return { model: config?.coder_options?.model ?? null, inherited: false };
481
494
  if (role === "reviewer") return { model: config?.reviewer_options?.model ?? null, inherited: false };
482
495
  if (CODER_INHERITED_ROLES.has(role)) {
@@ -495,8 +508,8 @@ export function resolveRole(config, role) {
495
508
  const provider = resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer);
496
509
  let { model, inherited: modelIsInherited } = resolveModel(roleConfig, role, config);
497
510
 
498
- // Drop inherited model if incompatible with the resolved provider
499
- if (modelIsInherited && provider && model && !isModelCompatible(provider, model)) {
511
+ // Drop model if incompatible with the resolved provider (inherited or explicit)
512
+ if (provider && model && !isModelCompatible(provider, model)) {
500
513
  model = null;
501
514
  }
502
515