@oh-my-pi/pi-coding-agent 15.5.0 → 15.5.2
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/CHANGELOG.md +23 -0
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/hashline/constants.d.ts +23 -0
- package/dist/types/hashline/executor.d.ts +7 -3
- package/dist/types/hashline/hash.d.ts +9 -7
- package/dist/types/hashline/tokenizer.d.ts +3 -0
- package/dist/types/tools/approval.d.ts +2 -2
- package/dist/types/tools/bash.d.ts +4 -4
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +0 -125
- package/src/config/settings-schema.ts +4 -4
- package/src/edit/streaming.ts +3 -4
- package/src/extensibility/extensions/wrapper.ts +2 -3
- package/src/hashline/anchors.ts +1 -1
- package/src/hashline/apply.ts +66 -56
- package/src/hashline/constants.ts +29 -0
- package/src/hashline/execute.ts +5 -3
- package/src/hashline/executor.ts +125 -30
- package/src/hashline/grammar.lark +1 -1
- package/src/hashline/hash.ts +9 -6
- package/src/hashline/recovery.ts +35 -1
- package/src/hashline/tokenizer.ts +10 -4
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/prompts/tools/hashline.md +47 -94
- package/src/task/executor.ts +2 -2
- package/src/tools/approval.ts +6 -2
- package/src/tools/bash.ts +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.5.2] - 2026-05-26
|
|
6
|
+
### Breaking Changes
|
|
7
|
+
|
|
8
|
+
- Changed the hashline patch format so payload continuation lines now require a leading `+`, rejecting unprefixed multiline payload rows that were previously accepted as fallback payload text
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Changed hashline payload parsing so blank lines are only preserved when prefixed with `+`, so blank separator lines between operations are ignored unless explicitly marked
|
|
13
|
+
- Changed payload escaping so a line beginning with `+` is now represented as `++...` while the leading marker is stripped before writing
|
|
14
|
+
- Changed the default `task.simple` mode from `default` to `schema-free`, so task-call `schema` inputs are disabled by default while shared `context` and user prompt/session-defined output schemas remain available
|
|
15
|
+
- Changed `tools.approvalMode: yolo` to auto-approve tool calls even when a tool marks `override: true`; user `tools.approval.<tool>` policies (`allow`/`prompt`/`deny`) now remain the only controls for yolo mode.
|
|
16
|
+
- Changed the hashline edit executor to coalesce two consecutive `A-B:` ops on the identical range last-wins (the model painted a before/after pair) and append a warning, instead of throwing `anchor line X is already targeted by the :/! op on line Y`. Other overlap shapes (different ranges, `A-B:`+`!`, `!`+`!`) still throw.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Fixed nested replace parsing so line-anchored `N:` rows inside a pending `A-B:` replacement now trigger overlap errors instead of being silently folded into the replacement payload
|
|
21
|
+
|
|
22
|
+
## [15.5.1] - 2026-05-26
|
|
23
|
+
|
|
24
|
+
### Breaking Changes
|
|
25
|
+
|
|
26
|
+
- Removed the `href`, `hrefr`, and `hline` Handlebars prompt helpers along with the shared hashline anchor state; none were referenced by any built-in or user prompt template
|
|
27
|
+
|
|
5
28
|
## [15.5.0] - 2026-05-26
|
|
6
29
|
|
|
7
30
|
### Added
|
|
@@ -2151,7 +2151,7 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
2151
2151
|
readonly ui: {
|
|
2152
2152
|
readonly tab: "interaction";
|
|
2153
2153
|
readonly label: "Tool Approval";
|
|
2154
|
-
readonly description: "Default approval behaviour for tool calls. 'Always ask' auto-approves read-only tools only. 'Write' auto-approves read and workspace-write tools. 'Yolo' auto-approves
|
|
2154
|
+
readonly description: "Default approval behaviour for tool calls. 'Always ask' auto-approves read-only tools only. 'Write' auto-approves read and workspace-write tools. 'Yolo' auto-approves all tiers; user policy may still prompt or block.";
|
|
2155
2155
|
readonly options: readonly [{
|
|
2156
2156
|
readonly value: "always-ask";
|
|
2157
2157
|
readonly label: "Always ask";
|
|
@@ -2163,7 +2163,7 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
2163
2163
|
}, {
|
|
2164
2164
|
readonly value: "yolo";
|
|
2165
2165
|
readonly label: "Yolo";
|
|
2166
|
-
readonly description: "Auto-approve read, write, and exec tools.
|
|
2166
|
+
readonly description: "Auto-approve read, write, and exec tools. User policy can still require confirmation or block calls.";
|
|
2167
2167
|
}];
|
|
2168
2168
|
};
|
|
2169
2169
|
};
|
|
@@ -2765,7 +2765,7 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
2765
2765
|
readonly "task.simple": {
|
|
2766
2766
|
readonly type: "enum";
|
|
2767
2767
|
readonly values: readonly ["default", "schema-free", "independent"];
|
|
2768
|
-
readonly default: "
|
|
2768
|
+
readonly default: "schema-free";
|
|
2769
2769
|
readonly ui: {
|
|
2770
2770
|
readonly tab: "tasks";
|
|
2771
2771
|
readonly label: "Task Input Mode";
|
|
@@ -15,3 +15,26 @@ export declare const END_PATCH_MARKER = "*** End Patch";
|
|
|
15
15
|
export declare const ABORT_MARKER = "*** Abort";
|
|
16
16
|
/** Warning text appended to the tool result when ABORT_MARKER terminates parsing. */
|
|
17
17
|
export declare const ABORT_WARNING = "Tool stream truncated mid-call due to detected output corruption. Applied ops above are valid. Re-issue any remaining edits.";
|
|
18
|
+
/**
|
|
19
|
+
* Warning text appended when two consecutive `A-B:` ops on the exact same
|
|
20
|
+
* range get coalesced (model painted a before/after pair). The second op
|
|
21
|
+
* wins; the first op's payload is silently discarded.
|
|
22
|
+
*/
|
|
23
|
+
export declare const REPLACE_PAIR_COALESCED_WARNING = "Detected an identical-range before/after replace pair; kept only the second block's payload. Issue ONE op per range \u2014 the payload is the final desired content, never both old and new.";
|
|
24
|
+
/**
|
|
25
|
+
* Warning text appended when un-prefixed continuation lines are accepted as
|
|
26
|
+
* implicit payload (lenient legacy behavior). The model authored a multi-line
|
|
27
|
+
* replace without `+` prefixes; the parser accepted it because the lines did
|
|
28
|
+
* not classify as ops/headers/payloads, but the canonical syntax requires `+`
|
|
29
|
+
* on every continuation line after the op.
|
|
30
|
+
*/
|
|
31
|
+
export declare const IMPLICIT_CONTINUATION_WARNING = "Accepted continuation line(s) without the `+` prefix as implicit payload. Canonical syntax is `A-B:` followed by `+` on every continuation row; without `+`, lines that look like ops will be parsed as new ops instead of payload. Prefer the explicit form.";
|
|
32
|
+
/**
|
|
33
|
+
* Warning text appended when an inner `LINE:TEXT` (or sub-range `A-B:TEXT`)
|
|
34
|
+
* op arrives while an outer `A-B:` replace is still pending and the inner
|
|
35
|
+
* anchor falls inside the outer range. The model used the read-output
|
|
36
|
+
* `LINE:TEXT` format as if it were a payload-continuation line; we strip the
|
|
37
|
+
* `LINE:` prefix and append the body to the pending payload, but warn so the
|
|
38
|
+
* canonical `+`-continuation form remains preferred.
|
|
39
|
+
*/
|
|
40
|
+
export declare const PAYLOAD_LINE_PREFIX_DEMOTED_WARNING = "Detected one or more `LINE:TEXT` lines whose anchors fell inside a pending replace range; treated them as payload-continuation lines and stripped the `LINE:` prefix. Inside a multi-line `A-B:` block, payload lines after the first should be prefixed with `+` \u2014 never reuse the read-output gutter format.";
|
|
@@ -28,9 +28,13 @@ export declare class HashlineExecutor {
|
|
|
28
28
|
*/
|
|
29
29
|
feed(token: HashlineToken): void;
|
|
30
30
|
/**
|
|
31
|
-
* Flush any open pending op (
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Flush any open pending op (with its full accumulated payload, including
|
|
32
|
+
* explicit `+` blank lines) and return the accumulated edits and warnings.
|
|
33
|
+
* The executor is single-use; reset() is required for reuse.
|
|
34
|
+
* Throws if two replace/delete ops target the same line with non-identical
|
|
35
|
+
* shapes (different ranges, replace+delete, delete+delete). Identical-range
|
|
36
|
+
* `A-B:` pairs in the same hunk are coalesced last-wins by `feed()` with a
|
|
37
|
+
* warning, so they never reach the validator.
|
|
34
38
|
*/
|
|
35
39
|
end(): {
|
|
36
40
|
edits: HashlineEdit[];
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Decoration prefix that may precede a line number in tool output:
|
|
7
|
-
* `>` (context line in grep),
|
|
8
|
-
*
|
|
7
|
+
* `>` (context line in grep), `-` (removed line), `*` (match line).
|
|
8
|
+
* Any combination, in any order, surrounded by optional
|
|
9
9
|
* whitespace. Output formatters emit at most one decoration per line; the
|
|
10
10
|
* parser stays liberal because it accepts whatever the model echoes back.
|
|
11
11
|
*/
|
|
12
|
-
export declare const HL_ANCHOR_DECORATION_RE_RAW = "\\s*[
|
|
12
|
+
export declare const HL_ANCHOR_DECORATION_RE_RAW = "\\s*[>\\-*]*\\s*";
|
|
13
13
|
/** Capture-group regex source for a decorated bare line-number anchor. */
|
|
14
|
-
export declare const HL_ANCHOR_RE_RAW = "\\s*[
|
|
14
|
+
export declare const HL_ANCHOR_RE_RAW = "\\s*[>\\-*]*\\s*(\\d+)";
|
|
15
15
|
/** Bare positive line-number Lid (no decorations, no captures, no anchors). */
|
|
16
16
|
export declare const HL_LINE_RE_RAW = "[1-9]\\d*";
|
|
17
17
|
/** Capture-group form of {@link HL_LINE_RE_RAW}. */
|
|
@@ -45,9 +45,9 @@ export declare function resolveHashlineGrammarPlaceholders(grammar: string): str
|
|
|
45
45
|
/**
|
|
46
46
|
* op lines have an `ANCHOR<SIGIL>[INLINE_PAYLOAD]` shape, where SIGIL is one of
|
|
47
47
|
* {@link HL_OP_INSERT_BEFORE}, {@link HL_OP_INSERT_AFTER}, {@link HL_OP_REPLACE},
|
|
48
|
-
* or {@link HL_OP_DELETE}.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* or {@link HL_OP_DELETE}. Multi-line payloads follow on subsequent lines
|
|
49
|
+
* prefixed with {@link HL_PAYLOAD_PREFIX}; that prefix is stripped before the
|
|
50
|
+
* payload is written.
|
|
51
51
|
*
|
|
52
52
|
* These constants are the single source of truth for the edit parser, grammar,
|
|
53
53
|
* renderer, and prompt.
|
|
@@ -56,6 +56,8 @@ export declare const HL_OP_INSERT_BEFORE = "\u2191";
|
|
|
56
56
|
export declare const HL_OP_INSERT_AFTER = "\u2193";
|
|
57
57
|
export declare const HL_OP_REPLACE = ":";
|
|
58
58
|
export declare const HL_OP_DELETE = "!";
|
|
59
|
+
/** Prefix for payload continuation lines. The prefix itself is not written. */
|
|
60
|
+
export declare const HL_PAYLOAD_PREFIX = "+";
|
|
59
61
|
/** All hashline edit op sigils, concatenated for fast membership tests. */
|
|
60
62
|
export declare const HL_OP_CHARS = "\u2191\u2193:!";
|
|
61
63
|
/** Hashline edit file section header marker. */
|
|
@@ -25,8 +25,8 @@ export interface ResolvedApproval {
|
|
|
25
25
|
* 2. User per-tool override, if set and valid.
|
|
26
26
|
* 3. Active mode tier comparison.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* In yolo mode, override-based tool prompts are ignored; user `tools.approval`
|
|
29
|
+
* settings remain authoritative.
|
|
30
30
|
*/
|
|
31
31
|
export declare function resolveApproval(tool: ApprovalSubject, args: unknown, mode: ApprovalMode, userConfig?: Record<string, unknown>): ResolvedApproval;
|
|
32
32
|
/**
|
|
@@ -7,11 +7,11 @@ import type { ToolSession } from ".";
|
|
|
7
7
|
import { type OutputMeta } from "./output-meta";
|
|
8
8
|
export declare const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
9
9
|
/**
|
|
10
|
-
* Bash patterns
|
|
10
|
+
* Bash patterns flagged as safety critical for approval policy.
|
|
11
11
|
*
|
|
12
|
-
* Kept intentionally tight — the cost of a false
|
|
13
|
-
*
|
|
14
|
-
* should target shapes that are virtually never legitimate in automation.
|
|
12
|
+
* Kept intentionally tight — the cost of a false negative is data loss or a compromised host,
|
|
13
|
+
* while false positives remain actionable through user policy control.
|
|
14
|
+
* New patterns should target shapes that are virtually never legitimate in automation.
|
|
15
15
|
*/
|
|
16
16
|
export declare const CRITICAL_BASH_PATTERNS: readonly [RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp];
|
|
17
17
|
declare const bashSchemaBase: z.ZodObject<{
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "15.5.
|
|
4
|
+
"version": "15.5.2",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
48
|
"@babel/parser": "^7.29.3",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/omp-stats": "15.5.
|
|
51
|
-
"@oh-my-pi/pi-agent-core": "15.5.
|
|
52
|
-
"@oh-my-pi/pi-ai": "15.5.
|
|
53
|
-
"@oh-my-pi/pi-natives": "15.5.
|
|
54
|
-
"@oh-my-pi/pi-tui": "15.5.
|
|
55
|
-
"@oh-my-pi/pi-utils": "15.5.
|
|
50
|
+
"@oh-my-pi/omp-stats": "15.5.2",
|
|
51
|
+
"@oh-my-pi/pi-agent-core": "15.5.2",
|
|
52
|
+
"@oh-my-pi/pi-ai": "15.5.2",
|
|
53
|
+
"@oh-my-pi/pi-natives": "15.5.2",
|
|
54
|
+
"@oh-my-pi/pi-tui": "15.5.2",
|
|
55
|
+
"@oh-my-pi/pi-utils": "15.5.2",
|
|
56
56
|
"@puppeteer/browsers": "^2.13.0",
|
|
57
57
|
"@types/turndown": "5.0.6",
|
|
58
58
|
"@xterm/headless": "^6.0.0",
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
parseFrontmatter,
|
|
9
9
|
prompt,
|
|
10
10
|
} from "@oh-my-pi/pi-utils";
|
|
11
|
-
import { HL_LINE_BODY_SEP } from "../hashline/hash";
|
|
12
11
|
import { jtdToTypeScript } from "../tools/jtd-to-typescript";
|
|
13
12
|
import { parseCommandArgs, substituteArgs } from "../utils/command-args";
|
|
14
13
|
|
|
@@ -30,130 +29,6 @@ prompt.registerHelper("jtdToTypeScript", (schema: unknown): string => {
|
|
|
30
29
|
}
|
|
31
30
|
});
|
|
32
31
|
|
|
33
|
-
function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; text: string; ref: string } {
|
|
34
|
-
const num = typeof lineNum === "number" ? lineNum : Number.parseInt(String(lineNum), 10);
|
|
35
|
-
const raw = typeof content === "string" ? content : String(content ?? "");
|
|
36
|
-
const text = raw.replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r");
|
|
37
|
-
const ref = `${num}`;
|
|
38
|
-
return { num, text, ref };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface HashlineHelperRef {
|
|
42
|
-
line: number;
|
|
43
|
-
ref: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface HashlineHelperState {
|
|
47
|
-
last?: HashlineHelperRef;
|
|
48
|
-
byLine: Map<number, HashlineHelperRef>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const HL_HELPER_STATE = Symbol("hashlineHelperState");
|
|
52
|
-
|
|
53
|
-
interface HashlineHelperStateHolder {
|
|
54
|
-
[HL_HELPER_STATE]?: HashlineHelperState;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function isHelperOptions(value: unknown): value is prompt.HelperOptions {
|
|
58
|
-
return typeof value === "object" && value !== null && "hash" in value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function splitHelperArgs(args: unknown[]): { positional: unknown[]; options?: prompt.HelperOptions } {
|
|
62
|
-
const maybeOptions = args.at(-1);
|
|
63
|
-
if (!isHelperOptions(maybeOptions)) return { positional: args };
|
|
64
|
-
return { positional: args.slice(0, -1), options: maybeOptions };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getHashlineHelperState(context: unknown, options: prompt.HelperOptions | undefined): HashlineHelperState {
|
|
68
|
-
const data = options?.data;
|
|
69
|
-
const root = data?.root;
|
|
70
|
-
const holderTarget = data && typeof data === "object" ? data : root && typeof root === "object" ? root : context;
|
|
71
|
-
if (!holderTarget || typeof holderTarget !== "object") {
|
|
72
|
-
throw new Error("hashline prompt helpers require an object render context");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const holder = holderTarget as HashlineHelperStateHolder;
|
|
76
|
-
if (!holder[HL_HELPER_STATE]) {
|
|
77
|
-
holder[HL_HELPER_STATE] = { byLine: new Map() };
|
|
78
|
-
}
|
|
79
|
-
return holder[HL_HELPER_STATE];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function isLineNumberArg(value: unknown): boolean {
|
|
83
|
-
const num = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
84
|
-
return Number.isFinite(num);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function rememberHashlineRef(state: HashlineHelperState, line: number, ref: string): void {
|
|
88
|
-
const entry = { line, ref };
|
|
89
|
-
state.last = entry;
|
|
90
|
-
state.byLine.set(line, entry);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function requireStoredHashlineRef(state: HashlineHelperState, lineArg?: unknown): string {
|
|
94
|
-
if (lineArg === undefined) {
|
|
95
|
-
if (!state.last) {
|
|
96
|
-
throw new Error("{{href}} requires a previous {{hline}} call in the same prompt render");
|
|
97
|
-
}
|
|
98
|
-
return state.last.ref;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const line = typeof lineArg === "number" ? lineArg : Number.parseInt(String(lineArg), 10);
|
|
102
|
-
const entry = state.byLine.get(line);
|
|
103
|
-
if (!entry) {
|
|
104
|
-
throw new Error(`{{href ${line}}} requires a previous {{hline ${line} ...}} call in the same prompt render`);
|
|
105
|
-
}
|
|
106
|
-
return entry.ref;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function wrapHashlineRef(ref: string, args: unknown[]): string {
|
|
110
|
-
const preStr = typeof args[0] === "string" ? args[0] : "";
|
|
111
|
-
const postStr = typeof args[1] === "string" ? args[1] : "";
|
|
112
|
-
return `${preStr}${ref}${postStr}`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function resolveHashlineRef(state: HashlineHelperState, args: unknown[]): string {
|
|
116
|
-
if (args.length === 0) return requireStoredHashlineRef(state);
|
|
117
|
-
const [first, second, ...rest] = args;
|
|
118
|
-
if (isLineNumberArg(first)) {
|
|
119
|
-
if (second === undefined) return requireStoredHashlineRef(state, first);
|
|
120
|
-
const { ref } = formatHashlineRef(first, second);
|
|
121
|
-
return wrapHashlineRef(ref, rest);
|
|
122
|
-
}
|
|
123
|
-
return wrapHashlineRef(requireStoredHashlineRef(state), args);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* {{href lineNum "content"}} — compute a hashline line ref for prompt examples.
|
|
128
|
-
* {{href lineNum}} — quote the ref remembered by the earlier {{hline lineNum "..."}}
|
|
129
|
-
* {{href}} — quote the ref from the previous {{hline}} call.
|
|
130
|
-
* {{href "[" "]"}} — wrap the previous {{hline}} ref with pre/post chars.
|
|
131
|
-
* Returns `"lineNum"` (e.g., `"42"`), or `"[42]"` when pre/post are supplied.
|
|
132
|
-
*/
|
|
133
|
-
prompt.registerHelper("href", function (this: unknown, ...args: unknown[]): string {
|
|
134
|
-
const { positional, options } = splitHelperArgs(args);
|
|
135
|
-
const state = getHashlineHelperState(this, options);
|
|
136
|
-
return JSON.stringify(resolveHashlineRef(state, positional));
|
|
137
|
-
});
|
|
138
|
-
prompt.registerHelper("hrefr", function (this: unknown, ...args: unknown[]): string {
|
|
139
|
-
const { positional, options } = splitHelperArgs(args);
|
|
140
|
-
const state = getHashlineHelperState(this, options);
|
|
141
|
-
return resolveHashlineRef(state, positional);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* {{hline lineNum "content"}} — format a full read-style line with prefix.
|
|
146
|
-
* Returns `"lineNum:content"` (colon between line number and content).
|
|
147
|
-
*/
|
|
148
|
-
prompt.registerHelper("hline", function (this: unknown, ...args: unknown[]): string {
|
|
149
|
-
const { positional, options } = splitHelperArgs(args);
|
|
150
|
-
const [lineNum, content] = positional;
|
|
151
|
-
const { num, ref, text } = formatHashlineRef(lineNum, content);
|
|
152
|
-
const state = getHashlineHelperState(this, options);
|
|
153
|
-
rememberHashlineRef(state, num, ref);
|
|
154
|
-
return `${ref}${HL_LINE_BODY_SEP}${text}`;
|
|
155
|
-
});
|
|
156
|
-
|
|
157
32
|
const INLINE_ARG_SHELL_PATTERN = /\$(?:ARGUMENTS|@(?:\[\d+(?::\d*)?\])?|\d+)/;
|
|
158
33
|
const INLINE_ARG_TEMPLATE_PATTERN = /\{\{[\s\S]*?(?:\b(?:arguments|ARGUMENTS|args)\b|\barg\s+[^}]+)[\s\S]*?\}\}/;
|
|
159
34
|
|
|
@@ -1805,7 +1805,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1805
1805
|
// Default tool approval mode (interaction tab, but governs the tool wrapper).
|
|
1806
1806
|
// "always-ask" — auto-approves read-tier tools only; prompts for write/exec.
|
|
1807
1807
|
// "write" — auto-approves read and write-tier tools; prompts for exec.
|
|
1808
|
-
// "yolo" — auto-approves every tier
|
|
1808
|
+
// "yolo" — auto-approves every tier.
|
|
1809
1809
|
"tools.approvalMode": {
|
|
1810
1810
|
type: "enum",
|
|
1811
1811
|
values: ["always-ask", "write", "yolo"] as const,
|
|
@@ -1814,7 +1814,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1814
1814
|
tab: "interaction",
|
|
1815
1815
|
label: "Tool Approval",
|
|
1816
1816
|
description:
|
|
1817
|
-
"Default approval behaviour for tool calls. 'Always ask' auto-approves read-only tools only. 'Write' auto-approves read and workspace-write tools. 'Yolo' auto-approves
|
|
1817
|
+
"Default approval behaviour for tool calls. 'Always ask' auto-approves read-only tools only. 'Write' auto-approves read and workspace-write tools. 'Yolo' auto-approves all tiers; user policy may still prompt or block.",
|
|
1818
1818
|
options: [
|
|
1819
1819
|
{
|
|
1820
1820
|
value: "always-ask",
|
|
@@ -1831,7 +1831,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1831
1831
|
value: "yolo",
|
|
1832
1832
|
label: "Yolo",
|
|
1833
1833
|
description:
|
|
1834
|
-
"Auto-approve read, write, and exec tools.
|
|
1834
|
+
"Auto-approve read, write, and exec tools. User policy can still require confirmation or block calls.",
|
|
1835
1835
|
},
|
|
1836
1836
|
],
|
|
1837
1837
|
},
|
|
@@ -2405,7 +2405,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2405
2405
|
"task.simple": {
|
|
2406
2406
|
type: "enum",
|
|
2407
2407
|
values: TASK_SIMPLE_MODES,
|
|
2408
|
-
default: "
|
|
2408
|
+
default: "schema-free",
|
|
2409
2409
|
ui: {
|
|
2410
2410
|
tab: "tasks",
|
|
2411
2411
|
label: "Task Input Mode",
|
package/src/edit/streaming.ts
CHANGED
|
@@ -388,6 +388,9 @@ function buildHashlineNaturalOrderPreviews(
|
|
|
388
388
|
case "abort":
|
|
389
389
|
case "op-delete":
|
|
390
390
|
continue;
|
|
391
|
+
case "blank":
|
|
392
|
+
case "raw":
|
|
393
|
+
continue;
|
|
391
394
|
case "header":
|
|
392
395
|
currentPath = token.path;
|
|
393
396
|
if (currentPath) ensure(currentPath);
|
|
@@ -404,10 +407,6 @@ function buildHashlineNaturalOrderPreviews(
|
|
|
404
407
|
if (!currentPath || token.inlineBody === undefined) continue;
|
|
405
408
|
ensure(currentPath).push(`+${token.inlineBody}`);
|
|
406
409
|
continue;
|
|
407
|
-
case "blank":
|
|
408
|
-
if (!currentPath) continue;
|
|
409
|
-
ensure(currentPath).push("+");
|
|
410
|
-
continue;
|
|
411
410
|
case "payload":
|
|
412
411
|
if (!currentPath) continue;
|
|
413
412
|
ensure(currentPath).push(`+${token.text}`);
|
|
@@ -111,9 +111,8 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
111
111
|
context?: AgentToolContext,
|
|
112
112
|
) {
|
|
113
113
|
// 1. Check approval policy (before extension handlers).
|
|
114
|
-
// CLI `--auto-approve` / `--yolo`
|
|
115
|
-
// tool
|
|
116
|
-
// policies are honored in every mode.
|
|
114
|
+
// CLI `--auto-approve` / `--yolo` sets approval mode to yolo.
|
|
115
|
+
// User `tools.approval.<tool>` policies are still applied in all modes.
|
|
117
116
|
const cliAutoApprove = context?.autoApprove === true;
|
|
118
117
|
const settings: Settings | undefined = context?.settings;
|
|
119
118
|
const configuredMode = (settings?.get("tools.approvalMode") ?? "yolo") as ApprovalMode;
|
package/src/hashline/anchors.ts
CHANGED
|
@@ -71,7 +71,7 @@ export class HashlineMismatchError extends Error {
|
|
|
71
71
|
const pathText = details.path ? ` for ${details.path}` : "";
|
|
72
72
|
return [
|
|
73
73
|
`Edit rejected${pathText}: file changed between read and edit.`,
|
|
74
|
-
`Section is bound to ${HL_FILE_HASH_SEP}${details.expectedFileHash}, but the current file hashes to ${HL_FILE_HASH_SEP}${details.actualFileHash}
|
|
74
|
+
`Section is bound to ${HL_FILE_HASH_SEP}${details.expectedFileHash}, but the current file hashes to ${HL_FILE_HASH_SEP}${details.actualFileHash}. If your previous edit in this session modified this file, copy the ${HL_FILE_PREFIX}path${HL_FILE_HASH_SEP}newhash from that edit's response. Otherwise re-read the file before retrying.`,
|
|
75
75
|
];
|
|
76
76
|
}
|
|
77
77
|
|
package/src/hashline/apply.ts
CHANGED
|
@@ -53,6 +53,57 @@ function validateHashlineLineBounds(edits: HashlineEdit[], fileLines: string[]):
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Refuse a single-line replace whose target line is blank and whose payload is
|
|
58
|
+
* non-empty. The model is almost certainly miscounting: `A:CONTENT` overwrites
|
|
59
|
+
* the existing line, so applying it to a blank target deletes the blank cadence
|
|
60
|
+
* and inserts content in its place. To insert content at a blank line, use
|
|
61
|
+
* `A↑` (insert before) or `A↓` (insert after) instead.
|
|
62
|
+
*
|
|
63
|
+
* Only fires for the simple shape: exactly one `insert(before_anchor A)` + one
|
|
64
|
+
* `delete(A)` sharing the same source op line, no other inserts/deletes from
|
|
65
|
+
* that op.
|
|
66
|
+
*/
|
|
67
|
+
function detectReplaceOnBlankTarget(edits: HashlineEdit[], fileLines: string[]): string | null {
|
|
68
|
+
type Pair = {
|
|
69
|
+
insert?: Extract<HashlineEdit, { kind: "insert" }>;
|
|
70
|
+
delete?: Extract<HashlineEdit, { kind: "delete" }>;
|
|
71
|
+
multi?: boolean;
|
|
72
|
+
};
|
|
73
|
+
const byOpLine = new Map<number, Pair>();
|
|
74
|
+
for (const edit of edits) {
|
|
75
|
+
const pair = byOpLine.get(edit.lineNum) ?? {};
|
|
76
|
+
if (pair.multi) continue;
|
|
77
|
+
if (edit.kind === "insert") {
|
|
78
|
+
if (pair.insert) pair.multi = true;
|
|
79
|
+
else pair.insert = edit;
|
|
80
|
+
} else {
|
|
81
|
+
if (pair.delete) pair.multi = true;
|
|
82
|
+
else pair.delete = edit;
|
|
83
|
+
}
|
|
84
|
+
byOpLine.set(edit.lineNum, pair);
|
|
85
|
+
}
|
|
86
|
+
for (const pair of byOpLine.values()) {
|
|
87
|
+
if (pair.multi || !pair.insert || !pair.delete) continue;
|
|
88
|
+
const insert = pair.insert;
|
|
89
|
+
const del = pair.delete;
|
|
90
|
+
if (insert.cursor.kind !== "before_anchor") continue;
|
|
91
|
+
if (insert.cursor.anchor.line !== del.anchor.line) continue;
|
|
92
|
+
if (insert.text.includes("\n")) continue;
|
|
93
|
+
if (insert.text.trim().length === 0) continue;
|
|
94
|
+
const targetLine = del.anchor.line;
|
|
95
|
+
const oldLine = fileLines[targetLine - 1];
|
|
96
|
+
if (oldLine === undefined || oldLine.trim().length !== 0) continue;
|
|
97
|
+
return (
|
|
98
|
+
`Edit rejected: replace at line ${targetLine} targets a blank line but the payload is non-empty. ` +
|
|
99
|
+
`'A:CONTENT' overwrites the line at A; to insert content next to a blank line, use 'A${"\u2191"}' (insert before) ` +
|
|
100
|
+
`or 'A${"\u2193"}' (insert after) instead. If you really meant to replace this blank with content, ` +
|
|
101
|
+
`widen the range to include surrounding non-blank lines so the intent is explicit.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
56
107
|
function insertAtStart(fileLines: string[], lineOrigins: HashlineLineOrigin[], lines: string[]): void {
|
|
57
108
|
if (lines.length === 0) return;
|
|
58
109
|
const origins = lines.map((): HashlineLineOrigin => "insert");
|
|
@@ -160,10 +211,10 @@ function countMatchingSuffixBlock(fileLines: string[], endLine: number, replacem
|
|
|
160
211
|
return 0;
|
|
161
212
|
}
|
|
162
213
|
|
|
163
|
-
// Single-line
|
|
164
|
-
// General one-line context is too easy to delete incorrectly, but
|
|
165
|
-
// `};` / `)` / `]` boundaries
|
|
166
|
-
// line early and would otherwise produce a syntax error.
|
|
214
|
+
// Single-line replacement-boundary absorption is limited to structural closing
|
|
215
|
+
// delimiters. General one-line context is too easy to delete incorrectly, but
|
|
216
|
+
// duplicated `};` / `)` / `]` boundaries often mean a replacement range stopped
|
|
217
|
+
// one line early and would otherwise produce a syntax error.
|
|
167
218
|
const STRUCTURAL_CLOSING_BOUNDARY_RE = /^\s*[\])}]+[;,]?\s*$/;
|
|
168
219
|
|
|
169
220
|
function isStructuralClosingBoundaryLine(line: string): boolean {
|
|
@@ -176,8 +227,6 @@ interface DelimiterBalance {
|
|
|
176
227
|
brace: number;
|
|
177
228
|
}
|
|
178
229
|
|
|
179
|
-
const ZERO_DELIMITER_BALANCE: DelimiterBalance = { paren: 0, bracket: 0, brace: 0 };
|
|
180
|
-
|
|
181
230
|
/**
|
|
182
231
|
* Naive bracket counter — does NOT skip string/template/comment contents. The
|
|
183
232
|
* single-line structural absorb relies on this being safe-by-asymmetry: the
|
|
@@ -285,6 +334,7 @@ function countMatchingSingleNonStructuralPrefixDuplicate(
|
|
|
285
334
|
): number {
|
|
286
335
|
if (replacement.length === 0 || startLine <= 1) return 0;
|
|
287
336
|
const line = replacement[0];
|
|
337
|
+
if (line.trim().length === 0) return 0;
|
|
288
338
|
if (isStructuralClosingBoundaryLine(line)) return 0;
|
|
289
339
|
if (fileLines[startLine - 2] !== line) return 0;
|
|
290
340
|
return 1;
|
|
@@ -297,6 +347,7 @@ function countMatchingSingleNonStructuralSuffixDuplicate(
|
|
|
297
347
|
): number {
|
|
298
348
|
if (replacement.length === 0 || endLine >= fileLines.length) return 0;
|
|
299
349
|
const line = replacement[replacement.length - 1];
|
|
350
|
+
if (line.trim().length === 0) return 0;
|
|
300
351
|
if (isStructuralClosingBoundaryLine(line)) return 0;
|
|
301
352
|
if (fileLines[endLine] !== line) return 0;
|
|
302
353
|
return 1;
|
|
@@ -400,12 +451,11 @@ interface PureInsertAbsorbResult {
|
|
|
400
451
|
}
|
|
401
452
|
|
|
402
453
|
/**
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* balance-validated structural rule below.
|
|
454
|
+
* For a pure-insert group, drop only multi-line context echoes that exactly
|
|
455
|
+
* duplicate the file lines adjacent to the insertion point. Single-line pure
|
|
456
|
+
* insert duplicates are ambiguous (`N↓}` may be an accidental anchor echo or an
|
|
457
|
+
* intentional inserted delimiter), so they are left literal even when generic
|
|
458
|
+
* duplicate absorption is enabled.
|
|
409
459
|
*/
|
|
410
460
|
function tryAbsorbPureInsertGroup(
|
|
411
461
|
group: HashlinePureInsertGroup,
|
|
@@ -435,33 +485,11 @@ function tryAbsorbPureInsertGroup(
|
|
|
435
485
|
}
|
|
436
486
|
}
|
|
437
487
|
}
|
|
438
|
-
if (
|
|
439
|
-
absorbedLeading === 0 &&
|
|
440
|
-
allowGenericBoundaryAbsorb &&
|
|
441
|
-
group.cursor.kind === "after_anchor" &&
|
|
442
|
-
group.payload.length > 0 &&
|
|
443
|
-
aboveEndIdx >= 0 &&
|
|
444
|
-
!isStructuralClosingBoundaryLine(group.payload[0]) &&
|
|
445
|
-
group.payload[0] === fileLines[aboveEndIdx]
|
|
446
|
-
) {
|
|
447
|
-
absorbedLeading = 1;
|
|
448
|
-
}
|
|
449
|
-
if (
|
|
450
|
-
absorbedLeading === 0 &&
|
|
451
|
-
group.payload.length > 0 &&
|
|
452
|
-
aboveEndIdx >= 0 &&
|
|
453
|
-
isStructuralClosingBoundaryLine(group.payload[0]) &&
|
|
454
|
-
group.payload[0] === fileLines[aboveEndIdx] &&
|
|
455
|
-
shouldDropSingleStructuralBoundary(group.payload, group.payload.slice(1), ZERO_DELIMITER_BALANCE)
|
|
456
|
-
) {
|
|
457
|
-
absorbedLeading = 1;
|
|
458
|
-
}
|
|
459
488
|
|
|
460
489
|
// Trailing: payload[len-k..len-1] vs fileLines[belowStartIdx..belowStartIdx+k-1].
|
|
461
490
|
// Don't double-count payload lines already absorbed as leading.
|
|
462
491
|
let absorbedTrailing = 0;
|
|
463
|
-
const
|
|
464
|
-
const remaining = remainingPayload.length;
|
|
492
|
+
const remaining = group.payload.length - absorbedLeading;
|
|
465
493
|
if (allowGenericBoundaryAbsorb) {
|
|
466
494
|
const maxTrail = Math.min(remaining, fileLines.length - belowStartIdx);
|
|
467
495
|
for (let count = maxTrail; count >= 2; count--) {
|
|
@@ -478,27 +506,6 @@ function tryAbsorbPureInsertGroup(
|
|
|
478
506
|
}
|
|
479
507
|
}
|
|
480
508
|
}
|
|
481
|
-
if (
|
|
482
|
-
absorbedTrailing === 0 &&
|
|
483
|
-
group.cursor.kind === "before_anchor" &&
|
|
484
|
-
allowGenericBoundaryAbsorb &&
|
|
485
|
-
remaining > 0 &&
|
|
486
|
-
belowStartIdx < fileLines.length &&
|
|
487
|
-
!isStructuralClosingBoundaryLine(remainingPayload[remainingPayload.length - 1]) &&
|
|
488
|
-
remainingPayload[remainingPayload.length - 1] === fileLines[belowStartIdx]
|
|
489
|
-
) {
|
|
490
|
-
absorbedTrailing = 1;
|
|
491
|
-
}
|
|
492
|
-
if (
|
|
493
|
-
absorbedTrailing === 0 &&
|
|
494
|
-
remaining > 0 &&
|
|
495
|
-
belowStartIdx < fileLines.length &&
|
|
496
|
-
isStructuralClosingBoundaryLine(remainingPayload[remainingPayload.length - 1]) &&
|
|
497
|
-
remainingPayload[remainingPayload.length - 1] === fileLines[belowStartIdx] &&
|
|
498
|
-
shouldDropSingleStructuralBoundary(remainingPayload, remainingPayload.slice(0, -1), ZERO_DELIMITER_BALANCE)
|
|
499
|
-
) {
|
|
500
|
-
absorbedTrailing = 1;
|
|
501
|
-
}
|
|
502
509
|
|
|
503
510
|
if (absorbedLeading === 0 && absorbedTrailing === 0) return empty;
|
|
504
511
|
|
|
@@ -683,6 +690,9 @@ export function applyHashlineEdits(
|
|
|
683
690
|
|
|
684
691
|
validateHashlineLineBounds(edits, fileLines);
|
|
685
692
|
|
|
693
|
+
const blankTargetError = detectReplaceOnBlankTarget(edits, fileLines);
|
|
694
|
+
if (blankTargetError !== null) throw new Error(blankTargetError);
|
|
695
|
+
|
|
686
696
|
const normalizedEdits = absorbReplacementBoundaryDuplicates(edits, fileLines, warnings, options);
|
|
687
697
|
|
|
688
698
|
// Normalize after_anchor inserts to before_anchor of the next line, or EOF
|