@tritard/waterbrother 0.16.44 → 0.16.45
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/agent.js +1 -0
- package/src/cli.js +423 -16
- package/src/config.js +117 -1
- package/src/corrections.js +256 -0
- package/src/episodic.js +1 -1
- package/src/evolution.js +432 -0
- package/src/helpers.js +5 -0
- package/src/regressions.js +182 -0
- package/src/scorecard.js +11 -1
- package/src/skills.js +255 -0
- package/src/workflow.js +109 -1
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -172,6 +172,7 @@ function buildSystemPrompt(profile, experienceMode = "standard", autonomyMode =
|
|
|
172
172
|
}
|
|
173
173
|
if (executionContext.calibration) ctxLines.push(`Scope calibration (from scored build history):\n${executionContext.calibration}`);
|
|
174
174
|
if (executionContext.reminders) ctxLines.push(`Scope reminders:\n${executionContext.reminders}`);
|
|
175
|
+
if (executionContext.evolution) ctxLines.push(executionContext.evolution);
|
|
175
176
|
if (ctxLines.length > 0) base += `\n\nExecution context:\n${ctxLines.join("\n")}`;
|
|
176
177
|
}
|
|
177
178
|
if (!memoryBlock) return base;
|
package/src/cli.js
CHANGED
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
} from "./frontend.js";
|
|
48
48
|
import { loadTask, saveTask, listTasks, setActiveTask, getActiveTask, closeTask } from "./task-store.js";
|
|
49
49
|
import { runDecisionPass, runInventPass, formatDecisionForDisplay, formatDecisionCompact, formatDecisionDetail } from "./decider.js";
|
|
50
|
-
import { runBuildWorkflow, startFeatureTask, runChallengeWorkflow } from "./workflow.js";
|
|
50
|
+
import { runBuildWorkflow, startFeatureTask, runChallengeWorkflow, evaluateChainRules, captureRegressionFromBuild, resolveRegressionsFromBuild, evaluatePostBuildEvolution, getEvolutionBlock } from "./workflow.js";
|
|
51
51
|
import { createPanelRenderer, buildPanelState } from "./panel.js";
|
|
52
52
|
import { deriveTaskNameFromPrompt, nextActionsForState, routeNaturalInput } from "./router.js";
|
|
53
53
|
import { compressEpisode, compressSessionEpisode, saveEpisode, loadRecentEpisodes, findRelevantEpisodes, buildEpisodicMemoryBlock, buildReminderBlock } from "./episodic.js";
|
|
@@ -94,6 +94,10 @@ import {
|
|
|
94
94
|
setSharedRoomMode,
|
|
95
95
|
upsertSharedMember
|
|
96
96
|
} from "./shared-project.js";
|
|
97
|
+
import { findRelevantRegressions, buildRegressionWarningBlock } from "./regressions.js";
|
|
98
|
+
import { loadUserSkills, approveSkill, disableSkill, removeSkill, buildSkillExecutionMessage, detectRepeatedPatterns } from "./skills.js";
|
|
99
|
+
import { captureCorrection, findRelevantCorrections, buildLessonBlock, detectUserEdits, findPendingCorrections, extractLessons, saveLessons } from "./corrections.js";
|
|
100
|
+
import { evaluateEvolution, loadProposals, approveProposal, rejectProposal, revertProposal, getActivePatches, buildEvolutionBlock, validateProposalSafety } from "./evolution.js";
|
|
97
101
|
|
|
98
102
|
const execFileAsync = promisify(execFile);
|
|
99
103
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -226,7 +230,18 @@ const INTERACTIVE_COMMANDS = [
|
|
|
226
230
|
{ name: "/cost", description: "Show session token usage and cost breakdown" },
|
|
227
231
|
{ name: "/diff", description: "Show git changes in the current repo" },
|
|
228
232
|
{ name: "/voice", description: "Toggle voice dictation (press space to record)" },
|
|
229
|
-
{ name: "/speak", description: "Toggle TTS — agent reads responses aloud (esc to stop)" }
|
|
233
|
+
{ name: "/speak", description: "Toggle TTS — agent reads responses aloud (esc to stop)" },
|
|
234
|
+
{ name: "/skill list", description: "Show all loaded skills" },
|
|
235
|
+
{ name: "/skill approve <name>", description: "Approve or re-approve a skill" },
|
|
236
|
+
{ name: "/skill disable <name>", description: "Disable a skill" },
|
|
237
|
+
{ name: "/skill remove <name>", description: "Delete a skill file" },
|
|
238
|
+
{ name: "/correct <file>", description: "Flag a file as a correction to agent work" },
|
|
239
|
+
{ name: "/evolve", description: "Show evolution summary — active patches, pending proposals, stats" },
|
|
240
|
+
{ name: "/evolve propose", description: "Manually trigger evolution evaluation" },
|
|
241
|
+
{ name: "/evolve approve <id>", description: "Approve a pending evolution proposal" },
|
|
242
|
+
{ name: "/evolve reject <id>", description: "Reject a pending evolution proposal" },
|
|
243
|
+
{ name: "/evolve revert <id>", description: "Revert an applied evolution patch" },
|
|
244
|
+
{ name: "/evolve history", description: "Show full evolution history with score deltas" }
|
|
230
245
|
];
|
|
231
246
|
|
|
232
247
|
const AGENT_PROFILES = ["coder", "designer", "reviewer", "planner"];
|
|
@@ -1253,6 +1268,7 @@ async function enrichTurnArtifacts({ agent, context, promptText, assistantText,
|
|
|
1253
1268
|
receipt = await agent.toolRuntime.updateReceipt(receipt.id, updates) || receipt;
|
|
1254
1269
|
}
|
|
1255
1270
|
|
|
1271
|
+
receipt._buildCompletedAt = receipt._buildCompletedAt || new Date().toISOString();
|
|
1256
1272
|
context.runtime.lastReceipt = receipt;
|
|
1257
1273
|
context.runtime.lastImpact = impact || receipt.impact || null;
|
|
1258
1274
|
return receipt;
|
|
@@ -1364,7 +1380,8 @@ async function finalizeReceiptArtifacts({
|
|
|
1364
1380
|
const finalReceipt = Object.keys(updates).length > 0
|
|
1365
1381
|
? (await agent.toolRuntime.updateReceipt(receipt.id, updates) || receipt)
|
|
1366
1382
|
: receipt;
|
|
1367
|
-
|
|
1383
|
+
finalReceipt._buildCompletedAt = new Date().toISOString();
|
|
1384
|
+
context.runtime.lastReceipt = finalReceipt;
|
|
1368
1385
|
context.runtime.lastImpact = artifacts?.impact || finalReceipt.impact || null;
|
|
1369
1386
|
return finalReceipt;
|
|
1370
1387
|
}
|
|
@@ -7325,7 +7342,30 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7325
7342
|
}
|
|
7326
7343
|
}
|
|
7327
7344
|
|
|
7328
|
-
|
|
7345
|
+
// Phase 2A: Skill dispatch — check user-authored skills when built-in router has no match
|
|
7346
|
+
if (!routed || routed.kind === "none" || routed.kind === "chat") {
|
|
7347
|
+
try {
|
|
7348
|
+
const loadedSkills = await loadUserSkills(context.cwd);
|
|
7349
|
+
const matchedSkill = loadedSkills.find((s) => line === s.name || line.startsWith(s.name + " "));
|
|
7350
|
+
if (matchedSkill) {
|
|
7351
|
+
const skillArgs = line.slice(matchedSkill.name.length).trim();
|
|
7352
|
+
const skillMessage = buildSkillExecutionMessage(matchedSkill, skillArgs);
|
|
7353
|
+
// Inject as user-role message, NOT system prompt
|
|
7354
|
+
const skillSpinner = createProgressSpinner(`running skill ${matchedSkill.name}...`);
|
|
7355
|
+
try {
|
|
7356
|
+
const skillResponse = await agent.runBuildTurn(skillMessage, {});
|
|
7357
|
+
skillSpinner.stop();
|
|
7358
|
+
const skillContent = skillResponse?.content || "";
|
|
7359
|
+
if (skillContent) printAssistantOutput(skillContent);
|
|
7360
|
+
} catch (skillErr) {
|
|
7361
|
+
skillSpinner.stop();
|
|
7362
|
+
console.log(`Skill ${matchedSkill.name} failed: ${skillErr instanceof Error ? skillErr.message : String(skillErr)}`);
|
|
7363
|
+
}
|
|
7364
|
+
return true;
|
|
7365
|
+
}
|
|
7366
|
+
} catch {}
|
|
7367
|
+
return false;
|
|
7368
|
+
}
|
|
7329
7369
|
|
|
7330
7370
|
try {
|
|
7331
7371
|
if (routed.kind === "start-work") {
|
|
@@ -7725,8 +7765,19 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7725
7765
|
if (line.startsWith("/") && !line.includes(" ")) {
|
|
7726
7766
|
const hasExact = INTERACTIVE_COMMANDS.some((command) => command.name === line);
|
|
7727
7767
|
if (!hasExact) {
|
|
7728
|
-
|
|
7729
|
-
|
|
7768
|
+
// Check user-authored skills before showing "no match"
|
|
7769
|
+
let isSkill = false;
|
|
7770
|
+
try {
|
|
7771
|
+
const { loadUserSkills } = await import("./skills.js");
|
|
7772
|
+
const skills = await loadUserSkills(context.cwd);
|
|
7773
|
+
isSkill = skills.some((s) => s.name === line);
|
|
7774
|
+
} catch (skillErr) {
|
|
7775
|
+
process.stderr.write(`[skills] Failed to load skills: ${skillErr instanceof Error ? skillErr.message : String(skillErr)}\n`);
|
|
7776
|
+
}
|
|
7777
|
+
if (!isSkill) {
|
|
7778
|
+
printSlashMenu(line);
|
|
7779
|
+
continue;
|
|
7780
|
+
}
|
|
7730
7781
|
}
|
|
7731
7782
|
}
|
|
7732
7783
|
|
|
@@ -8988,25 +9039,57 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
8988
9039
|
|
|
8989
9040
|
await maybeAutoCompactConversation({ agent, currentSession, context, pendingInput: buildPrompt });
|
|
8990
9041
|
|
|
8991
|
-
// Inject adaptive reminders from episodic memory
|
|
9042
|
+
// Inject adaptive reminders from episodic memory + regressions + corrections
|
|
8992
9043
|
try {
|
|
8993
9044
|
const contractPaths = task.activeContract?.paths || [];
|
|
8994
9045
|
const taskTags = [task.name, task.goal].filter(Boolean).join(" ").toLowerCase().split(/\s+/).filter((w) => w.length >= 3);
|
|
8995
9046
|
const relevant = await findRelevantEpisodes({ cwd: context.cwd, filePatterns: contractPaths, tags: taskTags, limit: 3 });
|
|
9047
|
+
let reminders = "";
|
|
8996
9048
|
if (relevant.length > 0) {
|
|
8997
|
-
|
|
9049
|
+
reminders = buildReminderBlock({
|
|
8998
9050
|
episodes: relevant,
|
|
8999
9051
|
memoryText: context.runtime.projectMemory?.raw || "",
|
|
9000
9052
|
contractPaths
|
|
9001
9053
|
});
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9054
|
+
}
|
|
9055
|
+
// Phase 1B: Inject regression warnings for overlapping paths
|
|
9056
|
+
try {
|
|
9057
|
+
const regressions = await findRelevantRegressions({ cwd: context.cwd, contractPaths, limit: 5 });
|
|
9058
|
+
const regBlock = buildRegressionWarningBlock(regressions);
|
|
9059
|
+
if (regBlock) reminders = (reminders || "") + regBlock;
|
|
9060
|
+
} catch {}
|
|
9061
|
+
// Phase 2B: Inject lesson block from corrections
|
|
9062
|
+
try {
|
|
9063
|
+
const corrections = await findRelevantCorrections({ cwd: context.cwd, contractPaths, limit: 5 });
|
|
9064
|
+
const lessonBlock = buildLessonBlock(corrections);
|
|
9065
|
+
if (lessonBlock) reminders = (reminders || "") + lessonBlock;
|
|
9066
|
+
} catch {}
|
|
9067
|
+
if (reminders) {
|
|
9068
|
+
agent.setExecutionContext({
|
|
9069
|
+
taskName: task.name,
|
|
9070
|
+
chosenOption: task.chosenOption || null,
|
|
9071
|
+
contractSummary: task.activeContract?.summary || null,
|
|
9072
|
+
phase: "build",
|
|
9073
|
+
reminders
|
|
9074
|
+
});
|
|
9075
|
+
}
|
|
9076
|
+
} catch {}
|
|
9077
|
+
|
|
9078
|
+
// Phase 2B: Detect and capture user corrections since last build
|
|
9079
|
+
try {
|
|
9080
|
+
const lastReceipt = context.runtime.lastReceipt;
|
|
9081
|
+
if (lastReceipt) {
|
|
9082
|
+
const editedFiles = await detectUserEdits({ cwd: context.cwd, receipt: lastReceipt });
|
|
9083
|
+
for (const ef of editedFiles) {
|
|
9084
|
+
try {
|
|
9085
|
+
let diff = "";
|
|
9086
|
+
try {
|
|
9087
|
+
const { stdout } = await execFileAsync("git", ["diff", "--", ef], { cwd: context.cwd, maxBuffer: 50 * 1024 });
|
|
9088
|
+
diff = stdout;
|
|
9089
|
+
} catch {}
|
|
9090
|
+
await captureCorrection({ cwd: context.cwd, filePath: ef, diff, type: "user-edit" });
|
|
9091
|
+
console.log(dim(`Correction captured: ${ef} (you edited it after the last build). Use /correct undo to revert if unintentional.`));
|
|
9092
|
+
} catch {}
|
|
9010
9093
|
}
|
|
9011
9094
|
}
|
|
9012
9095
|
} catch {}
|
|
@@ -9231,6 +9314,80 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
9231
9314
|
await setSessionRunState(currentSession, agent, "done");
|
|
9232
9315
|
await saveCurrentSession(currentSession, agent);
|
|
9233
9316
|
updatePanel();
|
|
9317
|
+
|
|
9318
|
+
// Phase 1: Post-build chain rules, regression capture/resolution
|
|
9319
|
+
if (buildResult.receipt) {
|
|
9320
|
+
const finalReceipt = buildResult.receipt;
|
|
9321
|
+
finalReceipt._buildCompletedAt = finalReceipt._buildCompletedAt || new Date().toISOString();
|
|
9322
|
+
context.runtime.lastReceipt = finalReceipt;
|
|
9323
|
+
|
|
9324
|
+
// Phase 1B: Capture regression on failure, resolve on success
|
|
9325
|
+
try {
|
|
9326
|
+
await captureRegressionFromBuild(finalReceipt, task, context.cwd);
|
|
9327
|
+
await resolveRegressionsFromBuild(finalReceipt, context.cwd);
|
|
9328
|
+
} catch {}
|
|
9329
|
+
|
|
9330
|
+
// Phase 1A: Evaluate chain rules
|
|
9331
|
+
try {
|
|
9332
|
+
const chainResult = evaluateChainRules(finalReceipt, buildResult.verifierResults, context.runtime.chainRules, {
|
|
9333
|
+
onChainSuggestion({ key }) {
|
|
9334
|
+
if (key === "build:sentinel-caution") {
|
|
9335
|
+
console.log(dim("Sentinel flagged caution. Use /challenge to adversarial review."));
|
|
9336
|
+
}
|
|
9337
|
+
}
|
|
9338
|
+
});
|
|
9339
|
+
|
|
9340
|
+
if (chainResult?.nextAction === "challenge") {
|
|
9341
|
+
console.log(dim("Sentinel blocked. Auto-challenging..."));
|
|
9342
|
+
const challengeSpinner = createProgressSpinner("challenging...");
|
|
9343
|
+
try {
|
|
9344
|
+
const challengeResult = await runChallengeWorkflow({
|
|
9345
|
+
apiKey: context.runtime.apiKey,
|
|
9346
|
+
baseUrl: context.runtime.baseUrl,
|
|
9347
|
+
model: context.runtime.reviewer?.model || agent.getModel(),
|
|
9348
|
+
receipt: chainResult.receipt,
|
|
9349
|
+
impact: finalReceipt.impact || null,
|
|
9350
|
+
task,
|
|
9351
|
+
maxDiffChars: context.runtime.reviewer?.maxDiffChars,
|
|
9352
|
+
signal: null
|
|
9353
|
+
});
|
|
9354
|
+
challengeSpinner.stop();
|
|
9355
|
+
console.log("--- Auto-Challenge Result ---");
|
|
9356
|
+
console.log(challengeResult.summary);
|
|
9357
|
+
if (challengeResult.concerns.length > 0) {
|
|
9358
|
+
for (const c of challengeResult.concerns) console.log(` ${red("•")} ${c}`);
|
|
9359
|
+
}
|
|
9360
|
+
console.log("---");
|
|
9361
|
+
await agent.toolRuntime.updateReceipt(finalReceipt.id, { challenge: challengeResult });
|
|
9362
|
+
finalReceipt.challenge = challengeResult;
|
|
9363
|
+
finalReceipt._buildCompletedAt = new Date().toISOString();
|
|
9364
|
+
context.runtime.lastReceipt = finalReceipt;
|
|
9365
|
+
} catch (challengeErr) {
|
|
9366
|
+
challengeSpinner.stop();
|
|
9367
|
+
console.log(`auto-challenge failed: ${challengeErr instanceof Error ? challengeErr.message : String(challengeErr)}`);
|
|
9368
|
+
}
|
|
9369
|
+
}
|
|
9370
|
+
} catch {}
|
|
9371
|
+
|
|
9372
|
+
// Phase 3: Post-build evolution evaluation
|
|
9373
|
+
try {
|
|
9374
|
+
const evolutionResult = await evaluatePostBuildEvolution({
|
|
9375
|
+
cwd: context.cwd,
|
|
9376
|
+
receipt: finalReceipt,
|
|
9377
|
+
task
|
|
9378
|
+
});
|
|
9379
|
+
if (evolutionResult.proposal) {
|
|
9380
|
+
const ep = evolutionResult.proposal;
|
|
9381
|
+
console.log(dim(`Evolution proposal: [${ep.category}] ${ep.description}`));
|
|
9382
|
+
console.log(dim(` Approve with: /evolve approve ${ep.id}`));
|
|
9383
|
+
}
|
|
9384
|
+
for (const ab of (evolutionResult.abResults || [])) {
|
|
9385
|
+
if (ab.suggestion === "revert") {
|
|
9386
|
+
console.log(yellow(`A/B: ${ab.message}`));
|
|
9387
|
+
}
|
|
9388
|
+
}
|
|
9389
|
+
} catch {}
|
|
9390
|
+
}
|
|
9234
9391
|
} catch (error) {
|
|
9235
9392
|
clearInterval(heartbeatTimer);
|
|
9236
9393
|
detachInterruptListener();
|
|
@@ -9339,6 +9496,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
9339
9496
|
// Persist challenge to receipt
|
|
9340
9497
|
await agent.toolRuntime.updateReceipt(receipt.id, { challenge: result });
|
|
9341
9498
|
receipt.challenge = result;
|
|
9499
|
+
receipt._buildCompletedAt = receipt._buildCompletedAt || new Date().toISOString();
|
|
9342
9500
|
context.runtime.lastReceipt = receipt;
|
|
9343
9501
|
} catch (error) {
|
|
9344
9502
|
spinner.stop();
|
|
@@ -9347,6 +9505,238 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
9347
9505
|
continue;
|
|
9348
9506
|
}
|
|
9349
9507
|
|
|
9508
|
+
// Phase 2A: /skill commands
|
|
9509
|
+
if (line === "/skill" || line.startsWith("/skill ")) {
|
|
9510
|
+
const skillArgs = line.replace("/skill", "").trim();
|
|
9511
|
+
const skillParts = skillArgs.split(/\s+/);
|
|
9512
|
+
const subCmd = skillParts[0] || "list";
|
|
9513
|
+
const skillName = skillParts.slice(1).join(" ");
|
|
9514
|
+
|
|
9515
|
+
if (subCmd === "list") {
|
|
9516
|
+
try {
|
|
9517
|
+
const skills = await loadUserSkills(context.cwd);
|
|
9518
|
+
if (skills.length === 0) {
|
|
9519
|
+
console.log("No approved skills loaded.");
|
|
9520
|
+
console.log(dim("Place .md skill files in .waterbrother/skills/ or ~/.waterbrother/skills/"));
|
|
9521
|
+
} else {
|
|
9522
|
+
console.log(`Loaded skills (${skills.length}):`);
|
|
9523
|
+
for (const s of skills) {
|
|
9524
|
+
console.log(` ${cyan(s.name)} — ${s.description || "(no description)"}`);
|
|
9525
|
+
if (s.trigger) console.log(` ${dim("trigger:")} ${s.trigger}`);
|
|
9526
|
+
console.log(` ${dim("source:")} ${s.source}`);
|
|
9527
|
+
}
|
|
9528
|
+
}
|
|
9529
|
+
} catch (err) {
|
|
9530
|
+
console.log(`Failed to load skills: ${err instanceof Error ? err.message : String(err)}`);
|
|
9531
|
+
}
|
|
9532
|
+
} else if (subCmd === "approve" && skillName) {
|
|
9533
|
+
try {
|
|
9534
|
+
const result = await approveSkill(context.cwd, skillName);
|
|
9535
|
+
if (result) {
|
|
9536
|
+
console.log(`Approved skill ${cyan(result.name)} (hash: ${result.contentHash})`);
|
|
9537
|
+
} else {
|
|
9538
|
+
console.log(`Skill "${skillName}" not found.`);
|
|
9539
|
+
}
|
|
9540
|
+
} catch (err) {
|
|
9541
|
+
console.log(`Failed to approve skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
9542
|
+
}
|
|
9543
|
+
} else if (subCmd === "disable" && skillName) {
|
|
9544
|
+
try {
|
|
9545
|
+
const result = await disableSkill(context.cwd, skillName);
|
|
9546
|
+
if (result) {
|
|
9547
|
+
console.log(`Disabled skill ${result.name}`);
|
|
9548
|
+
} else {
|
|
9549
|
+
console.log(`Skill "${skillName}" not found.`);
|
|
9550
|
+
}
|
|
9551
|
+
} catch (err) {
|
|
9552
|
+
console.log(`Failed to disable skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
9553
|
+
}
|
|
9554
|
+
} else if (subCmd === "remove" && skillName) {
|
|
9555
|
+
try {
|
|
9556
|
+
const confirmed = await promptYesNo(`Remove skill "${skillName}" permanently?`);
|
|
9557
|
+
if (!confirmed) { continue; }
|
|
9558
|
+
const result = await removeSkill(context.cwd, skillName);
|
|
9559
|
+
if (result) {
|
|
9560
|
+
console.log(`Removed skill ${result.name} from ${result.source}`);
|
|
9561
|
+
} else {
|
|
9562
|
+
console.log(`Skill "${skillName}" not found.`);
|
|
9563
|
+
}
|
|
9564
|
+
} catch (err) {
|
|
9565
|
+
console.log(`Failed to remove skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
9566
|
+
}
|
|
9567
|
+
} else {
|
|
9568
|
+
console.log("Usage: /skill list | approve <name> | disable <name> | remove <name>");
|
|
9569
|
+
}
|
|
9570
|
+
continue;
|
|
9571
|
+
}
|
|
9572
|
+
|
|
9573
|
+
// Phase 2B: /correct command
|
|
9574
|
+
if (line === "/correct" || line.startsWith("/correct ")) {
|
|
9575
|
+
const correctArgs = line.replace("/correct", "").trim();
|
|
9576
|
+
if (!correctArgs) {
|
|
9577
|
+
console.log("Usage: /correct <file> — Flag a file as a correction to agent work");
|
|
9578
|
+
continue;
|
|
9579
|
+
}
|
|
9580
|
+
try {
|
|
9581
|
+
let diff = "";
|
|
9582
|
+
try {
|
|
9583
|
+
const { stdout } = await execFileAsync("git", ["diff", "--", correctArgs], { cwd: context.cwd, maxBuffer: 50 * 1024 });
|
|
9584
|
+
diff = stdout;
|
|
9585
|
+
} catch {}
|
|
9586
|
+
if (!diff) {
|
|
9587
|
+
try {
|
|
9588
|
+
const { stdout } = await execFileAsync("git", ["diff", "HEAD", "--", correctArgs], { cwd: context.cwd, maxBuffer: 50 * 1024 });
|
|
9589
|
+
diff = stdout;
|
|
9590
|
+
} catch {}
|
|
9591
|
+
}
|
|
9592
|
+
const entry = await captureCorrection({ cwd: context.cwd, filePath: correctArgs, diff, type: "explicit" });
|
|
9593
|
+
console.log(`Captured correction ${dim(entry.id)} for ${correctArgs}`);
|
|
9594
|
+
} catch (err) {
|
|
9595
|
+
console.log(`Failed to capture correction: ${err instanceof Error ? err.message : String(err)}`);
|
|
9596
|
+
}
|
|
9597
|
+
continue;
|
|
9598
|
+
}
|
|
9599
|
+
|
|
9600
|
+
// Phase 3: /evolve commands
|
|
9601
|
+
if (line === "/evolve" || line.startsWith("/evolve ")) {
|
|
9602
|
+
const evolveArgs = line.replace("/evolve", "").trim();
|
|
9603
|
+
const evolveParts = evolveArgs.split(/\s+/);
|
|
9604
|
+
const evolveSubCmd = evolveParts[0] || "";
|
|
9605
|
+
const evolveTarget = evolveParts.slice(1).join(" ").trim();
|
|
9606
|
+
|
|
9607
|
+
try {
|
|
9608
|
+
if (!evolveSubCmd) {
|
|
9609
|
+
// /evolve — Show evolution summary
|
|
9610
|
+
const proposals = await loadProposals(context.cwd);
|
|
9611
|
+
const pending = proposals.filter((p) => p.status === "proposed");
|
|
9612
|
+
const approved = proposals.filter((p) => p.status === "approved");
|
|
9613
|
+
const rejected = proposals.filter((p) => p.status === "rejected");
|
|
9614
|
+
const reverted = proposals.filter((p) => p.status === "reverted");
|
|
9615
|
+
|
|
9616
|
+
console.log("--- Evolution Dashboard ---");
|
|
9617
|
+
console.log(`Proposals: ${proposals.length} total (${pending.length} pending, ${approved.length} active, ${rejected.length} rejected, ${reverted.length} reverted)`);
|
|
9618
|
+
|
|
9619
|
+
if (pending.length > 0) {
|
|
9620
|
+
console.log("\nPending proposals:");
|
|
9621
|
+
for (const p of pending) {
|
|
9622
|
+
console.log(` ${yellow(p.id)} [${p.category}] ${p.description}`);
|
|
9623
|
+
}
|
|
9624
|
+
}
|
|
9625
|
+
|
|
9626
|
+
if (approved.length > 0) {
|
|
9627
|
+
console.log("\nActive patches:");
|
|
9628
|
+
for (const p of approved) {
|
|
9629
|
+
const scoreDelta = p.scoreBeforeApply != null && p.scoreAfterApply != null
|
|
9630
|
+
? ` (before: ${(p.scoreBeforeApply * 100).toFixed(0)}%, after: ${(p.scoreAfterApply * 100).toFixed(0)}%)`
|
|
9631
|
+
: "";
|
|
9632
|
+
console.log(` ${green(p.id)} [${p.category}] ${p.description}${scoreDelta}`);
|
|
9633
|
+
}
|
|
9634
|
+
}
|
|
9635
|
+
|
|
9636
|
+
if (proposals.length === 0) {
|
|
9637
|
+
console.log(dim("No evolution proposals yet. Proposals are generated automatically after builds."));
|
|
9638
|
+
}
|
|
9639
|
+
} else if (evolveSubCmd === "propose") {
|
|
9640
|
+
// /evolve propose — Manual trigger
|
|
9641
|
+
console.log("Evaluating evolution...");
|
|
9642
|
+
const { proposal, abResults } = await evaluatePostBuildEvolution({
|
|
9643
|
+
cwd: context.cwd,
|
|
9644
|
+
receipt: context.runtime.lastReceipt || {},
|
|
9645
|
+
task: context.runtime.activeTask || null
|
|
9646
|
+
});
|
|
9647
|
+
if (proposal) {
|
|
9648
|
+
console.log(`New proposal: ${yellow(proposal.id)} [${proposal.category}]`);
|
|
9649
|
+
console.log(` ${proposal.description}`);
|
|
9650
|
+
console.log(` Patch: ${proposal.patch}`);
|
|
9651
|
+
console.log(dim(` Approve with: /evolve approve ${proposal.id}`));
|
|
9652
|
+
} else {
|
|
9653
|
+
console.log("No evolution proposals generated from current data.");
|
|
9654
|
+
}
|
|
9655
|
+
for (const ab of abResults) {
|
|
9656
|
+
if (ab.suggestion === "revert") {
|
|
9657
|
+
console.log(yellow(ab.message));
|
|
9658
|
+
} else {
|
|
9659
|
+
console.log(dim(ab.message));
|
|
9660
|
+
}
|
|
9661
|
+
}
|
|
9662
|
+
} else if (evolveSubCmd === "approve") {
|
|
9663
|
+
if (!evolveTarget) {
|
|
9664
|
+
console.log("Usage: /evolve approve <id>");
|
|
9665
|
+
} else {
|
|
9666
|
+
// Load scorecards to get actual build count (not proposal count)
|
|
9667
|
+
let buildCount = 0;
|
|
9668
|
+
try {
|
|
9669
|
+
const scDir = path.join(context.cwd, ".waterbrother", "memory", "scorecards");
|
|
9670
|
+
const scIndex = JSON.parse(await fs.readFile(path.join(scDir, "index.json"), "utf8"));
|
|
9671
|
+
buildCount = Array.isArray(scIndex) ? scIndex.length : 0;
|
|
9672
|
+
} catch {}
|
|
9673
|
+
const safety = validateProposalSafety(
|
|
9674
|
+
await loadProposals(context.cwd).then((all) => all.find((p) => p.id === evolveTarget)) || { category: "unknown", patch: "" },
|
|
9675
|
+
buildCount
|
|
9676
|
+
);
|
|
9677
|
+
if (!safety.safe) {
|
|
9678
|
+
console.log(red(`Safety check failed: ${safety.reason}`));
|
|
9679
|
+
} else {
|
|
9680
|
+
const approved = await approveProposal(context.cwd, evolveTarget);
|
|
9681
|
+
if (!approved) {
|
|
9682
|
+
console.log("Proposal not found.");
|
|
9683
|
+
} else if (approved.status !== "approved") {
|
|
9684
|
+
console.log(`Proposal ${evolveTarget} is in status "${approved.status}" — only pending proposals can be approved.`);
|
|
9685
|
+
} else {
|
|
9686
|
+
console.log(green(`Approved: ${approved.description}`));
|
|
9687
|
+
console.log(dim("Patch will be injected as a Learned Convention in the next build."));
|
|
9688
|
+
}
|
|
9689
|
+
}
|
|
9690
|
+
}
|
|
9691
|
+
} else if (evolveSubCmd === "reject") {
|
|
9692
|
+
if (!evolveTarget) {
|
|
9693
|
+
console.log("Usage: /evolve reject <id>");
|
|
9694
|
+
} else {
|
|
9695
|
+
const rejected = await rejectProposal(context.cwd, evolveTarget);
|
|
9696
|
+
if (!rejected) {
|
|
9697
|
+
console.log("Proposal not found.");
|
|
9698
|
+
} else {
|
|
9699
|
+
console.log(`Rejected: ${rejected.description}`);
|
|
9700
|
+
}
|
|
9701
|
+
}
|
|
9702
|
+
} else if (evolveSubCmd === "revert") {
|
|
9703
|
+
if (!evolveTarget) {
|
|
9704
|
+
console.log("Usage: /evolve revert <id>");
|
|
9705
|
+
} else {
|
|
9706
|
+
const proposals = await loadProposals(context.cwd);
|
|
9707
|
+
const target = proposals.find((p) => p.id === evolveTarget);
|
|
9708
|
+
if (!target) {
|
|
9709
|
+
console.log("Proposal not found.");
|
|
9710
|
+
} else {
|
|
9711
|
+
const reverted = await revertProposal(context.cwd, evolveTarget);
|
|
9712
|
+
if (reverted) {
|
|
9713
|
+
console.log(`Reverted: ${reverted.description}`);
|
|
9714
|
+
}
|
|
9715
|
+
}
|
|
9716
|
+
}
|
|
9717
|
+
} else if (evolveSubCmd === "history") {
|
|
9718
|
+
const proposals = await loadProposals(context.cwd);
|
|
9719
|
+
if (proposals.length === 0) {
|
|
9720
|
+
console.log("No evolution history.");
|
|
9721
|
+
} else {
|
|
9722
|
+
console.log("--- Evolution History ---");
|
|
9723
|
+
for (const p of proposals) {
|
|
9724
|
+
const statusColor = p.status === "approved" ? green : p.status === "rejected" ? red : p.status === "reverted" ? yellow : dim;
|
|
9725
|
+
const scoreDelta = p.scoreBeforeApply != null && p.scoreAfterApply != null
|
|
9726
|
+
? ` score: ${(p.scoreBeforeApply * 100).toFixed(0)}% -> ${(p.scoreAfterApply * 100).toFixed(0)}%`
|
|
9727
|
+
: "";
|
|
9728
|
+
console.log(` ${statusColor(p.status.padEnd(10))} ${p.id} [${p.category}] ${p.description}${scoreDelta}`);
|
|
9729
|
+
}
|
|
9730
|
+
}
|
|
9731
|
+
} else {
|
|
9732
|
+
console.log("Usage: /evolve [propose|approve <id>|reject <id>|revert <id>|history]");
|
|
9733
|
+
}
|
|
9734
|
+
} catch (err) {
|
|
9735
|
+
console.log(`Evolution error: ${err instanceof Error ? err.message : String(err)}`);
|
|
9736
|
+
}
|
|
9737
|
+
continue;
|
|
9738
|
+
}
|
|
9739
|
+
|
|
9350
9740
|
if (line === "/experiment" || line.startsWith("/experiment ")) {
|
|
9351
9741
|
// Parse: /experiment <goal> --metric "command" --attempts N --time M
|
|
9352
9742
|
const rawArgs = line.replace("/experiment", "").trim();
|
|
@@ -10066,7 +10456,23 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
10066
10456
|
continue;
|
|
10067
10457
|
}
|
|
10068
10458
|
|
|
10459
|
+
// Check user-authored skills before rejecting unknown slash commands
|
|
10069
10460
|
if (line.startsWith("/")) {
|
|
10461
|
+
let skillHandled = false;
|
|
10462
|
+
try {
|
|
10463
|
+
const { loadUserSkills, buildSkillExecutionMessage } = await import("./skills.js");
|
|
10464
|
+
const loadedSkills = await loadUserSkills(context.cwd);
|
|
10465
|
+
const matchedSkill = loadedSkills.find((s) => line === s.name || line.startsWith(s.name + " "));
|
|
10466
|
+
if (matchedSkill) {
|
|
10467
|
+
skillHandled = true;
|
|
10468
|
+
const skillArgs = line.slice(matchedSkill.name.length).trim();
|
|
10469
|
+
const skillMessage = buildSkillExecutionMessage(matchedSkill, skillArgs);
|
|
10470
|
+
await runTextTurnInteractive({ agent, currentSession, context, promptText: skillMessage, pendingInput: line, spinnerLabel: `running skill ${matchedSkill.name}...` });
|
|
10471
|
+
}
|
|
10472
|
+
} catch (skillErr) {
|
|
10473
|
+
process.stderr.write(`[skills] Skill dispatch failed: ${skillErr instanceof Error ? skillErr.message : String(skillErr)}\n`);
|
|
10474
|
+
}
|
|
10475
|
+
if (skillHandled) continue;
|
|
10070
10476
|
console.log("Unknown slash command. Use /help.");
|
|
10071
10477
|
continue;
|
|
10072
10478
|
}
|
|
@@ -10162,6 +10568,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
10162
10568
|
task.state = "review-ready";
|
|
10163
10569
|
await saveTask({ cwd: context.cwd, task });
|
|
10164
10570
|
context.runtime.activeTask = task;
|
|
10571
|
+
receipt._buildCompletedAt = receipt._buildCompletedAt || new Date().toISOString();
|
|
10165
10572
|
context.runtime.lastReceipt = receipt;
|
|
10166
10573
|
}
|
|
10167
10574
|
console.log(`────────────────────────────────────────────────────────────────────────`);
|