karajan-code 1.59.0 → 2.0.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 (115) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/scripts/install.js +18 -15
  4. package/src/agents/aider-agent.js +0 -7
  5. package/src/agents/claude-agent.js +34 -6
  6. package/src/agents/codex-agent.js +2 -6
  7. package/src/agents/gemini-agent.js +0 -4
  8. package/src/agents/opencode-agent.js +0 -4
  9. package/src/{becaria → ci}/dispatch.js +13 -13
  10. package/src/{becaria → ci}/pr-diff.js +1 -1
  11. package/src/cli.js +3 -2
  12. package/src/commands/agents.js +0 -1
  13. package/src/commands/doctor.js +22 -40
  14. package/src/commands/init.js +8 -8
  15. package/src/commands/report.js +0 -13
  16. package/src/commands/review.js +17 -17
  17. package/src/commands/run.js +1 -2
  18. package/src/commands/status.js +1 -7
  19. package/src/config.js +10 -52
  20. package/src/domains/domain-loader.js +85 -4
  21. package/src/git/automation.js +9 -9
  22. package/src/guards/output-guard.js +0 -17
  23. package/src/mcp/handlers/direct-handlers.js +1 -1
  24. package/src/mcp/handlers/run-handler.js +1 -2
  25. package/src/mcp/sovereignty-guard.js +2 -2
  26. package/src/mcp/tools.js +3 -2
  27. package/src/orchestrator/brain-coordinator.js +165 -0
  28. package/src/orchestrator/ci-integration.js +139 -0
  29. package/src/orchestrator/config-init.js +435 -0
  30. package/src/orchestrator/direct-actions.js +186 -0
  31. package/src/orchestrator/feedback-enrichment.js +140 -0
  32. package/src/orchestrator/feedback-queue.js +152 -0
  33. package/src/orchestrator/flow-control.js +159 -0
  34. package/src/orchestrator/post-loop-stages.js +20 -15
  35. package/src/orchestrator/preflight-checks.js +7 -3
  36. package/src/orchestrator/role-output-compressor.js +190 -0
  37. package/src/orchestrator/session-journal.js +344 -0
  38. package/src/orchestrator/solomon-escalation.js +38 -5
  39. package/src/orchestrator/solomon-rules.js +32 -33
  40. package/src/orchestrator/stages/reviewer-stage.js +16 -5
  41. package/src/orchestrator/stages/sonar-stage.js +24 -1
  42. package/src/orchestrator/verification-gate.js +146 -0
  43. package/src/orchestrator.js +435 -661
  44. package/src/planning-game/client.js +2 -2
  45. package/src/planning-game/pipeline-adapter.js +1 -1
  46. package/src/prompts/coder.js +17 -3
  47. package/src/prompts/reviewer.js +2 -2
  48. package/src/prompts/rtk-snippet.js +3 -5
  49. package/src/review/diff-generator.js +66 -17
  50. package/src/review/snapshot-diff.js +122 -0
  51. package/src/review/tdd-policy.js +4 -2
  52. package/src/roles/agent-role.js +135 -0
  53. package/src/roles/architect-role.js +61 -108
  54. package/src/roles/audit-role.js +25 -71
  55. package/src/roles/coder-role.js +32 -57
  56. package/src/roles/discover-role.js +30 -71
  57. package/src/roles/hu-reviewer-role.js +51 -99
  58. package/src/roles/impeccable-role.js +34 -91
  59. package/src/roles/index.js +2 -0
  60. package/src/roles/karajan-brain-role.js +134 -0
  61. package/src/roles/planner-role.js +40 -75
  62. package/src/roles/refactorer-role.js +22 -64
  63. package/src/roles/researcher-role.js +52 -118
  64. package/src/roles/reviewer-role.js +69 -126
  65. package/src/roles/security-role.js +38 -102
  66. package/src/roles/solomon-role.js +71 -15
  67. package/src/roles/tester-role.js +121 -95
  68. package/src/roles/triage-role.js +50 -111
  69. package/src/session-store.js +1 -1
  70. package/src/skills/skill-detector.js +11 -0
  71. package/src/sonar/config-resolver.js +20 -1
  72. package/src/sonar/credentials.js +17 -0
  73. package/src/sonar/project-key.js +1 -1
  74. package/src/sonar/scanner.js +1 -1
  75. package/src/utils/banner.js +66 -0
  76. package/src/utils/display.js +35 -17
  77. package/src/utils/injection-guard.js +23 -8
  78. package/src/utils/logger.js +6 -1
  79. package/src/utils/process.js +3 -4
  80. package/src/utils/role-assigner.js +131 -0
  81. package/src/utils/update-check.js +5 -1
  82. package/src/utils/welcome.js +3 -8
  83. package/src/webperf/devtools-detect.js +70 -23
  84. package/templates/roles/coder.md +7 -0
  85. package/templates/roles/karajan-brain.md +95 -0
  86. package/templates/roles/solomon.md +67 -106
  87. package/templates/workflows/automerge.yml +4 -4
  88. package/templates/workflows/houston-override.yml +4 -4
  89. package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
  90. package/src/proxy/adapters/anthropic.js +0 -131
  91. package/src/proxy/adapters/gemini.js +0 -106
  92. package/src/proxy/adapters/index.js +0 -51
  93. package/src/proxy/adapters/openai.js +0 -71
  94. package/src/proxy/compression/ai-compressor.js +0 -209
  95. package/src/proxy/compression/dedup-cache.js +0 -100
  96. package/src/proxy/compression/deterministic/bash-build.js +0 -60
  97. package/src/proxy/compression/deterministic/bash-git.js +0 -101
  98. package/src/proxy/compression/deterministic/bash-infra.js +0 -64
  99. package/src/proxy/compression/deterministic/bash-misc.js +0 -53
  100. package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
  101. package/src/proxy/compression/deterministic/bash-test.js +0 -61
  102. package/src/proxy/compression/deterministic/glob.js +0 -26
  103. package/src/proxy/compression/deterministic/grep.js +0 -53
  104. package/src/proxy/compression/deterministic/index.js +0 -58
  105. package/src/proxy/compression/deterministic/read.js +0 -22
  106. package/src/proxy/compression/deterministic/utils.js +0 -33
  107. package/src/proxy/compression/pipeline.js +0 -270
  108. package/src/proxy/middleware/prompt-injector.js +0 -165
  109. package/src/proxy/middleware/response-interceptor.js +0 -223
  110. package/src/proxy/progress-monitor.js +0 -98
  111. package/src/proxy/proxy-lifecycle.js +0 -198
  112. package/src/proxy/proxy-server.js +0 -239
  113. package/src/proxy/security.js +0 -45
  114. /package/src/{becaria → ci}/index.js +0 -0
  115. /package/src/{becaria → ci}/repo.js +0 -0
