karajan-code 1.36.1 → 1.38.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.
@@ -0,0 +1,166 @@
1
+ /**
2
+ * HU Sub-Pipeline — executes each certified HU as an independent sub-pipeline
3
+ * with per-HU state tracking, topological ordering, and dependency-aware blocking.
4
+ */
5
+ import { topologicalSort } from "../hu/graph.js";
6
+ import { updateStoryStatus, loadHuBatch, saveHuBatch, HU_STATUS } from "../hu/store.js";
7
+ import { emitProgress, makeEvent } from "../utils/events.js";
8
+
9
+ /**
10
+ * Determine whether the HU reviewer result requires a sub-pipeline
11
+ * (more than one certified story).
12
+ * @param {object|null} huReviewerResult - stageResults.huReviewer
13
+ * @returns {boolean}
14
+ */
15
+ export function needsSubPipeline(huReviewerResult) {
16
+ if (!huReviewerResult?.ok) return false;
17
+ const certified = (huReviewerResult.stories || []).filter(
18
+ s => s.status === "certified"
19
+ );
20
+ return certified.length > 1;
21
+ }
22
+
23
+ /**
24
+ * Mark all stories that depend (directly or transitively) on a failed story as "blocked".
25
+ * @param {object} batch - The HU batch object.
26
+ * @param {string} failedId - The ID of the failed story.
27
+ * @returns {string[]} IDs of newly blocked stories.
28
+ */
29
+ export function blockDependents(batch, failedId) {
30
+ const blocked = [];
31
+ const failedSet = new Set([failedId]);
32
+ // Iterate until no new blocked stories are found (handles transitive deps)
33
+ let changed = true;
34
+ while (changed) {
35
+ changed = false;
36
+ for (const story of batch.stories) {
37
+ if (failedSet.has(story.id)) continue;
38
+ if (story.status === HU_STATUS.BLOCKED) continue;
39
+ const deps = story.blocked_by || [];
40
+ if (deps.some(dep => failedSet.has(dep))) {
41
+ updateStoryStatus(batch, story.id, HU_STATUS.BLOCKED);
42
+ failedSet.add(story.id);
43
+ blocked.push(story.id);
44
+ changed = true;
45
+ }
46
+ }
47
+ }
48
+ return blocked;
49
+ }
50
+
51
+ /**
52
+ * Build task description for the coder from a certified HU story.
53
+ * @param {object} story - A story object from the batch.
54
+ * @returns {string}
55
+ */
56
+ function buildHuTask(story) {
57
+ if (story.certified?.text) return story.certified.text;
58
+ if (story.original?.text) return story.original.text;
59
+ return `Implement HU ${story.id}`;
60
+ }
61
+
62
+ /**
63
+ * Run all certified HUs as sub-pipelines in topological order.
64
+ *
65
+ * @param {object} params
66
+ * @param {object} params.huReviewerResult - stageResults.huReviewer (from pre-loop)
67
+ * @param {Function} params.runIterationFn - async (task) => { approved, result, ... }
68
+ * Callback that runs the full coder→sonar→reviewer iteration loop for a given task.
69
+ * Returns an object with at least { approved: boolean }.
70
+ * @param {object} params.emitter - Event emitter
71
+ * @param {object} params.eventBase - Base event fields
72
+ * @param {object} params.logger - Logger instance
73
+ * @returns {Promise<{approved: boolean, results: object[], blockedIds: string[]}>}
74
+ */
75
+ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitter, eventBase, logger }) {
76
+ const batchSessionId = huReviewerResult.batchSessionId;
77
+ let batch;
78
+ try {
79
+ batch = await loadHuBatch(batchSessionId);
80
+ } catch {
81
+ logger.warn("HU sub-pipeline: could not load batch, falling back to in-memory stories");
82
+ // Build a minimal batch from the stage result
83
+ batch = {
84
+ session_id: batchSessionId,
85
+ stories: huReviewerResult.stories || []
86
+ };
87
+ }
88
+
89
+ const certifiedStories = batch.stories.filter(s => s.status === HU_STATUS.CERTIFIED);
90
+ let orderedIds;
91
+ try {
92
+ orderedIds = topologicalSort(certifiedStories);
93
+ } catch {
94
+ orderedIds = certifiedStories.map(s => s.id);
95
+ }
96
+
97
+ const results = [];
98
+ const blockedIds = [];
99
+ let allApproved = true;
100
+
101
+ for (const storyId of orderedIds) {
102
+ const story = batch.stories.find(s => s.id === storyId);
103
+ if (!story) continue;
104
+
105
+ // Skip if already blocked by a failed dependency
106
+ if (story.status === HU_STATUS.BLOCKED) {
107
+ blockedIds.push(story.id);
108
+ continue;
109
+ }
110
+
111
+ const huTask = buildHuTask(story);
112
+
113
+ // --- hu:start ---
114
+ emitProgress(emitter, makeEvent("hu:start", { ...eventBase, stage: "hu-sub-pipeline" }, {
115
+ message: `Starting HU ${story.id}`,
116
+ detail: { huId: story.id, title: story.certified?.title || story.id }
117
+ }));
118
+
119
+ // Update status to coding
120
+ updateStoryStatus(batch, story.id, HU_STATUS.CODING);
121
+ await saveHuBatch(batchSessionId, batch);
122
+
123
+ try {
124
+ const iterResult = await runIterationFn(huTask);
125
+ const approved = Boolean(iterResult?.approved);
126
+
127
+ if (approved) {
128
+ updateStoryStatus(batch, story.id, HU_STATUS.DONE);
129
+ emitProgress(emitter, makeEvent("hu:end", { ...eventBase, stage: "hu-sub-pipeline" }, {
130
+ status: "ok",
131
+ message: `HU ${story.id} completed successfully`,
132
+ detail: { huId: story.id, approved: true }
133
+ }));
134
+ results.push({ huId: story.id, approved: true, result: iterResult });
135
+ } else {
136
+ updateStoryStatus(batch, story.id, HU_STATUS.FAILED);
137
+ const newlyBlocked = blockDependents(batch, story.id);
138
+ blockedIds.push(...newlyBlocked);
139
+ allApproved = false;
140
+
141
+ emitProgress(emitter, makeEvent("hu:end", { ...eventBase, stage: "hu-sub-pipeline" }, {
142
+ status: "fail",
143
+ message: `HU ${story.id} failed — ${newlyBlocked.length} dependent(s) blocked`,
144
+ detail: { huId: story.id, approved: false, blockedDependents: newlyBlocked, reason: iterResult?.reason }
145
+ }));
146
+ results.push({ huId: story.id, approved: false, result: iterResult, blockedDependents: newlyBlocked });
147
+ }
148
+ } catch (err) {
149
+ updateStoryStatus(batch, story.id, HU_STATUS.FAILED);
150
+ const newlyBlocked = blockDependents(batch, story.id);
151
+ blockedIds.push(...newlyBlocked);
152
+ allApproved = false;
153
+
154
+ emitProgress(emitter, makeEvent("hu:end", { ...eventBase, stage: "hu-sub-pipeline" }, {
155
+ status: "fail",
156
+ message: `HU ${story.id} threw: ${err.message}`,
157
+ detail: { huId: story.id, approved: false, error: err.message, blockedDependents: newlyBlocked }
158
+ }));
159
+ results.push({ huId: story.id, approved: false, error: err.message, blockedDependents: newlyBlocked });
160
+ }
161
+
162
+ await saveHuBatch(batchSessionId, batch);
163
+ }
164
+
165
+ return { approved: allApproved, results, blockedIds };
166
+ }
@@ -21,7 +21,7 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
21
21
  emitter,
