karajan-code 1.50.1 → 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/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.js +82 -11
- 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/rtk-install.js +38 -0
- package/src/utils/rtk-wrapper.js +87 -0
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";
|
package/src/orchestrator.js
CHANGED
|
@@ -41,6 +41,9 @@ import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js"
|
|
|
41
41
|
import { detectTestFramework } from "./utils/project-detect.js";
|
|
42
42
|
import { runPreflightChecks } from "./orchestrator/preflight-checks.js";
|
|
43
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";
|
|
44
47
|
import { detectNeededSkills, autoInstallSkills, cleanupAutoInstalledSkills } from "./skills/skill-detector.js";
|
|
45
48
|
import { isOpenSkillsAvailable } from "./skills/openskills-client.js";
|
|
46
49
|
|
|
@@ -278,6 +281,9 @@ function resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBa
|
|
|
278
281
|
if (!resolvedPolicies.reviewer) {
|
|
279
282
|
pipelineFlags.reviewerEnabled = false;
|
|
280
283
|
}
|
|
284
|
+
if (resolvedPolicies.coderRequired === false) {
|
|
285
|
+
pipelineFlags.coderRequired = false;
|
|
286
|
+
}
|
|
281
287
|
|
|
282
288
|
emitProgress(
|
|
283
289
|
emitter,
|
|
@@ -760,7 +766,7 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
760
766
|
return { action: "proceed" };
|
|
761
767
|
}
|
|
762
768
|
|
|
763
|
-
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 }) {
|
|
764
770
|
const gitResult = await finalizeGitAutomation({ config, gitCtx, task, logger, session, stageResults });
|
|
765
771
|
|
|
766
772
|
// Accumulate final commits for PG card lifecycle tracking
|
|
@@ -779,16 +785,24 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
|
|
|
779
785
|
await markPgCardToValidate({ pgCard, pgProject, config, session, gitResult, logger });
|
|
780
786
|
|
|
781
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;
|
|
782
794
|
emitProgress(
|
|
783
795
|
emitter,
|
|
784
796
|
makeEvent("session:end", { ...eventBase, stage: "done" }, {
|
|
785
797
|
message: deferredIssues.length > 0
|
|
786
798
|
? `Session approved (${deferredIssues.length} deferred issue(s) tracked as tech debt)`
|
|
787
799
|
: "Session approved",
|
|
788
|
-
detail:
|
|
800
|
+
detail: endDetail
|
|
789
801
|
})
|
|
790
802
|
);
|
|
791
|
-
|
|
803
|
+
const result = { approved: true, sessionId: session.id, review, git: gitResult, deferredIssues };
|
|
804
|
+
if (rtkSavings) result.rtk_savings = rtkSavings;
|
|
805
|
+
return result;
|
|
792
806
|
}
|
|
793
807
|
|
|
794
808
|
// PG card "To Validate" logic moved to src/planning-game/pipeline-adapter.js → markPgCardToValidate()
|
|
@@ -1132,7 +1146,7 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
|
|
|
1132
1146
|
return { action: "ok", review: reviewerResult.review };
|
|
1133
1147
|
}
|
|
1134
1148
|
|
|
1135
|
-
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 }) {
|
|
1136
1150
|
session.reviewer_retry_count = 0;
|
|
1137
1151
|
const postLoopResult = await handlePostLoopStages({
|
|
1138
1152
|
config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
|
|
@@ -1141,11 +1155,11 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
|
|
|
1141
1155
|
if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
|
|
1142
1156
|
if (postLoopResult.action === "continue") return { action: "continue" };
|
|
1143
1157
|
|
|
1144
|
-
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 });
|
|
1145
1159
|
return { action: "return", result };
|
|
1146
1160
|
}
|
|
1147
1161
|
|
|
1148
|
-
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 }) {
|
|
1149
1163
|
// Escalate to Solomon / human before giving up
|
|
1150
1164
|
const solomonResult = await invokeSolomon({
|
|
1151
1165
|
config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
|
|
@@ -1178,13 +1192,17 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
|
|
|
1178
1192
|
|
|
1179
1193
|
// Solomon also couldn't resolve — fail
|
|
1180
1194
|
session.budget = budgetSummary();
|
|
1195
|
+
const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
|
|
1196
|
+
if (rtkSavings) session.rtk_savings = rtkSavings;
|
|
1181
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;
|
|
1182
1200
|
emitProgress(
|
|
1183
1201
|
emitter,
|
|
1184
1202
|
makeEvent("session:end", { ...eventBase, stage: "done" }, {
|
|
1185
1203
|
status: "fail",
|
|
1186
1204
|
message: "Max iterations reached (Solomon could not resolve)",
|
|
1187
|
-
detail:
|
|
1205
|
+
detail: failDetail
|
|
1188
1206
|
})
|
|
1189
1207
|
);
|
|
1190
1208
|
return { approved: false, sessionId: session.id, reason: "max_iterations" };
|
|
@@ -1232,9 +1250,14 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1232
1250
|
const rtkResult = await detectRtk();
|
|
1233
1251
|
if (rtkResult.available) {
|
|
1234
1252
|
config = { ...config, rtk: { available: true, version: rtkResult.version } };
|
|
1235
|
-
|
|
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`);
|
|
1236
1259
|
emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
|
|
1237
|
-
message: "RTK detected —
|
|
1260
|
+
message: "RTK detected — internal commands wrapped for token optimization",
|
|
1238
1261
|
detail: { version: rtkResult.version, executorType: "local" }
|
|
1239
1262
|
}));
|
|
1240
1263
|
}
|
|
@@ -1351,7 +1374,8 @@ async function runSingleIteration(ctx) {
|
|
|
1351
1374
|
const approvedResult = await handleApprovedReview({
|
|
1352
1375
|
config, session, emitter, eventBase, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, i, task,
|
|
1353
1376
|
stageResults: ctx.stageResults, pipelineFlags: ctx.pipelineFlags, askQuestion: ctx.askQuestion, logger,
|
|
1354
|
-
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
|
|
1355
1379
|
});
|
|
1356
1380
|
if (approvedResult.action === "return" || approvedResult.action === "continue") return approvedResult;
|
|
1357
1381
|
}
|
|
@@ -1430,7 +1454,7 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
|
|
|
1430
1454
|
if (iterResult.action === "retry") { i -= 1; }
|
|
1431
1455
|
}
|
|
1432
1456
|
|
|
1433
|
-
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 });
|
|
1434
1458
|
return maxIterResult;
|
|
1435
1459
|
}
|
|
1436
1460
|
|
|
@@ -1444,6 +1468,53 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1444
1468
|
const ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
|
|
1445
1469
|
|
|
1446
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
|
+
|
|
1447
1518
|
// --- HU Sub-Pipeline: run each certified HU as an independent iteration loop ---
|
|
1448
1519
|
if (needsSubPipeline(ctx.stageResults.huReviewer)) {
|
|
1449
1520
|
logger.info(`HU sub-pipeline: ${ctx.stageResults.huReviewer.certified} certified stories — running each as a sub-pipeline`);
|
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,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
|
+
}
|