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.
- package/README.md +19 -7
- package/docs/README.es.md +19 -7
- package/package.json +1 -1
- package/scripts/ai-attribution-guard.yml +77 -0
- package/scripts/esbuild-sea.config.mjs +83 -15
- package/scripts/install-guard-all-repos.sh +85 -0
- package/scripts/protect-all-repos.sh +88 -0
- package/src/bootstrap.js +8 -4
- package/src/cli.js +7 -0
- package/src/commands/init.js +30 -0
- package/src/config.js +19 -6
- package/src/domains/domain-loader.js +164 -0
- package/src/domains/domain-registry.js +140 -0
- package/src/domains/domain-synthesizer.js +129 -0
- package/src/mcp/shared-helpers.js +132 -4
- package/src/orchestrator/pipeline-context.js +3 -0
- package/src/orchestrator/preflight-checks.js +16 -5
- package/src/orchestrator/stages/domain-curator-stage.js +82 -0
- package/src/orchestrator/stages/triage-stage.js +4 -1
- package/src/orchestrator.js +18 -0
- package/src/prompts/architect.js +5 -1
- package/src/prompts/coder.js +5 -1
- package/src/prompts/hu-reviewer.js +5 -1
- package/src/prompts/planner.js +5 -1
- package/src/prompts/reviewer.js +5 -1
- package/src/roles/architect-role.js +1 -1
- package/src/roles/coder-role.js +2 -1
- package/src/roles/domain-curator-role.js +141 -0
- package/src/roles/hu-reviewer-role.js +1 -1
- package/src/roles/planner-role.js +6 -2
- package/src/roles/reviewer-role.js +7 -2
- package/src/roles/triage-role.js +10 -0
- package/src/skills/skill-loader.js +35 -4
- package/src/utils/process.js +4 -0
- package/src/utils/welcome.js +52 -0
- package/templates/domains/example-dental.md +57 -0
- package/templates/roles/domain-curator.md +52 -0
- 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
|
|
64
|
-
|
|
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
|
|
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
|
-
**
|
|
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,
|
|
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
|
|
64
|
-
|
|
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
|
|
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
|
-
**
|
|
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,
|
|
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
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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:
|
|
133
|
+
// Auto-remediation: start and wait up to 60s for SonarQube to be ready
|
|
134
134
|
try {
|
|
135
135
|
await sonarUp(host);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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) {
|
package/src/commands/init.js
CHANGED
|
@@ -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)
|
|
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
|
|
499
|
-
if (
|
|
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
|
|