22
22
  makeEvent("coder:start", { ...eventBase, stage: "coder" }, {
23
23
  message: `Coder (${coderRole.provider}) running`,
24
- detail: { coder: coderRole.provider }
24
+ detail: { coder: coderRole.provider, provider: coderRole.provider, executorType: "agent" }
25
25
  })
26
26
  );
27
27
 
@@ -89,7 +89,8 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
89
89
  emitProgress(
90
90
  emitter,
91
91
  makeEvent("coder:end", { ...eventBase, stage: "coder" }, {
92
- message: `Coder completed (fallback: ${fallbackCoder})`
92
+ message: `Coder completed (fallback: ${fallbackCoder})`,
93
+ detail: { provider: fallbackCoder, executorType: "agent" }
93
94
  })
94
95
  );
95
96
  return;
@@ -114,7 +115,8 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
114
115
  emitter,
115
116
  makeEvent("coder:end", { ...eventBase, stage: "coder" }, {
116
117
  status: "fail",
117
- message: `Coder failed: ${details}`
118
+ message: `Coder failed: ${details}`,
119
+ detail: { provider: coderRole.provider, executorType: "agent" }
118
120
  })
119
121
  );
120
122
  throw new Error(`Coder failed: ${details}`);
@@ -124,7 +126,8 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
124
126
  emitProgress(
125
127
  emitter,
126
128
  makeEvent("coder:end", { ...eventBase, stage: "coder" }, {
127
- message: "Coder completed"
129
+ message: "Coder completed",
130
+ detail: { provider: coderRole.provider, executorType: "agent" }
128
131
  })
129
132
  );
