refactor(patch): restructured chunk edit schema to use explicit op enum and anchor field
- Refactored chunk edit schema to use explicit `op` enum field (replace, delete, append, prepend, after, before) instead of multiple boolean/string flags. - Renamed chunk edit parameters from `after`/`before` field names to `anchor` for sibling-relative insert operations. - Simplified chunk edit validation logic by consolidating nested conditionals into switch statement on operation type. - Fixed `log_experiment` to correctly identify run-modified files by removing pre-run dirty path filtering. - Simplified chunk edit and read prompt documentation to emphasize read-first workflow with consolidated examples.
This commit is contained in:
@@ -21,6 +21,15 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Simplified chunk edit schema to use explicit `op` field (replace, delete, append, prepend, after, before) instead of separate boolean flags for improved clarity
|
||||
- Updated chunk edit prompt documentation to consolidate operation guidance with streamlined rules, ops reference table, and simplified examples
|
||||
- Refactored chunk edit preview formatting to use `op` field and `anchor` parameter for sibling-relative inserts (after/before operations)
|
||||
- Renamed chunk edit parameter from `after`/`before` field names to use `anchor` field for specifying named children in sibling-relative insert operations
|
||||
- Simplified chunk read and edit prompt documentation to reduce verbosity and focus on essential guidance
|
||||
- Refactored chunk edit schema to use explicit `op` field (replace, delete, append, prepend, after, before) instead of boolean flags for improved clarity and consistency
|
||||
- Updated chunk edit prompt documentation to simplify operation guidance with consolidated rules, ops reference, and streamlined examples
|
||||
- Changed chunk edit preview formatting to use `op` field and `anchor` parameter for sibling-relative inserts (after/before operations)
|
||||
- Renamed chunk edit parameter from `after`/`before` field names to use `anchor` field for specifying named children in sibling-relative insert operations
|
||||
- Updated streaming edit preview to display chunk edits alongside hashline edits with operation-specific formatting
|
||||
- Improved chunk edit preview labels to show operation type (delete, append, prepend, insert, replace) with target and line ranges
|
||||
- Simplified chunk read path parsing to extract only the selector string without redundant CRC field
|
||||
@@ -72,6 +81,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `log_experiment` to correctly identify run-modified files by removing pre-run dirty path filtering, ensuring all uncommitted changes are properly validated
|
||||
- Fixed chunk edit operations to extract and use embedded CRC checksums when models write selectors like `fn_foo#ABCD` instead of separate `sel` and `crc` fields
|
||||
- Fixed shell command error handling to properly check for errors before processing chunks in bash executor and config resolution
|
||||
- Fixed `log_experiment` to correctly identify and revert only files modified by the run, leaving pre-existing uncommitted changes intact
|
||||
|
||||
@@ -209,7 +209,7 @@ export function createLogExperimentTool(
|
||||
const preRunDirtyPaths = pendingRun.preRunDirtyPaths;
|
||||
let keepScopeValidation: { committablePaths: string[] } | undefined;
|
||||
if (params.status === "keep") {
|
||||
const scopeValidation = await validateKeepPaths(options, workDir, state, preRunDirtyPaths);
|
||||
const scopeValidation = await validateKeepPaths(options, workDir, state);
|
||||
if (typeof scopeValidation === "string") {
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: ${scopeValidation}` }],
|
||||
@@ -605,7 +605,6 @@ async function validateKeepPaths(
|
||||
options: AutoresearchToolFactoryOptions,
|
||||
workDir: string,
|
||||
state: ExperimentState,
|
||||
preRunDirtyPaths: string[],
|
||||
): Promise<{ committablePaths: string[] } | string> {
|
||||
if (state.scopePaths.length === 0) {
|
||||
return "Files in Scope is empty for the current segment. Re-run init_experiment after fixing autoresearch.md.";
|
||||
@@ -624,7 +623,6 @@ async function validateKeepPaths(
|
||||
}
|
||||
|
||||
const workDirPrefix = await readGitWorkDirPrefix(options, workDir);
|
||||
const preRunSet = new Set(preRunDirtyPaths);
|
||||
const committablePaths: string[] = [];
|
||||
for (const entry of parseWorkDirDirtyPathsWithStatus(statusText, workDirPrefix)) {
|
||||
if (isAutoresearchLocalStatePath(entry.path)) {
|
||||
@@ -634,10 +632,6 @@ async function validateKeepPaths(
|
||||
committablePaths.push(entry.path);
|
||||
continue;
|
||||
}
|
||||
// Skip files that were already dirty before the run
|
||||
if (preRunSet.has(entry.path)) {
|
||||
continue;
|
||||
}
|
||||
if (state.offLimits.some(spec => pathMatchesContractPath(entry.path, spec))) {
|
||||
return `cannot keep this run because ${entry.path} is listed under Off Limits in autoresearch.md`;
|
||||
}
|
||||
|
||||
@@ -102,49 +102,30 @@ const patchEditSchema = Type.Object({
|
||||
diff: Type.Optional(Type.String({ description: "Diff hunks (update) or full content (create)" })),
|
||||
});
|
||||
|
||||
const pTarget = {
|
||||
target: Type.String({ description: "Chunk selector path with optional #CRC suffix copied from read output" }),
|
||||
} as const;
|
||||
const CHUNK_OP_VALUES = ["replace", "delete", "append", "prepend", "after", "before"] as const;
|
||||
|
||||
const pContent = {
|
||||
content: Type.Optional(
|
||||
Type.Union([Type.String(), Type.Array(Type.String())], { description: "Replacement content" }),
|
||||
),
|
||||
} as const;
|
||||
|
||||
const pDelete = {
|
||||
delete: Type.Optional(Type.Boolean({ description: "Delete the target chunk when true" })),
|
||||
} as const;
|
||||
|
||||
const pPosition = {
|
||||
append: Type.Optional(Type.Boolean({ description: "Insert content as the last child of target" })),
|
||||
prepend: Type.Optional(Type.Boolean({ description: "Insert content as the first child of target" })),
|
||||
after: Type.Optional(Type.String({ description: "Insert content after this named child within target" })),
|
||||
before: Type.Optional(Type.String({ description: "Insert content before this named child within target" })),
|
||||
} as const;
|
||||
|
||||
const pRangesOpt = {
|
||||
line: Type.Optional(
|
||||
Type.Integer({
|
||||
const chunkToolEditSchema = Type.Object({
|
||||
target: Type.String({
|
||||
description:
|
||||
"Chunk path from read output, with #CRC suffix for replace/delete (e.g. 'class_X.fn_y#A14F'). Use parent path without #CRC for insert ops.",
|
||||
}),
|
||||
op: Type.Optional(
|
||||
StringEnum(CHUNK_OP_VALUES, {
|
||||
description:
|
||||
"Start line for line-scoped replace (1-indexed file line from read gutter). Omit for whole-chunk replace.",
|
||||
"Edit op (default: replace). 'delete' removes target. 'append'/'prepend' insert as last/first child. 'after'/'before' insert at sibling position; require 'anchor'.",
|
||||
}),
|
||||
),
|
||||
end_line: Type.Optional(
|
||||
Type.Integer({ description: "End line for line-scoped replace. Omit for single-line edit." }),
|
||||
content: Type.Optional(
|
||||
Type.String({ description: "New content (required for replace/append/prepend/after/before)." }),
|
||||
),
|
||||
} as const;
|
||||
|
||||
const chunkToolEditSchema = Type.Object(
|
||||
{
|
||||
...pTarget,
|
||||
...pContent,
|
||||
...pDelete,
|
||||
...pPosition,
|
||||
...pRangesOpt,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
line: Type.Optional(
|
||||
Type.Integer({ description: "Absolute file line for line-scoped replace (omit for whole-chunk replace)." }),
|
||||
),
|
||||
end_line: Type.Optional(Type.Integer({ description: "End line (inclusive) for replacing a line range." })),
|
||||
anchor: Type.Optional(
|
||||
Type.String({ description: "Named child to insert relative to (required for op=after/before)." }),
|
||||
),
|
||||
});
|
||||
|
||||
const chunkEditParamsSchema = Type.Object(
|
||||
{
|
||||
@@ -584,75 +565,75 @@ export class EditTool implements AgentTool<TInput> {
|
||||
|
||||
for (const edit of edits) {
|
||||
const { selector, crc } = parseChunkTarget(edit.target);
|
||||
const op = edit.op ?? "replace";
|
||||
const hasContent = edit.content !== undefined;
|
||||
const positionModes =
|
||||
Number(Boolean(edit.append)) +
|
||||
Number(Boolean(edit.prepend)) +
|
||||
Number(edit.after !== undefined) +
|
||||
Number(edit.before !== undefined);
|
||||
const content = flattenContent(edit.content);
|
||||
|
||||
if (positionModes > 1) {
|
||||
throw new Error(`Edit target ${describeChunkTarget(selector)} must choose only one insertion mode.`);
|
||||
}
|
||||
switch (op) {
|
||||
case "delete": {
|
||||
if (hasContent || edit.line !== undefined || edit.end_line !== undefined) {
|
||||
throw new Error(
|
||||
`Delete edit on ${describeChunkTarget(selector)} cannot include content or line ranges.`,
|
||||
);
|
||||
}
|
||||
normalizedOperations.push({
|
||||
op: "delete",
|
||||
sel: selector,
|
||||
crc: await assertChecksum("delete", crc, selector),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (edit.delete) {
|
||||
if (hasContent || edit.line !== undefined || edit.end_line !== undefined || positionModes > 0) {
|
||||
throw new Error(
|
||||
`Delete edit on ${describeChunkTarget(selector)} cannot include content, line ranges, or insert positions.`,
|
||||
);
|
||||
}
|
||||
normalizedOperations.push({
|
||||
op: "delete",
|
||||
sel: selector,
|
||||
crc: await assertChecksum("delete", crc, selector),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (positionModes > 0) {
|
||||
if (edit.line !== undefined || edit.end_line !== undefined) {
|
||||
throw new Error(`Insert edit on ${describeChunkTarget(selector)} cannot include line ranges.`);
|
||||
}
|
||||
if (!hasContent) {
|
||||
throw new Error(`Content required for insert edit on ${describeChunkTarget(selector)}.`);
|
||||
}
|
||||
const content = flattenContent(edit.content);
|
||||
if (edit.append) {
|
||||
case "append": {
|
||||
if (!hasContent) throw new Error(`Content required for append on ${describeChunkTarget(selector)}.`);
|
||||
normalizedOperations.push({ op: "append_child", sel: selector, content });
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
if (edit.prepend) {
|
||||
case "prepend": {
|
||||
if (!hasContent) throw new Error(`Content required for prepend on ${describeChunkTarget(selector)}.`);
|
||||
normalizedOperations.push({ op: "prepend_child", sel: selector, content });
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
if (edit.after !== undefined) {
|
||||
|
||||
case "after": {
|
||||
if (!hasContent)
|
||||
throw new Error(`Content required for after-insert on ${describeChunkTarget(selector)}.`);
|
||||
if (!edit.anchor)
|
||||
throw new Error(`'anchor' required for op=after on ${describeChunkTarget(selector)}.`);
|
||||
normalizedOperations.push({
|
||||
op: "append_sibling",
|
||||
sel: joinChunkPath(selector, edit.after),
|
||||
sel: joinChunkPath(selector, edit.anchor),
|
||||
content,
|
||||
});
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
case "before": {
|
||||
if (!hasContent)
|
||||
throw new Error(`Content required for before-insert on ${describeChunkTarget(selector)}.`);
|
||||
if (!edit.anchor)
|
||||
throw new Error(`'anchor' required for op=before on ${describeChunkTarget(selector)}.`);
|
||||
normalizedOperations.push({
|
||||
op: "prepend_sibling",
|
||||
sel: joinChunkPath(selector, edit.anchor),
|
||||
content,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (!hasContent) {
|
||||
throw new Error(`Content required for replace edit on ${describeChunkTarget(selector)}.`);
|
||||
}
|
||||
normalizedOperations.push({
|
||||
op: "replace",
|
||||
sel: selector,
|
||||
crc: await assertChecksum("replace", crc, selector),
|
||||
content,
|
||||
line: edit.line,
|
||||
endLine: edit.end_line,
|
||||
});
|
||||
break;
|
||||
}
|
||||
normalizedOperations.push({
|
||||
op: "prepend_sibling",
|
||||
sel: joinChunkPath(selector, edit.before ?? ""),
|
||||
content,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasContent) {
|
||||
throw new Error(`Content required for replace edit on ${describeChunkTarget(selector)}.`);
|
||||
}
|
||||
|
||||
normalizedOperations.push({
|
||||
op: "replace",
|
||||
sel: selector,
|
||||
crc: await assertChecksum("replace", crc, selector),
|
||||
content: flattenContent(edit.content),
|
||||
line: edit.line,
|
||||
endLine: edit.end_line,
|
||||
});
|
||||
}
|
||||
|
||||
const chunkResult = applyChunkEdits({
|
||||
|
||||
@@ -198,27 +198,27 @@ function formatStreamingHashlineEdits(edits: Partial<HashlineToolEdit | ChunkToo
|
||||
? edit.content
|
||||
: "";
|
||||
const target = edit.target ?? "?";
|
||||
const op = edit.op ?? "replace";
|
||||
|
||||
if (edit.delete) {
|
||||
return { srcLabel: `\u2022 delete ${target}`, dst: "" };
|
||||
switch (op) {
|
||||
case "delete":
|
||||
return { srcLabel: `\u2022 delete ${target}`, dst: "" };
|
||||
case "append":
|
||||
return { srcLabel: `\u2022 append child ${target}`, dst: contentLines };
|
||||
case "prepend":
|
||||
return { srcLabel: `\u2022 prepend child ${target}`, dst: contentLines };
|
||||
case "after":
|
||||
return { srcLabel: `\u2022 insert after ${target}/${edit.anchor ?? "?"}`, dst: contentLines };
|
||||
case "before":
|
||||
return { srcLabel: `\u2022 insert before ${target}/${edit.anchor ?? "?"}`, dst: contentLines };
|
||||
default: {
|
||||
if (edit.line != null) {
|
||||
const range = edit.end_line != null ? `${edit.line}\u2026${edit.end_line}` : `${edit.line}`;
|
||||
return { srcLabel: `\u2022 replace ${target} L${range}`, dst: contentLines };
|
||||
}
|
||||
return { srcLabel: `\u2022 replace ${target}`, dst: contentLines };
|
||||
}
|
||||
}
|
||||
if (edit.append) {
|
||||
return { srcLabel: `\u2022 append child ${target}`, dst: contentLines };
|
||||
}
|
||||
if (edit.prepend) {
|
||||
return { srcLabel: `\u2022 prepend child ${target}`, dst: contentLines };
|
||||
}
|
||||
if (edit.after) {
|
||||
return { srcLabel: `\u2022 insert after ${target}.${edit.after}`, dst: contentLines };
|
||||
}
|
||||
if (edit.before) {
|
||||
return { srcLabel: `\u2022 insert before ${target}.${edit.before}`, dst: contentLines };
|
||||
}
|
||||
if (edit.line != null) {
|
||||
const range = edit.end_line != null ? `${edit.line}\u2026${edit.end_line}` : `${edit.line}`;
|
||||
return { srcLabel: `\u2022 replace ${target} L${range}`, dst: contentLines };
|
||||
}
|
||||
return { srcLabel: `\u2022 replace ${target}`, dst: contentLines };
|
||||
}
|
||||
}
|
||||
function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
|
||||
|
||||
@@ -1,132 +1,37 @@
|
||||
Edits files by addressing syntax-aware chunks from `read` output.
|
||||
Edits files via syntax-aware chunks. Run `read(path="file.ts")` first — it returns chunks with anchors `[name#CRC]` where `#CRC` is a 4-char hex checksum. Copy the full chunk path AND `#CRC` verbatim into `target`.
|
||||
|
||||
Read the file first with `read(path="file.ts")`. Copy `target` directly from the latest chunk anchors. The `#CRC` suffix in `target` is the chunk checksum.
|
||||
<rules>
|
||||
- **MUST** `read` first. NEVER invent chunk names or CRCs — copy them from the latest read output or edit response.
|
||||
- `target` **MUST** be the **fully-qualified** path (e.g. `class_X.fn_y.if_2`, not `if_2`), ending with `#CRC` for replace/delete.
|
||||
- Prefer `line`/`end_line` (absolute file line numbers from the read gutter) for small fixes over whole-chunk replace.
|
||||
- `content` must include the destination block's inner indentation.
|
||||
- Successful edits return refreshed anchors — use them for follow-ups, don't re-read just for new CRCs.
|
||||
</rules>
|
||||
|
||||
Successful edit responses include the updated chunk tree with checksums. Do not re-read just to refresh checksums unless the file changed externally.
|
||||
<ops>
|
||||
|op|fields|effect|
|
||||
|---|---|---|
|
||||
|`replace` (default)|`target#CRC`, `content`, opt. `line`/`end_line`|rewrite chunk or a line range within it|
|
||||
|`delete`|`target#CRC`|remove chunk|
|
||||
|`append` / `prepend`|`target`, `content`|insert as last/first child of target|
|
||||
|`after` / `before`|`target`, `anchor` (child name), `content`|insert at sibling position|
|
||||
|
||||
**Checksum scope:** Each chunk has its own CRC over its source span. Editing non-overlapping lines elsewhere does not change unrelated chunks' checksums.
|
||||
For file-root edits, `target` is the file CRC alone (e.g. `"#VSKB"`).
|
||||
</ops>
|
||||
|
||||
<operations>
|
||||
**Choosing the right edit shape:**
|
||||
- To rewrite an entire chunk → `{ "target": "chunk#CRC", "content": "…" }`
|
||||
- To fix a single line → add `"line": 13`
|
||||
- To fix a contiguous range → add `"line": 13, "end_line": 17`
|
||||
- To delete a chunk → `{ "target": "chunk#CRC", "delete": true }`
|
||||
- To append/prepend inside a container → add `"append": true` or `"prepend": true`
|
||||
- To insert relative to a named child → use `"after": "child_name"` or `"before": "child_name"` on the parent target
|
||||
|
||||
|shape|effect|
|
||||
|---|---|
|
||||
|`{ "target": "chunk#CRC", "content": "…" }`|replace the target chunk|
|
||||
|`{ "target": "chunk#CRC", "line": 13, "content": "…" }`|replace one line within the target chunk|
|
||||
|`{ "target": "chunk#CRC", "line": 13, "end_line": 17, "content": "…" }`|replace an inclusive line range within the target chunk|
|
||||
|`{ "target": "chunk#CRC", "delete": true }`|delete the target chunk|
|
||||
|`{ "target": "chunk", "append": true, "content": "…" }`|append as last child of the target chunk|
|
||||
|`{ "target": "chunk", "prepend": true, "content": "…" }`|prepend as first child of the target chunk|
|
||||
|`{ "target": "parent", "after": "child", "content": "…" }`|insert after the named child within `parent`|
|
||||
|`{ "target": "parent", "before": "child", "content": "…" }`|insert before the named child within `parent`|
|
||||
- `line`/`end_line` are **absolute file line numbers** from the `read` gutter. `line` alone = single line. `line` + `end_line` = inclusive range. `line` with `end_line` = `line`-1 = zero-width insert.
|
||||
- `path` is always just the file path. Do not embed `:chunk` selectors in `path`.
|
||||
- `target` is the chunk path, optionally followed by `#CRC` copied from the anchor. Example: `"class_Server.fn_start#HTST"`.
|
||||
- Insert edits usually omit the checksum because they do not rewrite the target chunk itself.
|
||||
- For file-root replace/delete, use the file header checksum as `"target": "#VSKB"`.
|
||||
- Content must already include the intended inner indentation for the destination block.
|
||||
- Batch edits observe earlier edits. If edit 1 changes checksum/span/path, edit 2 must use the post-edit anchor values.
|
||||
- `replace`/`delete` include leading comments/attributes attached to the chunk.
|
||||
</operations>
|
||||
|
||||
<examples>
|
||||
All examples reference this `read` output:
|
||||
<example>
|
||||
Given read output:
|
||||
```
|
||||
| server.ts·40L·ts·#VSKB
|
||||
5| class Server {
|
||||
| {{anchor "class_Server" "XKQZ"}}
|
||||
12| start(): void {
|
||||
| {{anchor "fn_start" "HTST"}}
|
||||
13| log("booting on " + this.port);
|
||||
14| for (let i = 0; i < MAX_RETRIES; i++) {
|
||||
15| this.tryBind();
|
||||
16| }
|
||||
17| }
|
||||
19| private tryBind(): boolean {
|
||||
| {{anchor "fn_tryBind" "VNWR"}}
|
||||
20| // TODO: add backoff
|
||||
21| return bind(this.port);
|
||||
22| }
|
||||
14| this.tryBind();
|
||||
15| }
|
||||
```
|
||||
|
||||
<example name="replace a method">
|
||||
```
|
||||
"path": "server.ts",
|
||||
"edits": [
|
||||
{
|
||||
"target": "{{sel "class_Server.fn_start"}}#HTST",
|
||||
"content": "start(): void {\n log(\"starting\");\n this.tryBind();\n}"
|
||||
}
|
||||
]
|
||||
Fix the typo on line 13:
|
||||
```json
|
||||
{"path":"server.ts","edits":[{"target":"{{sel "class_Server.fn_start"}}#HTST","line":13,"content":" warn(\"booting on \" + this.port);"}]}
|
||||
```
|
||||
</example>
|
||||
|
||||
<example name="replace a single line">
|
||||
```
|
||||
"path": "server.ts",
|
||||
"edits": [
|
||||
{
|
||||
"target": "{{sel "class_Server.fn_start"}}#HTST",
|
||||
"line": 13,
|
||||
"content": " warn(\"booting on \" + this.port);"
|
||||
}
|
||||
]
|
||||
```
|
||||
</example>
|
||||
|
||||
<example name="delete a chunk">
|
||||
```
|
||||
"path": "server.ts",
|
||||
"edits": [
|
||||
{
|
||||
"target": "{{sel "class_Server.fn_tryBind"}}#VNWR",
|
||||
"delete": true
|
||||
}
|
||||
]
|
||||
```
|
||||
</example>
|
||||
|
||||
<example name="append a child method">
|
||||
```
|
||||
"path": "server.ts",
|
||||
"edits": [
|
||||
{
|
||||
"target": "{{sel "class_Server"}}",
|
||||
"append": true,
|
||||
"content": "status(): string {\n return \"ok\";\n}"
|
||||
}
|
||||
]
|
||||
```
|
||||
</example>
|
||||
|
||||
<example name="insert after a named child">
|
||||
```
|
||||
"path": "server.ts",
|
||||
"edits": [
|
||||
{
|
||||
"target": "{{sel "class_Server"}}",
|
||||
"after": "fn_start",
|
||||
"content": "reset(): void {\n this.port = 0;\n}"
|
||||
}
|
||||
]
|
||||
```
|
||||
</example>
|
||||
</examples>
|
||||
|
||||
<critical>
|
||||
- **MUST** include `path` in every edit call.
|
||||
- **MUST** read the latest chunk output before editing.
|
||||
- **MUST** include a `#CRC` suffix in `target` for every replace or delete edit.
|
||||
- **MUST** use updated chunk output from the edit response for follow-up edits.
|
||||
- **MUST** use the smallest correct target; do not rewrite siblings unnecessarily.
|
||||
- **MUST** use the **fully qualified** chunk path. Copy the complete path shown in read output — e.g. `fn_handleTerraform.try.if_2`, never just `if_2`. Short/leaf names are ambiguous.
|
||||
- For line-scoped replace edits, use file line numbers from the `read` gutter.
|
||||
- Multiple line-scoped edits on the same chunk in one batch are fine — the engine auto-updates the checksum between operations.
|
||||
</critical>
|
||||
</output>
|
||||
|
||||
@@ -1,55 +1,16 @@
|
||||
Reads files using syntax-aware chunks.
|
||||
|
||||
<instruction>
|
||||
## Parameters
|
||||
- `path` -- file path or URL; may include `:selector` suffix as an alternative to `sel`
|
||||
- `sel` -- optional selector (see table below)
|
||||
- `timeout` -- seconds, for URLs only
|
||||
- `path` — file path or URL; may include `:selector` suffix
|
||||
- `sel` — optional selector: `class_Foo`, `class_Foo.fn_bar`, `L50`, `L50-L120`, or `raw`
|
||||
- `timeout` — seconds, for URLs only
|
||||
|
||||
## Selectors
|
||||
Each anchor `[name#CCCC]` in the output is a chunk ID. Copy `name#CCCC` into the edit tool's `target` field.
|
||||
Line numbers in the gutter are absolute — use them for `line`/`end_line` in edits.
|
||||
|
||||
|Input|Behavior|
|
||||
|---|---|
|
||||
|_(omitted)_|Render the file root chunk|
|
||||
|`class_Foo`|Read a chunk by path|
|
||||
|`class_Foo.fn_bar`|Read a nested chunk path|
|
||||
|`L50` or `L50-L120`|Absolute file line range|
|
||||
|`raw`|Read full raw file content (no chunk rendering)|
|
||||
|
||||
Each anchor line shows `[name#CCCC]` — `#CCCC` is the edit checksum. Copy it when editing with `chunk-edit`.
|
||||
|
||||
If `path:chunk` and `sel` are both provided, `sel` wins. Missing chunk paths return `[Chunk not found]`.
|
||||
|
||||
Code rows use **absolute file line numbers** in the gutter. `chunk-edit` `line`/`end_line` use those same numbers.
|
||||
|
||||
## Examples
|
||||
|
||||
`read(path="src/math.ts")`
|
||||
|
||||
```text
|
||||
| src/math.ts·120L·ts·#A744
|
||||
|
|
||||
5| export function sum(values: readonly number[]): number {
|
||||
| {{anchor "fn_sum" "3286"}}
|
||||
6| return values.reduce((total, value) => total + value, 0);
|
||||
7| }
|
||||
10| export class Calculator {
|
||||
| {{anchor "class_Calculator" "5D36"}}
|
||||
11| multiply(left: number, right: number): number {
|
||||
| {{anchor "fn_multiply" "B592"}}
|
||||
12| return left * right;
|
||||
13| }
|
||||
14| }
|
||||
```
|
||||
|
||||
## Language Support
|
||||
|
||||
Chunk trees: JavaScript, TypeScript, TSX, Python, Rust, Go. Others use blank-line fallback.
|
||||
Chunk trees: JS, TS, TSX, Python, Rust, Go. Others use blank-line fallback.
|
||||
</instruction>
|
||||
|
||||
<critical>
|
||||
- **MUST** use `read` instead of shell commands for file reading.
|
||||
- **MUST** copy the current checksum before editing a chunk with `chunk-edit`.
|
||||
- **MUST NOT** assume chunk names; always read the current output first.
|
||||
- **MUST** `read` before editing — never invent chunk names or CRCs.
|
||||
</critical>
|
||||
</output>
|
||||
|
||||
@@ -621,6 +621,101 @@ describe("autoresearch tools", () => {
|
||||
expect(runJson.status).toBe("keep");
|
||||
});
|
||||
|
||||
it("commits in-scope changes when those paths were already dirty before the run", async () => {
|
||||
const dir = makeTempDir();
|
||||
tempDirs.push(dir);
|
||||
writeAutoresearchWorkspace(dir, {
|
||||
contract: [
|
||||
"# Autoresearch",
|
||||
"",
|
||||
"## Benchmark",
|
||||
"- command: bash autoresearch.sh",
|
||||
"- primary metric: runtime_ms",
|
||||
"- metric unit: ms",
|
||||
"- direction: lower",
|
||||
"",
|
||||
"## Files in Scope",
|
||||
"- src/in-scope.ts",
|
||||
"",
|
||||
"## Off Limits",
|
||||
"",
|
||||
"## Constraints",
|
||||
"- keep behavior stable",
|
||||
"",
|
||||
].join("\n"),
|
||||
});
|
||||
fs.mkdirSync(path.join(dir, "src"), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "src", "in-scope.ts"), "export const value = 1;\n");
|
||||
|
||||
await $`git init`.cwd(dir).quiet();
|
||||
await $`git config user.email test@example.com`.cwd(dir).quiet();
|
||||
await $`git config user.name Test User`.cwd(dir).quiet();
|
||||
await $`git add .`.cwd(dir).quiet();
|
||||
await $`git commit -m initial`.cwd(dir).quiet();
|
||||
await $`git checkout -b autoresearch/test-keep-prerun-dirty`.cwd(dir).quiet();
|
||||
|
||||
fs.writeFileSync(path.join(dir, "src", "in-scope.ts"), "export const value = 3;\n");
|
||||
fs.writeFileSync(path.join(dir, "autoresearch.program.md"), "# Strategy\n\n- focus on in-scope edits\n");
|
||||
await Bun.write(
|
||||
path.join(dir, ".autoresearch", "runs", "0001", "run.json"),
|
||||
JSON.stringify({
|
||||
command: "bash autoresearch.sh",
|
||||
exitCode: 0,
|
||||
parsedMetrics: { runtime_ms: 9 },
|
||||
parsedPrimary: 9,
|
||||
runNumber: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
const runtime = createSessionRuntime();
|
||||
runtime.state.metricName = "runtime_ms";
|
||||
runtime.state.metricUnit = "ms";
|
||||
runtime.state.scopePaths = ["src/in-scope.ts"];
|
||||
runtime.state.constraints = ["keep behavior stable"];
|
||||
const runDirectory = path.join(dir, ".autoresearch", "runs", "0001");
|
||||
runtime.lastRunArtifactDir = runDirectory;
|
||||
runtime.lastRunNumber = 1;
|
||||
runtime.lastRunDuration = 1.2;
|
||||
runtime.lastRunSummary = {
|
||||
checksDurationSeconds: 0,
|
||||
checksPass: null,
|
||||
checksTimedOut: false,
|
||||
command: "bash autoresearch.sh",
|
||||
durationSeconds: 1.2,
|
||||
parsedAsi: null,
|
||||
parsedMetrics: { runtime_ms: 9 },
|
||||
parsedPrimary: 9,
|
||||
passed: true,
|
||||
preRunDirtyPaths: ["src/in-scope.ts"],
|
||||
runDirectory,
|
||||
runNumber: 1,
|
||||
};
|
||||
|
||||
const tool = createLogExperimentTool({
|
||||
dashboard: createDashboardStub(),
|
||||
getRuntime: () => runtime,
|
||||
pi: createGitApi(),
|
||||
});
|
||||
const result = await tool.execute(
|
||||
"call-keep-prerun-dirty",
|
||||
{
|
||||
commit: "initial",
|
||||
metric: 9,
|
||||
status: "keep",
|
||||
description: "Improve in scope after prior WIP",
|
||||
asi: { hypothesis: "refine further" },
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
createContext(dir),
|
||||
);
|
||||
|
||||
expect(firstTextBlockText(result.content)).toContain("Logged run #1: keep");
|
||||
const committedPaths = await $`git show --name-only --pretty=format: HEAD`.cwd(dir).text();
|
||||
expect(committedPaths).toContain("src/in-scope.ts");
|
||||
expect(committedPaths).toContain("autoresearch.program.md");
|
||||
});
|
||||
|
||||
it("rejects keep when an out-of-scope file is dirty", async () => {
|
||||
const dir = makeTempDir();
|
||||
tempDirs.push(dir);
|
||||
|
||||
@@ -296,7 +296,7 @@ describe("chunk mode tools", () => {
|
||||
|
||||
await editTool.execute("chunk-edit-string-content", {
|
||||
path: filePath,
|
||||
edits: [{ target: "class_Server", append: true, content: 'status(): string {\n return "ok";\n}\n' }],
|
||||
edits: [{ target: "class_Server", op: "append", content: 'status(): string {\n return "ok";\n}\n' }],
|
||||
} as never);
|
||||
|
||||
const updatedSource = await Bun.file(filePath).text();
|
||||
@@ -415,8 +415,8 @@ describe("chunk mode tools", () => {
|
||||
editTool.execute("chunk-edit-batch-rollback", {
|
||||
path: filePath,
|
||||
edits: [
|
||||
{ target: "class_Server", append: true, content: ' status(): string {\n return "ok";\n }' },
|
||||
{ target: "class_Server.fn_handleError#ZZZZ", delete: true },
|
||||
{ target: "class_Server", op: "append", content: ' status(): string {\n return "ok";\n }' },
|
||||
{ target: "class_Server.fn_handleError#ZZZZ", op: "delete" },
|
||||
],
|
||||
}),
|
||||
).rejects.toThrow(/No changes were saved/);
|
||||
@@ -491,7 +491,7 @@ describe("chunk mode tools", () => {
|
||||
line: 3,
|
||||
end_line: 3,
|
||||
content: " let total = 1;",
|
||||
} as never,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).rejects.toThrow(/Checksum required/);
|
||||
|
||||
Reference in New Issue
Block a user