Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3f927eb
Makes run_harness more generic by moving SANDBOX_COMMAND to defaults …
MKuckert Sep 2, 2026
441a15c
Prepares nono-here template
MKuckert Sep 2, 2026
2c32e4d
chore: Adds plan for new nono-here script
MKuckert Sep 4, 2026
1bd2532
Merge branch 'main' into feature/nono-here
MKuckert Sep 9, 2026
1ef1f7f
feat: add nono-here.sh skeleton with self-location and workdir resolu…
MKuckert Sep 10, 2026
7638391
refactor: scope self-location, guard symlink cycles in nono-here.sh
MKuckert Sep 10, 2026
a858fb1
chore: Extends agent permissions
MKuckert Sep 10, 2026
3a4fda4
chore: Adds cost-guard plugin
MKuckert Sep 10, 2026
b568f1e
feat: add handover() single exec site to nono-here.sh (Task 9)
MKuckert Sep 10, 2026
142935a
feat: add fast-path handover branch ladder to nono-here.sh (Task 2)
MKuckert Sep 10, 2026
29dd5b9
feat: add harness selection with override and select menu (Task 3)
MKuckert Sep 10, 2026
8b4a0c1
feat: add template resolution and in-place validation (Task 4)
MKuckert Sep 10, 2026
11e4814
feat: add stale .sandbox confirmation and removal (Task 5)
MKuckert Sep 10, 2026
de19bd3
feat: copy template into .sandbox with non-directory guard (Task 6)
MKuckert Sep 10, 2026
9ec305b
feat: move run_harness.sh into workspace with mode post-condition (Ta…
MKuckert Sep 10, 2026
fb0b9d0
feat: generate .sandbox/defaults.sh, preserving template-provided one…
MKuckert Sep 10, 2026
7f4459d
feat: hand over to run_harness.sh after provisioning (Task 8b)
MKuckert Sep 11, 2026
4fc989e
fix: guard empty SANDBOX_COMMAND_DEFAULTS under set -u on Bash 3.2 (T…
MKuckert Sep 11, 2026
79afda0
chore: Allow access to repo base
MKuckert Sep 11, 2026
df52a0b
test: add test_nono_here.sh test suite for nono-here.sh (Task 11)
MKuckert Sep 11, 2026
7ecb263
docs: archive nono-here bootstrap plan with post-mortem, add PROJECT_…
MKuckert Sep 11, 2026
52062d1
revert: restore agent permission guardrails (PR #110 review B1)
Sep 11, 2026
9a1667a
fix: address provisioning and startup review findings (PR #110)
Sep 11, 2026
d6823bb
fix: sync live run_harness.sh with template, harden test suite (PR #110)
Sep 11, 2026
f189fc8
docs: rework nono-here.sh comments to explain intent, not history
Sep 11, 2026
8da2c8f
Delete PROJECT_MAP.md
MKuckert Sep 11, 2026
84654d2
Merge remote-tracking branch 'origin/main' into feature/nono-here
Sep 11, 2026
d953c11
Merge remote-tracking branch 'origin/feature/nono-here' into feature/…
Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .opencode/cost-guard.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"maxCostUsd": 5.0,
"warnAtPercent": 80,
"mode": "block"
}
2 changes: 1 addition & 1 deletion .sandbox/defaults.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#!/usr/bin/env bash
SANDBOX_COMMAND_DEFAULTS=()
SANDBOX_COMMAND=opencode
3 changes: 2 additions & 1 deletion .sandbox/profile.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"extends": ["always-further/opencode"],
"meta": {
"name": "env",
"version": "2"
"version": "3"
},
"workdir": {
"access": "readwrite"
Expand All @@ -24,6 +24,7 @@
},
"network": {
"allow_domain": ["localhost", "github.com"],
"open_port": [12100],
"open_port_range": [[11400, 11500]]
},
"environment": {
Expand Down
18 changes: 14 additions & 4 deletions .sandbox/start.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
# VERSION 3

SELF=$(basename $BASH_SOURCE)
SELF=$(basename "$BASH_SOURCE")
WORKSPACE=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")
SANDBOX_DIR="$WORKSPACE/.sandbox"
PROFILE_JSON="$SANDBOX_DIR/profile.json"
Expand All @@ -20,7 +21,7 @@ if [[ ! -f "$PROFILE_JSON" ]]; then
echo "$SELF: Copying profile.json from template (v$local_ver). Check contents and adjust to your local environment." >&2
cp "$PROFILE_TEMPLATE" "$PROFILE_JSON"
else
echo "$SELF: Couldn't find neither profile.json nor profile.template.json" >&2
echo "$SELF: Could not find either profile.json or profile.template.json" >&2
exit 1
fi
else
Expand All @@ -33,13 +34,22 @@ else
echo -e "\n\033[33mYour '$PROFILE_JSON' (v$local_ver) is older than the template (v$tpl_ver)!\033[0m" >&2
echo -e "\033[36m (diff between local config (-) and template (+))\033[0m" >&2
echo -e "\033[36m--------------------------------------------------------\033[0m" >&2
diff -u --color=always "$PROFILE_JSON" "$PROFILE_TEMPLATE" || true
# --color=always is GNU-only; BSD diff (macOS) rejects it and the
# error would be swallowed by `|| true`, losing the diff itself.
# Probe once and degrade to plain output where unsupported.
color_opt=""
if diff --color=always /dev/null /dev/null >/dev/null 2>&1; then
color_opt="--color=always"
fi
diff -u $color_opt "$PROFILE_JSON" "$PROFILE_TEMPLATE" || true
echo -e "\033[36m--------------------------------------------------------\033[0m" >&2
echo "Please adjust your '$PROFILE_JSON' (at least the .meta.version field to $tpl_ver) or delete it to reset.\n" >&2
fi
else
if ! diff -q "$PROFILE_JSON" "$PROFILE_TEMPLATE" >/dev/null 2>&1; then
echo -e "\n\033[33mYour '$PROFILE_JSON' differs from the template (v$tpl_ver) but there's no 'jq' installed to check versions.[0m" >&2
# No version here: tpl_ver is only assigned in the jq branch, and
# interpolating it would abort under set -u on this degraded path.
echo -e "\n\033[33mYour '$PROFILE_JSON' differs from the template, but there's no 'jq' installed to check versions.\033[0m" >&2
fi
fi
fi
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ This file defines the DNA of our collaboration. Every instruction is binding. De
**Mission:** Document the progress and manage the sprint's legacy.
* **Completion:**
1. Update the `PROJECT_MAP.md`.
2. **Archiving:** Move the `PLAN.md` to `docs/plans/YYYY-MM-DD_[Feature-Name].md`.
2. **Archiving:** Move the `PLAN.md` to `plans/YYYY-MM-DD_[Feature-Name].md`.
3. **Post-Mortem:** Add an "Expectation vs. Reality" section to the archive.

### 8. The Dreamer (Metacognitive Consolidator)
Expand Down
Empty file removed PLAN.md
Empty file.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ For example:
./run_harness.sh --help
```

`run_harness.sh` launches `opencode` through [`nono`](https://nono.sh) (via `.sandbox/start.sh`), providing a sandboxed OpenCode execution. It uses the Git root as the workspace (or the current directory outside a Git repository) and loads optional defaults from `.sandbox/defaults.sh`. The sandbox profile is rendered from `.sandbox/profile.template.json`, and `nono` must be installed separately.
`run_harness.sh` launches `opencode` through [`nono`](https://nono.sh) (via `.sandbox/start.sh`), providing a sandboxed OpenCode execution. It uses the Git root as the workspace (or the current directory outside a Git repository) and loads its sandbox command and default arguments from `.sandbox/defaults.sh`. That file is generated by `nono-here.sh` during provisioning (a template-provided one is preserved untouched); without it, `run_harness.sh` exits with an error. The sandbox profile is rendered from `.sandbox/profile.template.json`, and `nono` must be installed separately.
Comment thread
MKuckert marked this conversation as resolved.

### macOS Shortcuts

Expand Down
203 changes: 203 additions & 0 deletions nono/nono-here.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
#!/usr/bin/env bash
set -euo pipefail
# VERSION 2

SELF="$(basename "$0")"

# Documented extension point: adding a harness is a one-token edit to this
# array and nothing else.
HARNESSES=(claude opencode codex copilot pi)

die() {
local code="$1"
shift
echo "$SELF: $*" >&2
exit "$code"
}

# Resolve the script's own directory, following symlinks (portable, no
# readlink -f / realpath — must work on macOS/Bash 3.2). Bounded to guard
# against symlink cycles (e.g. a -> b -> a).
resolve_script_dir() {
local src dir target hops
src="$0"
hops=0
while [[ -L "$src" ]]; do
hops=$((hops + 1))
if [[ "$hops" -gt 40 ]]; then
die 1 "symlink resolution exceeded 40 hops (possible cycle) for '$0'"
fi
dir="$(cd -P "$(dirname "$src")" && pwd)"
target="$(readlink "$src")"
case "$target" in
/*) src="$target" ;;
*) src="$dir/$target" ;;
esac
done
cd -P "$(dirname "$src")" && pwd
}

# Keep this assignment bare (no `local`/`export`): with `set -e`, a failure
# inside resolve_script_dir must abort the script. Wrapping it would replace
# $? with the local/export builtin's exit status, masking the failure.
script_dir="$(resolve_script_dir)"

# Exported so a future child process can inherit the resolved home; nothing
# consumes it yet.
export NONO_HERE_HOME="${NONO_HERE_HOME:-$script_dir}"

workdir=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")

# The single exec site, shared by the fast path and the end of provisioning
# so both hand over identically (same CWD, same argument handling).
handover() {
cd "$workdir" || die 1 "cannot enter $workdir"
exec ./run_harness.sh "$@"
}

# Fast path: an already-provisioned workspace hands straight over, silently.
# (workdir and template are echoed to stderr only when provisioning happens.)
if [[ -e "$workdir/run_harness.sh" || -L "$workdir/run_harness.sh" ]] && [[ ! -f "$workdir/run_harness.sh" ]]; then
die 9 "$workdir/run_harness.sh exists but is not a regular file"
elif [[ -f "$workdir/run_harness.sh" ]] && [[ ! -x "$workdir/run_harness.sh" ]]; then
die 2 "$workdir/run_harness.sh is not executable; run: chmod +x \"$workdir/run_harness.sh\""
elif [[ -f "$workdir/run_harness.sh" ]] && [[ -x "$workdir/run_harness.sh" ]]; then
if [[ ! -x "$workdir/.sandbox/start.sh" ]]; then
die 3 "$workdir/.sandbox/start.sh is missing or not executable; run: chmod +x \"$workdir/.sandbox/start.sh\""
fi
handover "$@"
fi

# Provisioning begins here: run_harness.sh is entirely absent. Nothing on
# disk changes until a harness and template have been validated.

if [[ -n "${NONO_HERE_HARNESS:-}" ]]; then
harness=""
for h in "${HARNESSES[@]}"; do
if [[ "$h" == "$NONO_HERE_HARNESS" ]]; then
harness="$h"
break
fi
done
if [[ -z "$harness" ]]; then
die 8 "invalid NONO_HERE_HARNESS '$NONO_HERE_HARNESS'; valid values: ${HARNESSES[*]}"
fi
elif [[ ! -t 0 ]]; then
die 4 "no TTY for interactive harness selection; set NONO_HERE_HARNESS to one of: ${HARNESSES[*]}"
else
PS3="harness> "
select harness in "${HARNESSES[@]}"; do
if [[ -n "${harness:-}" ]]; then
break
fi
echo "$SELF: invalid selection '$REPLY'; choose a number from the list" >&2
done
if [[ -z "${harness:-}" ]]; then
die 10 "no harness selected (input closed)"
fi
fi

# Template resolution: user overrides beat bundled templates,
# harness-specific beats default; the first existing directory wins.
template=""
for candidate in \
"${HOME:-}/.nono-here/templates/$harness" \
"${HOME:-}/.nono-here/templates/default" \
"$NONO_HERE_HOME/templates/$harness" \
"$NONO_HERE_HOME/templates/default"; do
if [[ -d "$candidate" ]]; then
template="$candidate"
break
fi
done

if [[ -z "$template" ]]; then
die 5 "no template directory found; probed in order:
${HOME:-}/.nono-here/templates/$harness
${HOME:-}/.nono-here/templates/default
$NONO_HERE_HOME/templates/$harness
$NONO_HERE_HOME/templates/default"
fi

echo "$SELF: workdir: $workdir" >&2
echo "$SELF: template: $template" >&2

for required in run_harness.sh start.sh; do
if [[ ! -f "$template/$required" ]]; then
die 7 "template '$template' is missing required file '$required'"
fi
if [[ ! -x "$template/$required" ]]; then
die 7 "template '$template' has '$required' without the executable bit; run: chmod +x \"$template/$required\""
fi
done

# A .sandbox without run_harness.sh is the remnant of an interrupted
# previous run. This point is only reached with a fully validated template
# in hand, so deleting it never leaves the workspace with neither sandbox
# nor replacement.
if [[ -e "$workdir/.sandbox" ]]; then
if [[ ! -t 0 ]]; then
die 6 "$workdir/.sandbox exists but is incomplete; remove it manually and re-run: rm -r \"$workdir/.sandbox\""
fi

echo -e "\033[33mWarning: '$workdir/.sandbox' exists but 'run_harness.sh' is missing — the sandbox is incomplete.\033[0m" >&2
echo -e "\033[33mIt will be re-created from template '$template'.\033[0m" >&2
echo -e "\033[33mThis is self-healing: it is the expected result of a previous run interrupted between the copy and completion; re-creating from the template repairs it.\033[0m" >&2

reply=""
read -r -p "delete .sandbox and re-create from $template? [y/N] " reply || reply=""
case "$reply" in
y | Y) ;;
*) die 6 "aborted; '$workdir/.sandbox' left untouched" ;;
esac

rm -r "$workdir/.sandbox"
fi

# A dangling symlink named .sandbox is invisible to the `-e` test above
# (false for a broken link). Without this guard, `mkdir -p` would abort via
# set -e with a bare, unexplained `File exists`. Every other kind of
# pre-existing .sandbox was already intercepted with exit 6.
if [[ -e "$workdir/.sandbox" || -L "$workdir/.sandbox" ]] && [[ ! -d "$workdir/.sandbox" ]]; then
die 9 "$workdir/.sandbox exists but is not a directory"
fi

mkdir -p "$workdir/.sandbox"
cp -R "$template/." "$workdir/.sandbox/"

# defaults.sh is generated before run_harness.sh is moved into place: if
# generation fails, run_harness.sh is still absent, so the next invocation
# re-enters provisioning instead of taking the fast path against a
# workspace that is missing its defaults file.
#
# A dangling symlink named defaults.sh is invisible to `-e`, and `cat >`
# would follow it, writing outside .sandbox. Reject any non-regular path.
if [[ -e "$workdir/.sandbox/defaults.sh" || -L "$workdir/.sandbox/defaults.sh" ]] && [[ ! -f "$workdir/.sandbox/defaults.sh" ]]; then
die 9 "$workdir/.sandbox/defaults.sh exists but is not a regular file"
fi
if [[ -f "$workdir/.sandbox/defaults.sh" ]]; then
echo "$SELF: '$workdir/.sandbox/defaults.sh' already exists; preserved untouched." >&2
else
cat >"$workdir/.sandbox/defaults.sh" <<EOF
# generated by nono-here.sh
SANDBOX_COMMAND="$harness"
SANDBOX_COMMAND_DEFAULTS=()
EOF
fi

mv "$workdir/.sandbox/run_harness.sh" "$workdir/run_harness.sh"

# The template's mode bits were validated above; if the copy lost them,
# name the template rather than suggest a `chmod` on a file that will be
# regenerated on the next run.
if [[ ! -x "$workdir/run_harness.sh" ]]; then
die 7 "template '$template' produced a non-executable 'run_harness.sh'; the copy did not preserve permissions"
fi
if [[ ! -x "$workdir/.sandbox/start.sh" ]]; then
die 7 "template '$template' produced a non-executable 'start.sh'; the copy did not preserve permissions"
fi

# Provisioning complete: run_harness.sh in place, .sandbox populated,
# defaults.sh generated or preserved. Hand over through the same single
# exec site as the fast path.
handover "$@"
1 change: 1 addition & 0 deletions nono/templates/default/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
profile.json
2 changes: 2 additions & 0 deletions nono/templates/default/hooks/after
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
set -euo pipefail
2 changes: 2 additions & 0 deletions nono/templates/default/hooks/before
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
set -euo pipefail
35 changes: 35 additions & 0 deletions nono/templates/default/profile.template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"extends": ["default"],
"meta": {
"name": "NAME",
"version": "1"
},
"workdir": {
"access": "readwrite"
},
"filesystem": {
"allow": [],
"deny": [],
"read_file": ["~/.gitignore"],
"read": ["~/.CFUserTextEncoding"]
},
"network": {
"allow_domain": []
},
"environment": {
"allow_vars": [
"HOME",
"SHELL",
"TERM",
"PATH",
"LSCOLORS",
"LS_COLORS",
"LC_ALL",
"LANG",
"PWD",
"USER",
"EDITOR",
"DO_NOT_TRACK"
]
}
}
28 changes: 28 additions & 0 deletions nono/templates/default/run_harness.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# VERSION 3
set -euo pipefail

WORKSPACE=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")
DEFAULTS_FILE="${DEFAULTS_FILE:-$WORKSPACE/.sandbox/defaults.sh}"

if [[ ! -f "$DEFAULTS_FILE" ]]; then
echo "missing defaults file $DEFAULTS_FILE"
exit 1
fi

source "$DEFAULTS_FILE"
SANDBOX_COMMAND="${SANDBOX_COMMAND:-}"
if [[ "$SANDBOX_COMMAND" = "" ]]; then
echo "missing 'SANDBOX_COMMAND' in $DEFAULTS_FILE"
exit 2
fi

if [[ $# -eq 0 || "${1:-}" == -* ]]; then
set -- ${SANDBOX_COMMAND_DEFAULTS[@]+"${SANDBOX_COMMAND_DEFAULTS[@]}"} "$@"
fi

if [[ "${1:-}" == "$SANDBOX_COMMAND" ]]; then
shift
fi

"$WORKSPACE/.sandbox/start.sh" "$SANDBOX_COMMAND" "$@"
Loading