130
133
  }
@@ -135,7 +138,7 @@ export async function runRefactorerStage({ refactorerRole, config, logger, emitt
135
138
  emitter,
136
139
  makeEvent("refactorer:start", { ...eventBase, stage: "refactorer" }, {
137
140
  message: `Refactorer (${refactorerRole.provider}) running`,
138
- detail: { refactorer: refactorerRole.provider }
141
+ detail: { refactorer: refactorerRole.provider, provider: refactorerRole.provider, executorType: "agent" }
139
142
  })
140
143
  );
141
144
  const refactorerOnOutput = ({ stream, line }) => {
@@ -184,7 +187,8 @@ export async function runRefactorerStage({ refactorerRole, config, logger, emitt
184
187
  emitter,
185
188
  makeEvent("refactorer:end", { ...eventBase, stage: "refactorer" }, {
186
189
  status: "fail",
187
- message: `Refactorer failed: ${details}`
190
+ message: `Refactorer failed: ${details}`,
191
+ detail: { provider: refactorerRole.provider, executorType: "agent" }
188
192
  })
189
193
  );
190
194
  throw new Error(`Refactorer failed: ${details}`);
@@ -193,7 +197,8 @@ export async function runRefactorerStage({ refactorerRole, config, logger, emitt
193
197
  emitProgress(
194
198
  emitter,
195
199
  makeEvent("refactorer:end", { ...eventBase, stage: "refactorer" }, {
196
- message: "Refactorer completed"
200
+ message: "Refactorer completed",
201
+ detail: { provider: refactorerRole.provider, executorType: "agent" }
197
202
  })
198
203
  );
199
204
  }
@@ -287,7 +292,8 @@ export async function runTddCheckStage({ config, logger, emitter, eventBase, ses
287
292
  ok: tddEval.ok,
288
293
  reason: tddEval.reason,
289
294
  sourceFiles: tddEval.sourceFiles?.length || 0,
290
- testFiles: tddEval.testFiles?.length || 0
295
+ testFiles: tddEval.testFiles?.length || 0,
296
+ executorType: "local"
291
297
  }
292
298
  })
293
299
  );
@@ -386,7 +392,8 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
386
392
  emitProgress(
387
393
  emitter,
388
394
  makeEvent("sonar:start", { ...eventBase, stage: "sonar" }, {
389
- message: "SonarQube scanning"
395
+ message: "SonarQube scanning",
396
+ detail: { provider: "sonarqube", executorType: "local" }
390
397
  })
391
398
  );
392
399
 
@@ -470,7 +477,7 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
470
477
  makeEvent("sonar:end", { ...eventBase, stage: "sonar" }, {
471
478
  status: sonarResult.blocking ? "fail" : "ok",
472
479
  message: `Quality gate: ${sonarResult.gateStatus}`,
473
- detail: { projectKey: sonarResult.projectKey, gateStatus: sonarResult.gateStatus, openIssues: sonarResult.openIssuesTotal }
480
+ detail: { projectKey: sonarResult.projectKey, gateStatus: sonarResult.gateStatus, openIssues: sonarResult.openIssuesTotal, provider: "sonarqube", executorType: "local" }
474
481
  })
475
482
  );
476
483
 
@@ -498,7 +505,8 @@ export async function runSonarCloudStage({ config, logger, emitter, eventBase, s
498
505
  emitProgress(
499
506
  emitter,
500
507
  makeEvent("sonarcloud:start", { ...eventBase, stage: "sonarcloud" }, {
501
- message: "SonarCloud scanning"
508
+ message: "SonarCloud scanning",
509
+ detail: { provider: "sonarcloud", executorType: "local" }
502
510
  })
503
511
  );
504
512
 
@@ -532,7 +540,7 @@ export async function runSonarCloudStage({ config, logger, emitter, eventBase, s
532
540
  makeEvent("sonarcloud:end", { ...eventBase, stage: "sonarcloud" }, {
533
541
  status,
534
542
  message,
535
- detail: { projectKey: result.projectKey, exitCode: result.exitCode }
543
+ detail: { projectKey: result.projectKey, exitCode: result.exitCode, provider: "sonarcloud", executorType: "local" }
536
544
  })
537
545
  );
538
546
 
@@ -681,7 +689,7 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
681
689
  emitter,
682
690
  makeEvent("reviewer:start", { ...eventBase, stage: "reviewer" }, {
683
691
  message: `Reviewer (${reviewerRole.provider}) running`,
684
- detail: { reviewer: reviewerRole.provider }
692
+ detail: { reviewer: reviewerRole.provider, provider: reviewerRole.provider, executorType: "agent" }
685
693
  })
686
694
  );
687
695
 
@@ -692,6 +700,30 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
692
700
  logger.warn(`Review diff generation failed: ${err.message}`);
693
701
  return { approved: false, blocking_issues: [{ description: `Diff generation failed: ${err.message}` }], non_blocking_suggestions: [], summary: `Reviewer failed: cannot generate diff — ${err.message}`, confidence: 0 };
694
702
  }
703
+
704
+ // Injection guard: scan diff before sending to AI reviewer
705
+ const { scanDiff } = await import("../utils/injection-guard.js");
706
+ const guardResult = scanDiff(diff);
707
+ if (!guardResult.clean) {
708
+ logger.warn(`Injection guard: ${guardResult.summary}`);
709
+ emitProgress(emitter, makeEvent("guard:injection", { ...eventBase, stage: "reviewer" }, {
710
+ message: `Injection guard blocked review: ${guardResult.summary}`,
711
+ detail: { findings: guardResult.findings, summary: guardResult.summary }
712
+ }));
713
+ return {
714
+ approved: false,
715
+ blocking_issues: guardResult.findings.map((f) => ({
716
+ id: `INJECTION_${f.type.toUpperCase()}`,
717
+ severity: "critical",
718
+ description: `Potential prompt injection (${f.type}): ${f.snippet}`,
719
+ line: f.line,
720
+ })),
721
+ non_blocking_suggestions: [],
722
+ summary: `Review blocked by injection guard: ${guardResult.summary}`,
723
+ confidence: 1,
724
+ };
725
+ }
726
+
695
727
  const reviewerOnOutput = ({ stream, line }) => {
696
728
  emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "reviewer" }, {
697
729
  message: line,
@@ -754,7 +786,8 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
754
786
  emitter,
755
787
  makeEvent("reviewer:end", { ...eventBase, stage: "reviewer" }, {
756
788
  status: "fail",
757
- message: `Reviewer failed: ${details}`
789
+ message: `Reviewer failed: ${details}`,
790
+ detail: { provider: reviewerRole.provider, executorType: "agent" }
758
791
  })
759
792
  );
760
793
  throw new Error(`Reviewer failed: ${details}`);
@@ -836,7 +869,9 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
836
869
  blockingCount: review.blocking_issues.length,
837
870
  issues: review.blocking_issues.map(
838
871
  (x) => `${x.id || "ISSUE"}: ${x.description || "Missing description"}`
839
- )
872
+ ),
873
+ provider: reviewerRole.provider,
874
+ executorType: "agent"
840
875
  }
