Compare commits

..

3 Commits

Author SHA1 Message Date
slhx agent c70fb20634 chore: untrack generated build cache 2026-07-20 16:45:18 +02:00
slhx agent b5fa585ce0 chore: remove generated migration artifacts 2026-07-20 16:44:53 +02:00
slhx agent 7b232412f5 chore: point to toolset migration 2026-07-20 16:44:40 +02:00
7 changed files with 12 additions and 601 deletions
+2 -8
View File
@@ -1,8 +1,2 @@
# Zig build artifacts
.zig-cache/
zig-out/
# Local editor/OS noise
*.swp
*~
.DS_Store
/.zig-cache/
/zig-out/
-37
View File
@@ -1,37 +0,0 @@
# files — AGENTS.md
## Repository shape
This repository contains one agent-facing CLI, `files`, implemented in `files.zig` and built by `build.zig`. Keep the tool small, native, stream-oriented, and easy to audit from argv alone.
## Work tracking
No tracker, git remote, or durable plan file is present in this checkout. Treat user chat as the active work authority. Do not create a local issue mirror, backlog, roadmap, or PLAN.md unless explicitly requested.
## Requirement governance
`REQUIREMENTS.md` is the requirement authority for this repository and is checked with `redgate`. Before implementation work, read the applicable requirement row; if none covers the durable behavior, update `REQUIREMENTS.md` first or stop and ask for authority. Choose the lowest honest ring by product foundation, not implementation size. If one row mixes foundational and optional behavior, split it before implementation.
For behavior intended to stick, write or update a failing POSIX-sh smoke, contract test, or executable proof before implementation code; run it and record the RED result. If RED proof would be fake, unsafe, or disproportionate, state that exception before implementation and use the strongest cheaper check. Before final handoff, compare the user request and actual diff against `REQUIREMENTS.md`. If there is no requirement impact, the handoff must include exactly `REQUIREMENT IMPACT: none - <specific reason>`.
## POSIX/suckless agent-tool contract
- `files` does one job: list file-like paths below path roots or echo file path roots.
- stdout is records only: one path per line, no color, progress, prompts, paging, sorting, or decoration.
- stderr is diagnostics only: usage errors, runtime failures, and test messages.
- Exit `0` on success, `2` for usage errors, and nonzero for runtime failures callers must notice.
- Keep argv permission-sized: zero or more path roots plus sole-argument `--help`/`-h`; no expression language, callbacks, deletion, exec, network, hidden writes, or config discovery.
- Prefer POSIX behavior and boring native code over clever features. Do not add filters, sorters, metadata output, pagers, JSON, or interactive modes; callers can compose `grep`, `sort`, `awk`, `cut`, `head`, `stat`, and redirection.
## Verification
Run these before handoff when touched files can affect behavior or governance:
```sh
zig build test
redgate list
redgate lint --strict
redgate health --strict
```
`zig build test` runs the POSIX-sh smoke suite in `tests/files-smoke.sh` against `zig-out/bin/files`.
+10
View File
@@ -0,0 +1,10 @@
# files — migrated
The canonical `files` agent tool moved to `/opt/repositories/toolset`.
This repository no longer builds or releases an executable. Its Git history is the migration record; source, requirements, tests, installation, and release authority now live in toolset.
```sh
cd /opt/repositories/toolset
zig build install-text -p "$HOME/.ink"
```
-19
View File
@@ -1,19 +0,0 @@
# Requirements
Rows are TSV: `RING<TAB>ID<TAB>SUMMARY`. Rings are product-foundation layers, not implementation size.
## files
0 001 `files` emits stdout records only: one file-like path per line, with no color, progress, prompts, paging, sorting, JSON, or decoration. [core]
0 002 `files` accepts zero or more PATH roots; `--help` and `-h` print complete usage, output, diagnostics, exit-status, and example guidance only when used as the sole argument. [core]
0 003 When a PATH root names a directory, `files` recursively traverses it without following symlinked directories and emits file-like entries below it. [core]
0 004 When a PATH root names a non-directory file-like path, `files` emits that path exactly once. Multiple roots are processed left-to-right without sorting or dedupe. [core]
0 005 Usage mistakes emit usage on stderr and exit `2`; runtime failures emit a diagnostic on stderr and exit nonzero without corrupting stdout. [core]
0 006 `files` has no mutation surface: no deletion, exec, callbacks, network access, hidden writes, config discovery, glob policy, or expression language. [agent]
1 007 The repository verifies `files` with POSIX-sh smoke coverage for help, stdout/stderr separation, recursive traversal, empty directory success, file input, default `.` input, single- and multi-root failure paths, single- and multi-root pipeline/SIGPIPE behavior, and argv permission boundaries. [test]
1 008 The smoke suite covers awkward POSIX path records such as spaces and leading dashes, plus generated-tree performance sanity without a benchmark framework. [test]
1 009 The smoke suite covers no-argument default `.` traversal and explicit empty PATH failure with stderr-only diagnostics. [test]
1 010 The smoke suite covers trailing-slash directory roots without double-slash records while preserving multi-root left-to-right output. [test]
1 011 The smoke suite covers symlink-to-file roots as file-like records and symlinked directory roots as non-followed records, including symlink records followed by real directory roots. [test]
1 012 The smoke suite covers duplicate PATH roots as repeated records supplied by argv, with no sorting or dedupe policy. [test]
1 013 The smoke suite checks help text for the `files [PATH...]` paths-only contract and explicit refusal of filters, sorting, metadata, deletion, exec, hidden config, and expression surfaces. [test]
1 014 The smoke suite checks representative listing and help runs do not create target, home, or cache files; stdout and stderr redirection remain the only outputs. [test]
-26
View File
@@ -1,26 +0,0 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "files",
.root_module = b.createModule(.{
.root_source_file = b.path("files.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
b.step("run", "Run files").dependOn(&run_cmd.step);
const smoke = b.addSystemCommand(&.{ "sh", "tests/files-smoke.sh" });
smoke.setCwd(b.path("."));
smoke.step.dependOn(b.getInstallStep());
b.step("test", "Run POSIX smoke tests").dependOn(&smoke.step);
}
-164
View File
@@ -1,164 +0,0 @@
const std = @import("std");
fn usage(file: std.Io.File, io: std.Io) !void {
var buffer: [1536]u8 = undefined;
var writer = file.writer(io, &buffer);
try writer.interface.writeAll(
\\usage: files [PATH...]
\\
\\Recursively print file-like paths under each PATH, one path per line.
\\With no PATH, . is used. Directories are traversed; symlinked directories are not followed.
\\If PATH is a non-directory file-like path, that path is printed exactly once.
\\
\\stdout:
\\ Paths only: One path per line. No color, progress, prompts, paging, sorting, JSON, or decoration.
\\
\\scope:
\\ Enumerates paths only. No filtering, sorting, metadata.
\\ No deletion, exec, callbacks, network, hidden writes, config, or expression language.
\\
\\stderr:
\\ Usage errors and runtime diagnostics only.
\\
\\exit status:
\\ 0 success
\\ 2 usage error
\\ 1 runtime failure
\\
\\side effects:
\\ None.
\\
\\examples:
\\ files src | grep '\\.[cz]$'
\\ files . | sort | sed 20q
\\
);
try writer.interface.flush();
}
fn fail(io: std.Io, comptime fmt: []const u8, args: anytype) noreturn {
var buffer: [512]u8 = undefined;
var writer = std.Io.File.stderr().writer(io, &buffer);
writer.interface.print(fmt, args) catch {};
writer.interface.flush() catch {};
std.process.exit(1);
}
fn stdoutFailure(io: std.Io, err: anyerror) noreturn {
fail(io, "files: stdout: {s}\n", .{@errorName(err)});
}
fn stdoutWriteError(out: anytype, err: anyerror) anyerror {
if (err == error.WriteFailed) return out.err orelse err;
return err;
}
fn writePath(out: anytype, root: []const u8, path: []const u8) !bool {
if (std.mem.eql(u8, root, ".")) {
out.interface.print("{s}\n", .{path}) catch |err| {
const cause = stdoutWriteError(out, err);
if (cause == error.BrokenPipe) return false;
return cause;
};
} else if (std.mem.endsWith(u8, root, "/")) {
out.interface.print("{s}{s}\n", .{ root, path }) catch |err| {
const cause = stdoutWriteError(out, err);
if (cause == error.BrokenPipe) return false;
return cause;
};
} else {
out.interface.print("{s}/{s}\n", .{ root, path }) catch |err| {
const cause = stdoutWriteError(out, err);
if (cause == error.BrokenPipe) return false;
return cause;
};
}
return true;
}
fn writeRawPath(out: anytype, path: []const u8) !bool {
out.interface.print("{s}\n", .{path}) catch |err| {
const cause = stdoutWriteError(out, err);
if (cause == error.BrokenPipe) return false;
return cause;
};
return true;
}
fn flushStdout(out: anytype) !bool {
out.interface.flush() catch |err| {
const cause = stdoutWriteError(out, err);
if (cause == error.BrokenPipe) return false;
return cause;
};
return true;
}
fn failAfterStdout(out: anytype, io: std.Io, comptime fmt: []const u8, args: anytype) noreturn {
const flushed = flushStdout(out) catch |err| stdoutFailure(io, err);
if (!flushed) std.process.exit(0);
fail(io, fmt, args);
}
fn emitRoot(root: []const u8, allocator: std.mem.Allocator, io: std.Io, out: anytype) bool {
if (root.len == 0) failAfterStdout(out, io, "files: empty PATH\n", .{});
const root_stat = std.Io.Dir.cwd().statFile(io, root, .{ .follow_symlinks = false }) catch |err| {
failAfterStdout(out, io, "files: {s}: {s}\n", .{ root, @errorName(err) });
};
if (root_stat.kind != .directory) {
const ok = writeRawPath(out, root) catch |write_err| stdoutFailure(io, write_err);
if (!ok) return false;
return true;
}
var dir = std.Io.Dir.cwd().openDir(io, root, .{ .iterate = true }) catch |err| {
failAfterStdout(out, io, "files: {s}: {s}\n", .{ root, @errorName(err) });
};
defer dir.close(io);
var walker = dir.walk(allocator) catch |err| failAfterStdout(out, io, "files: {s}: {s}\n", .{ root, @errorName(err) });
defer walker.deinit();
while (true) {
const maybe_entry = walker.next(io) catch |err| failAfterStdout(out, io, "files: {s}: {s}\n", .{ root, @errorName(err) });
const entry = maybe_entry orelse break;
if (entry.kind == .directory) continue;
const ok = writePath(out, root, entry.path) catch |err| stdoutFailure(io, err);
if (!ok) return false;
}
return true;
}
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, allocator);
defer args.deinit();
_ = args.skip();
const first_arg = args.next();
const second_arg = if (first_arg == null) null else args.next();
if (first_arg) |arg| {
if (second_arg == null and (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h"))) {
try usage(std.Io.File.stdout(), init.io);
return;
}
}
var out_buffer: [8192]u8 = undefined;
var out = std.Io.File.stdout().writer(init.io, &out_buffer);
if (first_arg) |root| {
if (!emitRoot(root, allocator, init.io, &out)) return;
if (second_arg) |next_root| {
if (!emitRoot(next_root, allocator, init.io, &out)) return;
}
while (args.next()) |next_root| {
if (!emitRoot(next_root, allocator, init.io, &out)) return;
}
} else {
if (!emitRoot(".", allocator, init.io, &out)) return;
}
const flushed = flushStdout(&out) catch |err| stdoutFailure(init.io, err);
if (!flushed) return;
}
-347
View File
@@ -1,347 +0,0 @@
#!/bin/sh
# POSIX smoke suite for the files agent tool.
# req: files/001 files/002 files/003 files/004 files/005 files/006 files/007 files/008 files/009 files/010 files/011 files/012 files/013 files/014
set -u
bin=${FILES_BIN:-./zig-out/bin/files}
fail() {
printf 'files-smoke: %s\n' "$*" >&2
exit 1
}
assert_empty() {
file=$1
label=$2
[ ! -s "$file" ] || fail "$label not empty: $(cat "$file")"
}
assert_contains() {
file=$1
text=$2
label=$3
grep -F "$text" "$file" >/dev/null 2>&1 || fail "$label missing: $text"
}
assert_not_contains() {
file=$1
text=$2
label=$3
if grep -F "$text" "$file" >/dev/null 2>&1; then
fail "$label unexpectedly contains: $text"
fi
}
assert_status() {
got=$1
want=$2
label=$3
[ "$got" -eq "$want" ] || fail "$label exit $got, want $want"
}
assert_same_file() {
want=$1
got=$2
label=$3
cmp -s "$want" "$got" || fail "$label output mismatch"
}
assert_unchanged_tree() {
dir=$1
before=$2
label=$3
find "$dir" -print | sort > "$sorted"
assert_same_file "$before" "$sorted" "$label"
}
assert_runtime_failure() {
got=$1
label=$2
[ "$got" -ne 0 ] || fail "$label exit was zero"
[ "$got" -ne 2 ] || fail "$label used usage exit 2 for runtime failure"
}
assert_help_contract() {
file=$1
label=$2
assert_contains "$file" "usage: files [PATH...]" "$label"
assert_contains "$file" "stdout:" "$label"
assert_contains "$file" "One path per line" "$label"
assert_contains "$file" "exit status:" "$label"
assert_contains "$file" "paths only" "$label"
assert_contains "$file" "No filtering, sorting, metadata" "$label"
assert_contains "$file" "No deletion, exec, callbacks, network, hidden writes, config, or expression language" "$label"
}
write_tree_listing_expected() {
prefix=$1
{
printf '%s\n' "${prefix}--dash"
printf '%s\n' "${prefix}a/name with space"
printf '%s\n' "${prefix}a/one"
if [ "$have_symlink" = yes ]; then
printf '%s\n' "${prefix}linkdir"
printf '%s\n' "${prefix}linkfile"
fi
printf '%s\n' "${prefix}two"
} > "$expected"
}
[ -x "$bin" ] || fail "executable not found: $bin"
case $bin in
/*) abs_bin=$bin ;;
*) abs_bin=$(pwd -P)/$bin ;;
esac
i=0
base=${TMPDIR:-/tmp}/files-smoke.$$
while :; do
work=$base.$i
if mkdir "$work" 2>/dev/null; then
break
fi
i=$((i + 1))
[ "$i" -lt 50 ] || fail "could not create temp directory under ${TMPDIR:-/tmp}"
done
trap 'rm -rf "$work"' EXIT HUP INT TERM
out=$work/out
err=$work/err
expected=$work/expected
sorted=$work/sorted
tree=$work/tree
mkdir -p "$tree/a" "$tree/empty" || fail "mkdir tree"
printf 'dash\n' > "$tree/--dash" || fail "write dash"
printf 'one\n' > "$tree/a/one" || fail "write one"
printf 'space\n' > "$tree/a/name with space" || fail "write space"
printf 'two\n' > "$tree/two" || fail "write two"
if ln -s a "$tree/linkdir" 2>/dev/null && ln -s two "$tree/linkfile" 2>/dev/null; then
have_symlink=yes
else
have_symlink=no
fi
"$bin" --help > "$out" 2> "$err"
assert_status $? 0 "--help"
assert_empty "$err" "--help stderr"
assert_help_contract "$out" "help"
"$bin" -h > "$out" 2> "$err"
assert_status $? 0 "-h"
assert_empty "$err" "-h stderr"
assert_help_contract "$out" "-h help"
help_roots=$work/help-roots
mkdir -p "$help_roots" || fail "mkdir help roots"
printf 'help\n' > "$help_roots/--help" || fail "write --help root"
printf 'short\n' > "$help_roots/-h" || fail "write -h root"
printf 'extra\n' > "$help_roots/extra" || fail "write extra root"
(
cd "$help_roots" || exit 1
"$abs_bin" --help extra > "$out" 2> "$err"
)
assert_status $? 0 "--help as path root"
assert_empty "$err" "--help as path root stderr"
{
printf '%s\n' "--help"
printf '%s\n' "extra"
} > "$expected"
assert_same_file "$expected" "$out" "--help as path root"
(
cd "$help_roots" || exit 1
"$abs_bin" -h extra > "$out" 2> "$err"
)
assert_status $? 0 "-h as path root"
assert_empty "$err" "-h as path root stderr"
{
printf '%s\n' "-h"
printf '%s\n' "extra"
} > "$expected"
assert_same_file "$expected" "$out" "-h as path root"
"$bin" "$tree" > "$out" 2> "$err"
assert_status $? 0 "directory listing"
assert_empty "$err" "directory listing stderr"
sort "$out" > "$sorted"
write_tree_listing_expected "$tree/"
sort "$expected" > "$expected.sorted"
mv "$expected.sorted" "$expected"
assert_same_file "$expected" "$sorted" "directory listing"
assert_not_contains "$out" "$tree/linkdir/one" "symlink traversal"
(
cd "$tree" || exit 1
"$abs_bin" . > "$out" 2> "$err"
)
assert_status $? 0 "default dot listing"
assert_empty "$err" "default dot stderr"
sort "$out" > "$sorted"
write_tree_listing_expected ""
sort "$expected" > "$expected.sorted"
mv "$expected.sorted" "$expected"
assert_same_file "$expected" "$sorted" "default dot"
(
cd "$tree" || exit 1
"$abs_bin" > "$out" 2> "$err"
)
assert_status $? 0 "no-arg default listing"
assert_empty "$err" "no-arg default stderr"
sort "$out" > "$sorted"
write_tree_listing_expected ""
sort "$expected" > "$expected.sorted"
mv "$expected.sorted" "$expected"
assert_same_file "$expected" "$sorted" "no-arg default"
"$bin" "$tree/empty" > "$out" 2> "$err"
assert_status $? 0 "empty directory"
assert_empty "$out" "empty directory stdout"
assert_empty "$err" "empty directory stderr"
"$bin" "$tree/two" > "$out" 2> "$err"
assert_status $? 0 "file input"
assert_empty "$err" "file input stderr"
printf '%s\n' "$tree/two" > "$expected"
assert_same_file "$expected" "$out" "file input"
single=$work/single
mkdir -p "$single" || fail "mkdir single"
printf 'single\n' > "$single/only" || fail "write single"
"$bin" "$tree/two" "$single" "$tree/--dash" > "$out" 2> "$err"
assert_status $? 0 "multiple roots"
assert_empty "$err" "multiple roots stderr"
{
printf '%s\n' "$tree/two"
printf '%s\n' "$single/only"
printf '%s\n' "$tree/--dash"
} > "$expected"
assert_same_file "$expected" "$out" "multiple roots"
"$bin" "$tree/two" "$tree/two" "$single" "$single" > "$out" 2> "$err"
assert_status $? 0 "duplicate roots"
assert_empty "$err" "duplicate roots stderr"
{
printf '%s\n' "$tree/two"
printf '%s\n' "$tree/two"
printf '%s\n' "$single/only"
printf '%s\n' "$single/only"
} > "$expected"
assert_same_file "$expected" "$out" "duplicate roots"
"$bin" "$single/" "$tree/two" > "$out" 2> "$err"
assert_status $? 0 "trailing slash root"
assert_empty "$err" "trailing slash root stderr"
{
printf '%s\n' "$single/only"
printf '%s\n' "$tree/two"
} > "$expected"
assert_same_file "$expected" "$out" "trailing slash root"
assert_not_contains "$out" "$single//only" "trailing slash double slash"
if [ "$have_symlink" = yes ]; then
"$bin" "$tree/linkfile" "$tree/linkdir" > "$out" 2> "$err"
assert_status $? 0 "symlink roots"
assert_empty "$err" "symlink roots stderr"
{
printf '%s\n' "$tree/linkfile"
printf '%s\n' "$tree/linkdir"
} > "$expected"
assert_same_file "$expected" "$out" "symlink roots"
assert_not_contains "$out" "$tree/linkdir/one" "symlink root traversal"
"$bin" "$tree/linkdir" "$single" > "$out" 2> "$err"
assert_status $? 0 "symlink then directory root"
assert_empty "$err" "symlink then directory root stderr"
{
printf '%s\n' "$tree/linkdir"
printf '%s\n' "$single/only"
} > "$expected"
assert_same_file "$expected" "$out" "symlink then directory root"
assert_not_contains "$out" "$tree/linkdir/one" "symlink then directory traversal"
fi
later_missing=$work/later-missing
if "$bin" "$tree/two" "$later_missing" > "$out" 2> "$err"; then
fail "missing later root unexpectedly succeeded"
else
status=$?
fi
assert_runtime_failure "$status" "missing later root"
printf '%s\n' "$tree/two" > "$expected"
assert_same_file "$expected" "$out" "missing later root partial stdout"
assert_contains "$err" "files: $later_missing:" "missing later root stderr"
if "$bin" "" > "$out" 2> "$err"; then
fail "empty path unexpectedly succeeded"
else
status=$?
fi
assert_runtime_failure "$status" "empty path"
assert_empty "$out" "empty path stdout"
assert_contains "$err" "files: empty PATH" "empty path stderr"
missing=$work/missing
if "$bin" "$missing" > "$out" 2> "$err"; then
fail "missing path unexpectedly succeeded"
else
status=$?
fi
assert_runtime_failure "$status" "missing path"
assert_empty "$out" "missing path stdout"
assert_contains "$err" "files: $missing:" "missing path stderr"
restricted=$work/restricted
mkdir -p "$restricted/blocked" || fail "mkdir restricted"
printf 'hidden\n' > "$restricted/blocked/file" || fail "write restricted file"
chmod 000 "$restricted/blocked" || fail "chmod restricted"
if "$bin" "$restricted" > "$out" 2> "$err"; then
status=0
else
status=$?
fi
chmod 700 "$restricted/blocked" || fail "restore restricted permissions"
if [ "$status" -eq 0 ]; then
if [ ! -r "$restricted/blocked" ] || [ ! -x "$restricted/blocked" ]; then
fail "unreadable directory unexpectedly succeeded"
fi
else
assert_runtime_failure "$status" "unreadable directory"
assert_empty "$out" "unreadable directory stdout"
assert_contains "$err" "files: $restricted:" "unreadable directory stderr"
fi
big=$work/big
mkdir -p "$big" || fail "mkdir big"
n=1
while [ "$n" -le 1000 ]; do
d=$big/d$((n % 10))
mkdir -p "$d" || fail "mkdir big dir $n"
: > "$d/$n" || fail "write big $n"
n=$((n + 1))
done
"$bin" "$big" > "$out" 2> "$err"
assert_status $? 0 "generated tree listing"
assert_empty "$err" "generated tree stderr"
count=$(wc -l < "$out" | tr -d ' ')
[ "$count" -eq 1000 ] || fail "generated tree count $count, want 1000"
{ "$bin" "$big" | head -n 1 > /dev/null; } 2> "$err"
assert_empty "$err" "pipeline stderr"
{ "$bin" "$big" "$work/pipe-missing" | head -n 1 > /dev/null; } 2> "$err"
assert_empty "$err" "multi-root pipeline stderr"
no_mutation=$work/no-mutation
mkdir -p "$no_mutation/home" "$no_mutation/cache" "$no_mutation/target/sub" || fail "mkdir no-mutation"
printf 'kept\n' > "$no_mutation/target/sub/file" || fail "write no-mutation file"
find "$no_mutation" -print | sort > "$expected"
HOME=$no_mutation/home XDG_CACHE_HOME=$no_mutation/cache "$bin" "$no_mutation/target" > "$out" 2> "$err"
assert_status $? 0 "no-mutation listing"
assert_empty "$err" "no-mutation listing stderr"
assert_unchanged_tree "$no_mutation" "$expected" "no-mutation listing"
HOME=$no_mutation/home XDG_CACHE_HOME=$no_mutation/cache "$bin" --help > "$out" 2> "$err"
assert_status $? 0 "no-mutation help"
assert_empty "$err" "no-mutation help stderr"
assert_unchanged_tree "$no_mutation" "$expected" "no-mutation help"
printf 'files-smoke: ok\n' >&2