diff --git a/tools/cascade b/tools/cascade old mode 100755 new mode 100644 index 1fe6f91..ecebba3 --- a/tools/cascade +++ b/tools/cascade @@ -38,11 +38,24 @@ prompt="[parent_session_id=${OLLIE_SESSION_ID}] [session_id=${id}] You are a worker agent. Execute the following task exactly. Output only the result. -When finished, write your result to: ${OLLIE}/s/${OLLIE_SESSION_ID}/prompt -Use this exact format: -[from=${id}] -STATUS: done -SUMMARY: + +TERMINATION RULES (mandatory — follow exactly): +1. Complete the task using whatever tools are needed. +2. When finished (success or failure), write your result to: ${OLLIE}/s/${OLLIE_SESSION_ID}/prompt + Use file_write with this exact format and nothing after it: + + [from=${id}] + STATUS: done + SUMMARY: + + On failure, use: + [from=${id}] + STATUS: error + REASON: + +3. After writing the result, STOP. Do not call any more tools. Do not write again. +4. Do not re-read the file to verify. Do not retry the write. One write, then stop. +5. Do not expand the task scope. If the task is ambiguous or impossible, report STATUS: error immediately. TASK: ${instruction}" diff --git a/tools/file_edit b/tools/file_edit old mode 100755 new mode 100644 index c35057d..d7d871c --- a/tools/file_edit +++ b/tools/file_edit @@ -11,7 +11,7 @@ def format_diff(path, old, new): new_lines = new.splitlines(keepends=True) diff = ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path)) if not diff: - return "(no changes)" + return None return f"```diff\n{diff}```" def find_exact(content, find): @@ -70,13 +70,13 @@ def robust_replace(content, old_string, new_string, replace_all): find_indent_flexible(content, old_string) or find_trimmed_boundary(content, old_string)) if match is None: - return None, "old_string not found in file" + return None, "old_string not found in file", False if replace_all: - return content.replace(match, new_string), None + return content.replace(match, new_string), None, False n = content.count(match) if n > 1: - return None, f"old_string is not unique ({n} occurrences); use replace_all=true or provide more context" - return content.replace(match, new_string, 1), None + return None, f"old_string is not unique ({n} occurrences); use replace_all=true or provide more context", True + return content.replace(match, new_string, 1), None, False if len(sys.argv) < 4: print("usage: file_edit [replace_all=true|1|yes]") @@ -89,20 +89,24 @@ _ra = sys.argv[4].lower() if len(sys.argv) > 4 else '' replace_all = _ra.split('=')[-1] in ('true', '1', 'yes') if not os.path.isabs(file_path): + print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") print(f"error: path must be absolute, got: {file_path}") sys.exit(1) if old_string == new_string: + print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false") print("error: old_string and new_string must be different") sys.exit(1) if not os.path.exists(file_path): + print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") print(f"error: file not found: {file_path}") sys.exit(1) with open(file_path, 'r', errors='replace') as f: original = f.read() -new_content, err = robust_replace(original, old_string, new_string, replace_all) +new_content, err, retryable = robust_replace(original, old_string, new_string, replace_all) if err: + print(f"STATUS=error STATE_CHANGED=false RETRYABLE={'true' if retryable else 'false'}") print(f"error: {err}") sys.exit(1) @@ -110,7 +114,14 @@ try: with open(file_path, 'w') as f: f.write(new_content) except OSError as e: + print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") print(f"error: {e}") sys.exit(1) -print(format_diff(file_path, original, new_content)) +diff = format_diff(file_path, original, new_content) +if diff is None: + print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false") + print("(no changes)") +else: + print("STATUS=done STATE_CHANGED=true") + print(diff) diff --git a/tools/file_glob b/tools/file_glob old mode 100755 new mode 100644 index 6a879b3..2706408 --- a/tools/file_glob +++ b/tools/file_glob @@ -9,6 +9,7 @@ search_path="${2:-$PWD}" [ -z "$pattern" ] && echo "usage: file_glob [path]" && exit 1 if [ ! -d "$search_path" ]; then + echo "STATUS=error" echo "error: path is not a directory: $search_path" exit 1 fi @@ -17,14 +18,21 @@ fi rg_out=$(rg --no-config --files -g "$pattern" "$search_path" 2>&1) rg_exit=$? if [ $rg_exit -gt 1 ]; then + echo "STATUS=error" echo "error: $rg_out" exit 1 fi if [ -z "$rg_out" ]; then + echo "STATUS=no_matches MATCH_COUNT=0" echo "no matches" exit 0 fi -echo "$rg_out" \ + +sorted=$(echo "$rg_out" \ | xargs -d '\n' stat --printf '%Y\t%n\n' 2>&1 \ | sort -rn \ - | cut -f2- + | cut -f2-) + +count=$(echo "$sorted" | wc -l | tr -d ' ') +echo "STATUS=ok MATCH_COUNT=$count" +echo "$sorted" diff --git a/tools/file_grep b/tools/file_grep old mode 100755 new mode 100644 index 1c7de22..1401bdb --- a/tools/file_grep +++ b/tools/file_grep @@ -26,7 +26,6 @@ parser.add_argument('--offset', type=int, default=0) args = parser.parse_args() -rg = 'rg' search_path = args.path or os.getcwd() cmd = ['rg', '--no-config', '--color=never'] @@ -58,9 +57,11 @@ cmd += ['-e', args.pattern, search_path] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode == 1: + print("STATUS=no_matches MATCH_COUNT=0") print("no matches") sys.exit(0) if result.returncode > 1: + print(f"STATUS=error") print(f"error: {result.stderr.strip() or f'rg exited with {result.returncode}'}") sys.exit(1) @@ -81,7 +82,6 @@ if args.mode == 'content': if line == '--': formatted.append(line) continue - # file:lineno:content or file-lineno-content (context lines) for sep in (':', '-'): parts = line.split(sep, 2) if len(parts) == 3 and parts[1].isdigit(): @@ -91,6 +91,7 @@ if args.mode == 'content': formatted.append(line) lines = formatted +print(f"STATUS=ok MATCH_COUNT={total}") print('\n'.join(lines)) if capped: print(f"\n(showing {max_matches} of {total} matches; use --head=N or --offset=N for more)") diff --git a/tools/file_read b/tools/file_read old mode 100755 new mode 100644 index d1f74ad..e99c65e --- a/tools/file_read +++ b/tools/file_read @@ -30,19 +30,23 @@ start = int(args[1]) if len(args) > 1 else 1 end = int(args[2]) if len(args) > 2 else None if not os.path.isabs(file_path): + print(f"STATUS=error") print(f"error: path must be absolute, got: {file_path}") sys.exit(1) if not os.path.exists(file_path): + print("STATUS=not_found") print("(new file)") sys.exit(0) if not os.path.isfile(file_path): + print(f"STATUS=error") print(f"error: not a regular file: {file_path}") sys.exit(1) with open(file_path, 'rb') as f: if b'\x00' in f.read(8192): + print("STATUS=ok BINARY=true") print("(binary file)") sys.exit(0) @@ -55,14 +59,17 @@ else: end = min(end, start + MAX_SEGMENT - 1) with open(file_path, 'r', errors='replace') as f: - # Skip lines before start for _ in islice(f, start - 1): pass - # Read only the lines we need segment_lines = list(islice(f, end - start + 1)) if not segment_lines: - print("(empty file)" if start == 1 else "(no lines in range)") + if start == 1: + print("STATUS=ok EMPTY=true") + print("(empty file)") + else: + print("STATUS=ok EMPTY=true") + print("(no lines in range)") sys.exit(0) out = [] @@ -73,4 +80,5 @@ for i, line in enumerate(segment_lines): text = text[:MAX_LINE_LEN] + '...' out.append(f"{lineno:>6}\t{text}") +print(f"STATUS=ok LINES={len(out)}") print('\n'.join(out)) diff --git a/tools/file_write b/tools/file_write old mode 100755 new mode 100644 index 74911cb..6797d6b --- a/tools/file_write +++ b/tools/file_write @@ -11,7 +11,7 @@ def format_diff(path, old, new): new_lines = new.splitlines(keepends=True) diff = ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path)) if not diff: - return "(no changes)" + return None return f"```diff\n{diff}```" if len(sys.argv) < 3: @@ -22,11 +22,13 @@ file_path = sys.argv[1] content = sys.argv[2] if not os.path.isabs(file_path): + print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") print(f"error: path must be absolute, got: {file_path}") sys.exit(1) parent = os.path.dirname(file_path) if parent and not os.path.isdir(parent): + print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") print(f"error: parent directory does not exist: {parent}") sys.exit(1) @@ -39,7 +41,14 @@ try: with open(file_path, 'w') as f: f.write(content) except OSError as e: + print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") print(f"error: {e}") sys.exit(1) -print(format_diff(file_path, old_content, content)) +diff = format_diff(file_path, old_content, content) +if diff is None: + print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false") + print("(no changes)") +else: + print("STATUS=done STATE_CHANGED=true") + print(diff) diff --git a/tools/task_add b/tools/task_add old mode 100755 new mode 100644 index 47b9f51..3831820 --- a/tools/task_add +++ b/tools/task_add @@ -20,4 +20,5 @@ added=0 done } > "$plan" -echo "added $added task(s)" +total=$(grep -c '^- \[' "$plan" 2>/dev/null || echo 0) +echo "STATUS=ok ADDED=$added TOTAL_TASKS=$total" diff --git a/tools/task_check b/tools/task_check old mode 100755 new mode 100644 index 3009d4b..7083bc0 --- a/tools/task_check +++ b/tools/task_check @@ -23,6 +23,7 @@ lines = content.splitlines(keepends=True) tasks = [(i, l) for i, l in enumerate(lines) if l.startswith("- [ ]") or l.startswith("- [x]")] if not tasks: + print("STATUS=done TASK_COMPLETE=true REMAINING=0") print("no tasks in plan") sys.exit(0) @@ -32,6 +33,8 @@ if n is None: # First unchecked target = next(((i, l) for i, l in tasks if l.startswith("- [ ]")), None) if target is None: + remaining = 0 + print(f"STATUS=done TASK_COMPLETE=true REMAINING=0") print("all tasks already checked") sys.exit(0) else: @@ -43,4 +46,8 @@ else: idx, line = target lines[idx] = line.replace("- [ ]", "- [x]", 1) open(plan_path, "w").write("".join(lines)) + +remaining = sum(1 for _, l in tasks if l.startswith("- [ ]") and _ != idx) +task_complete = remaining == 0 +print(f"STATUS=done TASK_COMPLETE={'true' if task_complete else 'false'} REMAINING={remaining}") print(f"checked: {lines[idx].rstrip()}") diff --git a/tools/task_clear b/tools/task_clear old mode 100755 new mode 100644 index 81dfcb2..48337f6 --- a/tools/task_clear +++ b/tools/task_clear @@ -7,11 +7,15 @@ OLLIE="${OLLIE:-$HOME/mnt/ollie}" plan="$OLLIE/s/$OLLIE_SESSION_ID/plan" -[ ! -f "$plan" ] && echo "no plan found" && exit 0 +[ ! -f "$plan" ] && echo "STATUS=ok CLEARED=0 REMAINING=0 TASK_COMPLETE=true" && echo "no plan found" && exit 0 before=$(grep -c '^\- \[.\]' "$plan" 2>/dev/null || echo 0) grep -v '^\- \[x\]' "$plan" > "$plan.tmp" && mv "$plan.tmp" "$plan" after=$(grep -c '^\- \[.\]' "$plan" 2>/dev/null || echo 0) removed=$(( before - after )) +task_complete="false" +[ "$after" -eq 0 ] && task_complete="true" + +echo "STATUS=ok CLEARED=$removed REMAINING=$after TASK_COMPLETE=$task_complete" echo "cleared $removed completed task(s), $after remaining"