841
876
  })
842
877
  );
@@ -98,7 +98,8 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
98
98
  emitProgress(
99
99
  emitter,
100
100
  makeEvent("tester:start", { ...eventBase, stage: "tester" }, {
101
- message: "Tester evaluating test quality"
101
+ message: "Tester evaluating test quality",
102
+ detail: { provider: config?.roles?.tester?.provider || config?.roles?.coder?.provider || config?.coder || "claude", executorType: "agent" }
102
103
  })
103
104
  );
104
105
 
@@ -126,11 +127,13 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
126
127
  attempts: attempts.length > 1 ? attempts : undefined
127
128
  });
128
129
 
130
+ const testerProvider = provider || coderRole.provider;
129
131
  emitProgress(
130
132
  emitter,
131
133
  makeEvent("tester:end", { ...eventBase, stage: "tester" }, {
132
134
  status: testerOutput.ok ? "ok" : "fail",
133
- message: testerOutput.ok ? "Tester passed" : `Tester: ${testerOutput.summary}`
135
+ message: testerOutput.ok ? "Tester passed" : `Tester: ${testerOutput.summary}`,
136
+ detail: { ok: testerOutput.ok, summary: testerOutput.summary, provider: testerProvider, executorType: "agent" }
134
137
  })
135
138
  );
