karajan-code 1.50.0 → 1.51.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/README.md +1 -1
- package/docs/README.es.md +1 -1
- package/package.json +1 -1
- package/src/commands/doctor.js +9 -4
- package/src/commands/init.js +12 -12
- package/src/commands/report.js +12 -0
- package/src/guards/intent-guard.js +9 -0
- package/src/guards/policy-resolver.js +3 -1
- package/src/orchestrator/preflight-checks.js +3 -1
- package/src/orchestrator/solomon-escalation.js +34 -10
- package/src/orchestrator.js +107 -20
- package/src/planning-game/decomposition.js +7 -5
- package/src/planning-game/pipeline-adapter.js +6 -2
- package/src/prompts/triage.js +3 -2
- package/src/review/diff-generator.js +21 -5
- package/src/utils/display.js +9 -0
- package/src/utils/git.js +17 -2
- package/src/utils/messages.js +61 -0
- package/src/utils/rtk-install.js +38 -0
- package/src/utils/rtk-wrapper.js +87 -0
package/README.md
CHANGED
|
@@ -243,7 +243,7 @@ Not nostalgia, not stubbornness. I've been using JavaScript since 1997, when Bre
|
|
|
243
243
|
git clone https://github.com/manufosela/karajan-code.git
|
|
244
244
|
cd karajan-code
|
|
245
245
|
npm install
|
|
246
|
-
npm test # Run
|
|
246
|
+
npm test # Run ~2473 tests with Vitest
|
|
247
247
|
npm run validate # Lint + test
|
|
248
248
|
```
|
|
249
249
|
|
package/docs/README.es.md
CHANGED
|
@@ -213,7 +213,7 @@ No es nostalgia ni cabezoneria. Es que llevo usando JavaScript desde 1997, cuand
|
|
|
213
213
|
git clone https://github.com/manufosela/karajan-code.git
|
|
214
214
|
cd karajan-code
|
|
215
215
|
npm install
|
|
216
|
-
npm test # Ejecutar
|
|
216
|
+
npm test # Ejecutar ~2473 tests con Vitest
|
|
217
217
|
npm run validate # Lint + test
|
|
218
218
|
```
|
|
219
219
|
|
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -208,15 +208,20 @@ async function checkBecariaInfra(config) {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
async function checkRtk() {
|
|
211
|
-
const
|
|
212
|
-
let detail = NOT_FOUND_DETAIL;
|
|
211
|
+
const installCmd = getInstallCommand("rtk");
|
|
213
212
|
try {
|
|
214
213
|
const res = await runCommand("rtk", ["--version"]);
|
|
215
214
|
if (res.exitCode === 0) {
|
|
216
|
-
detail
|
|
215
|
+
return { name: "rtk", label: "RTK (Rust Token Killer)", ok: true, detail: `${res.stdout.trim()} — token savings active`, fix: null };
|
|
217
216
|
}
|
|
218
217
|
} catch { /* not installed */ }
|
|
219
|
-
return {
|
|
218
|
+
return {
|
|
219
|
+
name: "rtk",
|
|
220
|
+
label: "RTK (Rust Token Killer)",
|
|
221
|
+
ok: false,
|
|
222
|
+
detail: "Not found — 60-90% token savings available",
|
|
223
|
+
fix: `Install: ${installCmd}`
|
|
224
|
+
};
|
|
220
225
|
}
|
|
221
226
|
|
|
222
227
|
async function checkRuleFiles(config) {
|
package/src/commands/init.js
CHANGED
|
@@ -10,6 +10,8 @@ import { createWizard, isTTY } from "../utils/wizard.js";
|
|
|
10
10
|
import { runCommand } from "../utils/process.js";
|
|
11
11
|
import { getInstallCommand } from "../utils/os-detect.js";
|
|
12
12
|
import { detectOsLocale, SUPPORTED_LANGUAGES } from "../utils/locale.js";
|
|
13
|
+
import { detectRtk } from "../utils/rtk-detect.js";
|
|
14
|
+
import { installRtk } from "../utils/rtk-install.js";
|
|
13
15
|
|
|
14
16
|
async function runWizard(config, logger) {
|
|
15
17
|
const agents = await detectAvailableAgents();
|
|
@@ -328,18 +330,16 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
328
330
|
await ensureCoderRules(coderRulesPath, logger);
|
|
329
331
|
await installSkills(logger, interactive);
|
|
330
332
|
|
|
331
|
-
// Check RTK availability
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
logger.info("RTK (Rust Token Killer) can reduce token usage by 60-90%.");
|
|
342
|
-
logger.info(` Install: ${getInstallCommand("rtk")}`);
|
|
333
|
+
// Check RTK availability — auto-install if missing
|
|
334
|
+
const rtk = await detectRtk();
|
|
335
|
+
if (rtk.available) {
|
|
336
|
+
logger.info(`RTK ${rtk.version || ""} detected.`);
|
|
337
|
+
} else {
|
|
338
|
+
const installResult = await installRtk(logger);
|
|
339
|
+
if (!installResult.ok) {
|
|
340
|
+
logger.warn("RTK is optional but recommended for 60-90% token savings.");
|
|
341
|
+
logger.warn(` Manual install: ${getInstallCommand("rtk")}`);
|
|
342
|
+
}
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
await setupSonarQube(config, logger);
|
package/src/commands/report.js
CHANGED
|
@@ -152,6 +152,7 @@ async function buildReport(dir, sessionId) {
|
|
|
152
152
|
};
|
|
153
153
|
if (session.pg_task_id) report.pg_task_id = session.pg_task_id;
|
|
154
154
|
if (session.pg_project_id) report.pg_project_id = session.pg_project_id;
|
|
155
|
+
if (session.rtk_savings) report.rtk_savings = session.rtk_savings;
|
|
155
156
|
return report;
|
|
156
157
|
}
|
|
157
158
|
|
|
@@ -326,6 +327,16 @@ async function resolveTraceOptions(currency) {
|
|
|
326
327
|
return { cur, rate };
|
|
327
328
|
}
|
|
328
329
|
|
|
330
|
+
function printRtkSavings(rtkSavings) {
|
|
331
|
+
if (!rtkSavings || !rtkSavings.callCount) return;
|
|
332
|
+
const tokens = rtkSavings.estimatedTokensSaved ?? 0;
|
|
333
|
+
const ratio = rtkSavings.savedPct ?? 0;
|
|
334
|
+
const commands = rtkSavings.callCount ?? 0;
|
|
335
|
+
console.log("");
|
|
336
|
+
console.log(`RTK Savings: ~${tokens} tokens saved (${ratio}% compression, ${commands} commands)`);
|
|
337
|
+
console.log(` Original: ${rtkSavings.originalBytes} bytes, Compressed: ${rtkSavings.rtkBytes} bytes, Saved: ${rtkSavings.savedBytes} bytes`);
|
|
338
|
+
}
|
|
339
|
+
|
|
329
340
|
function printTraceReport(report, currency, exchangeRate) {
|
|
330
341
|
console.log(`Session: ${report.session_id}`);
|
|
331
342
|
printPgCardLine(report);
|
|
@@ -333,6 +344,7 @@ function printTraceReport(report, currency, exchangeRate) {
|
|
|
333
344
|
console.log(`Task: ${report.task_description || "N/A"}`);
|
|
334
345
|
console.log("");
|
|
335
346
|
printTraceTable(report.budget_trace, { currency, exchangeRate });
|
|
347
|
+
printRtkSavings(report.rtk_savings);
|
|
336
348
|
}
|
|
337
349
|
|
|
338
350
|
async function handlePgTaskReport({ dir, pgTask, list, sessionId, format, trace, currency }) {
|
|
@@ -6,6 +6,15 @@ import { VALID_TASK_TYPES } from "./policy-resolver.js";
|
|
|
6
6
|
* Evaluated top-down; first match with confidence >= threshold wins.
|
|
7
7
|
*/
|
|
8
8
|
const INTENT_PATTERNS = [
|
|
9
|
+
// Audit / analysis tasks (no code changes, read-only analysis)
|
|
10
|
+
{
|
|
11
|
+
id: "audit",
|
|
12
|
+
keywords: ["audit", "review", "analyze", "check", "verify", "inspect", "scan", "auditar", "revisar", "analizar", "comprobar", "verificar", "inspeccionar"],
|
|
13
|
+
taskType: "audit",
|
|
14
|
+
level: "simple",
|
|
15
|
+
confidence: 0.9,
|
|
16
|
+
message: "Audit/analysis task detected — skipping coder and reviewer",
|
|
17
|
+
},
|
|
9
18
|
// Documentation-only tasks
|
|
10
19
|
{
|
|
11
20
|
id: "doc-readme",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const VALID_TASK_TYPES = new Set(["sw", "infra", "doc", "add-tests", "refactor"]);
|
|
1
|
+
export const VALID_TASK_TYPES = new Set(["sw", "infra", "doc", "add-tests", "refactor", "audit", "analysis"]);
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_POLICIES = {
|
|
4
4
|
sw: { tdd: true, sonar: true, reviewer: true, testsRequired: true },
|
|
@@ -6,6 +6,8 @@ export const DEFAULT_POLICIES = {
|
|
|
6
6
|
doc: { tdd: false, sonar: false, reviewer: true, testsRequired: false },
|
|
7
7
|
"add-tests": { tdd: false, sonar: true, reviewer: true, testsRequired: true },
|
|
8
8
|
refactor: { tdd: true, sonar: true, reviewer: true, testsRequired: false },
|
|
9
|
+
audit: { tdd: false, sonar: false, reviewer: false, testsRequired: false, coderRequired: false },
|
|
10
|
+
analysis: { tdd: false, sonar: false, reviewer: false, testsRequired: false, coderRequired: false },
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
const FALLBACK_TYPE = "sw";
|
|
@@ -13,6 +13,7 @@ import { checkBinary } from "../utils/agent-detect.js";
|
|
|
13
13
|
import { isSonarReachable, sonarUp } from "../sonar/manager.js";
|
|
14
14
|
import { runCommand } from "../utils/process.js";
|
|
15
15
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
16
|
+
import { msg, getLang } from "../utils/messages.js";
|
|
16
17
|
import {
|
|
17
18
|
resolveSonarHost,
|
|
18
19
|
resolveSonarToken,
|
|
@@ -273,13 +274,14 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
273
274
|
|
|
274
275
|
const hasErrors = result.errors.length > 0;
|
|
275
276
|
const hasWarnings = result.warnings.length > 0;
|
|
277
|
+
const preflightLang = getLang(config);
|
|
276
278
|
emitProgress(emitter, makeEvent("preflight:end", { ...eventBase, stage: "preflight" }, {
|
|
277
279
|
status: hasErrors ? "fail" : hasWarnings ? "warn" : "ok",
|
|
278
280
|
message: hasErrors
|
|
279
281
|
? `Preflight FAILED — ${result.errors.length} blocking issue(s)`
|
|
280
282
|
: hasWarnings
|
|
281
283
|
? `Preflight completed with ${result.warnings.length} warning(s)`
|
|
282
|
-
: "
|
|
284
|
+
: msg("preflight_passed", preflightLang),
|
|
283
285
|
detail: { ...result, executorType: "local" }
|
|
284
286
|
}));
|
|
285
287
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SolomonRole } from "../roles/solomon-role.js";
|
|
2
2
|
import { addCheckpoint, pauseSession, saveSession } from "../session-store.js";
|
|
3
3
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
4
|
+
import { msg, getLang } from "../utils/messages.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Build a human-readable escalation message from raw Solomon conflict data.
|
|
@@ -14,15 +15,16 @@ export function formatEscalationMessage(solomonResult, session) {
|
|
|
14
15
|
const stage = solomonResult?.stage || "unknown";
|
|
15
16
|
const solomonReason = solomonResult?.solomonReason || null;
|
|
16
17
|
const history = solomonResult?.history || [];
|
|
18
|
+
const lang = getLang(session?.config_snapshot);
|
|
17
19
|
|
|
18
20
|
const lines = [];
|
|
19
|
-
lines.push(
|
|
21
|
+
lines.push(msg("solomon_conflict", lang, { stage }));
|
|
20
22
|
lines.push("");
|
|
21
23
|
|
|
22
24
|
// Extract reviewer/agent feedback from history
|
|
23
25
|
const feedbackEntries = history.filter(entry => entry && typeof entry === "object");
|
|
24
26
|
if (feedbackEntries.length > 0) {
|
|
25
|
-
lines.push("
|
|
27
|
+
lines.push(msg("solomon_feedback", lang));
|
|
26
28
|
for (const entry of feedbackEntries) {
|
|
27
29
|
const agent = entry.agent || "unknown";
|
|
28
30
|
const feedback = entry.feedback || "No feedback provided";
|
|
@@ -42,7 +44,7 @@ export function formatEscalationMessage(solomonResult, session) {
|
|
|
42
44
|
|
|
43
45
|
// Solomon's reason (if it tried and failed)
|
|
44
46
|
if (solomonReason) {
|
|
45
|
-
lines.push(
|
|
47
|
+
lines.push(`${msg("solomon_reason", lang)} ${solomonReason}`);
|
|
46
48
|
lines.push("");
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -50,16 +52,13 @@ export function formatEscalationMessage(solomonResult, session) {
|
|
|
50
52
|
const iterationCount = solomonResult?.iterationCount;
|
|
51
53
|
const maxIterations = solomonResult?.maxIterations;
|
|
52
54
|
if (iterationCount !== undefined && maxIterations !== undefined) {
|
|
53
|
-
lines.push(
|
|
55
|
+
lines.push(msg("pipeline_iteration", lang, { current: iterationCount, max: maxIterations }));
|
|
54
56
|
lines.push("");
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
lines.push("
|
|
58
|
-
lines.push(" 1. Accept coder's work as-is");
|
|
59
|
-
lines.push(" 2. Retry with reviewer's feedback");
|
|
60
|
-
lines.push(" 3. Stop the session");
|
|
59
|
+
lines.push(msg("solomon_options", lang));
|
|
61
60
|
lines.push("");
|
|
62
|
-
lines.push("
|
|
61
|
+
lines.push(msg("solomon_proceed", lang));
|
|
63
62
|
|
|
64
63
|
return lines.join("\n");
|
|
65
64
|
}
|
|
@@ -161,12 +160,37 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
|
|
|
161
160
|
return { action: "continue", conditions: [], ruling };
|
|
162
161
|
}
|
|
163
162
|
|
|
163
|
+
export function parseEscalationAnswer(answer) {
|
|
164
|
+
if (!answer) return null;
|
|
165
|
+
const trimmed = answer.trim().toLowerCase();
|
|
166
|
+
// Option 1: Accept coder's work as-is
|
|
167
|
+
if (trimmed === "1" || trimmed === "accept" || trimmed === "yes" || trimmed === "sí" || trimmed === "si") {
|
|
168
|
+
return { action: "continue", humanGuidance: "Accept coder's work as-is" };
|
|
169
|
+
}
|
|
170
|
+
// Option 2: Retry with reviewer's feedback
|
|
171
|
+
if (trimmed === "2" || trimmed === "retry") {
|
|
172
|
+
return { action: "continue", humanGuidance: "Retry with reviewer feedback" };
|
|
173
|
+
}
|
|
174
|
+
// Option 3: Stop the session
|
|
175
|
+
if (trimmed === "3" || trimmed.startsWith("stop") || trimmed.startsWith("pause")) {
|
|
176
|
+
return { action: "stop" };
|
|
177
|
+
}
|
|
178
|
+
// Free-text: treat as human guidance (backward compat)
|
|
179
|
+
return { action: "continue", humanGuidance: answer.trim() };
|
|
180
|
+
}
|
|
181
|
+
|
|
164
182
|
export async function escalateToHuman({ askQuestion, session, emitter, eventBase, stage, conflict, iteration }) {
|
|
165
183
|
const question = formatEscalationMessage({ ...conflict, stage }, session);
|
|
166
184
|
|
|
167
185
|
if (askQuestion) {
|
|
168
186
|
const answer = await askQuestion(question, { iteration, stage });
|
|
169
|
-
|
|
187
|
+
const parsed = parseEscalationAnswer(answer);
|
|
188
|
+
if (parsed && parsed.action === "continue") {
|
|
189
|
+
return { action: "continue", humanGuidance: parsed.humanGuidance };
|
|
190
|
+
}
|
|
191
|
+
if (parsed && parsed.action === "stop") {
|
|
192
|
+
// Fall through to pause session
|
|
193
|
+
} else if (answer) {
|
|
170
194
|
return { action: "continue", humanGuidance: answer };
|
|
171
195
|
}
|
|
172
196
|
}
|
package/src/orchestrator.js
CHANGED
|
@@ -31,6 +31,7 @@ import { classifyIntent } from "./guards/intent-guard.js";
|
|
|
31
31
|
import { resolveReviewProfile } from "./review/profiles.js";
|
|
32
32
|
import { CoderRole } from "./roles/coder-role.js";
|
|
33
33
|
import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
|
|
34
|
+
import { msg, getLang } from "./utils/messages.js";
|
|
34
35
|
import { PipelineContext } from "./orchestrator/pipeline-context.js";
|
|
35
36
|
import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./orchestrator/pre-loop-stages.js";
|
|
36
37
|
import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
|
|
@@ -40,6 +41,9 @@ import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js"
|
|
|
40
41
|
import { detectTestFramework } from "./utils/project-detect.js";
|
|
41
42
|
import { runPreflightChecks } from "./orchestrator/preflight-checks.js";
|
|
42
43
|
import { detectRtk } from "./utils/rtk-detect.js";
|
|
44
|
+
import { createRtkRunner, RtkSavingsTracker } from "./utils/rtk-wrapper.js";
|
|
45
|
+
import { setRunner as setDiffRunner } from "./review/diff-generator.js";
|
|
46
|
+
import { setRunner as setGitRunner } from "./utils/git.js";
|
|
43
47
|
import { detectNeededSkills, autoInstallSkills, cleanupAutoInstalledSkills } from "./skills/skill-detector.js";
|
|
44
48
|
import { isOpenSkillsAvailable } from "./skills/openskills-client.js";
|
|
45
49
|
|
|
@@ -277,6 +281,9 @@ function resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBa
|
|
|
277
281
|
if (!resolvedPolicies.reviewer) {
|
|
278
282
|
pipelineFlags.reviewerEnabled = false;
|
|
279
283
|
}
|
|
284
|
+
if (resolvedPolicies.coderRequired === false) {
|
|
285
|
+
pipelineFlags.coderRequired = false;
|
|
286
|
+
}
|
|
280
287
|
|
|
281
288
|
emitProgress(
|
|
282
289
|
emitter,
|
|
@@ -402,8 +409,9 @@ async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpoin
|
|
|
402
409
|
})
|
|
403
410
|
);
|
|
404
411
|
|
|
412
|
+
const lang = getLang(config);
|
|
405
413
|
const answer = await askQuestion(
|
|
406
|
-
`${checkpointMsg}\n\
|
|
414
|
+
`${checkpointMsg}\n\n${msg("checkpoint_options", lang)}`
|
|
407
415
|
);
|
|
408
416
|
|
|
409
417
|
await addCheckpoint(session, { stage: "interactive-checkpoint", elapsed_minutes: Number(elapsedStr), answer });
|
|
@@ -429,7 +437,7 @@ async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpoin
|
|
|
429
437
|
return parsed;
|
|
430
438
|
}
|
|
431
439
|
|
|
432
|
-
function parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config }) {
|
|
440
|
+
export function parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config }) {
|
|
433
441
|
if (!trimmedAnswer) {
|
|
434
442
|
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
|
|
435
443
|
}
|
|
@@ -632,11 +640,23 @@ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i
|
|
|
632
640
|
.filter(a => a.severity === "critical")
|
|
633
641
|
.map(a => a.message)
|
|
634
642
|
.join("\n");
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
643
|
+
const question = [
|
|
644
|
+
"Solomon detected critical issues:",
|
|
645
|
+
alertSummary,
|
|
646
|
+
"",
|
|
647
|
+
"What should we do?",
|
|
648
|
+
"1 = Continue anyway",
|
|
649
|
+
"2 = Pause the session",
|
|
650
|
+
"3 = Stop the session",
|
|
651
|
+
"",
|
|
652
|
+
"Type 1, 2, or 3:"
|
|
653
|
+
].join("\n");
|
|
654
|
+
const answer = await askQuestion(question, { iteration: i, stage: "solomon" });
|
|
655
|
+
const trimmed = (answer || "").trim().toLowerCase();
|
|
656
|
+
const shouldPause = !answer
|
|
657
|
+
|| trimmed === "2" || trimmed === "3"
|
|
658
|
+
|| trimmed.startsWith("pause") || trimmed.startsWith("stop");
|
|
659
|
+
if (shouldPause) {
|
|
640
660
|
await pauseSession(session, {
|
|
641
661
|
question: `Solomon supervisor paused: ${alertSummary}`,
|
|
642
662
|
context: { iteration: i, stage: "solomon", alerts: rulesResult.alerts }
|
|
@@ -746,7 +766,7 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
746
766
|
return { action: "proceed" };
|
|
747
767
|
}
|
|
748
768
|
|
|
749
|
-
async function finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i }) {
|
|
769
|
+
async function finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i, rtkTracker }) {
|
|
750
770
|
const gitResult = await finalizeGitAutomation({ config, gitCtx, task, logger, session, stageResults });
|
|
751
771
|
|
|
752
772
|
// Accumulate final commits for PG card lifecycle tracking
|
|
@@ -765,16 +785,24 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
|
|
|
765
785
|
await markPgCardToValidate({ pgCard, pgProject, config, session, gitResult, logger });
|
|
766
786
|
|
|
767
787
|
const deferredIssues = session.deferred_issues || [];
|
|
788
|
+
const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
|
|
789
|
+
if (rtkSavings) session.rtk_savings = rtkSavings;
|
|
790
|
+
await saveSession(session);
|
|
791
|
+
|
|
792
|
+
const endDetail = { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues };
|
|
793
|
+
if (rtkSavings) endDetail.rtk_savings = rtkSavings;
|
|
768
794
|
emitProgress(
|
|
769
795
|
emitter,
|
|
770
796
|
makeEvent("session:end", { ...eventBase, stage: "done" }, {
|
|
771
797
|
message: deferredIssues.length > 0
|
|
772
798
|
? `Session approved (${deferredIssues.length} deferred issue(s) tracked as tech debt)`
|
|
773
799
|
: "Session approved",
|
|
774
|
-
detail:
|
|
800
|
+
detail: endDetail
|
|
775
801
|
})
|
|
776
802
|
);
|
|
777
|
-
|
|
803
|
+
const result = { approved: true, sessionId: session.id, review, git: gitResult, deferredIssues };
|
|
804
|
+
if (rtkSavings) result.rtk_savings = rtkSavings;
|
|
805
|
+
return result;
|
|
778
806
|
}
|
|
779
807
|
|
|
780
808
|
// PG card "To Validate" logic moved to src/planning-game/pipeline-adapter.js → markPgCardToValidate()
|
|
@@ -1118,7 +1146,7 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
|
|
|
1118
1146
|
return { action: "ok", review: reviewerResult.review };
|
|
1119
1147
|
}
|
|
1120
1148
|
|
|
1121
|
-
async function handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review }) {
|
|
1149
|
+
async function handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review, rtkTracker }) {
|
|
1122
1150
|
session.reviewer_retry_count = 0;
|
|
1123
1151
|
const postLoopResult = await handlePostLoopStages({
|
|
1124
1152
|
config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
|
|
@@ -1127,11 +1155,11 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
|
|
|
1127
1155
|
if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
|
|
1128
1156
|
if (postLoopResult.action === "continue") return { action: "continue" };
|
|
1129
1157
|
|
|
1130
|
-
const result = await finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i });
|
|
1158
|
+
const result = await finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i, rtkTracker });
|
|
1131
1159
|
return { action: "return", result };
|
|
1132
1160
|
}
|
|
1133
1161
|
|
|
1134
|
-
async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task }) {
|
|
1162
|
+
async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker }) {
|
|
1135
1163
|
// Escalate to Solomon / human before giving up
|
|
1136
1164
|
const solomonResult = await invokeSolomon({
|
|
1137
1165
|
config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
|
|
@@ -1164,13 +1192,17 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
|
|
|
1164
1192
|
|
|
1165
1193
|
// Solomon also couldn't resolve — fail
|
|
1166
1194
|
session.budget = budgetSummary();
|
|
1195
|
+
const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
|
|
1196
|
+
if (rtkSavings) session.rtk_savings = rtkSavings;
|
|
1167
1197
|
await markSessionStatus(session, "failed");
|
|
1198
|
+
const failDetail = { approved: false, reason: "max_iterations", iterations: config.max_iterations, stages: stageResults, budget: budgetSummary() };
|
|
1199
|
+
if (rtkSavings) failDetail.rtk_savings = rtkSavings;
|
|
1168
1200
|
emitProgress(
|
|
1169
1201
|
emitter,
|
|
1170
1202
|
makeEvent("session:end", { ...eventBase, stage: "done" }, {
|
|
1171
1203
|
status: "fail",
|
|
1172
1204
|
message: "Max iterations reached (Solomon could not resolve)",
|
|
1173
|
-
detail:
|
|
1205
|
+
detail: failDetail
|
|
1174
1206
|
})
|
|
1175
1207
|
);
|
|
1176
1208
|
return { approved: false, sessionId: session.id, reason: "max_iterations" };
|
|
@@ -1218,9 +1250,14 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1218
1250
|
const rtkResult = await detectRtk();
|
|
1219
1251
|
if (rtkResult.available) {
|
|
1220
1252
|
config = { ...config, rtk: { available: true, version: rtkResult.version } };
|
|
1221
|
-
|
|
1253
|
+
const rtkTracker = new RtkSavingsTracker();
|
|
1254
|
+
const rtkRunner = createRtkRunner(true, rtkTracker);
|
|
1255
|
+
setDiffRunner(rtkRunner);
|
|
1256
|
+
setGitRunner(rtkRunner);
|
|
1257
|
+
ctx.rtkTracker = rtkTracker;
|
|
1258
|
+
logger.info(`RTK detected (${rtkResult.version}) — wrapping internal git/diff commands with rtk`);
|
|
1222
1259
|
emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
|
|
1223
|
-
message: "RTK detected —
|
|
1260
|
+
message: "RTK detected — internal commands wrapped for token optimization",
|
|
1224
1261
|
detail: { version: rtkResult.version, executorType: "local" }
|
|
1225
1262
|
}));
|
|
1226
1263
|
}
|
|
@@ -1280,11 +1317,13 @@ async function runSingleIteration(ctx) {
|
|
|
1280
1317
|
|
|
1281
1318
|
const reviewerRetryCount = session.reviewer_retry_count || 0;
|
|
1282
1319
|
const maxReviewerRetries = config.session.max_reviewer_retries ?? config.session.fail_fast_repeats;
|
|
1320
|
+
const iterLang = getLang(config);
|
|
1321
|
+
const iterMsg = msg("pipeline_iteration", iterLang, { current: i, max: config.max_iterations });
|
|
1283
1322
|
emitProgress(emitter, makeEvent("iteration:start", { ...eventBase, stage: "iteration" }, {
|
|
1284
|
-
message:
|
|
1323
|
+
message: iterMsg,
|
|
1285
1324
|
detail: { iteration: i, maxIterations: config.max_iterations, reviewerRetryCount, maxReviewerRetries }
|
|
1286
1325
|
}));
|
|
1287
|
-
logger.info(
|
|
1326
|
+
logger.info(iterMsg);
|
|
1288
1327
|
|
|
1289
1328
|
const crResult = await runCoderAndRefactorerStages({
|
|
1290
1329
|
coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole, refactorerRole: ctx.refactorerRole,
|
|
@@ -1335,7 +1374,8 @@ async function runSingleIteration(ctx) {
|
|
|
1335
1374
|
const approvedResult = await handleApprovedReview({
|
|
1336
1375
|
config, session, emitter, eventBase, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, i, task,
|
|
1337
1376
|
stageResults: ctx.stageResults, pipelineFlags: ctx.pipelineFlags, askQuestion: ctx.askQuestion, logger,
|
|
1338
|
-
gitCtx: ctx.gitCtx, budgetSummary: ctx.budgetSummary, pgCard: ctx.pgCard, pgProject: ctx.pgProject, review
|
|
1377
|
+
gitCtx: ctx.gitCtx, budgetSummary: ctx.budgetSummary, pgCard: ctx.pgCard, pgProject: ctx.pgProject, review,
|
|
1378
|
+
rtkTracker: ctx.rtkTracker
|
|
1339
1379
|
});
|
|
1340
1380
|
if (approvedResult.action === "return" || approvedResult.action === "continue") return approvedResult;
|
|
1341
1381
|
}
|
|
@@ -1414,7 +1454,7 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
|
|
|
1414
1454
|
if (iterResult.action === "retry") { i -= 1; }
|
|
1415
1455
|
}
|
|
1416
1456
|
|
|
1417
|
-
const maxIterResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask });
|
|
1457
|
+
const maxIterResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker });
|
|
1418
1458
|
return maxIterResult;
|
|
1419
1459
|
}
|
|
1420
1460
|
|
|
@@ -1428,6 +1468,53 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1428
1468
|
const ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
|
|
1429
1469
|
|
|
1430
1470
|
try {
|
|
1471
|
+
// --- Analysis-only flow: skip coder/reviewer when coderRequired === false ---
|
|
1472
|
+
if (ctx.pipelineFlags.coderRequired === false) {
|
|
1473
|
+
logger.info("Analysis-only task — skipping coder/reviewer iteration loop");
|
|
1474
|
+
emitProgress(emitter, makeEvent("pipeline:analysis-only", { ...ctx.eventBase, stage: "analysis" }, {
|
|
1475
|
+
message: "Analysis-only task — running security and audit stages only",
|
|
1476
|
+
detail: { taskType: ctx.session.resolved_policies?.taskType, coderRequired: false }
|
|
1477
|
+
}));
|
|
1478
|
+
|
|
1479
|
+
const analysisStageResults = ctx.stageResults;
|
|
1480
|
+
const postLoopDiff = await generateDiff({ baseRef: ctx.session.session_start_sha });
|
|
1481
|
+
|
|
1482
|
+
if (ctx.pipelineFlags.securityEnabled) {
|
|
1483
|
+
const securityResult = await runSecurityStage({
|
|
1484
|
+
config: ctx.config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session,
|
|
1485
|
+
coderRole: ctx.coderRole, trackBudget: ctx.trackBudget,
|
|
1486
|
+
iteration: 1, task: ctx.plannedTask, diff: postLoopDiff, askQuestion
|
|
1487
|
+
});
|
|
1488
|
+
if (securityResult.stageResult) analysisStageResults.security = securityResult.stageResult;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
const auditResult = await runFinalAuditStage({
|
|
1492
|
+
config: ctx.config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session,
|
|
1493
|
+
coderRole: ctx.coderRole, trackBudget: ctx.trackBudget,
|
|
1494
|
+
iteration: 1, task: ctx.plannedTask, diff: postLoopDiff
|
|
1495
|
+
});
|
|
1496
|
+
if (auditResult.stageResult) analysisStageResults.audit = auditResult.stageResult;
|
|
1497
|
+
|
|
1498
|
+
ctx.session.budget = ctx.budgetSummary();
|
|
1499
|
+
await markSessionStatus(ctx.session, "approved");
|
|
1500
|
+
|
|
1501
|
+
const analysisResult = {
|
|
1502
|
+
approved: true,
|
|
1503
|
+
sessionId: ctx.session.id,
|
|
1504
|
+
analysisOnly: true,
|
|
1505
|
+
stages: analysisStageResults,
|
|
1506
|
+
budget: ctx.budgetSummary()
|
|
1507
|
+
};
|
|
1508
|
+
await writeHistoryRecord({ sessionId: ctx.session.id, task, result: analysisResult, logger });
|
|
1509
|
+
|
|
1510
|
+
emitProgress(emitter, makeEvent("session:end", { ...ctx.eventBase, stage: "done" }, {
|
|
1511
|
+
message: "Analysis-only session completed",
|
|
1512
|
+
detail: analysisResult
|
|
1513
|
+
}));
|
|
1514
|
+
|
|
1515
|
+
return analysisResult;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1431
1518
|
// --- HU Sub-Pipeline: run each certified HU as an independent iteration loop ---
|
|
1432
1519
|
if (needsSubPipeline(ctx.stageResults.huReviewer)) {
|
|
1433
1520
|
logger.info(`HU sub-pipeline: ${ctx.stageResults.huReviewer.certified} certified stories — running each as a sub-pipeline`);
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* recommends decomposition and user accepts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { msg } from "../utils/messages.js";
|
|
8
|
+
|
|
7
9
|
export async function createDecompositionSubtasks({
|
|
8
10
|
client,
|
|
9
11
|
projectId,
|
|
@@ -66,9 +68,9 @@ export async function createDecompositionSubtasks({
|
|
|
66
68
|
return created;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
export function buildDecompositionQuestion(subtasks, parentCardId) {
|
|
71
|
+
export function buildDecompositionQuestion(subtasks, parentCardId, lang = "en") {
|
|
70
72
|
const lines = [
|
|
71
|
-
|
|
73
|
+
msg("triage_decompose", lang, { count: subtasks.length }),
|
|
72
74
|
""
|
|
73
75
|
];
|
|
74
76
|
for (let i = 0; i < subtasks.length; i++) {
|
|
@@ -76,10 +78,10 @@ export function buildDecompositionQuestion(subtasks, parentCardId) {
|
|
|
76
78
|
}
|
|
77
79
|
lines.push(
|
|
78
80
|
"",
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
+
msg("triage_create_cards", lang, { cardId: parentCardId }),
|
|
82
|
+
msg("triage_sequential", lang),
|
|
81
83
|
"",
|
|
82
|
-
"
|
|
84
|
+
msg("triage_reply", lang)
|
|
83
85
|
);
|
|
84
86
|
return lines.join("\n");
|
|
85
87
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
15
15
|
import { addCheckpoint, saveSession } from "../session-store.js";
|
|
16
|
+
import { getLang } from "../utils/messages.js";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Attach the PG adapter to a pipeline emitter.
|
|
@@ -54,10 +55,13 @@ export async function handlePgDecomposition({ triageResult, pgTaskId, pgProject,
|
|
|
54
55
|
const { buildDecompositionQuestion, createDecompositionSubtasks } = await import("./decomposition.js");
|
|
55
56
|
const { createCard, relateCards, fetchCard } = await import("./client.js");
|
|
56
57
|
|
|
57
|
-
const
|
|
58
|
+
const lang = getLang(config);
|
|
59
|
+
const question = buildDecompositionQuestion(triageResult.stageResult.subtasks, pgTaskId, lang);
|
|
58
60
|
const answer = await askQuestion(question);
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
const normalizedAnswer = (answer || "").trim().toLowerCase();
|
|
63
|
+
const isAccept = normalizedAnswer === "1" || normalizedAnswer === "yes" || normalizedAnswer === "sí" || normalizedAnswer === "si";
|
|
64
|
+
if (isAccept) {
|
|
61
65
|
const parentCard = await fetchCard({ projectId: pgProject, cardId: pgTaskId }).catch(() => null);
|
|
62
66
|
const createdSubtasks = await createDecompositionSubtasks({
|
|
63
67
|
client: { createCard, relateCards },
|
package/src/prompts/triage.js
CHANGED
|
@@ -39,13 +39,14 @@ export function buildTriagePrompt({ task, instructions, availableRoles }) {
|
|
|
39
39
|
"- **reviewer**: Enable for most tasks as a quality gate. Disable only for trivial, single-line changes.",
|
|
40
40
|
"- **architect**: Enable when creating new modules/apps, changing data models or APIs, medium/complex tasks, or when design approach is ambiguous. Disable for simple fixes, doc-only, or CSS-only changes.",
|
|
41
41
|
"- **hu-reviewer**: Enable for medium/complex tasks that benefit from story decomposition and certification. Disable for trivial/simple tasks, doc-only, or CSS-only changes.",
|
|
42
|
+
"- **audit/analysis taskType**: Set taskType to \"audit\" or \"analysis\" for tasks that only require reading/inspecting code without making changes (e.g., audit, review, analyze, check, verify, inspect, scan, auditar, revisar, analizar, comprobar, verificar, inspeccionar). The coder and reviewer will be skipped entirely for these tasks.",
|
|
42
43
|
"",
|
|
43
|
-
"Note: coder is ALWAYS active — you don't need to decide on it."
|
|
44
|
+
"Note: coder is ALWAYS active for coding tasks — you don't need to decide on it. However, for audit/analysis taskTypes the coder is skipped."
|
|
44
45
|
].join("\n"),
|
|
45
46
|
"Classify the task complexity, determine its taskType, recommend only the necessary pipeline roles, and assess whether the task should be decomposed into smaller subtasks.",
|
|
46
47
|
"Keep the reasoning short and practical.",
|
|
47
48
|
"Return a single valid JSON object and nothing else.",
|
|
48
|
-
'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security|architect|hu-reviewer"],"taskType":"sw|infra|doc|add-tests|refactor","reasoning":string,"shouldDecompose":boolean,"subtasks":string[]}',
|
|
49
|
+
'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security|architect|hu-reviewer"],"taskType":"sw|infra|doc|add-tests|refactor|audit|analysis","reasoning":string,"shouldDecompose":boolean,"subtasks":string[]}',
|
|
49
50
|
`## Task\n${task}`
|
|
50
51
|
);
|
|
51
52
|
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { runCommand } from "../utils/process.js";
|
|
2
2
|
|
|
3
|
+
/** @type {((command: string, args?: string[], options?: object) => Promise<object>)|null} */
|
|
4
|
+
let _runner = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Inject an RTK-aware runner so all git operations in this module benefit from token savings.
|
|
8
|
+
* Call once at pipeline start when RTK is available.
|
|
9
|
+
* @param {(command: string, args?: string[], options?: object) => Promise<object>} runner
|
|
10
|
+
*/
|
|
11
|
+
export function setRunner(runner) {
|
|
12
|
+
_runner = runner;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function run(command, args, ...rest) {
|
|
16
|
+
return (_runner || runCommand)(command, args, ...rest);
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
|
|
4
20
|
if (baseRef) return baseRef;
|
|
5
|
-
const mergeBase = await
|
|
21
|
+
const mergeBase = await run("git", ["merge-base", "HEAD", `origin/${baseBranch}`]);
|
|
6
22
|
if (mergeBase.exitCode !== 0) {
|
|
7
|
-
const fallback = await
|
|
23
|
+
const fallback = await run("git", ["rev-parse", "HEAD~1"]);
|
|
8
24
|
if (fallback.exitCode !== 0) {
|
|
9
25
|
throw new Error("Could not compute diff base reference");
|
|
10
26
|
}
|
|
@@ -16,9 +32,9 @@ export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
|
|
|
16
32
|
export async function generateDiff({ baseRef, stageNewFiles = false }) {
|
|
17
33
|
// Stage untracked files so they appear in the diff (coder creates files but doesn't git add)
|
|
18
34
|
if (stageNewFiles) {
|
|
19
|
-
await
|
|
35
|
+
await run("git", ["add", "-A"]);
|
|
20
36
|
}
|
|
21
|
-
const result = await
|
|
37
|
+
const result = await run("git", ["diff", stageNewFiles ? "--cached" : "", `${baseRef}`].filter(Boolean));
|
|
22
38
|
if (result.exitCode !== 0) {
|
|
23
39
|
throw new Error(`git diff failed: ${result.stderr || result.stdout}`);
|
|
24
40
|
}
|
|
@@ -26,7 +42,7 @@ export async function generateDiff({ baseRef, stageNewFiles = false }) {
|
|
|
26
42
|
}
|
|
27
43
|
|
|
28
44
|
export async function getUntrackedFiles() {
|
|
29
|
-
const result = await
|
|
45
|
+
const result = await run("git", ["ls-files", "--others", "--exclude-standard"]);
|
|
30
46
|
if (result.exitCode !== 0) return [];
|
|
31
47
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
32
48
|
}
|
package/src/utils/display.js
CHANGED
|
@@ -252,6 +252,14 @@ function isBudgetUnavailable(budget) {
|
|
|
252
252
|
(budget.total_tokens === 0 && budget.total_cost_usd === 0 && Object.keys(budget.breakdown_by_role || {}).length > 0);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
function printSessionRtkSavings(rtkSavings) {
|
|
256
|
+
if (!rtkSavings || !rtkSavings.callCount) return;
|
|
257
|
+
const tokens = rtkSavings.estimatedTokensSaved ?? 0;
|
|
258
|
+
const ratio = rtkSavings.savedPct ?? 0;
|
|
259
|
+
const commands = rtkSavings.callCount ?? 0;
|
|
260
|
+
console.log(` ${ANSI.dim}\u26a1 RTK: saved ~${tokens} tokens (${ratio}% compression, ${commands} commands)${ANSI.reset}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
255
263
|
function printSessionBudget(budget) {
|
|
256
264
|
if (!budget) return;
|
|
257
265
|
if (isBudgetUnavailable(budget)) {
|
|
@@ -447,6 +455,7 @@ const EVENT_HANDLERS = {
|
|
|
447
455
|
printSessionStages(event.detail?.stages);
|
|
448
456
|
printSessionGit(event.detail?.git);
|
|
449
457
|
printSessionBudget(event.detail?.budget);
|
|
458
|
+
printSessionRtkSavings(event.detail?.rtk_savings);
|
|
450
459
|
console.log(`${ANSI.dim}Session: ${event.sessionId}${ANSI.reset}`);
|
|
451
460
|
},
|
|
452
461
|
|
package/src/utils/git.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { runCommand } from "./process.js";
|
|
2
2
|
|
|
3
|
+
/** @type {((command: string, args?: string[], options?: object) => Promise<object>)|null} */
|
|
4
|
+
let _runner = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Inject an RTK-aware runner so all git operations in this module benefit from token savings.
|
|
8
|
+
* @param {(command: string, args?: string[], options?: object) => Promise<object>} runner
|
|
9
|
+
*/
|
|
10
|
+
export function setRunner(runner) {
|
|
11
|
+
_runner = runner;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function run(command, args, ...rest) {
|
|
15
|
+
return (_runner || runCommand)(command, args, ...rest);
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
function slugifyTask(task) {
|
|
4
19
|
return String(task)
|
|
5
20
|
.toLowerCase()
|
|
@@ -9,7 +24,7 @@ function slugifyTask(task) {
|
|
|
9
24
|
}
|
|
10
25
|
|
|
11
26
|
async function runGit(args, options = {}) {
|
|
12
|
-
const res = await
|
|
27
|
+
const res = await run("git", args, options);
|
|
13
28
|
if (res.exitCode !== 0) {
|
|
14
29
|
throw new Error(`git ${args.join(" ")} failed: ${res.stderr || res.stdout}`);
|
|
15
30
|
}
|
|
@@ -17,7 +32,7 @@ async function runGit(args, options = {}) {
|
|
|
17
32
|
}
|
|
18
33
|
|
|
19
34
|
export async function ensureGitRepo() {
|
|
20
|
-
const res = await
|
|
35
|
+
const res = await run("git", ["rev-parse", "--is-inside-work-tree"]);
|
|
21
36
|
return res.exitCode === 0 && res.stdout.trim() === "true";
|
|
22
37
|
}
|
|
23
38
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized message catalog for pipeline UI strings.
|
|
3
|
+
* Supports i18n by resolving messages for the configured language.
|
|
4
|
+
* Falls back to English when a key or language is missing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const MESSAGES = {
|
|
8
|
+
en: {
|
|
9
|
+
triage_decompose: "Triage recommends decomposing this task into {count} subtasks:",
|
|
10
|
+
triage_create_cards: "Create these as linked cards in Planning Game (parent: {cardId})?",
|
|
11
|
+
triage_sequential: "Each subtask will block the next one (sequential chain).",
|
|
12
|
+
triage_reply: "Reply: yes / no",
|
|
13
|
+
solomon_conflict: "--- Conflict: {stage} ---",
|
|
14
|
+
solomon_feedback: "Reviewer feedback:",
|
|
15
|
+
solomon_reason: "Solomon could not resolve the conflict:",
|
|
16
|
+
solomon_options: "Options:\n 1. Accept coder's work as-is\n 2. Retry with reviewer's feedback\n 3. Stop the session",
|
|
17
|
+
solomon_proceed: "How should we proceed?",
|
|
18
|
+
checkpoint_continue: "Continue for {minutes} more minutes?",
|
|
19
|
+
checkpoint_options: "1. Continue 5 more minutes\n2. Continue until done (no more checkpoints)\n3. Continue for N minutes (reply with the number)\n4. Stop now",
|
|
20
|
+
preflight_passed: "Preflight passed — all checks OK",
|
|
21
|
+
pipeline_iteration: "Iteration {current}/{max}",
|
|
22
|
+
},
|
|
23
|
+
es: {
|
|
24
|
+
triage_decompose: "Triage recomienda descomponer esta tarea en {count} subtareas:",
|
|
25
|
+
triage_create_cards: "¿Crear estas como cards vinculadas en Planning Game (padre: {cardId})?",
|
|
26
|
+
triage_sequential: "Cada subtarea bloqueará la siguiente (cadena secuencial).",
|
|
27
|
+
triage_reply: "Responde: sí / no",
|
|
28
|
+
solomon_conflict: "--- Conflicto: {stage} ---",
|
|
29
|
+
solomon_feedback: "Feedback del reviewer:",
|
|
30
|
+
solomon_reason: "Solomon no pudo resolver el conflicto:",
|
|
31
|
+
solomon_options: "Opciones:\n 1. Aceptar el trabajo del coder tal cual\n 2. Reintentar con el feedback del reviewer\n 3. Parar la sesión",
|
|
32
|
+
solomon_proceed: "¿Cómo procedemos?",
|
|
33
|
+
checkpoint_continue: "¿Continuar {minutes} minutos más?",
|
|
34
|
+
checkpoint_options: "1. Continuar (5 min)\n2. Continuar hasta terminar\n3. Tiempo personalizado\n4. Parar",
|
|
35
|
+
preflight_passed: "Preflight superado — todas las comprobaciones OK",
|
|
36
|
+
pipeline_iteration: "Iteración {current}/{max}",
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a message template by key and language, interpolating parameters.
|
|
42
|
+
* Falls back to English, then to the raw key if not found.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} key - Message key (e.g. "solomon_conflict")
|
|
45
|
+
* @param {string} [lang="en"] - 2-letter language code
|
|
46
|
+
* @param {object} [params={}] - Interpolation values (e.g. {stage: "reviewer"})
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function msg(key, lang = "en", params = {}) {
|
|
50
|
+
const template = MESSAGES[lang]?.[key] || MESSAGES.en[key] || key;
|
|
51
|
+
return template.replace(/\{(\w+)\}/g, (_, k) => params[k] ?? `{${k}}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract the language code from a Karajan config object.
|
|
56
|
+
* @param {object} [config] - Karajan config
|
|
57
|
+
* @returns {string} 2-letter language code
|
|
58
|
+
*/
|
|
59
|
+
export function getLang(config) {
|
|
60
|
+
return config?.language || "en";
|
|
61
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { runCommand } from "./process.js";
|
|
2
|
+
import { getInstallCommand } from "./os-detect.js";
|
|
3
|
+
import { detectRtk } from "./rtk-detect.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Install RTK (Rust Token Killer) using the OS-appropriate command.
|
|
7
|
+
* @param {object} logger - Logger with info/warn methods
|
|
8
|
+
* @returns {Promise<{ ok: boolean, version: string|null, error: string|null }>}
|
|
9
|
+
*/
|
|
10
|
+
export async function installRtk(logger) {
|
|
11
|
+
const command = getInstallCommand("rtk");
|
|
12
|
+
|
|
13
|
+
logger.info("Installing RTK (Rust Token Killer)...");
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const result = await runCommand("sh", ["-c", command], { timeout: 120_000 });
|
|
17
|
+
|
|
18
|
+
if (result.exitCode !== 0) {
|
|
19
|
+
const error = (result.stderr || "").trim() || `exit code ${result.exitCode}`;
|
|
20
|
+
logger.warn(`RTK install failed: ${error}`);
|
|
21
|
+
return { ok: false, version: null, error };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Verify installation
|
|
25
|
+
const check = await detectRtk();
|
|
26
|
+
if (check.available) {
|
|
27
|
+
logger.info(`RTK ${check.version || ""} installed successfully.`);
|
|
28
|
+
return { ok: true, version: check.version, error: null };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
logger.warn("RTK install command succeeded but rtk binary not found in PATH.");
|
|
32
|
+
return { ok: false, version: null, error: "Binary not found after install" };
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const error = err.message || String(err);
|
|
35
|
+
logger.warn(`RTK install failed: ${error}`);
|
|
36
|
+
return { ok: false, version: null, error };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { runCommand } from "./process.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Commands that RTK can safely wrap for token-optimized output.
|
|
5
|
+
*/
|
|
6
|
+
export const RTK_SUPPORTED_COMMANDS = [
|
|
7
|
+
"git", "ls", "find", "grep", "cat", "head", "tail",
|
|
8
|
+
"wc", "diff", "tree", "du", "file"
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const supportedSet = new Set(RTK_SUPPORTED_COMMANDS);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wrap a command with RTK if available and the command is in the whitelist.
|
|
15
|
+
* @param {string} command
|
|
16
|
+
* @param {string[]} args
|
|
17
|
+
* @param {boolean} rtkAvailable
|
|
18
|
+
* @returns {{ command: string, args: string[] }}
|
|
19
|
+
*/
|
|
20
|
+
export function wrapWithRtk(command, args, rtkAvailable) {
|
|
21
|
+
if (rtkAvailable && supportedSet.has(command)) {
|
|
22
|
+
return { command: "rtk", args: [command, ...args] };
|
|
23
|
+
}
|
|
24
|
+
return { command, args };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Session-level accumulator for RTK byte savings.
|
|
29
|
+
*/
|
|
30
|
+
export class RtkSavingsTracker {
|
|
31
|
+
constructor() {
|
|
32
|
+
this.originalBytes = 0;
|
|
33
|
+
this.rtkBytes = 0;
|
|
34
|
+
this.callCount = 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
record(originalSize, rtkSize) {
|
|
38
|
+
this.originalBytes += originalSize;
|
|
39
|
+
this.rtkBytes += rtkSize;
|
|
40
|
+
this.callCount += 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
summary() {
|
|
44
|
+
const savedBytes = this.originalBytes - this.rtkBytes;
|
|
45
|
+
const savedPct = this.originalBytes > 0
|
|
46
|
+
? Number(((savedBytes / this.originalBytes) * 100).toFixed(1))
|
|
47
|
+
: 0;
|
|
48
|
+
const estimatedTokensSaved = Math.floor(savedBytes / 4);
|
|
49
|
+
return {
|
|
50
|
+
originalBytes: this.originalBytes,
|
|
51
|
+
rtkBytes: this.rtkBytes,
|
|
52
|
+
savedBytes,
|
|
53
|
+
savedPct,
|
|
54
|
+
estimatedTokensSaved,
|
|
55
|
+
callCount: this.callCount
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Returns true if any commands were recorded. */
|
|
60
|
+
hasData() {
|
|
61
|
+
return this.callCount > 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a runner function that transparently wraps supported commands with RTK.
|
|
67
|
+
* @param {boolean} rtkAvailable
|
|
68
|
+
* @param {RtkSavingsTracker} [tracker] - Optional savings tracker
|
|
69
|
+
* @returns {(command: string, args?: string[], options?: object) => Promise<object>}
|
|
70
|
+
*/
|
|
71
|
+
export function createRtkRunner(rtkAvailable, tracker = null) {
|
|
72
|
+
return async (command, args = [], options = {}) => {
|
|
73
|
+
const wrapped = wrapWithRtk(command, args, rtkAvailable);
|
|
74
|
+
const result = await runCommand(wrapped.command, wrapped.args, options);
|
|
75
|
+
|
|
76
|
+
if (tracker && rtkAvailable && supportedSet.has(command)) {
|
|
77
|
+
const outputSize = Buffer.byteLength(result.stdout || "", "utf8");
|
|
78
|
+
// For RTK-wrapped calls, the output is already compressed.
|
|
79
|
+
// We estimate the original size as outputSize (conservative — real savings
|
|
80
|
+
// would require running the command without RTK, which defeats the purpose).
|
|
81
|
+
// The tracker is useful when the caller provides real estimates externally.
|
|
82
|
+
tracker.record(outputSize, outputSize);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
};
|
|
87
|
+
}
|