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.
Files changed (72) hide show
  1. package/package.json +1 -1
  2. package/src/activity-log.js +13 -13
  3. package/src/agents/availability.js +2 -3
  4. package/src/agents/claude-agent.js +42 -21
  5. package/src/agents/model-registry.js +1 -1
  6. package/src/becaria/dispatch.js +1 -1
  7. package/src/becaria/repo.js +3 -3
  8. package/src/cli.js +5 -2
  9. package/src/commands/doctor.js +154 -108
  10. package/src/commands/init.js +101 -90
  11. package/src/commands/plan.js +1 -1
  12. package/src/commands/report.js +77 -71
  13. package/src/commands/roles.js +0 -1
  14. package/src/commands/run.js +2 -3
  15. package/src/config.js +174 -93
  16. package/src/git/automation.js +3 -4
  17. package/src/guards/intent-guard.js +123 -0
  18. package/src/guards/output-guard.js +158 -0
  19. package/src/guards/perf-guard.js +126 -0
  20. package/src/guards/policy-resolver.js +3 -3
  21. package/src/mcp/orphan-guard.js +1 -2
  22. package/src/mcp/progress.js +4 -3
  23. package/src/mcp/run-kj.js +1 -0
  24. package/src/mcp/server-handlers.js +242 -253
  25. package/src/mcp/server.js +4 -3
  26. package/src/mcp/tools.js +2 -0
  27. package/src/orchestrator/agent-fallback.js +1 -3
  28. package/src/orchestrator/iteration-stages.js +206 -170
  29. package/src/orchestrator/pre-loop-stages.js +200 -34
  30. package/src/orchestrator/solomon-rules.js +2 -2
  31. package/src/orchestrator.js +902 -746
  32. package/src/planning-game/adapter.js +23 -20
  33. package/src/planning-game/architect-adrs.js +45 -0
  34. package/src/planning-game/client.js +15 -1
  35. package/src/planning-game/decomposition.js +7 -5
  36. package/src/prompts/architect.js +88 -0
  37. package/src/prompts/discover.js +54 -53
  38. package/src/prompts/planner.js +53 -33
  39. package/src/prompts/triage.js +8 -16
  40. package/src/review/parser.js +18 -19
  41. package/src/review/profiles.js +2 -2
  42. package/src/review/schema.js +3 -3
  43. package/src/review/scope-filter.js +3 -4
  44. package/src/roles/architect-role.js +122 -0
  45. package/src/roles/commiter-role.js +2 -2
  46. package/src/roles/discover-role.js +59 -67
  47. package/src/roles/index.js +1 -0
  48. package/src/roles/planner-role.js +54 -38
  49. package/src/roles/refactorer-role.js +8 -7
  50. package/src/roles/researcher-role.js +6 -7
  51. package/src/roles/reviewer-role.js +4 -5
  52. package/src/roles/security-role.js +3 -4
  53. package/src/roles/solomon-role.js +6 -18
  54. package/src/roles/sonar-role.js +5 -1
  55. package/src/roles/tester-role.js +8 -5
  56. package/src/roles/triage-role.js +2 -2
  57. package/src/session-cleanup.js +29 -24
  58. package/src/session-store.js +1 -1
  59. package/src/sonar/api.js +1 -1
  60. package/src/sonar/manager.js +1 -1
  61. package/src/sonar/project-key.js +5 -5
  62. package/src/sonar/scanner.js +34 -65
  63. package/src/utils/display.js +312 -272
  64. package/src/utils/git.js +3 -3
  65. package/src/utils/logger.js +6 -1
  66. package/src/utils/model-selector.js +5 -5
  67. package/src/utils/process.js +80 -102
  68. package/src/utils/rate-limit-detector.js +13 -13
  69. package/src/utils/run-log.js +55 -52
  70. package/templates/kj.config.yml +33 -0
  71. package/templates/roles/architect.md +62 -0
  72. 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