136
139
 
@@ -143,7 +146,7 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
143
146
  makeEvent("tester:auto-continue", { ...eventBase, stage: "tester" }, {
144
147
  status: "warn",
145
148
  message: `Tester issues are advisory (reviewer approved), continuing: ${testerOutput.summary}`,
146
- detail: { summary: testerOutput.summary, auto_continued: true }
149
+ detail: { summary: testerOutput.summary, auto_continued: true, provider: testerProvider, executorType: "agent" }
147
150
  })
148
151
  );
149
152
  return { action: "ok", stageResult: { ok: false, summary: testerOutput.summary || "Tester issues (advisory)", auto_continued: true } };
@@ -158,7 +161,8 @@ export async function runSecurityStage({ config, logger, emitter, eventBase, ses
158
161
  emitProgress(
159
162
  emitter,
160
163
  makeEvent("security:start", { ...eventBase, stage: "security" }, {
161
- message: "Security auditing code"
164
+ message: "Security auditing code",
165
+ detail: { provider: config?.roles?.security?.provider || config?.roles?.coder?.provider || config?.coder || "claude", executorType: "agent" }
162
166
  })
163
167
  );
164
168
 
@@ -186,11 +190,13 @@ export async function runSecurityStage({ config, logger, emitter, eventBase, ses
186
190
  attempts: attempts.length > 1 ? attempts : undefined
187
191
  });
188
192
 
193
+ const securityProvider = provider || coderRole.provider;
189
194
  emitProgress(
190
195
  emitter,
191
196
  makeEvent("security:end", { ...eventBase, stage: "security" }, {
192
197
  status: securityOutput.ok ? "ok" : "fail",
193
- message: securityOutput.ok ? "Security audit passed" : `Security: ${securityOutput.summary}`
198
+ message: securityOutput.ok ? "Security audit passed" : `Security: ${securityOutput.summary}`,
199
+ detail: { ok: securityOutput.ok, summary: securityOutput.summary, provider: securityProvider, executorType: "agent" }
194
200
  })
195
201
  );
196
202
 
@@ -246,7 +252,8 @@ export async function runImpeccableStage({ config, logger, emitter, eventBase, s
246
252
  emitProgress(
247
253
  emitter,
248
254
  makeEvent("impeccable:start", { ...eventBase, stage: "impeccable" }, {
249
- message: "Impeccable auditing frontend design quality"
255
+ message: "Impeccable auditing frontend design quality",
256
+ detail: { provider: config?.roles?.impeccable?.provider || coderRole.provider, executorType: "agent" }
250
257
  })
251
258
  );
252
259
 
@@ -277,13 +284,15 @@ export async function runImpeccableStage({ config, logger, emitter, eventBase, s
277
284
  });
278
285
 
279
286
  const verdict = impeccableOutput.result?.verdict || "APPROVED";
287
+ const impeccableProvider = config?.roles?.impeccable?.provider || coderRole.provider;
280
288
  emitProgress(
281
289
  emitter,
282
290
  makeEvent("impeccable:end", { ...eventBase, stage: "impeccable" }, {
283
291
  status: impeccableOutput.ok ? "ok" : "fail",
284
292
  message: impeccableOutput.ok
285
293
  ? (verdict === "IMPROVED" ? "Impeccable applied design fixes" : "Impeccable audit passed")
286
- : `Impeccable: ${impeccableOutput.summary}`
294
+ : `Impeccable: ${impeccableOutput.summary}`,
295
+ detail: { provider: impeccableProvider, executorType: "agent" }
287
296
  })
288
297
  );
289
298
 
@@ -296,7 +305,8 @@ export async function runFinalAuditStage({ config, logger, emitter, eventBase, s
296
305
  emitProgress(
297
306
  emitter,
298
307
  makeEvent("audit:start", { ...eventBase, stage: "audit" }, {
299
- message: "Final audit — verifying code quality"
308
+ message: "Final audit — verifying code quality",
309
+ detail: { provider: config?.roles?.audit?.provider || config?.roles?.coder?.provider || config?.coder || "claude", executorType: "agent" }
300
310
  })
301
311
  );
302
312
 
@@ -324,6 +334,7 @@ export async function runFinalAuditStage({ config, logger, emitter, eventBase, s
324
334
  attempts: attempts.length > 1 ? attempts : undefined
325
335
  });
326
336
 
337
+ const auditProvider = provider || coderRole.provider;
327
338
  if (!auditOutput.ok) {
328
339
  // Audit agent failed to run — treat as advisory, don't block pipeline
329
340
  logger.warn(`Audit agent error (advisory): ${auditOutput.summary}`);
@@ -331,7 +342,8 @@ export async function runFinalAuditStage({ config, logger, emitter, eventBase, s
331
342
  emitter,
332
343
  makeEvent("audit:end", { ...eventBase, stage: "audit" }, {
333
344
  status: "warn",
334
- message: `Audit: agent error (advisory), continuing — ${auditOutput.summary}`
345
+ message: `Audit: agent error (advisory), continuing — ${auditOutput.summary}`,
346
+ detail: { provider: auditProvider, executorType: "agent" }
335
347
  })
336
348
  );
337
349
  return { action: "ok", stageResult: { ok: false, summary: auditOutput.summary || "Audit agent error (advisory)", auto_continued: true } };
@@ -374,7 +386,8 @@ export async function runFinalAuditStage({ config, logger, emitter, eventBase, s
374
386
  emitter,
375
387
  makeEvent("audit:end", { ...eventBase, stage: "audit" }, {
376
388
  status: "fail",
377
- message: `Audit: ${criticalCount + highCount} issue(s) found, sending back to coder`
389
+ message: `Audit: ${criticalCount + highCount} issue(s) found, sending back to coder`,
390
+ detail: { provider: auditProvider, executorType: "agent" }
378
391
  })
379
392
  );
380
393
 
@@ -392,7 +405,8 @@ export async function runFinalAuditStage({ config, logger, emitter, eventBase, s
392
405
  emitter,
393
406
  makeEvent("audit:end", { ...eventBase, stage: "audit" }, {
394
407
  status: "ok",
395
- message: certifiedMsg
408
+ message: certifiedMsg,
409
+ detail: { provider: auditProvider, executorType: "agent" }
396
410
  })
397
411
  );
398
412