reasonix 0.4.17 → 0.4.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/cli/{chunk-3YQRWFES.js → chunk-HNEWBEWZ.js} +22 -1
- package/dist/cli/chunk-HNEWBEWZ.js.map +1 -0
- package/dist/cli/index.js +651 -235
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-HK5XLH55.js → prompt-JNNNJLYF.js} +2 -2
- package/dist/index.d.ts +160 -3
- package/dist/index.js +216 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-3YQRWFES.js.map +0 -1
- /package/dist/cli/{prompt-HK5XLH55.js.map → prompt-JNNNJLYF.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -499,9 +499,25 @@ function setByPath(target, path, value) {
|
|
|
499
499
|
var ToolRegistry = class {
|
|
500
500
|
_tools = /* @__PURE__ */ new Map();
|
|
501
501
|
_autoFlatten;
|
|
502
|
+
/**
|
|
503
|
+
* When true, `dispatch` refuses any tool whose `readOnly` flag isn't
|
|
504
|
+
* set (and whose `readOnlyCheck` doesn't pass on the specific args).
|
|
505
|
+
* Drives `reasonix code`'s Plan Mode — the model can still explore
|
|
506
|
+
* via read tools but its writes and non-allowlisted shell calls are
|
|
507
|
+
* bounced until the user approves a submitted plan.
|
|
508
|
+
*/
|
|
509
|
+
_planMode = false;
|
|
502
510
|
constructor(opts = {}) {
|
|
503
511
|
this._autoFlatten = opts.autoFlatten !== false;
|
|
504
512
|
}
|
|
513
|
+
/** Enable / disable plan-mode enforcement at dispatch. */
|
|
514
|
+
setPlanMode(on) {
|
|
515
|
+
this._planMode = Boolean(on);
|
|
516
|
+
}
|
|
517
|
+
/** True when the registry is currently refusing non-readonly calls. */
|
|
518
|
+
get planMode() {
|
|
519
|
+
return this._planMode;
|
|
520
|
+
}
|
|
505
521
|
register(def) {
|
|
506
522
|
if (!def.name) throw new Error("tool requires a name");
|
|
507
523
|
const internal = { ...def };
|
|
@@ -553,16 +569,38 @@ var ToolRegistry = class {
|
|
|
553
569
|
if (tool.flatSchema && args && typeof args === "object" && hasDotKey(args)) {
|
|
554
570
|
args = nestArguments(args);
|
|
555
571
|
}
|
|
572
|
+
if (this._planMode && !isReadOnlyCall(tool, args)) {
|
|
573
|
+
return JSON.stringify({
|
|
574
|
+
error: `${name}: unavailable in plan mode \u2014 this is a read-only exploration phase. Use read_file / list_directory / search_files / directory_tree / web_search / allowlisted shell commands to investigate. Call submit_plan with your proposed plan when you're ready for the user's review.`
|
|
575
|
+
});
|
|
576
|
+
}
|
|
556
577
|
try {
|
|
557
578
|
const result = await tool.fn(args, { signal: opts.signal });
|
|
558
579
|
return typeof result === "string" ? result : JSON.stringify(result);
|
|
559
580
|
} catch (err) {
|
|
581
|
+
const e = err;
|
|
582
|
+
if (typeof e.toToolResult === "function") {
|
|
583
|
+
try {
|
|
584
|
+
return JSON.stringify(e.toToolResult());
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
}
|
|
560
588
|
return JSON.stringify({
|
|
561
|
-
error: `${
|
|
589
|
+
error: `${e.name}: ${e.message}`
|
|
562
590
|
});
|
|
563
591
|
}
|
|
564
592
|
}
|
|
565
593
|
};
|
|
594
|
+
function isReadOnlyCall(tool, args) {
|
|
595
|
+
if (tool.readOnlyCheck) {
|
|
596
|
+
try {
|
|
597
|
+
return Boolean(tool.readOnlyCheck(args));
|
|
598
|
+
} catch {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return tool.readOnly === true;
|
|
603
|
+
}
|
|
566
604
|
function hasDotKey(obj) {
|
|
567
605
|
for (const k of Object.keys(obj)) {
|
|
568
606
|
if (k.includes(".")) return true;
|
|
@@ -949,6 +987,16 @@ var ToolCallRepair = class {
|
|
|
949
987
|
this.opts = opts;
|
|
950
988
|
this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3);
|
|
951
989
|
}
|
|
990
|
+
/**
|
|
991
|
+
* Drop the StormBreaker's sliding window of recent (name, args)
|
|
992
|
+
* signatures. Called at the start of every user turn — a fresh user
|
|
993
|
+
* message is a new intent, so carrying old repetition state into it
|
|
994
|
+
* would turn a valid "try again with different input" flow into a
|
|
995
|
+
* false-positive block.
|
|
996
|
+
*/
|
|
997
|
+
resetStorm() {
|
|
998
|
+
this.storm.reset();
|
|
999
|
+
}
|
|
952
1000
|
process(declaredCalls, reasoningContent, content = null) {
|
|
953
1001
|
const report = {
|
|
954
1002
|
scavenged: 0,
|
|
@@ -1401,6 +1449,7 @@ var CacheFirstLoop = class {
|
|
|
1401
1449
|
async *step(userInput) {
|
|
1402
1450
|
this._turn++;
|
|
1403
1451
|
this.scratch.reset();
|
|
1452
|
+
this.repair.resetStorm();
|
|
1404
1453
|
this._turnAbort = new AbortController();
|
|
1405
1454
|
const signal = this._turnAbort.signal;
|
|
1406
1455
|
let pendingUser = userInput;
|
|
@@ -1624,6 +1673,16 @@ var CacheFirstLoop = class {
|
|
|
1624
1673
|
repair: report,
|
|
1625
1674
|
branch: branchSummary
|
|
1626
1675
|
};
|
|
1676
|
+
if (report.stormsBroken > 0) {
|
|
1677
|
+
const noteTail = report.notes.length ? ` \u2014 ${report.notes[report.notes.length - 1]}` : "";
|
|
1678
|
+
const allSuppressed = repairedCalls.length === 0 && toolCalls.length > 0;
|
|
1679
|
+
const phrase = allSuppressed ? `stopped the model from calling the same tool with identical args repeatedly (all ${toolCalls.length} call(s) this turn were already in the recent-repeat window). Likely a stuck retry \u2014 reword your instruction, rule out the underlying blocker, or try /retry after fixing it` : `suppressed ${report.stormsBroken} repeat tool call(s) that had fired 3+ times with identical args in a sliding window`;
|
|
1680
|
+
yield {
|
|
1681
|
+
turn: this._turn,
|
|
1682
|
+
role: "warning",
|
|
1683
|
+
content: `${phrase}${noteTail}`
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1627
1686
|
if (repairedCalls.length === 0) {
|
|
1628
1687
|
yield { turn: this._turn, role: "done", content: assistantContent };
|
|
1629
1688
|
return;
|
|
@@ -1916,6 +1975,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
1916
1975
|
registry.register({
|
|
1917
1976
|
name: "read_file",
|
|
1918
1977
|
description: "Read a file under the sandbox root. Returns the full contents (truncated with a notice if larger than the per-call cap). Paths may be relative to the root or absolute-under-root.",
|
|
1978
|
+
readOnly: true,
|
|
1919
1979
|
parameters: {
|
|
1920
1980
|
type: "object",
|
|
1921
1981
|
properties: {
|
|
@@ -1953,6 +2013,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
1953
2013
|
registry.register({
|
|
1954
2014
|
name: "list_directory",
|
|
1955
2015
|
description: "List entries in a directory under the sandbox root. Returns one line per entry, marking directories with a trailing slash. Not recursive \u2014 use directory_tree for that.",
|
|
2016
|
+
readOnly: true,
|
|
1956
2017
|
parameters: {
|
|
1957
2018
|
type: "object",
|
|
1958
2019
|
properties: {
|
|
@@ -1972,6 +2033,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
1972
2033
|
registry.register({
|
|
1973
2034
|
name: "directory_tree",
|
|
1974
2035
|
description: "Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Caps output so a huge tree doesn't drown the context.",
|
|
2036
|
+
readOnly: true,
|
|
1975
2037
|
parameters: {
|
|
1976
2038
|
type: "object",
|
|
1977
2039
|
properties: {
|
|
@@ -2018,6 +2080,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2018
2080
|
registry.register({
|
|
2019
2081
|
name: "search_files",
|
|
2020
2082
|
description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line.",
|
|
2083
|
+
readOnly: true,
|
|
2021
2084
|
parameters: {
|
|
2022
2085
|
type: "object",
|
|
2023
2086
|
properties: {
|
|
@@ -2070,6 +2133,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2070
2133
|
registry.register({
|
|
2071
2134
|
name: "get_file_info",
|
|
2072
2135
|
description: "Stat a path under the sandbox root. Returns type (file|directory|symlink), size in bytes, mtime in ISO-8601.",
|
|
2136
|
+
readOnly: true,
|
|
2073
2137
|
parameters: {
|
|
2074
2138
|
type: "object",
|
|
2075
2139
|
properties: {
|
|
@@ -2226,8 +2290,54 @@ function lineDiff(a, b) {
|
|
|
2226
2290
|
return out;
|
|
2227
2291
|
}
|
|
2228
2292
|
|
|
2293
|
+
// src/tools/plan.ts
|
|
2294
|
+
var PlanProposedError = class extends Error {
|
|
2295
|
+
plan;
|
|
2296
|
+
constructor(plan) {
|
|
2297
|
+
super(
|
|
2298
|
+
"PlanProposedError: plan submitted. STOP calling tools now \u2014 the TUI has shown the plan to the user. Wait for their next message; it will either approve (you'll then implement the plan), request a refinement (you should explore more and submit an updated plan), or cancel (drop the plan and ask what they want instead). Don't call any tools in the meantime."
|
|
2299
|
+
);
|
|
2300
|
+
this.name = "PlanProposedError";
|
|
2301
|
+
this.plan = plan;
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Structured tool-result shape. Consumed by the TUI to extract the
|
|
2305
|
+
* plan without regex-scraping the error message.
|
|
2306
|
+
*/
|
|
2307
|
+
toToolResult() {
|
|
2308
|
+
return { error: `${this.name}: ${this.message}`, plan: this.plan };
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
function registerPlanTool(registry, opts = {}) {
|
|
2312
|
+
registry.register({
|
|
2313
|
+
name: "submit_plan",
|
|
2314
|
+
description: "Submit a concrete plan to the user for review before executing. Use this for tasks that warrant a review gate \u2014 multi-file refactors, architecture changes, anything that would be expensive or confusing to undo. Skip it for small fixes (one-line typo, obvious bug with a clear fix) \u2014 just make the change. The user will either approve (you then implement it), ask for refinement, or cancel. If the user has already enabled /plan mode, writes are blocked at dispatch and you MUST use this. Write the plan as markdown with a one-line summary, a bulleted list of files to touch and what will change, and any risks or open questions.",
|
|
2315
|
+
readOnly: true,
|
|
2316
|
+
parameters: {
|
|
2317
|
+
type: "object",
|
|
2318
|
+
properties: {
|
|
2319
|
+
plan: {
|
|
2320
|
+
type: "string",
|
|
2321
|
+
description: "Markdown-formatted plan. Lead with a one-sentence summary. Then a file-by-file breakdown of what you'll change and why. Flag any risks or open questions at the end so the user can weigh in before you start."
|
|
2322
|
+
}
|
|
2323
|
+
},
|
|
2324
|
+
required: ["plan"]
|
|
2325
|
+
},
|
|
2326
|
+
fn: async (args) => {
|
|
2327
|
+
const plan = (args?.plan ?? "").trim();
|
|
2328
|
+
if (!plan) {
|
|
2329
|
+
throw new Error("submit_plan: empty plan \u2014 write a markdown plan and try again.");
|
|
2330
|
+
}
|
|
2331
|
+
opts.onPlanSubmitted?.(plan);
|
|
2332
|
+
throw new PlanProposedError(plan);
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
return registry;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2229
2338
|
// src/tools/shell.ts
|
|
2230
2339
|
import { spawn } from "child_process";
|
|
2340
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
2231
2341
|
import * as pathMod2 from "path";
|
|
2232
2342
|
var DEFAULT_TIMEOUT_SEC = 60;
|
|
2233
2343
|
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
@@ -2345,10 +2455,12 @@ async function runCommand(cmd, opts) {
|
|
|
2345
2455
|
windowsHide: true,
|
|
2346
2456
|
env: process.env
|
|
2347
2457
|
};
|
|
2458
|
+
const { bin, args, spawnOverrides } = prepareSpawn(argv);
|
|
2459
|
+
const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
|
|
2348
2460
|
return await new Promise((resolve5, reject) => {
|
|
2349
2461
|
let child;
|
|
2350
2462
|
try {
|
|
2351
|
-
child = spawn(
|
|
2463
|
+
child = spawn(bin, args, effectiveSpawnOpts);
|
|
2352
2464
|
} catch (err) {
|
|
2353
2465
|
reject(err);
|
|
2354
2466
|
return;
|
|
@@ -2382,6 +2494,59 @@ async function runCommand(cmd, opts) {
|
|
|
2382
2494
|
});
|
|
2383
2495
|
});
|
|
2384
2496
|
}
|
|
2497
|
+
function resolveExecutable(cmd, opts = {}) {
|
|
2498
|
+
const platform = opts.platform ?? process.platform;
|
|
2499
|
+
if (platform !== "win32") return cmd;
|
|
2500
|
+
if (!cmd) return cmd;
|
|
2501
|
+
if (cmd.includes("/") || cmd.includes("\\") || pathMod2.isAbsolute(cmd)) return cmd;
|
|
2502
|
+
if (pathMod2.extname(cmd)) return cmd;
|
|
2503
|
+
const env = opts.env ?? process.env;
|
|
2504
|
+
const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
|
|
2505
|
+
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod2.delimiter);
|
|
2506
|
+
const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
|
|
2507
|
+
const isFile = opts.isFile ?? defaultIsFile;
|
|
2508
|
+
for (const dir of pathDirs) {
|
|
2509
|
+
for (const ext of pathExt) {
|
|
2510
|
+
const full = pathMod2.join(dir, cmd + ext);
|
|
2511
|
+
if (isFile(full)) return full;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
return cmd;
|
|
2515
|
+
}
|
|
2516
|
+
function defaultIsFile(full) {
|
|
2517
|
+
try {
|
|
2518
|
+
return existsSync3(full) && statSync2(full).isFile();
|
|
2519
|
+
} catch {
|
|
2520
|
+
return false;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
function prepareSpawn(argv, opts = {}) {
|
|
2524
|
+
const head = argv[0] ?? "";
|
|
2525
|
+
const tail = argv.slice(1);
|
|
2526
|
+
const platform = opts.platform ?? process.platform;
|
|
2527
|
+
const resolved = resolveExecutable(head, opts);
|
|
2528
|
+
if (platform !== "win32") {
|
|
2529
|
+
return { bin: resolved, args: [...tail], spawnOverrides: {} };
|
|
2530
|
+
}
|
|
2531
|
+
if (/\.(cmd|bat)$/i.test(resolved)) {
|
|
2532
|
+
const cmdline = [resolved, ...tail].map(quoteForCmdExe).join(" ");
|
|
2533
|
+
return {
|
|
2534
|
+
bin: "cmd.exe",
|
|
2535
|
+
args: ["/d", "/s", "/c", cmdline],
|
|
2536
|
+
// windowsVerbatimArguments prevents Node from re-quoting the /c
|
|
2537
|
+
// payload — we've already composed an exact cmd.exe command
|
|
2538
|
+
// line. Without this Node wraps our already-quoted string in
|
|
2539
|
+
// another round of quotes and cmd.exe can't parse it.
|
|
2540
|
+
spawnOverrides: { windowsVerbatimArguments: true }
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
return { bin: resolved, args: [...tail], spawnOverrides: {} };
|
|
2544
|
+
}
|
|
2545
|
+
function quoteForCmdExe(arg) {
|
|
2546
|
+
if (arg === "") return '""';
|
|
2547
|
+
if (!/[\s"&|<>^%(),;!]/.test(arg)) return arg;
|
|
2548
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
2549
|
+
}
|
|
2385
2550
|
var NeedsConfirmationError = class extends Error {
|
|
2386
2551
|
command;
|
|
2387
2552
|
constructor(command) {
|
|
@@ -2401,6 +2566,16 @@ function registerShellTools(registry, opts) {
|
|
|
2401
2566
|
registry.register({
|
|
2402
2567
|
name: "run_command",
|
|
2403
2568
|
description: "Run a shell command in the project root and return its combined stdout+stderr. Read-only and test commands (git status, ls, npm test, pytest, cargo test, grep, etc.) run immediately. Anything that could mutate state (npm install, git commit, rm, chmod) is refused and the user has to confirm in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
|
|
2569
|
+
// Plan-mode gate: allow allowlisted commands through (git status,
|
|
2570
|
+
// cargo check, ls, grep …) so the model can actually investigate
|
|
2571
|
+
// during planning. Anything that would otherwise trigger a
|
|
2572
|
+
// confirmation prompt is treated as "not read-only" and bounced.
|
|
2573
|
+
readOnlyCheck: (args) => {
|
|
2574
|
+
if (allowAll) return true;
|
|
2575
|
+
const cmd = typeof args?.command === "string" ? args.command.trim() : "";
|
|
2576
|
+
if (!cmd) return false;
|
|
2577
|
+
return isAllowed(cmd, extraAllowed);
|
|
2578
|
+
},
|
|
2404
2579
|
parameters: {
|
|
2405
2580
|
type: "object",
|
|
2406
2581
|
properties: {
|
|
@@ -2567,6 +2742,7 @@ function registerWebTools(registry, opts = {}) {
|
|
|
2567
2742
|
registry.register({
|
|
2568
2743
|
name: "web_search",
|
|
2569
2744
|
description: "Search the public web. Returns ranked results with title, url, and snippet. Use this when the question needs information more current than your training data, when you're unsure of a factual detail, or when the user asks about a specific webpage/library/release you haven't seen.",
|
|
2745
|
+
readOnly: true,
|
|
2570
2746
|
parameters: {
|
|
2571
2747
|
type: "object",
|
|
2572
2748
|
properties: {
|
|
@@ -2589,6 +2765,7 @@ function registerWebTools(registry, opts = {}) {
|
|
|
2589
2765
|
registry.register({
|
|
2590
2766
|
name: "web_fetch",
|
|
2591
2767
|
description: "Download a URL and return its visible text content (HTML pages get scripts/styles/nav stripped). Truncated at the tool-result cap. Use after web_search when a snippet isn't enough.",
|
|
2768
|
+
readOnly: true,
|
|
2592
2769
|
parameters: {
|
|
2593
2770
|
type: "object",
|
|
2594
2771
|
properties: {
|
|
@@ -3753,7 +3930,7 @@ async function trySection(load) {
|
|
|
3753
3930
|
}
|
|
3754
3931
|
|
|
3755
3932
|
// src/code/edit-blocks.ts
|
|
3756
|
-
import { existsSync as
|
|
3933
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3757
3934
|
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
3758
3935
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
3759
3936
|
function parseEditBlocks(text) {
|
|
@@ -3782,7 +3959,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
3782
3959
|
};
|
|
3783
3960
|
}
|
|
3784
3961
|
const searchEmpty = block.search.length === 0;
|
|
3785
|
-
const exists =
|
|
3962
|
+
const exists = existsSync4(absTarget);
|
|
3786
3963
|
try {
|
|
3787
3964
|
if (!exists) {
|
|
3788
3965
|
if (!searchEmpty) {
|
|
@@ -3830,7 +4007,7 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
3830
4007
|
if (seen.has(b.path)) continue;
|
|
3831
4008
|
seen.add(b.path);
|
|
3832
4009
|
const abs = resolve4(absRoot, b.path);
|
|
3833
|
-
if (!
|
|
4010
|
+
if (!existsSync4(abs)) {
|
|
3834
4011
|
snapshots.push({ path: b.path, prevContent: null });
|
|
3835
4012
|
continue;
|
|
3836
4013
|
}
|
|
@@ -3855,7 +4032,7 @@ function restoreSnapshots(snapshots, rootDir) {
|
|
|
3855
4032
|
}
|
|
3856
4033
|
try {
|
|
3857
4034
|
if (snap.prevContent === null) {
|
|
3858
|
-
if (
|
|
4035
|
+
if (existsSync4(abs)) unlinkSync2(abs);
|
|
3859
4036
|
return {
|
|
3860
4037
|
path: snap.path,
|
|
3861
4038
|
status: "applied",
|
|
@@ -3878,10 +4055,31 @@ function sep() {
|
|
|
3878
4055
|
}
|
|
3879
4056
|
|
|
3880
4057
|
// src/code/prompt.ts
|
|
3881
|
-
import { existsSync as
|
|
3882
|
-
import { join as
|
|
4058
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
4059
|
+
import { join as join5 } from "path";
|
|
3883
4060
|
var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, list_directory, search_files, etc.) rooted at the user's working directory.
|
|
3884
4061
|
|
|
4062
|
+
# When to propose a plan (submit_plan)
|
|
4063
|
+
|
|
4064
|
+
You have a \`submit_plan\` tool that shows the user a markdown plan and lets them Approve / Refine / Cancel before you execute. Use it proactively when the task is large enough to deserve a review gate:
|
|
4065
|
+
|
|
4066
|
+
- Multi-file refactors or renames.
|
|
4067
|
+
- Architecture changes (moving modules, splitting / merging files, new abstractions).
|
|
4068
|
+
- Anything where "undo" after the fact would be expensive \u2014 migrations, destructive cleanups, API shape changes.
|
|
4069
|
+
- When the user's request is ambiguous and multiple reasonable interpretations exist \u2014 propose your reading as a plan and let them confirm.
|
|
4070
|
+
|
|
4071
|
+
Skip submit_plan for small, obvious changes: one-line typo, clear bug with a clear fix, adding a missing import, renaming a local variable. Just do those.
|
|
4072
|
+
|
|
4073
|
+
Plan body: one-sentence summary, then a file-by-file breakdown of what you'll change and why, and any risks or open questions. If some decisions are genuinely up to the user (naming, tradeoffs, out-of-scope possibilities), list them in an "Open questions" or "\u5F85\u786E\u8BA4" section \u2014 the user sees the plan in a picker and has a text input to answer your questions before approving. Don't pretend certainty you don't have; flagged questions are how the user tells you what they care about. After calling submit_plan, STOP \u2014 don't call any more tools, wait for the user's verdict.
|
|
4074
|
+
|
|
4075
|
+
# Plan mode (/plan)
|
|
4076
|
+
|
|
4077
|
+
The user can ALSO enter "plan mode" via /plan, which is a stronger, explicit constraint:
|
|
4078
|
+
- Write tools (edit_file, write_file, create_directory, move_file) and non-allowlisted run_command calls are BOUNCED at dispatch \u2014 you'll get a tool result like "unavailable in plan mode". Don't retry them.
|
|
4079
|
+
- Read tools (read_file, list_directory, search_files, directory_tree, get_file_info) and allowlisted shell (git status/log/diff, ls, cat, grep, cargo check, npm test) still work \u2014 use them to investigate.
|
|
4080
|
+
- You MUST call submit_plan before anything will execute. Approve exits plan mode; Refine stays in; Cancel exits without implementing.
|
|
4081
|
+
|
|
4082
|
+
|
|
3885
4083
|
# When to edit vs. when to explore
|
|
3886
4084
|
|
|
3887
4085
|
Only propose edits when the user explicitly asks you to change, fix, add, remove, refactor, or write something. Do NOT propose edits when the user asks you to:
|
|
@@ -3929,8 +4127,8 @@ Rules:
|
|
|
3929
4127
|
`;
|
|
3930
4128
|
function codeSystemPrompt(rootDir) {
|
|
3931
4129
|
const withMemory = applyProjectMemory(CODE_SYSTEM_PROMPT, rootDir);
|
|
3932
|
-
const gitignorePath =
|
|
3933
|
-
if (!
|
|
4130
|
+
const gitignorePath = join5(rootDir, ".gitignore");
|
|
4131
|
+
if (!existsSync5(gitignorePath)) return withMemory;
|
|
3934
4132
|
let content;
|
|
3935
4133
|
try {
|
|
3936
4134
|
content = readFileSync6(gitignorePath, "utf8");
|
|
@@ -3955,9 +4153,9 @@ ${truncated}
|
|
|
3955
4153
|
// src/config.ts
|
|
3956
4154
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
3957
4155
|
import { homedir as homedir2 } from "os";
|
|
3958
|
-
import { dirname as dirname4, join as
|
|
4156
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
3959
4157
|
function defaultConfigPath() {
|
|
3960
|
-
return
|
|
4158
|
+
return join6(homedir2(), ".reasonix", "config.json");
|
|
3961
4159
|
}
|
|
3962
4160
|
function readConfig(path = defaultConfigPath()) {
|
|
3963
4161
|
try {
|
|
@@ -3996,7 +4194,7 @@ function redactKey(key) {
|
|
|
3996
4194
|
}
|
|
3997
4195
|
|
|
3998
4196
|
// src/index.ts
|
|
3999
|
-
var VERSION = "0.4.
|
|
4197
|
+
var VERSION = "0.4.19";
|
|
4000
4198
|
export {
|
|
4001
4199
|
AppendOnlyLog,
|
|
4002
4200
|
CODE_SYSTEM_PROMPT,
|
|
@@ -4009,6 +4207,7 @@ export {
|
|
|
4009
4207
|
NeedsConfirmationError,
|
|
4010
4208
|
PROJECT_MEMORY_FILE,
|
|
4011
4209
|
PROJECT_MEMORY_MAX_CHARS,
|
|
4210
|
+
PlanProposedError,
|
|
4012
4211
|
SessionStats,
|
|
4013
4212
|
SseTransport,
|
|
4014
4213
|
StdioTransport,
|
|
@@ -4061,18 +4260,22 @@ export {
|
|
|
4061
4260
|
parseMcpSpec,
|
|
4062
4261
|
parseMojeekResults,
|
|
4063
4262
|
parseTranscript,
|
|
4263
|
+
prepareSpawn,
|
|
4264
|
+
quoteForCmdExe,
|
|
4064
4265
|
readConfig,
|
|
4065
4266
|
readProjectMemory,
|
|
4066
4267
|
readTranscript,
|
|
4067
4268
|
recordFromLoopEvent,
|
|
4068
4269
|
redactKey,
|
|
4069
4270
|
registerFilesystemTools,
|
|
4271
|
+
registerPlanTool,
|
|
4070
4272
|
registerShellTools,
|
|
4071
4273
|
registerWebTools,
|
|
4072
4274
|
renderMarkdown as renderDiffMarkdown,
|
|
4073
4275
|
renderSummaryTable as renderDiffSummary,
|
|
4074
4276
|
repairTruncatedJson,
|
|
4075
4277
|
replayFromFile,
|
|
4278
|
+
resolveExecutable,
|
|
4076
4279
|
restoreSnapshots,
|
|
4077
4280
|
runBranches,
|
|
4078
4281
|
runCommand,
|