- export function applyRunOverrides(config, flags) {
225
- const out = mergeDeep(config, {});
226
- out.coder_options = out.coder_options || {};
227
- out.reviewer_options = out.reviewer_options || {};
228
- out.session = out.session || {};
229
- out.git = out.git || {};
230
- out.development = out.development || {};
231
- out.sonarqube = out.sonarqube || {};
232
- if (out.max_budget_usd === undefined || out.max_budget_usd === null) {
233
- out.max_budget_usd = out.session.max_budget_usd ?? null;
234
- }
235
- out.budget = mergeDeep(DEFAULTS.budget, out.budget || {});
236
- out.roles = mergeDeep(DEFAULTS.roles, out.roles || {});
237
- out.pipeline = mergeDeep(DEFAULTS.pipeline, out.pipeline || {});
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
- if (flags.planner) out.roles.planner.provider = flags.planner;
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
- if (flags.reviewer) out.roles.reviewer.provider = flags.reviewer;
244
- if (flags.refactorer) out.roles.refactorer.provider = flags.refactorer;
245
- if (flags.solomon) out.roles.solomon.provider = flags.solomon;
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
- if (flags.reviewerModel) {
258
- out.roles.reviewer.model = String(flags.reviewerModel);
259
- out.reviewer_options.model = String(flags.reviewerModel);
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
- if (flags.enableTriage !== undefined) out.pipeline.triage.enabled = Boolean(flags.enableTriage);
274
- if (flags.mode) out.review_mode = flags.mode;
275
- if (flags.maxIterations) out.max_iterations = Number(flags.maxIterations);
276
- if (flags.maxIterationMinutes) out.session.max_iteration_minutes = Number(flags.maxIterationMinutes);
277
- if (flags.maxTotalMinutes) out.session.max_total_minutes = Number(flags.maxTotalMinutes);
278
- if (flags.checkpointInterval) out.session.checkpoint_interval_minutes = Number(flags.checkpointInterval);
279
- if (flags.baseBranch) out.base_branch = flags.baseBranch;
280
- if (flags.coderFallback) out.coder_options.fallback_coder = flags.coderFallback;
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
- if (flags.taskType) out.taskType = String(flags.taskType);
295
- if (flags.noSonar || flags.sonar === false) out.sonarqube.enabled = false;
296
- out.serena = out.serena || { enabled: false };
297
- if (flags.enableSerena !== undefined) out.serena.enabled = Boolean(flags.enableSerena);
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 !== undefined) {
300
- out.becaria.enabled = Boolean(flags.enableBecaria);
301
- // BecarIA requires git automation (commit + push + PR)
302
- if (out.becaria.enabled) {
303
- out.git.auto_commit = true;
304
- out.git.auto_push = true;
305
- out.git.auto_pr = true;
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 === "run") {
342
- const required = ["coder"];
343
- if (config?.pipeline?.reviewer?.enabled !== false) required.push("reviewer");
344
- if (config?.pipeline?.triage?.enabled) required.push("triage");
345
- if (config?.pipeline?.planner?.enabled) required.push("planner");
346
- if (config?.pipeline?.refactorer?.enabled) required.push("refactorer");
347
- if (config?.pipeline?.researcher?.enabled) required.push("researcher");
348
- if (config?.pipeline?.tester?.enabled) required.push("tester");
349
- if (config?.pipeline?.security?.enabled) required.push("security");
350
- return required;
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
- if (commandName === "discover") return ["discover"];
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"].includes(config.review_mode)) {
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"].includes(config.development?.methodology)) {
445
+ if (!new Set(["tdd", "standard"]).has(config.development?.methodology)) {
365
446
  errors.push(`Invalid development.methodology: ${config.development?.methodology}`);
366
447
  }
367
448
 
@@ -19,7 +19,7 @@ import {
19
19
 
20
20
  export function commitMessageFromTask(task) {
21
21
  const clean = String(task || "")
22
- .replace(/\s+/g, " ")
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 };