@@ -17,8 +17,10 @@ import { msg, getLang } from "../utils/messages.js";
17
17
  import {
18
18
  resolveSonarHost,
19
19
  resolveSonarToken,
20
+ resolveSonarTokenAsync,
20
21
  resolveSonarCredentials,
21
22
  } from "../sonar/config-resolver.js";
23
+ import { saveSonarToken } from "../sonar/credentials.js";
22
24
  import { withDocLink } from "../utils/doc-links.js";
23
25
 
24
26
  function parseJsonSafe(text) {
@@ -75,8 +77,8 @@ async function checkSonarReachable(host) {
75
77
  async function checkSonarAuth(config) {
76
78
  const host = resolveSonarHost(config.sonarqube?.host);
77
79
 
78
- // Check explicit token first
79
- const explicitToken = resolveSonarToken(config);
80
+ // Check explicit token first (including persisted tokens from credentials file)
81
+ const explicitToken = await resolveSonarTokenAsync(config);
80
82
  if (explicitToken) {
81
83
  // Validate the token works
82
84
  const res = await runCommand("curl", [
@@ -117,7 +119,9 @@ async function checkSonarAuth(config) {
117
119
  if (tokenRes.exitCode !== 0) continue;
118
120
  const tokenParsed = parseJsonSafe(tokenRes.stdout);
119
121
  if (tokenParsed?.token) {
120
- return { name: "sonar-auth", ok: true, detail: "Sonar token generated", token: tokenParsed.token };
122
+ // Persist the generated token for future sessions
123
+ try { await saveSonarToken(tokenParsed.token); } catch { /* non-blocking */ }
124
+ return { name: "sonar-auth", ok: true, detail: "Sonar token generated and saved", token: tokenParsed.token };
121
125
  }
122
126
  }
123
127
 
@@ -0,0 +1,190 @@
1
+ // Compress role outputs before passing as context to the next role.
2
+ // Reduces token usage by extracting essentials and dropping verbose explanations.
3
+
4
+ /**
5
+ * Compression strategies per role output type.
6
+ */
7
+ const COMPRESSORS = {
8
+ researcher: compressResearcher,
9
+ architect: compressArchitect,
10
+ planner: compressPlanner,
11
+ reviewer: compressReviewer,
12
+ tester: compressTester,
13
+ security: compressSecurity,
14
+ sonar: compressSonar,
15
+ default: compressDefault
16
+ };
17
+
18
+ /**
19
+ * Extract essentials from researcher output.
20
+ */
21
+ function compressResearcher(output) {
22
+ if (!output || typeof output !== "object") return output;
23
+ const parts = [];
24
+ if (output.affected_files?.length) parts.push(`Files: ${output.affected_files.join(", ")}`);
25
+ if (output.patterns?.length) parts.push(`Patterns: ${output.patterns.join(", ")}`);
26
+ if (output.risks?.length) parts.push(`Risks: ${output.risks.join(", ")}`);
27
+ if (output.constraints?.length) parts.push(`Constraints: ${output.constraints.join(", ")}`);
28
+ return parts.join("\n");
29
+ }
30
+
31
+ /**
32
+ * Extract essentials from architect output.
33
+ */
34
+ function compressArchitect(output) {
35
+ if (!output || typeof output !== "object") return output;
36
+ const arch = output.architecture || output;
37
+ const parts = [];
38
+ if (arch.type) parts.push(`Type: ${arch.type}`);
39
+ if (arch.layers?.length) parts.push(`Layers: ${arch.layers.join(" → ")}`);
40
+ if (arch.patterns?.length) parts.push(`Patterns: ${arch.patterns.join(", ")}`);
41
+ const entities = arch.dataModel?.entities;
42
+ if (entities?.length) {
43
+ const names = entities.map(e => typeof e === "string" ? e : e.name).filter(Boolean);
44
+ if (names.length) parts.push(`Entities: ${names.join(", ")}`);
45
+ }
46
+ if (output.verdict) parts.push(`Verdict: ${output.verdict}`);
47
+ return parts.join("\n");
48
+ }
49
+
50
+ /**
51
+ * Extract essentials from planner output — keep numbered steps only.
52
+ */
53
+ function compressPlanner(output) {
54
+ if (!output || typeof output !== "object") return output;
55
+ const plan = output.plan || output;
56
+ if (typeof plan !== "string") return JSON.stringify(plan);
57
+
58
+ // Keep only numbered steps, drop prose
59
+ const lines = plan.split("\n").filter(l => l.trim());
60
+ const steps = lines.filter(l => /^\s*\d+[.)]\s/.test(l) || /^\s*[-*]\s/.test(l));
61
+ if (steps.length >= 3) return steps.join("\n");
62
+ return plan.slice(0, 1500);
63
+ }
64
+
65
+ /**
66
+ * Compress reviewer output — drop non-blocking suggestions, keep blocking.
67
+ */
68
+ function compressReviewer(output) {
69
+ if (!output || typeof output !== "object") return output;
70
+ const blocking = output.blocking_issues || [];
71
+ if (!blocking.length) return `Approved${output.summary ? `: ${output.summary.slice(0, 200)}` : ""}`;
72
+
73
+ const items = blocking.map((x, i) => {
74
+ const sev = x.severity ? `[${x.severity}]` : "";
75
+ const loc = x.file ? ` (${x.file}${x.line ? `:${x.line}` : ""})` : "";
76
+ return `${i + 1}. ${sev}${loc} ${x.description || "no description"}${x.suggested_fix ? ` — Fix: ${x.suggested_fix}` : ""}`;
77
+ });
78
+ return `Rejected (${blocking.length} blocking):\n${items.join("\n")}`;
79
+ }
80
+
81
+ /**
82
+ * Compress tester output — keep verdict + coverage + missing scenarios.
83
+ */
84
+ function compressTester(output) {
85
+ if (!output || typeof output !== "object") return output;
86
+ const parts = [];
87
+ parts.push(`Verdict: ${output.verdict || "unknown"}`);
88
+ if (output.coverage?.overall != null) parts.push(`Coverage: ${output.coverage.overall}%`);
89
+ if (output.missing_scenarios?.length) {
90
+ parts.push(`Missing: ${output.missing_scenarios.slice(0, 5).join("; ")}`);
91
+ }
92
+ if (output.quality_issues?.length) {
93
+ parts.push(`Quality: ${output.quality_issues.slice(0, 3).join("; ")}`);
94
+ }
95
+ return parts.join("\n");
96
+ }
97
+
98
+ /**
99
+ * Compress security output — keep verdict + grouped vulnerabilities.
100
+ */
101
+ function compressSecurity(output) {
102
+ if (!output || typeof output !== "object") return output;
103
+ const vulns = output.vulnerabilities || [];
104
+ if (vulns.length === 0) return `Verdict: ${output.verdict || "pass"}; no vulnerabilities`;
105
+
106
+ const bySev = {};
107
+ for (const v of vulns) {
108
+ const sev = v.severity || "unknown";
109
+ bySev[sev] = bySev[sev] || [];
110
+ bySev[sev].push(`${v.file || "?"}${v.line ? `:${v.line}` : ""} ${v.description}`);
111
+ }
112
+
113
+ const sevOrder = ["critical", "high", "medium", "low"];
114
+ const lines = [`Verdict: ${output.verdict || "fail"}`];
115
+ for (const sev of sevOrder) {
116
+ if (bySev[sev]?.length) lines.push(`${sev}: ${bySev[sev].slice(0, 3).join("; ")}`);
117
+ }
118
+ return lines.join("\n");
119
+ }
120
+
121
+ /**
122
+ * Compress sonar output — keep gate status + top issues.
123
+ */
124
+ function compressSonar(output) {
125
+ if (!output || typeof output !== "object") return output;
126
+ const parts = [];
127
+ if (output.gateStatus) parts.push(`Gate: ${output.gateStatus}`);
128
+ if (output.issues?.length) {
129
+ parts.push(`${output.issues.length} issue(s)`);
130
+ const top = output.issues.slice(0, 5).map(i => `${i.severity}: ${i.message}`);
131
+ parts.push(top.join("; "));
132
+ }
133
+ return parts.join("\n");
134
+ }
135
+
136
+ /**
137
+ * Default compression: truncate long strings, drop noise.
138
+ */
139
+ function compressDefault(output) {
140
+ if (!output) return "";
141
+ if (typeof output === "string") {
142
+ return output.length > 2000 ? output.slice(0, 2000) + "...[truncated]" : output;
143
+ }
144
+ if (typeof output === "object") {
145
+ const str = JSON.stringify(output, null, 2);
146
+ return str.length > 2000 ? str.slice(0, 2000) + "...[truncated]" : str;
147
+ }
148
+ return String(output);
149
+ }
150
+
151
+ /**
152
+ * Compress a role output.
153
+ * @param {string} roleName - e.g. "researcher", "reviewer"
154
+ * @param {*} output - role's result object
155
+ * @returns {string} compressed text
156
+ */
157
+ export function compressRoleOutput(roleName, output) {
158
+ const compressor = COMPRESSORS[roleName] || COMPRESSORS.default;
159
+ try {
160
+ return compressor(output);
161
+ } catch {
162
+ return COMPRESSORS.default(output);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Estimate token count for a compressed string (rough: chars/4).
168
+ */
169
+ export function estimateTokens(text) {
170
+ if (!text) return 0;
171
+ return Math.ceil(String(text).length / 4);
172
+ }
173
+
174
+ /**
175
+ * Measure compression ratio.
176
+ */
177
+ export function measureCompression(original, compressed) {
178
+ const origTokens = estimateTokens(typeof original === "string" ? original : JSON.stringify(original));
179
+ const compTokens = estimateTokens(compressed);
180
+ const saved = origTokens - compTokens;
181
+ const ratio = origTokens > 0 ? saved / origTokens : 0;
182
+ return {
183
+ originalTokens: origTokens,
184
+ compressedTokens: compTokens,
185
+ savedTokens: saved,
186
+ savedPct: Number((ratio * 100).toFixed(1))
187
+ };
188
+ }
189
+
190
+ export { COMPRESSORS };
@@ -0,0 +1,344 @@
1
+ // Session Journal: persists structured stage outputs and generates summaries.
2
+ // Creates .reviews/session_*/ directories containing markdown files per stage.
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { runCommand } from "../utils/process.js";
6
+
7
+ /**
8
+ * Create the journal directory for a session.
9
+ * @param {string} reportDir -base directory (e.g. ".reviews")
10
+ * @param {string} sessionId
11
+ * @returns {Promise<string>} absolute path to the journal directory
12
+ */
13
+ export async function createJournalDir(reportDir, sessionId) {
14
+ const journalDir = path.resolve(reportDir, sessionId);
15
+ await fs.mkdir(journalDir, { recursive: true });
16
+ return journalDir;
17
+ }
18
+
19
+ /**
20
+ * Write a markdown file to the journal directory.
21
+ * Only writes if content is non-empty. Skips silently otherwise.
22
+ */
23
+ async function writeJournalFile(journalDir, filename, content) {
24
+ if (!content || !journalDir) return;
25
+ const filePath = path.join(journalDir, filename);
26
+ await fs.writeFile(filePath, content, "utf8");
27
+ }
28
+
29
+ // ── Stage serializers ────────────────────────────────────────────────────────
30
+
31
+ function serializeDiscovery(stageResult) {
32
+ if (!stageResult) return null;
33
+ const lines = ["# Discovery\n"];
34
+ lines.push(`**Verdict**: ${stageResult.verdict || "N/A"}`);
35
+ lines.push(`**Mode**: ${stageResult.mode || "gaps"}\n`);
36
+
37
+ const gaps = stageResult.gaps || [];
38
+ if (gaps.length) {
39
+ lines.push("## Gaps Found\n");
40
+ for (const gap of gaps) {
41
+ const severity = gap.severity ? ` [${gap.severity}]` : "";
42
+ lines.push(`- ${gap.title || gap.description || gap}${severity}`);
43
+ }
44
+ }
45
+
46
+ if (stageResult.momTestQuestions?.length) {
47
+ lines.push("\n## Mom Test Questions\n");
48
+ for (const q of stageResult.momTestQuestions) lines.push(`- ${q}`);
49
+ }
50
+ if (stageResult.wendelChecklist?.length) {
51
+ lines.push("\n## Wendel Checklist\n");
52
+ for (const item of stageResult.wendelChecklist) lines.push(`- [${item.status}] ${item.condition}`);
53
+ }
54
+
55
+ return lines.join("\n");
56
+ }
57
+
58
+ function serializeResearch(stageResult) {
59
+ if (!stageResult) return null;
60
+ const lines = ["# Research\n"];
61
+
62
+ const sections = [
63
+ { key: "affected_files", title: "Affected Files" },
64
+ { key: "patterns", title: "Patterns" },
65
+ { key: "constraints", title: "Constraints" },
66
+ { key: "risks", title: "Risks" },
67
+ { key: "prior_decisions", title: "Prior Decisions" }
68
+ ];
69
+
70
+ for (const { key, title } of sections) {
71
+ const items = stageResult[key];
72
+ if (items?.length) {
73
+ lines.push(`## ${title}\n`);
74
+ for (const item of items) lines.push(`- ${item}`);
75
+ lines.push("");
76
+ }
77
+ }
78
+
79
+ if (stageResult.test_coverage) {
80
+ lines.push(`## Test Coverage\n\n${stageResult.test_coverage}`);
81
+ }
82
+
83
+ return lines.join("\n");
84
+ }
85
+
86
+ function serializeArchitecture(stageResult) {
87
+ if (!stageResult) return null;
88
+ const lines = ["# Architecture\n"];
89
+ lines.push(`**Verdict**: ${stageResult.verdict || "N/A"}\n`);
90
+
91
+ const arch = stageResult.architecture || {};
92
+ if (arch.type) lines.push(`**Type**: ${arch.type}\n`);
93
+ if (arch.layers?.length) {
94
+ lines.push("## Layers\n");
95
+ for (const l of arch.layers) lines.push(`- ${l}`);
96
+ lines.push("");
97
+ }
98
+ if (arch.patterns?.length) {
99
+ lines.push("## Patterns\n");
100
+ for (const p of arch.patterns) lines.push(`- ${p}`);
101
+ lines.push("");
102
+ }
103
+ if (arch.dataModel?.entities?.length) {
104
+ lines.push("## Data Model\n");
105
+ for (const e of arch.dataModel.entities) lines.push(`- ${typeof e === "string" ? e : e.name || JSON.stringify(e)}`);
106
+ lines.push("");
107
+ }
108
+ if (arch.apiContracts?.length) {
109
+ lines.push("## API Contracts\n");
110
+ for (const a of arch.apiContracts) lines.push(`- ${typeof a === "string" ? a : JSON.stringify(a)}`);
111
+ lines.push("");
112
+ }
113
+ if (arch.tradeoffs?.length) {
114
+ lines.push("## Tradeoffs\n");
115
+ for (const t of arch.tradeoffs) lines.push(`- ${t}`);
116
+ }
117
+ if (stageResult.questions?.length) {
118
+ lines.push("\n## Open Questions\n");
119
+ for (const q of stageResult.questions) lines.push(`- ${q}`);
120
+ }
121
+
122
+ return lines.join("\n");
123
+ }
124
+
125
+ function serializePlan(stageResult) {
126
+ if (!stageResult?.plan) return null;
127
+ return `# Implementation Plan\n\n${stageResult.plan}`;
128
+ }
129
+
130
+ function serializeTriage(stageResult) {
131
+ if (!stageResult) return null;
132
+ const lines = ["# Triage\n"];
133
+ lines.push(`**Level**: ${stageResult.level || "N/A"}`);
134
+ lines.push(`**Task Type**: ${stageResult.taskType || "N/A"}`);
135
+ lines.push(`**Roles**: ${(stageResult.roles || []).join(", ") || "none"}`);
136
+ if (stageResult.reasoning) lines.push(`\n**Reasoning**: ${stageResult.reasoning}`);
137
+ if (stageResult.shouldDecompose) {
138
+ lines.push("\n## Decomposition\n");
139
+ for (const sub of stageResult.subtasks || []) lines.push(`- ${sub}`);
140
+ }
141
+ return lines.join("\n");
142
+ }
143
+
144
+ // ── Iteration log ────────────────────────────────────────────────────────────
145
+
146
+ /**
147
+ * Append one iteration entry to the iterations buffer.
148
+ * Called after each iteration completes.
149
+ */
150
+ export function formatIteration({ iteration, coderSummary, reviewerSummary, sonarSummary, solomonRuling, testerSummary, securitySummary, durationMs }) {
151
+ const lines = [`## Iteration ${iteration}\n`];
152
+ lines.push(`**Duration**: ${Math.round((durationMs || 0) / 1000)}s\n`);
153
+
154
+ if (coderSummary) lines.push(`**Coder**: ${coderSummary}`);
155
+ if (reviewerSummary) lines.push(`**Reviewer**: ${reviewerSummary}`);
156
+ if (sonarSummary) lines.push(`**Sonar**: ${sonarSummary}`);
157
+ if (testerSummary) lines.push(`**Tester**: ${testerSummary}`);
158
+ if (securitySummary) lines.push(`**Security**: ${securitySummary}`);
159
+ if (solomonRuling) lines.push(`**Solomon**: ${solomonRuling}`);
160
+
161
+ lines.push("");
162
+ return lines.join("\n");
163
+ }
164
+
165
+ // ── Decisions log ────────────────────────────────────────────────────────────
166
+
167
+ export function formatDecision({ timestamp, trigger, context, action, reasoning }) {
168
+ const lines = [`## ${timestamp || new Date().toISOString()}\n`];
169
+ lines.push(`**Trigger**: ${trigger}`);
170
+ if (context) lines.push(`**Context**: ${context}`);
171
+ lines.push(`**Action**: ${action}`);
172
+ if (reasoning) lines.push(`**Reasoning**: ${reasoning}`);
173
+ lines.push("");
174
+ return lines.join("\n");
175
+ }
176
+
177
+ // ── Tree of affected files ───────────────────────────────────────────────────
178
+
179
+ export async function generateFileTree(baseRef) {
180
+ try {
181
+ const result = await runCommand("git", ["diff", "--name-status", `${baseRef}...HEAD`]);
182
+ if (result.exitCode !== 0 || !result.stdout?.trim()) return null;
183
+
184
+ const lines = ["# Affected Files\n"];
185
+ const statusMap = { M: "modified", A: "added", D: "deleted", R: "renamed" };
186
+
187
+ for (const line of result.stdout.trim().split("\n")) {
188
+ const [status, ...fileParts] = line.split("\t");
189
+ const file = fileParts.join("\t");
190
+ const label = statusMap[status?.[0]] || status;
191
+ lines.push(`- [${label}] ${file}`);
192
+ }
193
+
194
+ return lines.join("\n");
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ // ── Summary ──────────────────────────────────────────────────────────────────
201
+
202
+ export function generateSummary({ task, result, sessionId, iterations, durationMs, budget, stages, commits, files }) {
203
+ const lines = ["# Session Summary\n"];
204
+ lines.push(`**Session**: ${sessionId}`);
205
+ lines.push(`**Task**: ${task}`);
206
+ lines.push(`**Result**: ${result}`);
207
+ lines.push(`**Iterations**: ${iterations}`);
208
+ lines.push(`**Duration**: ${Math.round((durationMs || 0) / 1000)}s`);
209
+
210
+ if (budget) {
211
+ lines.push(`**Budget**: ~$${budget.total_cost_usd?.toFixed(2) || "?"} / ~${budget.total_tokens?.toLocaleString() || "?"} tokens`);
212
+ }
213
+
214
+ if (stages && Object.keys(stages).length) {
215
+ lines.push("\n## Stages Executed\n");
216
+ for (const [name, stageResult] of Object.entries(stages)) {
217
+ const status = stageResult?.ok !== false ? "pass" : "fail";
218
+ const summary = stageResult?.summary || "";
219
+ lines.push(`- **${name}**: ${status}${summary ? ` -${summary}` : ""}`);
220
+ }
221
+ }
222
+
223
+ if (commits?.length) {
224
+ lines.push("\n## Commits\n");
225
+ for (const c of commits) lines.push(`- \`${(c.hash || "").slice(0, 7)}\` ${c.message || ""}`);
226
+ }
227
+
228
+ // List journal files
229
+ lines.push("\n## Journal Files\n");
230
+ for (const f of files) lines.push(`- [${f}](./${f})`);
231
+
232
+ return lines.join("\n");
233
+ }
234
+
235
+ // ── Pipeline plan display ────────────────────────────────────────────────────
236
+
237
+ /**
238
+ * Build a console-friendly pipeline plan summary.
239
+ * Shown before the iteration loop starts.
240
+ */
241
+ export function buildPlanSummary({ pipelineFlags, config, stageResults, task }) {
242
+ const lines = [];
243
+ lines.push("┌─ Pipeline Plan ─────────────────────────────");
244
+
245
+ // Task (truncated)
246
+ const taskPreview = task.length > 80 ? task.slice(0, 77) + "..." : task;
247
+ lines.push(`│ Task: ${taskPreview}`);
248
+
249
+ // Triage result
250
+ const triage = stageResults?.triage;
251
+ if (triage) {
252
+ lines.push(`│ Triage: ${triage.level} (${triage.taskType || "sw"}) → roles: ${(triage.roles || []).join(", ")}`);
253
+ }
254
+
255
+ // Active stages
256
+ const activeStages = [];
257
+ if (pipelineFlags.researcherEnabled) activeStages.push("Researcher");
258
+ if (pipelineFlags.architectEnabled) activeStages.push("Architect");
259
+ if (pipelineFlags.plannerEnabled) activeStages.push("Planner");
260
+ activeStages.push("Coder");
261
+ if (pipelineFlags.refactorerEnabled) activeStages.push("Refactorer");
262
+ if (pipelineFlags.reviewerEnabled) activeStages.push("Reviewer");
263
+ if (pipelineFlags.testerEnabled) activeStages.push("Tester");
264
+ if (pipelineFlags.securityEnabled) activeStages.push("Security");
265
+ if (pipelineFlags.impeccableEnabled) activeStages.push("Impeccable");
266
+ lines.push(`│ Stages: ${activeStages.join(" → ")}`);
267
+
268
+ // Config
269
+ const methodology = config.development?.methodology || "standard";
270
+ const maxIter = config.max_iterations || 5;
271
+ const solomonOn = config.pipeline?.solomon?.enabled !== false;
272
+ lines.push(`│ Method: ${methodology} | Max iterations: ${maxIter} | Solomon: ${solomonOn ? "on" : "off"}`);
273
+
274
+ // Plan summary (if planner ran)
275
+ const plan = stageResults?.planner;
276
+ if (plan?.plan) {
277
+ const planLines = plan.plan.split("\n").filter(l => l.trim());
278
+ const preview = planLines.slice(0, 5);
279
+ lines.push("│");
280
+ lines.push("│ Plan:");
281
+ for (const pl of preview) lines.push(`│ ${pl}`);
282
+ if (planLines.length > 5) lines.push(`│ ... (${planLines.length - 5} more steps)`);
283
+ }
284
+
285
+ lines.push("└──────────────────────────────────────────────");
286
+ return lines.join("\n");
287
+ }
288
+
289
+ // ── Main write functions ─────────────────────────────────────────────────────
290
+
291
+ /**
292
+ * Write all pre-loop stage outputs to the journal.
293
+ */
294
+ export async function writePreLoopJournal(journalDir, stageResults) {
295
+ const writes = [
296
+ { file: "triage.md", content: serializeTriage(stageResults.triage) },
297
+ { file: "discovery.md", content: serializeDiscovery(stageResults.discover) },
298
+ { file: "research.md", content: serializeResearch(stageResults.researcher) },
299
+ { file: "architecture.md", content: serializeArchitecture(stageResults.architect) },
300
+ { file: "plan.md", content: serializePlan(stageResults.planner) }
301
+ ];
302
+
303
+ await Promise.all(
304
+ writes.filter(w => w.content).map(w => writeJournalFile(journalDir, w.file, w.content))
305
+ );
306
+
307
+ return writes.filter(w => w.content).map(w => w.file);
308
+ }
309
+
310
+ /**
311
+ * Write the iterations log.
312
+ */
313
+ export async function writeIterationsJournal(journalDir, iterationEntries) {
314
+ if (!iterationEntries?.length) return;
315
+ const content = "# Iterations\n\n" + iterationEntries.join("\n");
316
+ await writeJournalFile(journalDir, "iterations.md", content);
317
+ }
318
+
319
+ /**
320
+ * Write the decisions log.
321
+ */
322
+ export async function writeDecisionsJournal(journalDir, decisionEntries) {
323
+ if (!decisionEntries?.length) return;
324
+ const content = "# Solomon Decisions\n\n" + decisionEntries.join("\n");
325
+ await writeJournalFile(journalDir, "decisions.md", content);
326
+ }
327
+
328
+ /**
329
+ * Write the file tree.
330
+ */
331
+ export async function writeTreeJournal(journalDir, baseRef) {
332
+ const tree = await generateFileTree(baseRef);
333
+ if (tree) await writeJournalFile(journalDir, "tree.txt", tree);
334
+ return Boolean(tree);
335
+ }
336
+
337
+ /**
338
+ * Write the session summary.
339
+ */
340
+ export async function writeSummaryJournal(journalDir, summaryData) {
341
+ const files = summaryData.files || [];
342
+ const content = generateSummary({ ...summaryData, files: [...files, "summary.md"] });
343
+ await writeJournalFile(journalDir, "summary.md", content);
344
+ }
@@ -63,6 +63,24 @@ export function formatEscalationMessage(solomonResult, session) {
63
63
  return lines.join("\n");
64
64
  }
65
65
 
66
+ /**
67
+ * Extract Solomon's previous rulings from session checkpoints.
68
+ * Gives Solomon memory of its own decisions to avoid repeating failed strategies.
69
+ */
70
+ function extractSolomonHistory(session) {
71
+ const checkpoints = session?.checkpoints || [];
72
+ return checkpoints
73
+ .filter(cp => cp.stage === "solomon")
74
+ .map(cp => ({
75
+ conflictStage: cp.conflictStage,
76
+ ruling: cp.ruling,
77
+ alternativeAgent: cp.alternativeAgent,
78
+ waitUntil: cp.waitUntil,
79
+ conditions: cp.conditions,
80
+ error: cp.error || null
81
+ }));
82
+ }
83
+
66
84
  export async function invokeSolomon({ config, logger, emitter, eventBase, stage, conflict, askQuestion, session, iteration }) {
67
85
  const solomonEnabled = Boolean(config.pipeline?.solomon?.enabled);
68
86
 
@@ -88,10 +106,14 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
88
106
  await solomon.init({ task: conflict.task || session.task, iteration });
89
107
  let ruling;
90
108
  try {
91
- const conflictWithSuggestions = pendingSuggestions
92
- ? { ...conflict, hostSuggestions: pendingSuggestions }
93
- : conflict;
94
- ruling = await solomon.run({ conflict: conflictWithSuggestions });
109
+ // Inject Solomon's own history so it doesn't repeat failed strategies
110
+ const solomonHistory = extractSolomonHistory(session);
111
+ const enrichedConflict = {
112
+ ...conflict,
113
+ ...(pendingSuggestions && { hostSuggestions: pendingSuggestions }),
114
+ ...(solomonHistory.length > 0 && { previousSolomonRulings: solomonHistory })
115
+ };
116
+ ruling = await solomon.run({ conflict: enrichedConflict });
95
117
  } catch (err) {
96
118
  logger.warn(`Solomon threw: ${err.message}`);
97
119
  return escalateToHuman({
@@ -124,7 +146,11 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
124
146
  await addCheckpoint(session, {
125
147
  stage: "solomon",
126
148
  iteration,
149
+ conflictStage: conflict?.stage || stage,
127
150
  ruling: ruling.result?.ruling,
151
+ alternativeAgent: ruling.result?.alternativeAgent || null,
152
+ waitUntil: ruling.result?.waitUntil || null,
153
+ conditions: ruling.result?.conditions || [],
128
154
  escalate: ruling.result?.escalate,
129
155
  error: solomonError ? solomonError.slice(0, 500) : undefined,
130
156
  subtask: ruling.result?.subtask?.title || null
@@ -143,7 +169,14 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
143
169
  return { action: "approve", conditions: [], ruling };
144
170
  }
145
171
  if (r === "approve_with_conditions") {
146
- return { action: "continue", conditions: ruling.result?.conditions || [], ruling };
172
+ return {
173
+ action: "continue",
174
+ conditions: ruling.result?.conditions || [],
175
+ extraIterations: ruling.result?.extraIterations || null,
176
+ alternativeAgent: ruling.result?.alternativeAgent || null,
177
+ waitUntil: ruling.result?.waitUntil || null,
178
+ ruling
179
+ };
147
180
  }
148
181
 
149
182
  if (r === "escalate_human") {