@tritard/waterbrother 0.5.13 → 0.6.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/cli.js +283 -41
- package/src/decider.js +84 -30
- package/src/panel.js +67 -113
- package/src/router.js +88 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -15,9 +15,10 @@ import { AUTONOMY_MODES, buildOperatorIdentity, EXPERIENCE_MODES, modeDefaults,
|
|
|
15
15
|
import { computeImpactMap } from "./impact.js";
|
|
16
16
|
import { reviewTurn } from "./reviewer.js";
|
|
17
17
|
import { loadTask, saveTask, listTasks, setActiveTask, getActiveTask, closeTask } from "./task-store.js";
|
|
18
|
-
import { runDecisionPass, formatDecisionForDisplay } from "./decider.js";
|
|
18
|
+
import { runDecisionPass, runInventPass, formatDecisionForDisplay, formatDecisionCompact, formatDecisionDetail } from "./decider.js";
|
|
19
19
|
import { runBuildWorkflow, startFeatureTask, runChallengeWorkflow } from "./workflow.js";
|
|
20
20
|
import { createPanelRenderer, buildPanelState } from "./panel.js";
|
|
21
|
+
import { deriveTaskNameFromPrompt, nextActionsForState, routeNaturalInput } from "./router.js";
|
|
21
22
|
|
|
22
23
|
const execFileAsync = promisify(execFile);
|
|
23
24
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -1633,28 +1634,6 @@ async function pathExists(targetPath) {
|
|
|
1633
1634
|
}
|
|
1634
1635
|
}
|
|
1635
1636
|
|
|
1636
|
-
function detectCasualInput(line) {
|
|
1637
|
-
const text = line.trim().toLowerCase();
|
|
1638
|
-
const words = text.split(/\s+/);
|
|
1639
|
-
// Short messages (1-4 words) that don't look like commands or code tasks
|
|
1640
|
-
if (words.length > 8) return false;
|
|
1641
|
-
// Greetings and casual phrases
|
|
1642
|
-
const casualPatterns = [
|
|
1643
|
-
/^(hey|hi|hello|sup|yo|what'?s? up|howdy|hola|heya|hiya|good (morning|evening|afternoon|night))[\s!.?]*$/,
|
|
1644
|
-
/^(thanks|thank you|thx|ty|cheers|cool|nice|ok|okay|sure|yep|yup|nope|nah|lol|lmao|haha|heh|hmm|bruh|bro|dude)[\s!.?]*$/,
|
|
1645
|
-
/^(how are you|how'?s? it going|what'?s? good|what'?s? new|how do you do)[\s!.?]*$/,
|
|
1646
|
-
/^(bye|goodbye|see ya|later|peace|gn|good night|ttyl|cya)[\s!.?]*$/,
|
|
1647
|
-
/^(who are you|what are you|tell me about yourself)[\s!.?]*$/,
|
|
1648
|
-
];
|
|
1649
|
-
for (const pattern of casualPatterns) {
|
|
1650
|
-
if (pattern.test(text)) return true;
|
|
1651
|
-
}
|
|
1652
|
-
// Very short non-code messages (1-3 words, no code-like tokens)
|
|
1653
|
-
if (words.length <= 3 && !/[./\\{}()\[\]<>=;:|`$@#]/.test(text) && !/\b(fix|build|create|add|remove|delete|update|install|run|test|write|read|edit|debug|deploy|commit|push|pull|merge|refactor|implement|check)\b/.test(text)) {
|
|
1654
|
-
return true;
|
|
1655
|
-
}
|
|
1656
|
-
return false;
|
|
1657
|
-
}
|
|
1658
1637
|
|
|
1659
1638
|
async function detectDroppedImageInput(line, cwd) {
|
|
1660
1639
|
const tokens = tokenizeShellInput(line);
|
|
@@ -4087,9 +4066,240 @@ async function promptLoop(agent, session, context) {
|
|
|
4087
4066
|
cwd: context.cwd,
|
|
4088
4067
|
task,
|
|
4089
4068
|
agent,
|
|
4090
|
-
receipt
|
|
4069
|
+
receipt,
|
|
4070
|
+
runtime: context.runtime
|
|
4091
4071
|
}));
|
|
4092
4072
|
}
|
|
4073
|
+
|
|
4074
|
+
function setCockpitState({ pass = null, summary = null, actions = null } = {}) {
|
|
4075
|
+
if (pass) context.runtime.activePass = String(pass).toUpperCase();
|
|
4076
|
+
if (summary !== null) context.runtime.panelSummary = summary;
|
|
4077
|
+
if (actions) context.runtime.actionHints = actions;
|
|
4078
|
+
const task = context.runtime.activeTask;
|
|
4079
|
+
if (task && pass) task.lastPass = String(pass).toUpperCase();
|
|
4080
|
+
updatePanel();
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
function renderCompactDecisionBlock(task, decision, { invent = false } = {}) {
|
|
4084
|
+
const mode = agent.getExperienceMode();
|
|
4085
|
+
const title = task?.name || decision?.goal || (invent ? "invent" : "decision");
|
|
4086
|
+
console.log(formatDecisionCompact(decision, { mode, title }));
|
|
4087
|
+
const actions = decision.options.map((_, i) => String(i + 1));
|
|
4088
|
+
actions.push("go");
|
|
4089
|
+
if (decision.options.length >= 2) actions.push("tell me more about 2");
|
|
4090
|
+
setCockpitState({
|
|
4091
|
+
pass: invent ? "INVENT" : "DECIDE",
|
|
4092
|
+
summary: invent ? "divergent options ready" : "options ready",
|
|
4093
|
+
actions
|
|
4094
|
+
});
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
function renderDecisionDetailBlock(task, decision, optionIndex) {
|
|
4098
|
+
console.log(formatDecisionDetail(decision, optionIndex));
|
|
4099
|
+
const actions = decision.options.map((_, i) => String(i + 1));
|
|
4100
|
+
actions.push("go");
|
|
4101
|
+
setCockpitState({ pass: task?.lastPass || "DECIDE", summary: `details for option ${optionIndex}`, actions });
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
function renderReviewCockpit(task, receipt) {
|
|
4105
|
+
const concerns = [];
|
|
4106
|
+
if (receipt?.review?.concerns?.length) concerns.push(...receipt.review.concerns);
|
|
4107
|
+
if (receipt?.challenge?.concerns?.length) {
|
|
4108
|
+
for (const c of receipt.challenge.concerns) if (!concerns.includes(c)) concerns.push(c);
|
|
4109
|
+
}
|
|
4110
|
+
const verificationFailed = Array.isArray(receipt?.verification) && receipt.verification.some((v) => !v.ok);
|
|
4111
|
+
const blocked = verificationFailed || receipt?.review?.verdict === "block";
|
|
4112
|
+
if (concerns.length === 0 && !blocked) {
|
|
4113
|
+
console.log(`${green("✓")} no issues found`);
|
|
4114
|
+
console.log(dim("accept · redo · challenge harder · ship it"));
|
|
4115
|
+
setCockpitState({ pass: "CHALLENGE", summary: "no issues found", actions: ["accept", "redo", "challenge harder", "ship it"] });
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
for (const concern of concerns.slice(0, 3)) {
|
|
4119
|
+
console.log(`${yellow("⚠")} ${concern}`);
|
|
4120
|
+
}
|
|
4121
|
+
console.log(dim(blocked ? "fix these · ignore · challenge harder" : "fix these · ignore · challenge harder · ship it"));
|
|
4122
|
+
setCockpitState({ pass: "CHALLENGE", summary: `${concerns.length || 1} issue${(concerns.length || 1) === 1 ? "" : "s"} found`, actions: blocked ? ["fix these", "ignore", "challenge harder"] : ["fix these", "ignore", "challenge harder", "ship it"] });
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
function latestReviewConcerns(receipt) {
|
|
4126
|
+
const items = [];
|
|
4127
|
+
if (receipt?.review?.concerns?.length) items.push(...receipt.review.concerns);
|
|
4128
|
+
if (receipt?.challenge?.concerns?.length) {
|
|
4129
|
+
for (const c of receipt.challenge.concerns) if (!items.includes(c)) items.push(c);
|
|
4130
|
+
}
|
|
4131
|
+
return items;
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
function canShipReceipt(receipt) {
|
|
4135
|
+
if (!receipt) return { ok: false, reason: "no receipt available" };
|
|
4136
|
+
if (Array.isArray(receipt.verification) && receipt.verification.some((v) => !v.ok)) {
|
|
4137
|
+
return { ok: false, reason: "verification has failing checks" };
|
|
4138
|
+
}
|
|
4139
|
+
if (receipt.review?.verdict === "block") {
|
|
4140
|
+
return { ok: false, reason: "sentinel marked this receipt as blocking" };
|
|
4141
|
+
}
|
|
4142
|
+
return { ok: true, reason: "" };
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
async function ensureTaskFromNaturalInput(line) {
|
|
4146
|
+
const existing = context.runtime.activeTask;
|
|
4147
|
+
if (existing) return existing;
|
|
4148
|
+
const taskName = deriveTaskNameFromPrompt(line);
|
|
4149
|
+
const branchPrefix = context.runtime.taskDefaults?.branchPrefix || "wb/";
|
|
4150
|
+
const { task } = await startFeatureTask({
|
|
4151
|
+
cwd: context.cwd,
|
|
4152
|
+
name: taskName,
|
|
4153
|
+
sessionId: currentSession.id,
|
|
4154
|
+
mode: agent.getExperienceMode(),
|
|
4155
|
+
autonomy: agent.getAutonomyMode(),
|
|
4156
|
+
branchPrefix
|
|
4157
|
+
});
|
|
4158
|
+
attachTaskToSession(currentSession, task.id);
|
|
4159
|
+
context.runtime.activeTask = task;
|
|
4160
|
+
agent.toolRuntime.setTaskContext({ taskId: task.id, taskName: task.name });
|
|
4161
|
+
await saveCurrentSession(currentSession, agent);
|
|
4162
|
+
return task;
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
async function runNaturalDecision({ task, goal, invent = false }) {
|
|
4166
|
+
const spinner = createProgressSpinner(invent ? "thinking wider..." : "running decision pass...");
|
|
4167
|
+
try {
|
|
4168
|
+
const decisionModel = context.runtime.decisionModel || agent.getModel();
|
|
4169
|
+
const runner = invent ? runInventPass : runDecisionPass;
|
|
4170
|
+
const { decision } = await runner({
|
|
4171
|
+
apiKey: context.runtime.apiKey,
|
|
4172
|
+
baseUrl: context.runtime.baseUrl,
|
|
4173
|
+
model: decisionModel,
|
|
4174
|
+
goal,
|
|
4175
|
+
taskName: task.name,
|
|
4176
|
+
memory: context.runtime.projectMemory?.promptText || "",
|
|
4177
|
+
signal: null
|
|
4178
|
+
});
|
|
4179
|
+
spinner.stop();
|
|
4180
|
+
task.goal = goal;
|
|
4181
|
+
task.decisionId = `dec_${Date.now()}`;
|
|
4182
|
+
task.lastDecision = decision;
|
|
4183
|
+
task.lastDecisionType = invent ? "invent" : "decide";
|
|
4184
|
+
task.state = "decision-ready";
|
|
4185
|
+
task.lastPass = invent ? "INVENT" : "DECIDE";
|
|
4186
|
+
await saveTask({ cwd: context.cwd, task });
|
|
4187
|
+
context.runtime.activeTask = task;
|
|
4188
|
+
renderCompactDecisionBlock(task, decision, { invent });
|
|
4189
|
+
return decision;
|
|
4190
|
+
} catch (error) {
|
|
4191
|
+
spinner.stop();
|
|
4192
|
+
throw error;
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
async function chooseDecisionOption(task, optionIndexOrId) {
|
|
4197
|
+
if (!task?.lastDecision) throw new Error("no decision available");
|
|
4198
|
+
let option = null;
|
|
4199
|
+
if (typeof optionIndexOrId === "number") {
|
|
4200
|
+
option = task.lastDecision.options[Math.max(0, optionIndexOrId - 1)] || null;
|
|
4201
|
+
} else {
|
|
4202
|
+
const choiceId = String(optionIndexOrId || "").trim().toLowerCase();
|
|
4203
|
+
option = task.lastDecision.options.find((o) => o.id.toLowerCase() === choiceId) || null;
|
|
4204
|
+
}
|
|
4205
|
+
if (!option) throw new Error("option not found");
|
|
4206
|
+
task.chosenOption = option.id;
|
|
4207
|
+
task.activeContract = {
|
|
4208
|
+
summary: `${task.name}: ${option.title}`,
|
|
4209
|
+
paths: option.scope?.paths || [],
|
|
4210
|
+
commands: option.scope?.commands || [],
|
|
4211
|
+
verification: option.scope?.commands || [],
|
|
4212
|
+
risk: option.risk || "medium"
|
|
4213
|
+
};
|
|
4214
|
+
task.state = "build-ready";
|
|
4215
|
+
task.lastPass = task.lastDecisionType === "invent" ? "INVENT" : "DECIDE";
|
|
4216
|
+
agent.toolRuntime.setCurrentContract(task.activeContract);
|
|
4217
|
+
agent.toolRuntime.setTaskContext({ taskId: task.id, taskName: task.name, decisionId: task.decisionId, chosenOption: option.id });
|
|
4218
|
+
await saveTask({ cwd: context.cwd, task });
|
|
4219
|
+
context.runtime.activeTask = task;
|
|
4220
|
+
setCockpitState({ pass: task.lastPass || "DECIDE", summary: `option locked: ${option.title}`, actions: ["build it", "check it", "think deeper"] });
|
|
4221
|
+
console.log(`${cyan("locked")} ${option.title}`);
|
|
4222
|
+
return option;
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
async function acceptLatestReceipt(task, { natural = false } = {}) {
|
|
4226
|
+
if (!task) throw new Error("no active task");
|
|
4227
|
+
if (!task.latestReceiptId) throw new Error("no receipt to accept");
|
|
4228
|
+
const receipt = context.runtime.lastReceipt || await agent.toolRuntime.readReceipt(task.latestReceiptId);
|
|
4229
|
+
const gate = canShipReceipt(receipt);
|
|
4230
|
+
if (!gate.ok) throw new Error(gate.reason);
|
|
4231
|
+
await agent.toolRuntime.markReceiptAccepted(task.latestReceiptId);
|
|
4232
|
+
task.accepted = true;
|
|
4233
|
+
task.state = "accepted";
|
|
4234
|
+
task.lastPass = "CHALLENGE";
|
|
4235
|
+
await saveTask({ cwd: context.cwd, task });
|
|
4236
|
+
context.runtime.activeTask = task;
|
|
4237
|
+
setCockpitState({ pass: "CHALLENGE", summary: natural ? "accepted — ready to close or ship" : "receipt accepted", actions: ["close", "start next task"] });
|
|
4238
|
+
console.log(`receipt ${task.latestReceiptId} accepted`);
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
async function handleNaturalInput(line) {
|
|
4242
|
+
const routed = routeNaturalInput(line, { task: context.runtime.activeTask });
|
|
4243
|
+
if (!routed || routed.kind === "none" || routed.kind === "chat") return false;
|
|
4244
|
+
|
|
4245
|
+
try {
|
|
4246
|
+
if (routed.kind === "start-work") {
|
|
4247
|
+
const task = await ensureTaskFromNaturalInput(line);
|
|
4248
|
+
await runNaturalDecision({ task, goal: line, invent: false });
|
|
4249
|
+
return true;
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
const task = context.runtime.activeTask;
|
|
4253
|
+
if (!task) return false;
|
|
4254
|
+
|
|
4255
|
+
if (routed.kind === "decision-detail") {
|
|
4256
|
+
renderDecisionDetailBlock(task, task.lastDecision, routed.optionIndex);
|
|
4257
|
+
return true;
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
if (routed.kind === "choose") {
|
|
4261
|
+
await chooseDecisionOption(task, routed.optionIndex);
|
|
4262
|
+
return true;
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
if (routed.kind === "choose-recommended-and-build") {
|
|
4266
|
+
const pick = task.lastDecision?.recommendation || 1;
|
|
4267
|
+
await chooseDecisionOption(task, pick);
|
|
4268
|
+
line = "/build";
|
|
4269
|
+
} else if (routed.kind === "decide") {
|
|
4270
|
+
await runNaturalDecision({ task, goal: line, invent: false });
|
|
4271
|
+
return true;
|
|
4272
|
+
} else if (routed.kind === "invent") {
|
|
4273
|
+
await runNaturalDecision({ task, goal: line, invent: true });
|
|
4274
|
+
return true;
|
|
4275
|
+
} else if (routed.kind === "accept") {
|
|
4276
|
+
await acceptLatestReceipt(task, { natural: true });
|
|
4277
|
+
return true;
|
|
4278
|
+
} else if (routed.kind === "fix-review-findings") {
|
|
4279
|
+
const receipt = context.runtime.lastReceipt || await agent.toolRuntime.readReceipt(task.latestReceiptId || "last");
|
|
4280
|
+
const concerns = latestReviewConcerns(receipt);
|
|
4281
|
+
const prompt = concerns.length > 0
|
|
4282
|
+
? `Fix these review findings for task ${task.name}: ${concerns.join("; ")}`
|
|
4283
|
+
: `Redo the last build for task ${task.name}, tightening any obvious gaps.`;
|
|
4284
|
+
return { rewrittenLine: `/build ${prompt}` };
|
|
4285
|
+
} else if (routed.kind === "build" && (task.state === "build-ready" || task.state === "review-ready")) {
|
|
4286
|
+
line = "/build";
|
|
4287
|
+
} else if (routed.kind === "challenge") {
|
|
4288
|
+
line = "/challenge";
|
|
4289
|
+
} else if (routed.kind === "ignore") {
|
|
4290
|
+
console.log("ignored for now — you can say ship it, build it, or challenge harder");
|
|
4291
|
+
setCockpitState({ pass: "CHALLENGE", summary: "review concerns acknowledged", actions: ["fix these", "challenge harder", "ship it"] });
|
|
4292
|
+
return true;
|
|
4293
|
+
} else {
|
|
4294
|
+
return false;
|
|
4295
|
+
}
|
|
4296
|
+
} catch (error) {
|
|
4297
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
4298
|
+
return true;
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
return { rewrittenLine: line };
|
|
4302
|
+
}
|
|
4093
4303
|
if (context.runtime.activeTask) {
|
|
4094
4304
|
updatePanel();
|
|
4095
4305
|
}
|
|
@@ -4118,7 +4328,7 @@ async function promptLoop(agent, session, context) {
|
|
|
4118
4328
|
}
|
|
4119
4329
|
|
|
4120
4330
|
while (true) {
|
|
4121
|
-
|
|
4331
|
+
let line = normalizeInteractiveInput(
|
|
4122
4332
|
await readInteractiveLine({
|
|
4123
4333
|
getFooterText(inputBuffer) {
|
|
4124
4334
|
return buildInteractiveFooter({
|
|
@@ -5564,26 +5774,56 @@ async function promptLoop(agent, session, context) {
|
|
|
5564
5774
|
continue;
|
|
5565
5775
|
}
|
|
5566
5776
|
|
|
5567
|
-
//
|
|
5568
|
-
const
|
|
5569
|
-
if (
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5777
|
+
// Natural language intent routing
|
|
5778
|
+
const naturalResult = await handleNaturalInput(line);
|
|
5779
|
+
if (naturalResult === true) {
|
|
5780
|
+
continue;
|
|
5781
|
+
}
|
|
5782
|
+
if (naturalResult && typeof naturalResult === "object" && naturalResult.rewrittenLine) {
|
|
5783
|
+
// Rewritten to a slash command — execute it directly as if typed
|
|
5784
|
+
const rewritten = naturalResult.rewrittenLine;
|
|
5785
|
+
if (rewritten === "/build" || rewritten.startsWith("/build ")) {
|
|
5786
|
+
// Trigger the /build handler with the rewritten prompt
|
|
5787
|
+
line = rewritten;
|
|
5788
|
+
// Fall through — the /build handler above already ran or line is now /build
|
|
5789
|
+
// We need to re-trigger, so just run build inline
|
|
5790
|
+
const task = context.runtime.activeTask;
|
|
5791
|
+
if (task) {
|
|
5792
|
+
const buildArg = rewritten.replace("/build", "").trim();
|
|
5793
|
+
const buildPrompt = buildArg || (task.chosenOption ? `Execute the chosen approach: ${task.chosenOption}` : `Build: ${task.name}`);
|
|
5794
|
+
try {
|
|
5795
|
+
await runTextTurnInteractive({
|
|
5796
|
+
agent,
|
|
5797
|
+
currentSession,
|
|
5798
|
+
context,
|
|
5799
|
+
promptText: buildPrompt,
|
|
5800
|
+
pendingInput: buildPrompt,
|
|
5801
|
+
spinnerLabel: "building..."
|
|
5802
|
+
});
|
|
5803
|
+
} catch (error) {
|
|
5804
|
+
console.log(`build failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
} else if (rewritten === "/challenge") {
|
|
5808
|
+
const task = context.runtime.activeTask;
|
|
5809
|
+
const receipt = context.runtime.lastReceipt || (task && await agent.toolRuntime.readReceipt(task?.latestReceiptId || "last"));
|
|
5810
|
+
if (receipt) {
|
|
5811
|
+
try {
|
|
5812
|
+
const challengeResult = await runChallengeWorkflow({ agent, context, receipt });
|
|
5813
|
+
if (challengeResult?.challenge) {
|
|
5814
|
+
renderReviewCockpit(task, { ...receipt, challenge: challengeResult.challenge });
|
|
5815
|
+
}
|
|
5816
|
+
} catch (error) {
|
|
5817
|
+
console.log(`challenge failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5583
5820
|
}
|
|
5584
5821
|
continue;
|
|
5585
5822
|
}
|
|
5586
5823
|
|
|
5824
|
+
// Chat fallback: router returned false (chat/unhandled) — send to model without tools
|
|
5825
|
+
const prevTools = agent.enableTools;
|
|
5826
|
+
agent.enableTools = false;
|
|
5587
5827
|
try {
|
|
5588
5828
|
await runTextTurnInteractive({
|
|
5589
5829
|
agent,
|
|
@@ -5595,6 +5835,8 @@ async function promptLoop(agent, session, context) {
|
|
|
5595
5835
|
});
|
|
5596
5836
|
} catch (error) {
|
|
5597
5837
|
console.log(`request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5838
|
+
} finally {
|
|
5839
|
+
agent.enableTools = prevTools;
|
|
5598
5840
|
}
|
|
5599
5841
|
}
|
|
5600
5842
|
|
package/src/decider.js
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import { createJsonCompletion } from "./grok-client.js";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
You will be given a task goal and optionally some project context. Your job is to:
|
|
6
|
-
1. Analyze the goal and propose 2-4 concrete implementation options
|
|
7
|
-
2. Each option should have a clear scope (file paths, commands)
|
|
8
|
-
3. Recommend one option with a rationale
|
|
9
|
-
4. Flag open risks
|
|
10
|
-
|
|
11
|
-
Respond with ONLY a JSON object matching this schema:
|
|
3
|
+
const DECISION_SCHEMA = `Respond with ONLY a JSON object matching this schema:
|
|
12
4
|
{
|
|
13
5
|
"goal": "one-line restatement of the goal",
|
|
14
6
|
"options": [
|
|
@@ -29,20 +21,44 @@ Respond with ONLY a JSON object matching this schema:
|
|
|
29
21
|
"recommendation": "option_id",
|
|
30
22
|
"rationale": "Why this option fits best given current constraints",
|
|
31
23
|
"openRisks": ["risk description"]
|
|
32
|
-
}
|
|
24
|
+
}`;
|
|
25
|
+
|
|
26
|
+
const DECISION_SYSTEM_PROMPT = `You are a senior software architect helping a developer choose an implementation strategy.
|
|
27
|
+
|
|
28
|
+
Your job is to:
|
|
29
|
+
1. Analyze the goal and propose 2-4 concrete implementation options
|
|
30
|
+
2. Keep options terse, concrete, and scoped
|
|
31
|
+
3. Recommend one option with a rationale
|
|
32
|
+
4. Flag open risks
|
|
33
33
|
|
|
34
34
|
Rules:
|
|
35
35
|
- Always include at least a "minimal" option
|
|
36
36
|
- Be concrete about file paths and commands — guess from the goal if needed
|
|
37
37
|
- Keep summaries factual, not promotional
|
|
38
38
|
- If you cannot determine scope, say so in openRisks
|
|
39
|
-
- Do not include markdown, code fences, or explanatory text outside the JSON
|
|
39
|
+
- Do not include markdown, code fences, or explanatory text outside the JSON
|
|
40
|
+
|
|
41
|
+
${DECISION_SCHEMA}`;
|
|
42
|
+
|
|
43
|
+
const INVENT_SYSTEM_PROMPT = `You are a sharp, divergent product-and-engineering strategist.
|
|
44
|
+
|
|
45
|
+
Your job is to generate non-obvious ways to approach the goal.
|
|
46
|
+
- Prefer surprising but plausible alternatives
|
|
47
|
+
- Surface hidden assumptions and second-order effects
|
|
48
|
+
- Do not just rename the same idea three times
|
|
49
|
+
- At least one option should feel unconventional but still implementable
|
|
50
|
+
- Still provide realistic scope and commands when possible
|
|
40
51
|
|
|
41
|
-
|
|
52
|
+
Keep the output terse and concrete.
|
|
53
|
+
Do not include markdown, code fences, or explanatory text outside the JSON.
|
|
54
|
+
|
|
55
|
+
${DECISION_SCHEMA}`;
|
|
56
|
+
|
|
57
|
+
function buildDecisionPrompt({ goal, memory, taskName, invent = false }) {
|
|
42
58
|
const parts = [`Goal: ${goal}`];
|
|
43
59
|
if (taskName) parts.push(`Task: ${taskName}`);
|
|
44
60
|
if (memory) parts.push(`Project context (WATERBROTHER.md):\n${memory}`);
|
|
45
|
-
parts.push("Propose implementation options as JSON.");
|
|
61
|
+
parts.push(invent ? "Generate divergent implementation options as JSON." : "Propose implementation options as JSON.");
|
|
46
62
|
return parts.join("\n\n");
|
|
47
63
|
}
|
|
48
64
|
|
|
@@ -51,13 +67,13 @@ export function normalizeDecision(decision) {
|
|
|
51
67
|
const options = Array.isArray(decision.options) ? decision.options : [];
|
|
52
68
|
return {
|
|
53
69
|
goal: String(decision.goal || "").trim(),
|
|
54
|
-
options: options.map((opt) => ({
|
|
55
|
-
id: String(opt.id ||
|
|
70
|
+
options: options.map((opt, index) => ({
|
|
71
|
+
id: String(opt.id || `option-${index + 1}`).trim(),
|
|
56
72
|
title: String(opt.title || "").trim(),
|
|
57
73
|
summary: String(opt.summary || "").trim(),
|
|
58
74
|
pros: Array.isArray(opt.pros) ? opt.pros.map(String) : [],
|
|
59
75
|
cons: Array.isArray(opt.cons) ? opt.cons.map(String) : [],
|
|
60
|
-
risk: ["low", "medium", "high"].includes(opt.risk) ? opt.risk : "medium",
|
|
76
|
+
risk: ["low", "medium", "high"].includes(String(opt.risk || "").trim()) ? String(opt.risk).trim() : "medium",
|
|
61
77
|
fileCount: Number.isFinite(Number(opt.fileCount)) ? Math.max(0, Math.floor(Number(opt.fileCount))) : null,
|
|
62
78
|
scope: opt.scope && typeof opt.scope === "object"
|
|
63
79
|
? {
|
|
@@ -72,20 +88,12 @@ export function normalizeDecision(decision) {
|
|
|
72
88
|
};
|
|
73
89
|
}
|
|
74
90
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
baseUrl,
|
|
78
|
-
model,
|
|
79
|
-
goal,
|
|
80
|
-
taskName,
|
|
81
|
-
memory,
|
|
82
|
-
signal
|
|
83
|
-
}) {
|
|
84
|
-
if (!goal) throw new Error("goal is required for /decide");
|
|
91
|
+
async function runPlannerPass({ apiKey, baseUrl, model, goal, taskName, memory, signal, invent = false }) {
|
|
92
|
+
if (!goal) throw new Error("goal is required for planning");
|
|
85
93
|
|
|
86
94
|
const messages = [
|
|
87
|
-
{ role: "system", content: DECISION_SYSTEM_PROMPT },
|
|
88
|
-
{ role: "user", content: buildDecisionPrompt({ goal, memory, taskName }) }
|
|
95
|
+
{ role: "system", content: invent ? INVENT_SYSTEM_PROMPT : DECISION_SYSTEM_PROMPT },
|
|
96
|
+
{ role: "user", content: buildDecisionPrompt({ goal, memory, taskName, invent }) }
|
|
89
97
|
];
|
|
90
98
|
|
|
91
99
|
const completion = await createJsonCompletion({
|
|
@@ -93,13 +101,13 @@ export async function runDecisionPass({
|
|
|
93
101
|
baseUrl,
|
|
94
102
|
model,
|
|
95
103
|
messages,
|
|
96
|
-
temperature: 0.3,
|
|
104
|
+
temperature: invent ? 0.7 : 0.3,
|
|
97
105
|
signal
|
|
98
106
|
});
|
|
99
107
|
|
|
100
108
|
const decision = normalizeDecision(completion.json);
|
|
101
109
|
if (!decision || decision.options.length === 0) {
|
|
102
|
-
throw new Error("Decision pass returned no options
|
|
110
|
+
throw new Error(`${invent ? "Invent" : "Decision"} pass returned no options`);
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
return {
|
|
@@ -108,6 +116,14 @@ export async function runDecisionPass({
|
|
|
108
116
|
};
|
|
109
117
|
}
|
|
110
118
|
|
|
119
|
+
export async function runDecisionPass(args) {
|
|
120
|
+
return runPlannerPass({ ...args, invent: false });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function runInventPass(args) {
|
|
124
|
+
return runPlannerPass({ ...args, invent: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
111
127
|
export function formatDecisionForDisplay(decision) {
|
|
112
128
|
if (!decision) return "No decision available.";
|
|
113
129
|
const lines = [];
|
|
@@ -129,3 +145,41 @@ export function formatDecisionForDisplay(decision) {
|
|
|
129
145
|
if (decision.openRisks.length > 0) lines.push(`Open risks: ${decision.openRisks.join("; ")}`);
|
|
130
146
|
return lines.join("\n");
|
|
131
147
|
}
|
|
148
|
+
|
|
149
|
+
export function formatDecisionCompact(decision, { mode = "standard", title = null } = {}) {
|
|
150
|
+
if (!decision) return "No options available.";
|
|
151
|
+
const lines = [];
|
|
152
|
+
const heading = title || decision.goal || "Decision";
|
|
153
|
+
const rule = "─".repeat(Math.min(56, Math.max(28, heading.length + 4)));
|
|
154
|
+
lines.push(rule);
|
|
155
|
+
lines.push(heading);
|
|
156
|
+
lines.push(rule);
|
|
157
|
+
for (let index = 0; index < decision.options.length; index += 1) {
|
|
158
|
+
const opt = decision.options[index];
|
|
159
|
+
const recommended = opt.id === decision.recommendation ? " ← recommended" : "";
|
|
160
|
+
const files = opt.fileCount != null ? `~${opt.fileCount} files` : "scope varies";
|
|
161
|
+
const summary = mode === "guide" ? opt.summary : opt.title;
|
|
162
|
+
lines.push(` ${index + 1}. ${summary} ${files} ${opt.risk} risk${recommended}`);
|
|
163
|
+
}
|
|
164
|
+
lines.push(rule);
|
|
165
|
+
if (mode === "guide" && decision.rationale) {
|
|
166
|
+
lines.push(`Why this one: ${decision.rationale}`);
|
|
167
|
+
}
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function formatDecisionDetail(decision, selector) {
|
|
172
|
+
if (!decision || !Array.isArray(decision.options) || decision.options.length === 0) return "No option details available.";
|
|
173
|
+
const option = Number.isInteger(selector)
|
|
174
|
+
? decision.options[Math.max(0, selector - 1)]
|
|
175
|
+
: decision.options.find((opt) => opt.id === selector);
|
|
176
|
+
if (!option) return "Option not found.";
|
|
177
|
+
const lines = [];
|
|
178
|
+
lines.push(`${option.title} (${option.risk} risk)`);
|
|
179
|
+
lines.push(option.summary || "No summary.");
|
|
180
|
+
if (option.pros.length) lines.push(`Pros: ${option.pros.join(", ")}`);
|
|
181
|
+
if (option.cons.length) lines.push(`Cons: ${option.cons.join(", ")}`);
|
|
182
|
+
if (option.scope.paths.length) lines.push(`Scope: ${option.scope.paths.join(", ")}`);
|
|
183
|
+
if (option.scope.commands.length) lines.push(`Commands: ${option.scope.commands.join(", ")}`);
|
|
184
|
+
return lines.join("\n");
|
|
185
|
+
}
|
package/src/panel.js
CHANGED
|
@@ -1,122 +1,75 @@
|
|
|
1
1
|
const ESC = "\x1b";
|
|
2
2
|
const CSI = `${ESC}[`;
|
|
3
|
-
const SAVE_CURSOR = `${ESC}7`;
|
|
4
|
-
const RESTORE_CURSOR = `${ESC}8`;
|
|
5
3
|
|
|
6
|
-
function dim(text) {
|
|
7
|
-
|
|
4
|
+
function dim(text) { return `${CSI}2m${text}${CSI}0m`; }
|
|
5
|
+
function bold(text) { return `${CSI}1m${text}${CSI}0m`; }
|
|
6
|
+
function cyan(text) { return `${CSI}36m${text}${CSI}0m`; }
|
|
7
|
+
function yellow(text) { return `${CSI}33m${text}${CSI}0m`; }
|
|
8
|
+
function green(text) { return `${CSI}32m${text}${CSI}0m`; }
|
|
9
|
+
function red(text) { return `${CSI}31m${text}${CSI}0m`; }
|
|
10
|
+
|
|
11
|
+
function formatPass(pass) {
|
|
12
|
+
const p = String(pass || "idle").toUpperCase();
|
|
13
|
+
if (p === "BUILD") return yellow(p);
|
|
14
|
+
if (p === "CHALLENGE") return red(p);
|
|
15
|
+
if (p === "DECIDE") return cyan(p);
|
|
16
|
+
if (p === "INVENT") return green(p);
|
|
17
|
+
return dim(p);
|
|
8
18
|
}
|
|
9
19
|
|
|
10
|
-
function
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return `${CSI}36m${text}${CSI}0m`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function yellow(text) {
|
|
19
|
-
return `${CSI}33m${text}${CSI}0m`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function green(text) {
|
|
23
|
-
return `${CSI}32m${text}${CSI}0m`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function red(text) {
|
|
27
|
-
return `${CSI}31m${text}${CSI}0m`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function verdictColor(verdict) {
|
|
31
|
-
if (verdict === "ship") return green(verdict);
|
|
32
|
-
if (verdict === "block") return red(verdict);
|
|
33
|
-
if (verdict === "caution") return yellow(verdict);
|
|
34
|
-
return verdict || "";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function stateLabel(state) {
|
|
38
|
-
if (!state) return dim("idle");
|
|
39
|
-
if (state === "build-ready") return green("build-ready");
|
|
40
|
-
if (state === "review-ready") return yellow("review-ready");
|
|
41
|
-
if (state === "accepted") return green("accepted");
|
|
42
|
-
if (state === "closed") return dim("closed");
|
|
43
|
-
if (state === "decide-required") return cyan("decide-required");
|
|
44
|
-
if (state === "decision-ready") return cyan("decision-ready");
|
|
45
|
-
return state;
|
|
20
|
+
function formatVerdict(verdict) {
|
|
21
|
+
if (verdict === "ship") return green("✓ no issues found");
|
|
22
|
+
if (verdict === "block") return red("⚠ issues found");
|
|
23
|
+
if (verdict === "caution") return yellow("⚠ caution");
|
|
24
|
+
return null;
|
|
46
25
|
}
|
|
47
26
|
|
|
48
27
|
function truncate(text, max) {
|
|
49
28
|
const s = String(text || "");
|
|
50
|
-
return s.length > max ? s.slice(0, max - 1)
|
|
29
|
+
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
51
30
|
}
|
|
52
31
|
|
|
53
|
-
function
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (state.
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (state.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
32
|
+
function buildLines(state) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
const width = Math.min(72, state.columns || 72);
|
|
35
|
+
const rule = dim("─".repeat(width));
|
|
36
|
+
lines.push(rule);
|
|
37
|
+
const title = truncate(state.taskName || state.repoName || "waterbrother", Math.max(24, width - 28));
|
|
38
|
+
const scope = state.contractSummary ? `scope: ${truncate(state.contractSummary, 28)}` : null;
|
|
39
|
+
const headline = [bold(title), "▸", formatPass(state.activePass || state.taskState || "idle"), scope ? dim(scope) : null].filter(Boolean).join(" ");
|
|
40
|
+
lines.push(headline);
|
|
41
|
+
if (state.summary) lines.push(truncate(state.summary, width));
|
|
42
|
+
if (state.issues && state.issues.length > 0) {
|
|
43
|
+
for (const issue of state.issues.slice(0, 2)) {
|
|
44
|
+
lines.push(` ${yellow("⚠")} ${truncate(issue, width - 6)}`);
|
|
45
|
+
}
|
|
46
|
+
} else if (state.verdict) {
|
|
47
|
+
const verdictLine = formatVerdict(state.verdict);
|
|
48
|
+
if (verdictLine) lines.push(` ${verdictLine}`);
|
|
49
|
+
}
|
|
50
|
+
if (state.actions?.length > 0) {
|
|
51
|
+
lines.push(dim(` ${state.actions.join(" · ")}`));
|
|
52
|
+
}
|
|
53
|
+
lines.push(rule);
|
|
54
|
+
return lines;
|
|
74
55
|
}
|
|
75
56
|
|
|
76
57
|
export function createPanelRenderer({ output } = {}) {
|
|
77
58
|
const stream = output || process.stdout;
|
|
78
59
|
let enabled = true;
|
|
79
60
|
let lastState = null;
|
|
80
|
-
let visible = false;
|
|
81
61
|
|
|
82
62
|
function renderPanel(state) {
|
|
83
|
-
if (!enabled
|
|
63
|
+
if (!enabled) return;
|
|
84
64
|
lastState = state;
|
|
85
|
-
const lines =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// Print panel lines separated by border
|
|
89
|
-
const width = Math.min(stream.columns || 80, 120);
|
|
90
|
-
const border = dim("─".repeat(width));
|
|
91
|
-
const panelText = lines.join("\n");
|
|
92
|
-
|
|
93
|
-
if (visible) {
|
|
94
|
-
// Just print the panel as a status update
|
|
95
|
-
stream.write(`${border}\n${panelText}\n${border}\n`);
|
|
96
|
-
} else {
|
|
97
|
-
stream.write(`${border}\n${panelText}\n${border}\n`);
|
|
98
|
-
visible = true;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function hidePanel() {
|
|
103
|
-
visible = false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function showPanel() {
|
|
107
|
-
if (lastState) renderPanel(lastState);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function setEnabled(next) {
|
|
111
|
-
enabled = Boolean(next);
|
|
112
|
-
if (!enabled) hidePanel();
|
|
65
|
+
const lines = buildLines({ ...state, columns: stream.columns || 80 });
|
|
66
|
+
stream.write(`${lines.join("\n")}\n`);
|
|
113
67
|
}
|
|
114
68
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
69
|
+
function hidePanel() {}
|
|
70
|
+
function showPanel() { if (lastState) renderPanel(lastState); }
|
|
71
|
+
function setEnabled(next) { enabled = Boolean(next); }
|
|
72
|
+
function dispose() { enabled = false; lastState = null; }
|
|
120
73
|
|
|
121
74
|
return {
|
|
122
75
|
renderPanel,
|
|
@@ -125,30 +78,31 @@ export function createPanelRenderer({ output } = {}) {
|
|
|
125
78
|
setEnabled,
|
|
126
79
|
dispose,
|
|
127
80
|
get enabled() { return enabled; },
|
|
128
|
-
get visible() { return
|
|
81
|
+
get visible() { return Boolean(lastState); }
|
|
129
82
|
};
|
|
130
83
|
}
|
|
131
84
|
|
|
132
|
-
export function buildPanelState({
|
|
133
|
-
cwd,
|
|
134
|
-
task,
|
|
135
|
-
agent,
|
|
136
|
-
receipt,
|
|
137
|
-
impactSummary
|
|
138
|
-
} = {}) {
|
|
85
|
+
export function buildPanelState({ cwd, task, agent, receipt, runtime } = {}) {
|
|
139
86
|
const repoName = cwd ? cwd.split(/[/\\]/).pop() : "";
|
|
87
|
+
const review = receipt?.review || null;
|
|
88
|
+
const challenge = receipt?.challenge || null;
|
|
89
|
+
const issues = [];
|
|
90
|
+
if (review?.concerns?.length) issues.push(...review.concerns);
|
|
91
|
+
if (challenge?.concerns?.length) {
|
|
92
|
+
for (const concern of challenge.concerns) {
|
|
93
|
+
if (!issues.includes(concern)) issues.push(concern);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
140
96
|
return {
|
|
141
97
|
repoName,
|
|
142
98
|
taskName: task?.name || null,
|
|
143
|
-
branch: task?.branch || null,
|
|
144
|
-
mode: agent?.getExperienceMode?.() || null,
|
|
145
|
-
autonomy: agent?.getAutonomyMode?.() || null,
|
|
146
|
-
taskState: task?.state || null,
|
|
147
99
|
contractSummary: task?.activeContract?.summary || null,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
100
|
+
taskState: task?.state || null,
|
|
101
|
+
activePass: runtime?.activePass || task?.lastPass || null,
|
|
102
|
+
summary: runtime?.panelSummary || challenge?.summary || review?.summary || null,
|
|
103
|
+
issues,
|
|
104
|
+
verdict: review?.verdict || task?.lastVerdict || null,
|
|
105
|
+
actions: Array.isArray(runtime?.actionHints) ? runtime.actionHints : [],
|
|
152
106
|
readOnly: agent?.getExperienceMode?.() === "auditor"
|
|
153
107
|
};
|
|
154
108
|
}
|
package/src/router.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
function normalize(text) {
|
|
2
|
+
return String(text || "").trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function lower(text) {
|
|
6
|
+
return normalize(text).toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function looksLikeWorkRequest(text) {
|
|
10
|
+
const raw = normalize(text);
|
|
11
|
+
if (!raw) return false;
|
|
12
|
+
if (raw.startsWith("/")) return false;
|
|
13
|
+
const l = raw.toLowerCase();
|
|
14
|
+
if (/^(hi|hello|thanks|thank you|help|what can you do)\b/.test(l)) return false;
|
|
15
|
+
if (/^(build it|go|check it|ship it|fix these|ignore|challenge harder|think deeper)\b/.test(l)) return false;
|
|
16
|
+
if (/^[1-9]\d*$/.test(l)) return false;
|
|
17
|
+
const verbStart = /^(add|build|implement|create|fix|refactor|remove|update|wire|support|migrate|improve|audit|review|debug|investigate|clean up|set up|setup|rewrite|port|optimize)\b/;
|
|
18
|
+
const suffixHint = /( to the | for the | in the | across | using | with | without )/;
|
|
19
|
+
return verbStart.test(l) || suffixHint.test(l) || (!l.endsWith("?") && raw.split(/\s+/).length >= 3);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function inferPassFromText(text) {
|
|
23
|
+
const l = lower(text);
|
|
24
|
+
if (!l) return null;
|
|
25
|
+
if (/^(ship it|accept|merge it|looks good ship it)\b/.test(l)) return { intent: "accept", confidence: "high" };
|
|
26
|
+
if (/^(fix these|fix that|address those|redo|rebuild|try again|patch it|build it|go ahead|go$|implement it)\b/.test(l)) return { intent: "build", confidence: "high" };
|
|
27
|
+
if (/^(check it|what'?s wrong|what is wrong|review it|challenge( harder)?|find bugs|poke holes|stress test it)\b/.test(l)) return { intent: "challenge", confidence: "high" };
|
|
28
|
+
if (/^(what am i not thinking of|what else|what if|surprise me|invent|give me alternatives|think wider|out of the box)\b/.test(l)) return { intent: "invent", confidence: "high" };
|
|
29
|
+
if (/^(should we|which should|which option|what should we|think deeper|tell me more about|compare |tradeoff|jwt or |sessions or |oauth|oauth2)\b/.test(l)) return { intent: "decide", confidence: "medium" };
|
|
30
|
+
if (/^[1-9]\d*$/.test(l)) return { intent: "choose", confidence: "high", optionIndex: Number.parseInt(l, 10) };
|
|
31
|
+
const moreMatch = l.match(/^(tell me more about|more on|details on|expand)\s+(\d+)\b/);
|
|
32
|
+
if (moreMatch) return { intent: "decision-detail", confidence: "high", optionIndex: Number.parseInt(moreMatch[2], 10) };
|
|
33
|
+
if (/^ignore\b/.test(l)) return { intent: "ignore", confidence: "medium" };
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function deriveTaskNameFromPrompt(text) {
|
|
38
|
+
const raw = normalize(text)
|
|
39
|
+
.replace(/["'`]/g, "")
|
|
40
|
+
.replace(/[?!.]+$/g, "")
|
|
41
|
+
.replace(/^please\s+/i, "")
|
|
42
|
+
.trim();
|
|
43
|
+
if (!raw) return `task-${Date.now().toString(36)}`;
|
|
44
|
+
const words = raw.split(/\s+/).slice(0, 6);
|
|
45
|
+
return words.join(" ");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function routeNaturalInput(text, { task = null } = {}) {
|
|
49
|
+
const raw = normalize(text);
|
|
50
|
+
if (!raw || raw.startsWith("/")) return { kind: "none", confidence: "low" };
|
|
51
|
+
|
|
52
|
+
const routed = inferPassFromText(raw);
|
|
53
|
+
if (routed) return { kind: routed.intent, confidence: routed.confidence, raw, ...routed };
|
|
54
|
+
|
|
55
|
+
if (!task && looksLikeWorkRequest(raw)) {
|
|
56
|
+
return { kind: "start-work", confidence: "medium", raw, taskName: deriveTaskNameFromPrompt(raw) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (task) {
|
|
60
|
+
if (task.state === "decision-ready") {
|
|
61
|
+
if (/^(go|build it|do it|sounds good|recommended)\b/i.test(raw)) {
|
|
62
|
+
return { kind: "choose-recommended-and-build", confidence: "high", raw };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (task.state === "review-ready") {
|
|
66
|
+
if (/^(fix these|fix them|address those|redo)\b/i.test(raw)) {
|
|
67
|
+
return { kind: "fix-review-findings", confidence: "high", raw };
|
|
68
|
+
}
|
|
69
|
+
if (/^(think deeper|re-think|rethink|step back)\b/i.test(raw)) {
|
|
70
|
+
return { kind: "decide", confidence: "high", raw };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { kind: "chat", confidence: "low", raw };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function nextActionsForState({ task, receipt } = {}) {
|
|
79
|
+
if (!task) return [];
|
|
80
|
+
if (task.state === "decision-ready") return ["1", "2", "3", "go", "tell me more about 2"];
|
|
81
|
+
if (task.state === "build-ready") return ["build it", "check it", "think deeper"];
|
|
82
|
+
if (task.state === "review-ready") {
|
|
83
|
+
const blocked = receipt?.review?.verdict === "block" || (Array.isArray(receipt?.verification) && receipt.verification.some((v) => !v.ok));
|
|
84
|
+
return blocked ? ["fix these", "ignore", "challenge harder"] : ["accept", "redo", "challenge harder", "ship it"];
|
|
85
|
+
}
|
|
86
|
+
if (task.state === "accepted") return ["close", "start next task"];
|
|
87
|
+
return [];
|
|
88
|
+
}
|