karajan-code 1.16.0 → 1.18.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/activity-log.js +13 -13
- package/src/agents/availability.js +2 -3
- package/src/agents/claude-agent.js +42 -21
- package/src/agents/model-registry.js +1 -1
- package/src/becaria/dispatch.js +1 -1
- package/src/becaria/repo.js +3 -3
- package/src/cli.js +5 -2
- package/src/commands/doctor.js +154 -108
- package/src/commands/init.js +101 -90
- package/src/commands/plan.js +1 -1
- package/src/commands/report.js +77 -71
- package/src/commands/roles.js +0 -1
- package/src/commands/run.js +2 -3
- package/src/config.js +174 -93
- package/src/git/automation.js +3 -4
- package/src/guards/intent-guard.js +123 -0
- package/src/guards/output-guard.js +158 -0
- package/src/guards/perf-guard.js +126 -0
- package/src/guards/policy-resolver.js +3 -3
- package/src/mcp/orphan-guard.js +1 -2
- package/src/mcp/progress.js +4 -3
- package/src/mcp/run-kj.js +1 -0
- package/src/mcp/server-handlers.js +242 -253
- package/src/mcp/server.js +4 -3
- package/src/mcp/tools.js +2 -0
- package/src/orchestrator/agent-fallback.js +1 -3
- package/src/orchestrator/iteration-stages.js +206 -170
- package/src/orchestrator/pre-loop-stages.js +200 -34
- package/src/orchestrator/solomon-rules.js +2 -2
- package/src/orchestrator.js +902 -746
- package/src/planning-game/adapter.js +23 -20
- package/src/planning-game/architect-adrs.js +45 -0
- package/src/planning-game/client.js +15 -1
- package/src/planning-game/decomposition.js +7 -5
- package/src/prompts/architect.js +88 -0
- package/src/prompts/discover.js +54 -53
- package/src/prompts/planner.js +53 -33
- package/src/prompts/triage.js +8 -16
- package/src/review/parser.js +18 -19
- package/src/review/profiles.js +2 -2
- package/src/review/schema.js +3 -3
- package/src/review/scope-filter.js +3 -4
- package/src/roles/architect-role.js +122 -0
- package/src/roles/commiter-role.js +2 -2
- package/src/roles/discover-role.js +59 -67
- package/src/roles/index.js +1 -0
- package/src/roles/planner-role.js +54 -38
- package/src/roles/refactorer-role.js +8 -7
- package/src/roles/researcher-role.js +6 -7
- package/src/roles/reviewer-role.js +4 -5
- package/src/roles/security-role.js +3 -4
- package/src/roles/solomon-role.js +6 -18
- package/src/roles/sonar-role.js +5 -1
- package/src/roles/tester-role.js +8 -5
- package/src/roles/triage-role.js +2 -2
- package/src/session-cleanup.js +29 -24
- package/src/session-store.js +1 -1
- package/src/sonar/api.js +1 -1
- package/src/sonar/manager.js +1 -1
- package/src/sonar/project-key.js +5 -5
- package/src/sonar/scanner.js +34 -65
- package/src/utils/display.js +312 -272
- package/src/utils/git.js +3 -3
- package/src/utils/logger.js +6 -1
- package/src/utils/model-selector.js +5 -5
- package/src/utils/process.js +80 -102
- package/src/utils/rate-limit-detector.js +13 -13
- package/src/utils/run-log.js +55 -52
- package/templates/kj.config.yml +33 -0
- package/templates/roles/architect.md +62 -0
- package/templates/roles/planner.md +1 -0
package/src/config.js
CHANGED
|
@@ -17,7 +17,8 @@ const DEFAULTS = {
|
|
|
17
17
|
tester: { provider: null, model: null },
|
|
18
18
|
security: { provider: null, model: null },
|
|
19
19
|
triage: { provider: null, model: null },
|
|
20
|
-
discover: { provider: null, model: null }
|
|
20
|
+
discover: { provider: null, model: null },
|
|
21
|
+
architect: { provider: null, model: null }
|
|
21
22
|
},
|
|
22
23
|
pipeline: {
|
|
23
24
|
planner: { enabled: false },
|
|
@@ -27,7 +28,8 @@ const DEFAULTS = {
|
|
|
27
28
|
tester: { enabled: true },
|
|
28
29
|
security: { enabled: true },
|
|
29
30
|
triage: { enabled: true },
|
|
30
|
-
discover: { enabled: false }
|
|
31
|
+
discover: { enabled: false },
|
|
32
|
+
architect: { enabled: false }
|
|
31
33
|
},
|
|
32
34
|
review_mode: "standard",
|
|
33
35
|
max_iterations: 5,
|
|
@@ -138,6 +140,25 @@ const DEFAULTS = {
|
|
|
138
140
|
max_backoff_ms: 30000,
|
|
139
141
|
backoff_multiplier: 2,
|
|
140
142
|
jitter_factor: 0.1
|
|
143
|
+
},
|
|
144
|
+
guards: {
|
|
145
|
+
output: {
|
|
146
|
+
enabled: true,
|
|
147
|
+
patterns: [],
|
|
148
|
+
protected_files: [],
|
|
149
|
+
on_violation: "block"
|
|
150
|
+
},
|
|
151
|
+
perf: {
|
|
152
|
+
enabled: true,
|
|
153
|
+
patterns: [],
|
|
154
|
+
block_on_warning: false,
|
|
155
|
+
frontend_extensions: []
|
|
156
|
+
},
|
|
157
|
+
intent: {
|
|
158
|
+
enabled: false,
|
|
159
|
+
patterns: [],
|
|
160
|
+
confidence_threshold: 0.85
|
|
161
|
+
}
|
|
141
162
|
}
|
|
142
163
|
};
|
|
143
164
|
|
|
@@ -221,96 +242,144 @@ export async function writeConfig(configPath, config) {
|
|
|
221
242
|
await fs.writeFile(configPath, yaml.dump(config, { lineWidth: 120 }), "utf8");
|
|
222
243
|
}
|
|
223
244
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
245
|
+
// Declarative mappings for applyRunOverrides to reduce cognitive complexity.
|
|
246
|
+
|
|
247
|
+
// Role provider flags: [flagName, roleName] — truthy check
|
|
248
|
+
const ROLE_PROVIDER_FLAGS = [
|
|
249
|
+
["planner", "planner"], ["coder", "coder"], ["reviewer", "reviewer"],
|
|
250
|
+
["refactorer", "refactorer"], ["solomon", "solomon"], ["researcher", "researcher"],
|
|
251
|
+
["tester", "tester"], ["security", "security"], ["triage", "triage"],
|
|
252
|
+
["discover", "discover"], ["architect", "architect"]
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
// Role model flags: [flagName, roleName] — truthy check, String coercion
|
|
256
|
+
const ROLE_MODEL_FLAGS = [
|
|
257
|
+
["plannerModel", "planner"], ["coderModel", "coder"], ["reviewerModel", "reviewer"],
|
|
258
|
+
["refactorerModel", "refactorer"], ["solomonModel", "solomon"], ["discoverModel", "discover"],
|
|
259
|
+
["architectModel", "architect"]
|
|
260
|
+
];
|
|
238
261
|
|
|
239
|
-
|
|
262
|
+
// Pipeline enable flags: [flagName, pipelineKey] — !== undefined check, Boolean coercion
|
|
263
|
+
const PIPELINE_ENABLE_FLAGS = [
|
|
264
|
+
["enablePlanner", "planner"], ["enableRefactorer", "refactorer"],
|
|
265
|
+
["enableSolomon", "solomon"], ["enableResearcher", "researcher"],
|
|
266
|
+
["enableTester", "tester"], ["enableSecurity", "security"],
|
|
267
|
+
["enableTriage", "triage"], ["enableDiscover", "discover"],
|
|
268
|
+
["enableArchitect", "architect"]
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
// Scalar flags: [flagName, setter] — truthy check
|
|
272
|
+
const SCALAR_FLAGS = [
|
|
273
|
+
["mode", (out, v) => { out.review_mode = v; }],
|
|
274
|
+
["maxIterations", (out, v) => { out.max_iterations = Number(v); }],
|
|
275
|
+
["maxIterationMinutes", (out, v) => { out.session.max_iteration_minutes = Number(v); }],
|
|
276
|
+
["maxTotalMinutes", (out, v) => { out.session.max_total_minutes = Number(v); }],
|
|
277
|
+
["checkpointInterval", (out, v) => { out.session.checkpoint_interval_minutes = Number(v); }],
|
|
278
|
+
["baseBranch", (out, v) => { out.base_branch = v; }],
|
|
279
|
+
["coderFallback", (out, v) => { out.coder_options.fallback_coder = v; }],
|
|
280
|
+
["reviewerFallback", (out, v) => { out.reviewer_options.fallback_reviewer = v; }],
|
|
281
|
+
["taskType", (out, v) => { out.taskType = String(v); }],
|
|
282
|
+
["branchPrefix", (out, v) => { out.git.branch_prefix = String(v); }]
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
// Boolean/undefined-check flags: [flagName, setter] — !== undefined check
|
|
286
|
+
const UNDEF_CHECK_FLAGS = [
|
|
287
|
+
["reviewerRetries", (out, v) => { out.reviewer_options.retries = Number(v); }],
|
|
288
|
+
["autoCommit", (out, v) => { out.git.auto_commit = Boolean(v); }],
|
|
289
|
+
["autoPush", (out, v) => { out.git.auto_push = Boolean(v); }],
|
|
290
|
+
["autoPr", (out, v) => { out.git.auto_pr = Boolean(v); }],
|
|
291
|
+
["autoRebase", (out, v) => { out.git.auto_rebase = Boolean(v); }],
|
|
292
|
+
["enableSerena", (out, v) => { out.serena.enabled = Boolean(v); }]
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
function applyRoleOverrides(out, flags) {
|
|
296
|
+
for (const [flag, role] of ROLE_PROVIDER_FLAGS) {
|
|
297
|
+
if (flags[flag]) out.roles[role].provider = flags[flag];
|
|
298
|
+
}
|
|
299
|
+
// coder/reviewer also update top-level aliases
|
|
240
300
|
if (flags.coder) out.coder = flags.coder;
|
|
241
|
-
if (flags.coder) out.roles.coder.provider = flags.coder;
|
|
242
301
|
if (flags.reviewer) out.reviewer = flags.reviewer;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (flags.researcher) out.roles.researcher.provider = flags.researcher;
|
|
247
|
-
if (flags.tester) out.roles.tester.provider = flags.tester;
|
|
248
|
-
if (flags.security) out.roles.security.provider = flags.security;
|
|
249
|
-
if (flags.triage) out.roles.triage.provider = flags.triage;
|
|
250
|
-
if (flags.discover) out.roles.discover.provider = flags.discover;
|
|
251
|
-
if (flags.discoverModel) out.roles.discover.model = String(flags.discoverModel);
|
|
252
|
-
if (flags.enableDiscover !== undefined) out.pipeline.discover.enabled = Boolean(flags.enableDiscover);
|
|
253
|
-
if (flags.plannerModel) out.roles.planner.model = String(flags.plannerModel);
|
|
254
|
-
if (flags.coderModel) {
|
|
255
|
-
out.roles.coder.model = String(flags.coderModel);
|
|
302
|
+
|
|
303
|
+
for (const [flag, role] of ROLE_MODEL_FLAGS) {
|
|
304
|
+
if (flags[flag]) out.roles[role].model = String(flags[flag]);
|
|
256
305
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
306
|
+
// reviewerModel also updates reviewer_options
|
|
307
|
+
if (flags.reviewerModel) out.reviewer_options.model = String(flags.reviewerModel);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function applyPipelineOverrides(out, flags) {
|
|
311
|
+
for (const [flag, key] of PIPELINE_ENABLE_FLAGS) {
|
|
312
|
+
if (flags[flag] !== undefined) out.pipeline[key].enabled = Boolean(flags[flag]);
|
|
260
313
|
}
|
|
261
|
-
if (flags.refactorerModel) out.roles.refactorer.model = String(flags.refactorerModel);
|
|
262
|
-
if (flags.solomonModel) out.roles.solomon.model = String(flags.solomonModel);
|
|
263
|
-
if (flags.enablePlanner !== undefined) out.pipeline.planner.enabled = Boolean(flags.enablePlanner);
|
|
264
|
-
if (flags.enableRefactorer !== undefined) out.pipeline.refactorer.enabled = Boolean(flags.enableRefactorer);
|
|
265
|
-
if (flags.enableSolomon !== undefined) out.pipeline.solomon.enabled = Boolean(flags.enableSolomon);
|
|
266
|
-
if (flags.enableResearcher !== undefined) out.pipeline.researcher.enabled = Boolean(flags.enableResearcher);
|
|
267
|
-
if (flags.enableTester !== undefined) out.pipeline.tester.enabled = Boolean(flags.enableTester);
|
|
268
|
-
if (flags.enableSecurity !== undefined) out.pipeline.security.enabled = Boolean(flags.enableSecurity);
|
|
269
314
|
if (flags.enableReviewer !== undefined) {
|
|
270
315
|
out.pipeline.reviewer = out.pipeline.reviewer || {};
|
|
271
316
|
out.pipeline.reviewer.enabled = Boolean(flags.enableReviewer);
|
|
272
317
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (flags.reviewerFallback) out.reviewer_options.fallback_reviewer = flags.reviewerFallback;
|
|
282
|
-
if (flags.reviewerRetries !== undefined) out.reviewer_options.retries = Number(flags.reviewerRetries);
|
|
283
|
-
if (flags.autoCommit !== undefined) out.git.auto_commit = Boolean(flags.autoCommit);
|
|
284
|
-
if (flags.autoPush !== undefined) out.git.auto_push = Boolean(flags.autoPush);
|
|
285
|
-
if (flags.autoPr !== undefined) out.git.auto_pr = Boolean(flags.autoPr);
|
|
286
|
-
if (flags.autoRebase !== undefined) out.git.auto_rebase = Boolean(flags.autoRebase);
|
|
287
|
-
if (flags.branchPrefix) out.git.branch_prefix = String(flags.branchPrefix);
|
|
288
|
-
if (flags.methodology) {
|
|
289
|
-
const methodology = String(flags.methodology).toLowerCase();
|
|
290
|
-
out.development = out.development || {};
|
|
291
|
-
out.development.methodology = methodology;
|
|
292
|
-
out.development.require_test_changes = methodology === "tdd";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function applyScalarAndBooleanOverrides(out, flags) {
|
|
321
|
+
for (const [flag, setter] of SCALAR_FLAGS) {
|
|
322
|
+
if (flags[flag]) setter(out, flags[flag]);
|
|
323
|
+
}
|
|
324
|
+
for (const [flag, setter] of UNDEF_CHECK_FLAGS) {
|
|
325
|
+
if (flags[flag] !== undefined) setter(out, flags[flag]);
|
|
293
326
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (flags.
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function applyMethodologyOverride(out, flags) {
|
|
330
|
+
if (!flags.methodology) return;
|
|
331
|
+
const methodology = String(flags.methodology).toLowerCase();
|
|
332
|
+
out.development.methodology = methodology;
|
|
333
|
+
out.development.require_test_changes = methodology === "tdd";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function applyBecariaOverride(out, flags) {
|
|
298
337
|
out.becaria = out.becaria || { enabled: false };
|
|
299
|
-
if (flags.enableBecaria
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
338
|
+
if (flags.enableBecaria === undefined) return;
|
|
339
|
+
out.becaria.enabled = Boolean(flags.enableBecaria);
|
|
340
|
+
// BecarIA requires git automation (commit + push + PR)
|
|
341
|
+
if (out.becaria.enabled) {
|
|
342
|
+
out.git.auto_commit = true;
|
|
343
|
+
out.git.auto_push = true;
|
|
344
|
+
out.git.auto_pr = true;
|
|
307
345
|
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function applyMiscOverrides(out, flags) {
|
|
349
|
+
if (flags.noSonar || flags.sonar === false) out.sonarqube.enabled = false;
|
|
350
|
+
|
|
308
351
|
out.planning_game = out.planning_game || {};
|
|
309
352
|
if (flags.pgTask) out.planning_game.enabled = true;
|
|
310
353
|
if (flags.pgProject) out.planning_game.project_id = flags.pgProject;
|
|
354
|
+
|
|
311
355
|
out.model_selection = out.model_selection || { enabled: true, tiers: {}, role_overrides: {} };
|
|
312
356
|
if (flags.smartModels === true) out.model_selection.enabled = true;
|
|
313
357
|
if (flags.smartModels === false || flags.noSmartModels === true) out.model_selection.enabled = false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function applyRunOverrides(config, flags) {
|
|
361
|
+
const out = mergeDeep(config, {});
|
|
362
|
+
out.coder_options = out.coder_options || {};
|
|
363
|
+
out.reviewer_options = out.reviewer_options || {};
|
|
364
|
+
out.session = out.session || {};
|
|
365
|
+
out.git = out.git || {};
|
|
366
|
+
out.development = out.development || {};
|
|
367
|
+
out.sonarqube = out.sonarqube || {};
|
|
368
|
+
if (out.max_budget_usd === undefined || out.max_budget_usd === null) {
|
|
369
|
+
out.max_budget_usd = out.session.max_budget_usd ?? null;
|
|
370
|
+
}
|
|
371
|
+
out.budget = mergeDeep(DEFAULTS.budget, out.budget || {});
|
|
372
|
+
out.roles = mergeDeep(DEFAULTS.roles, out.roles || {});
|
|
373
|
+
out.pipeline = mergeDeep(DEFAULTS.pipeline, out.pipeline || {});
|
|
374
|
+
out.serena = out.serena || { enabled: false };
|
|
375
|
+
|
|
376
|
+
applyRoleOverrides(out, flags);
|
|
377
|
+
applyPipelineOverrides(out, flags);
|
|
378
|
+
applyScalarAndBooleanOverrides(out, flags);
|
|
379
|
+
applyMethodologyOverride(out, flags);
|
|
380
|
+
applyBecariaOverride(out, flags);
|
|
381
|
+
applyMiscOverrides(out, flags);
|
|
382
|
+
|
|
314
383
|
return out;
|
|
315
384
|
}
|
|
316
385
|
|
|
@@ -323,45 +392,57 @@ export function resolveRole(config, role) {
|
|
|
323
392
|
let provider = roleConfig.provider ?? null;
|
|
324
393
|
if (!provider && role === "coder") provider = legacyCoder;
|
|
325
394
|
if (!provider && role === "reviewer") provider = legacyReviewer;
|
|
326
|
-
if (!provider && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "triage" || role === "discover")) {
|
|
395
|
+
if (!provider && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "triage" || role === "discover" || role === "architect")) {
|
|
327
396
|
provider = roles.coder?.provider || legacyCoder;
|
|
328
397
|
}
|
|
329
398
|
|
|
330
399
|
let model = roleConfig.model ?? null;
|
|
331
400
|
if (!model && role === "coder") model = config?.coder_options?.model ?? null;
|
|
332
401
|
if (!model && role === "reviewer") model = config?.reviewer_options?.model ?? null;
|
|
333
|
-
if (!model && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "triage" || role === "discover")) {
|
|
402
|
+
if (!model && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "triage" || role === "discover" || role === "architect")) {
|
|
334
403
|
model = config?.coder_options?.model ?? null;
|
|
335
404
|
}
|
|
336
405
|
|
|
337
406
|
return { provider, model };
|
|
338
407
|
}
|
|
339
408
|
|
|
409
|
+
// Pipeline roles checked when commandName is "run": [pipelineKey, roleName]
|
|
410
|
+
const RUN_PIPELINE_ROLES = [
|
|
411
|
+
["reviewer", "reviewer"], ["triage", "triage"], ["planner", "planner"],
|
|
412
|
+
["refactorer", "refactorer"], ["researcher", "researcher"],
|
|
413
|
+
["tester", "tester"], ["security", "security"]
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
// Direct command-to-role mapping for non-"run" commands
|
|
417
|
+
const COMMAND_ROLE_MAP = {
|
|
418
|
+
discover: ["discover"],
|
|
419
|
+
plan: ["planner"],
|
|
420
|
+
code: ["coder"],
|
|
421
|
+
review: ["reviewer"]
|
|
422
|
+
};
|
|
423
|
+
|
|
340
424
|
function requiredRolesFor(commandName, config) {
|
|
341
|
-
if (commandName
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
425
|
+
if (commandName !== "run") {
|
|
426
|
+
return COMMAND_ROLE_MAP[commandName] || [];
|
|
427
|
+
}
|
|
428
|
+
const required = ["coder"];
|
|
429
|
+
for (const [pipelineKey, roleName] of RUN_PIPELINE_ROLES) {
|
|
430
|
+
const pipelineEntry = config?.pipeline?.[pipelineKey];
|
|
431
|
+
// reviewer defaults to enabled (only excluded if explicitly false)
|
|
432
|
+
const isEnabled = pipelineKey === "reviewer"
|
|
433
|
+
? pipelineEntry?.enabled !== false
|
|
434
|
+
: Boolean(pipelineEntry?.enabled);
|
|
435
|
+
if (isEnabled) required.push(roleName);
|
|
351
436
|
}
|
|
352
|
-
|
|
353
|
-
if (commandName === "plan") return ["planner"];
|
|
354
|
-
if (commandName === "code") return ["coder"];
|
|
355
|
-
if (commandName === "review") return ["reviewer"];
|
|
356
|
-
return [];
|
|
437
|
+
return required;
|
|
357
438
|
}
|
|
358
439
|
|
|
359
440
|
export function validateConfig(config, commandName = "run") {
|
|
360
441
|
const errors = [];
|
|
361
|
-
if (!["paranoid", "strict", "standard", "relaxed", "custom"].
|
|
442
|
+
if (!new Set(["paranoid", "strict", "standard", "relaxed", "custom"]).has(config.review_mode)) {
|
|
362
443
|
errors.push(`Invalid review_mode: ${config.review_mode}`);
|
|
363
444
|
}
|
|
364
|
-
if (!["tdd", "standard"].
|
|
445
|
+
if (!new Set(["tdd", "standard"]).has(config.development?.methodology)) {
|
|
365
446
|
errors.push(`Invalid development.methodology: ${config.development?.methodology}`);
|
|
366
447
|
}
|
|
367
448
|
|
package/src/git/automation.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
|
|
20
20
|
export function commitMessageFromTask(task) {
|
|
21
21
|
const clean = String(task || "")
|
|
22
|
-
.
|
|
22
|
+
.replaceAll(/\s+/g, " ")
|
|
23
23
|
.trim();
|
|
24
24
|
return `feat: ${clean.slice(0, 72) || "karajan update"}`;
|
|
25
25
|
}
|
|
@@ -72,8 +72,7 @@ export function buildPrBody({ task, stageResults }) {
|
|
|
72
72
|
const shouldDecompose = stageResults?.triage?.shouldDecompose;
|
|
73
73
|
const pendingSubtasks = shouldDecompose && triageSubtasks?.length > 1 ? triageSubtasks.slice(1) : [];
|
|
74
74
|
if (pendingSubtasks.length > 0) {
|
|
75
|
-
sections.push("", "## Pending subtasks");
|
|
76
|
-
sections.push("This PR addresses part of a larger task. The following subtasks were identified but not included:");
|
|
75
|
+
sections.push("", "## Pending subtasks", "This PR addresses part of a larger task. The following subtasks were identified but not included:");
|
|
77
76
|
for (const subtask of pendingSubtasks) {
|
|
78
77
|
sections.push(`- [ ] ${subtask}`);
|
|
79
78
|
}
|
|
@@ -115,7 +114,7 @@ export async function earlyPrCreation({ gitCtx, task, logger, session, stageResu
|
|
|
115
114
|
logger.info(`Early PR created: ${prUrl}`);
|
|
116
115
|
|
|
117
116
|
// Extract PR number from URL (e.g. https://github.com/owner/repo/pull/42)
|
|
118
|
-
const prNumber = parseInt(prUrl.split("/").pop(), 10) || null;
|
|
117
|
+
const prNumber = Number.parseInt(prUrl.split("/").pop(), 10) || null;
|
|
119
118
|
return { prNumber, prUrl, commits };
|
|
120
119
|
}
|
|
121
120
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { VALID_TASK_TYPES } from "./policy-resolver.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in intent patterns for deterministic pre-triage classification.
|
|
5
|
+
* Each pattern maps keywords/regex to a taskType + complexity level.
|
|
6
|
+
* Evaluated top-down; first match with confidence >= threshold wins.
|
|
7
|
+
*/
|
|
8
|
+
const INTENT_PATTERNS = [
|
|
9
|
+
// Documentation-only tasks
|
|
10
|
+
{
|
|
11
|
+
id: "doc-readme",
|
|
12
|
+
keywords: ["readme", "docs", "documentation", "jsdoc", "typedoc", "changelog"],
|
|
13
|
+
taskType: "doc",
|
|
14
|
+
level: "trivial",
|
|
15
|
+
confidence: 0.95,
|
|
16
|
+
message: "Documentation-only task detected",
|
|
17
|
+
},
|
|
18
|
+
// Test-only tasks
|
|
19
|
+
{
|
|
20
|
+
id: "add-tests",
|
|
21
|
+
keywords: ["add test", "write test", "missing test", "test coverage", "add spec", "write spec", "unit test", "integration test"],
|
|
22
|
+
taskType: "add-tests",
|
|
23
|
+
level: "simple",
|
|
24
|
+
confidence: 0.9,
|
|
25
|
+
message: "Test-addition task detected",
|
|
26
|
+
},
|
|
27
|
+
// Refactoring tasks
|
|
28
|
+
{
|
|
29
|
+
id: "refactor",
|
|
30
|
+
keywords: ["refactor", "rename", "extract method", "extract function", "clean up", "cleanup", "reorganize", "restructure", "simplify"],
|
|
31
|
+
taskType: "refactor",
|
|
32
|
+
level: "simple",
|
|
33
|
+
confidence: 0.85,
|
|
34
|
+
message: "Refactoring task detected",
|
|
35
|
+
},
|
|
36
|
+
// Infrastructure / DevOps tasks
|
|
37
|
+
{
|
|
38
|
+
id: "infra-devops",
|
|
39
|
+
keywords: ["ci/cd", "pipeline", "dockerfile", "docker-compose", "kubernetes", "k8s", "terraform", "deploy", "nginx", "github actions", "gitlab ci"],
|
|
40
|
+
taskType: "infra",
|
|
41
|
+
level: "simple",
|
|
42
|
+
confidence: 0.85,
|
|
43
|
+
message: "Infrastructure/DevOps task detected",
|
|
44
|
+
},
|
|
45
|
+
// Trivial fixes (typos, comments, formatting)
|
|
46
|
+
{
|
|
47
|
+
id: "trivial-fix",
|
|
48
|
+
keywords: ["typo", "fix typo", "spelling", "comment", "fix comment", "formatting", "lint", "fix lint", "whitespace"],
|
|
49
|
+
taskType: "sw",
|
|
50
|
+
level: "trivial",
|
|
51
|
+
confidence: 0.9,
|
|
52
|
+
message: "Trivial fix detected",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compile custom intent patterns from config.guards.intent.patterns
|
|
58
|
+
* Custom patterns are evaluated BEFORE built-in ones.
|
|
59
|
+
*/
|
|
60
|
+
export function compileIntentPatterns(configGuards) {
|
|
61
|
+
const custom = Array.isArray(configGuards?.intent?.patterns)
|
|
62
|
+
? configGuards.intent.patterns.map(p => ({
|
|
63
|
+
id: p.id || "custom-intent",
|
|
64
|
+
keywords: Array.isArray(p.keywords) ? p.keywords : [],
|
|
65
|
+
taskType: VALID_TASK_TYPES.has(p.taskType) ? p.taskType : "sw",
|
|
66
|
+
level: p.level || "simple",
|
|
67
|
+
confidence: typeof p.confidence === "number" ? p.confidence : 0.85,
|
|
68
|
+
message: p.message || "Custom intent pattern matched",
|
|
69
|
+
}))
|
|
70
|
+
: [];
|
|
71
|
+
|
|
72
|
+
return [...custom, ...INTENT_PATTERNS];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a task description matches any of the keywords.
|
|
77
|
+
* Returns true if at least one keyword (case-insensitive substring) appears in the task.
|
|
78
|
+
*/
|
|
79
|
+
function matchesKeywords(task, keywords) {
|
|
80
|
+
const lower = task.toLowerCase();
|
|
81
|
+
for (const kw of keywords) {
|
|
82
|
+
if (lower.includes(kw.toLowerCase())) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Classify a task description using deterministic keyword patterns.
|
|
91
|
+
*
|
|
92
|
+
* Returns:
|
|
93
|
+
* { classified: true, taskType, level, confidence, patternId, message }
|
|
94
|
+
* or { classified: false } if no pattern matches above threshold
|
|
95
|
+
*/
|
|
96
|
+
export function classifyIntent(task, config = {}) {
|
|
97
|
+
if (!task || typeof task !== "string") {
|
|
98
|
+
return { classified: false };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const configGuards = config?.guards || {};
|
|
102
|
+
const threshold = configGuards?.intent?.confidence_threshold ?? 0.85;
|
|
103
|
+
const patterns = compileIntentPatterns(configGuards);
|
|
104
|
+
|
|
105
|
+
for (const pattern of patterns) {
|
|
106
|
+
if (!matchesKeywords(task, pattern.keywords)) continue;
|
|
107
|
+
|
|
108
|
+
if (pattern.confidence >= threshold) {
|
|
109
|
+
return {
|
|
110
|
+
classified: true,
|
|
111
|
+
taskType: pattern.taskType,
|
|
112
|
+
level: pattern.level,
|
|
113
|
+
confidence: pattern.confidence,
|
|
114
|
+
patternId: pattern.id,
|
|
115
|
+
message: pattern.message,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { classified: false };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { INTENT_PATTERNS };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { runCommand } from "../utils/process.js";
|
|
2
|
+
|
|
3
|
+
// Built-in destructive patterns
|
|
4
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
5
|
+
{ id: "rm-rf", pattern: /rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|--force\s+)*-[a-zA-Z]*r/, severity: "critical", message: "Recursive file deletion detected" },
|
|
6
|
+
{ id: "drop-table", pattern: /DROP\s+(TABLE|DATABASE|SCHEMA)/i, severity: "critical", message: "SQL destructive operation detected" },
|
|
7
|
+
{ id: "git-reset-hard", pattern: /git\s+reset\s+--hard/i, severity: "critical", message: "Hard git reset detected" },
|
|
8
|
+
{ id: "git-push-force", pattern: /git\s+push\s+.*--force/i, severity: "critical", message: "Force push detected" },
|
|
9
|
+
{ id: "truncate-table", pattern: /TRUNCATE\s+TABLE/i, severity: "critical", message: "SQL truncate detected" },
|
|
10
|
+
{ id: "format-disk", pattern: /mkfs\.|fdisk|dd\s+if=/, severity: "critical", message: "Disk format operation detected" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
// Built-in credential patterns
|
|
14
|
+
const CREDENTIAL_PATTERNS = [
|
|
15
|
+
{ id: "aws-key", pattern: /AKIA[0-9A-Z]{16}/, severity: "critical", message: "AWS access key exposed" },
|
|
16
|
+
{ id: "private-key", pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/, severity: "critical", message: "Private key exposed" },
|
|
17
|
+
{ id: "generic-secret", pattern: /(password|secret|token|api_key|apikey)\s*[:=]\s*["'][^"']{8,}["']/i, severity: "warning", message: "Potential secret/credential exposed" },
|
|
18
|
+
{ id: "github-token", pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/, severity: "critical", message: "GitHub token exposed" },
|
|
19
|
+
{ id: "npm-token", pattern: /npm_[A-Za-z0-9]{36,}/, severity: "critical", message: "npm token exposed" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Default protected files (block if these appear in added/modified lines)
|
|
23
|
+
const DEFAULT_PROTECTED_FILES = [
|
|
24
|
+
".env",
|
|
25
|
+
".env.local",
|
|
26
|
+
".env.production",
|
|
27
|
+
"serviceAccountKey.json",
|
|
28
|
+
"credentials.json",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function compilePatterns(configGuards) {
|
|
32
|
+
const customPatterns = Array.isArray(configGuards?.output?.patterns)
|
|
33
|
+
? configGuards.output.patterns.map(p => ({
|
|
34
|
+
id: p.id || "custom",
|
|
35
|
+
pattern: typeof p.pattern === "string" ? new RegExp(p.pattern, p.flags || "") : p.pattern,
|
|
36
|
+
severity: p.severity || "warning",
|
|
37
|
+
message: p.message || "Custom pattern matched",
|
|
38
|
+
}))
|
|
39
|
+
: [];
|
|
40
|
+
|
|
41
|
+
return [...DESTRUCTIVE_PATTERNS, ...CREDENTIAL_PATTERNS, ...customPatterns];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function compileProtectedFiles(configGuards) {
|
|
45
|
+
const custom = Array.isArray(configGuards?.output?.protected_files)
|
|
46
|
+
? configGuards.output.protected_files
|
|
47
|
+
: [];
|
|
48
|
+
return [...new Set([...DEFAULT_PROTECTED_FILES, ...custom])];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse a unified diff to extract only added lines (lines starting with +, not ++)
|
|
53
|
+
*/
|
|
54
|
+
export function extractAddedLines(diff) {
|
|
55
|
+
if (!diff) return [];
|
|
56
|
+
const results = [];
|
|
57
|
+
let currentFile = null;
|
|
58
|
+
let lineNum = 0;
|
|
59
|
+
|
|
60
|
+
for (const line of diff.split("\n")) {
|
|
61
|
+
if (line.startsWith("+++ b/")) {
|
|
62
|
+
currentFile = line.slice(6);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (line.startsWith("@@ ")) {
|
|
66
|
+
const match = /@@ -\d+(?:,\d+)? \+(\d+)/.exec(line);
|
|
67
|
+
lineNum = match ? Number.parseInt(match[1], 10) - 1 : 0;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
71
|
+
lineNum += 1;
|
|
72
|
+
results.push({ file: currentFile, line: lineNum, content: line.slice(1) });
|
|
73
|
+
} else if (!line.startsWith("-")) {
|
|
74
|
+
lineNum += 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if any modified files are in the protected list
|
|
82
|
+
*/
|
|
83
|
+
export function checkProtectedFiles(diff, protectedFiles) {
|
|
84
|
+
const violations = [];
|
|
85
|
+
const modifiedFiles = [];
|
|
86
|
+
|
|
87
|
+
for (const line of diff.split("\n")) {
|
|
88
|
+
if (line.startsWith("+++ b/")) {
|
|
89
|
+
modifiedFiles.push(line.slice(6));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const file of modifiedFiles) {
|
|
94
|
+
const basename = file.split("/").pop();
|
|
95
|
+
if (protectedFiles.some(pf => file === pf || file.endsWith(`/${pf}`) || basename === pf)) {
|
|
96
|
+
violations.push({
|
|
97
|
+
id: "protected-file",
|
|
98
|
+
severity: "critical",
|
|
99
|
+
file,
|
|
100
|
+
line: 0,
|
|
101
|
+
message: `Protected file modified: ${file}`,
|
|
102
|
+
matchedContent: "",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return violations;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Scan a diff for pattern violations.
|
|
112
|
+
* Returns { pass: boolean, violations: Array<{id, severity, file, line, message, matchedContent}> }
|
|
113
|
+
*/
|
|
114
|
+
export function scanDiff(diff, config = {}) {
|
|
115
|
+
if (!diff || typeof diff !== "string") {
|
|
116
|
+
return { pass: true, violations: [] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const configGuards = config?.guards || {};
|
|
120
|
+
const patterns = compilePatterns(configGuards);
|
|
121
|
+
const protectedFiles = compileProtectedFiles(configGuards);
|
|
122
|
+
const addedLines = extractAddedLines(diff);
|
|
123
|
+
const violations = [];
|
|
124
|
+
|
|
125
|
+
// Check patterns against added lines
|
|
126
|
+
for (const { file, line, content } of addedLines) {
|
|
127
|
+
for (const { id, pattern, severity, message } of patterns) {
|
|
128
|
+
if (pattern.test(content)) {
|
|
129
|
+
violations.push({ id, severity, file, line, message, matchedContent: content.trim().slice(0, 200) });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check protected files
|
|
135
|
+
violations.push(...checkProtectedFiles(diff, protectedFiles));
|
|
136
|
+
|
|
137
|
+
const hasCritical = violations.some(v => v.severity === "critical");
|
|
138
|
+
return { pass: !hasCritical, violations };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Run output guard on the current git diff.
|
|
143
|
+
* This is the main entry point for the pipeline integration.
|
|
144
|
+
*/
|
|
145
|
+
export async function runOutputGuard(config = {}, baseBranch = "main") {
|
|
146
|
+
const diffResult = await runCommand("git", ["diff", `origin/${baseBranch}...HEAD`]);
|
|
147
|
+
if (diffResult.exitCode !== 0) {
|
|
148
|
+
// Fallback: diff against HEAD~1
|
|
149
|
+
const fallback = await runCommand("git", ["diff", "HEAD~1"]);
|
|
150
|
+
if (fallback.exitCode !== 0) {
|
|
151
|
+
return { pass: true, violations: [], error: "Could not generate diff" };
|
|
152
|
+
}
|
|
153
|
+
return scanDiff(fallback.stdout, config);
|
|
154
|
+
}
|
|
155
|
+
return scanDiff(diffResult.stdout, config);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export { DESTRUCTIVE_PATTERNS, CREDENTIAL_PATTERNS, DEFAULT_PROTECTED_FILES };
|