diff --git a/.github/workflows/authority.yml b/.github/workflows/authority.yml index 3ea5249..9758752 100644 --- a/.github/workflows/authority.yml +++ b/.github/workflows/authority.yml @@ -15,7 +15,7 @@ permissions: jobs: protected-surfaces: name: Protected surfaces - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout @@ -28,7 +28,7 @@ jobs: authority-patch-lock: name: Authority patch lock - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout @@ -41,7 +41,7 @@ jobs: doctrine-snapshot: name: Doctrine snapshot - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c485984..9385e82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ permissions: jobs: syntax: name: Shell syntax - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout @@ -30,7 +30,7 @@ jobs: shellcheck: name: ShellCheck - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout @@ -38,17 +38,16 @@ jobs: with: persist-credentials: false - - name: Install ShellCheck - run: | - sudo apt-get update - sudo apt-get install -y shellcheck + - name: Install pinned ShellCheck + run: scripts/install-shellcheck.sh - name: Run ShellCheck run: shellcheck bin/agently lib/*.sh tests/*.sh smoke: name: Smoke tests - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 15 steps: - name: Checkout @@ -56,5 +55,8 @@ jobs: with: persist-credentials: false + - name: Install pinned ShellCheck + run: scripts/install-shellcheck.sh + - name: Run smoke tests run: ./tests/smoke.sh diff --git a/.github/workflows/promotion-source.yml b/.github/workflows/promotion-source.yml index ad97802..34e9320 100644 --- a/.github/workflows/promotion-source.yml +++ b/.github/workflows/promotion-source.yml @@ -12,7 +12,7 @@ permissions: jobs: source: name: Require dev source for main - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index de0aafc..14b604a 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -15,7 +15,7 @@ permissions: jobs: security-adversarial: name: Security adversarial - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout @@ -28,7 +28,7 @@ jobs: config-allowlist: name: Config allowlist - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e920b7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ + +# Local planning / scratch artifacts +docs/tmp/ diff --git a/README.md b/README.md index 957a920..3fb70a4 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,10 @@ A local-first control plane for bounded agentic development loops. -Status: pre-release | v0.5.0 | CLI-only | Bash core | alpha - not production-ready - Agently runs today as a Bash CLI. It writes workflow state as files, compiles bounded context packets, and runs guard/eval checks through detected local tools. -## Hook / Positioning +## What Agently Is > Agently is a loop control plane, not a runaway loop runner. > @@ -20,42 +18,42 @@ not a model, IDE, cloud service, or CI system; it does not write code or operate agents for you. Its state is local files under `.agently/`, so the loop stays visible, diffable, and under developer authority. -| Agently is | Agently is not | -| --- | --- | -| A control plane for bounded agent-assisted loops. | An autonomous coding agent or model provider. | -| Local, file-backed workflow state under `.agently/`. | A SaaS, daemon, database, dashboard, or cloud orchestrator. | -| A compiler for copy-safe packets, prompts, evidence, and handoffs. | An IDE, CI/CD platform, or generic agent framework. | -| A guard/eval and reviewable patch surface for local checks. | A runaway retry engine or auto-promotion system. | -| A review gate around candidate work. | A replacement for maintainer approval. | -| Tool-agnostic around external agents/tools. | An integration with every named coding tool. | +| Agently is | Agently is not | +| ------------------------------------------------------------------ | ----------------------------------------------------------- | +| A control plane for bounded agent-assisted loops. | An autonomous coding agent or model provider. | +| Local, file-backed workflow state under `.agently/`. | A SaaS, daemon, database, dashboard, or cloud orchestrator. | +| A compiler for copy-safe packets, prompts, evidence, and handoffs. | An IDE, CI/CD platform, or generic agent framework. | +| A guard/eval and reviewable patch surface for local checks. | A runaway retry engine or auto-promotion system. | +| A review gate around candidate work. | A replacement for maintainer approval. | +| Tool-agnostic around external agents/tools. | An integration with every named coding tool. | ## Core Concepts -- **loop control plane** - the layer that names, sequences, and gates the steps +* **loop control plane** - the layer that names, sequences, and gates the steps of an agent-assisted development loop; it coordinates the work around your coding agents instead of doing the coding. -- **bounded loop** - a cycle with an explicit start, explicit scope, compiled +* **bounded loop** - a cycle with an explicit start, explicit scope, compiled context instead of the whole repo, and an explicit review gate to close it. -- **packet** - a compiled, copy-safe Markdown context surface for a role/agent, +* **packet** - a compiled, copy-safe Markdown context surface for a role/agent, built from file-backed state with size budgets. -- **handoff** - a recorded request/response exchange passed between roles or +* **handoff** - a recorded request/response exchange passed between roles or agents so work transfers as durable files, not hidden chat state. -- **gate** - a required human decision point, such as accept, revise, reject, or +* **gate** - a required human decision point, such as accept, revise, reject, or promote, before anything becomes canonical workflow state. -- **guard / eval** - local validation; guard runs bounded checks through +* **guard / eval** - local validation; guard runs bounded checks through detected tools, and eval aggregates guard evidence or checks proposed patches inside a throwaway git worktree. -- **local-first** - Agently runs on your machine from visible files; no daemon, +* **local-first** - Agently runs on your machine from visible files; no daemon, database, dashboard, or hidden network calls. -- **human authority** - agent output is candidate context until reviewed; the +* **human authority** - agent output is candidate context until reviewed; the developer or maintainer remains the final authority. -- **MCP** - Model Context Protocol. Agently currently has optional MCP config +* **MCP** - Model Context Protocol. Agently currently has optional MCP config helper commands for Serena; a dedicated workflow adapter, `agently-mcp`, is planned/deferred and not implemented in this Bash core. -- **doctrine / project rules** - versioned Markdown rules under - `docs/doctrine/` defining authority, scope, command contracts, and safety - boundaries; `init` copies a read-only snapshot into `.agently/doctrine/`, and - `agently guard doctrine` checks drift. +* **doctrine / project rules** - versioned Markdown rules whose canonical source + lives in Agently source-repo-only `docs/doctrine/`; `init` copies a read-only + runtime/reference snapshot into `.agently/doctrine/`, and `agently guard + doctrine` checks drift. ## How Agently Works: The Loop @@ -149,27 +147,34 @@ agently decide accept --workstream main --task implementation-pass --note "Accep ## Current Scope & Roadmap -| Current in v0.5.0 pre-release | Directional or deferred | -| --- | --- | -| Bash CLI under `bin/` and `lib/`. | Packaged or remote install paths. | -| Managed user-local `self install`, `self status`, and `self uninstall`. | Self update and rollback. | +| Current in v0.5.0 pre-release | Directional or deferred | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| Bash CLI under `bin/` and `lib/`. | Packaged or remote install paths. | +| Managed user-local `self install`, `self status`, and `self uninstall`. | Self update and rollback. | | `init`, `doctor`, `status`, workstreams, tasks, docs, packets, prompts, context, compact, inspect, evidence, report, decide, guard, eval, and patch commands. | Richer packet diagnostics, broader guard adapters, and project migration/template refresh. | -| File-backed `.agently/` state, handoffs, decisions, and doctrine snapshots. | A dedicated `agently-mcp` workflow adapter in a separate Python project. | -| Reviewable patch lane with `patch apply` gated by `--reviewed`; Git remains source-control authority. | A separate future TUI client over CLI contracts. | -| Optional Serena capability pack and explicit MCP config helper commands. | Full Serena setup automation or Agently-owned semantic code-intelligence reports. | +| File-backed `.agently/` state, handoffs, decisions, and doctrine snapshots. | A dedicated `agently-mcp` workflow adapter in a separate Python project. | +| Reviewable patch lane with `patch apply` gated by `--reviewed`; Git remains source-control authority. | A separate future TUI client over CLI contracts. | +| Optional Serena capability pack and explicit MCP config helper commands. | Full Serena setup automation or Agently-owned semantic code-intelligence reports. | Out of scope for this Bash core today: production readiness, a daemon, database, web UI, dashboard, embedded MCP server, autonomous source mutation, CI/CD replacement, enterprise RBAC/SSO, hidden global config mutation, and hidden network behavior. -Doctrine and project rules live in `docs/doctrine/`. For development on -Agently itself, those files define authority, architecture, command contracts, +## Doctrine + +`docs/doctrine/` is Agently source-repo-only. It is the canonical doctrine source +for Agently development, defining authority, architecture, command contracts, safety boundaries, testing, packet behavior, MCP boundaries, and roadmap -discipline. Initialized projects receive a read-only snapshot under -`.agently/doctrine/` so project-local loops can inspect the same rules. +discipline. + +Initialized projects receive a read-only snapshot under `.agently/doctrine/` so +project-local loops can inspect the same rules. That snapshot is runtime +reference material only; it does not make `docs/doctrine/` target-project +authority, and Agently runtime commands do not get write authority over doctrine +source. -Development validation: +## Development Validation ```bash bash -n bin/agently lib/*.sh tests/*.sh diff --git a/bin/agently b/bin/agently index fd2855f..7810247 100755 --- a/bin/agently +++ b/bin/agently @@ -3,6 +3,7 @@ set -euo pipefail if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then echo "FAIL: agently must be executed, not sourced" >&2 + # shellcheck disable=SC2317 return 1 2>/dev/null || exit 1 fi diff --git a/ci/toolchain.env b/ci/toolchain.env new file mode 100644 index 0000000..dee1581 --- /dev/null +++ b/ci/toolchain.env @@ -0,0 +1,8 @@ +# Pinned CI/dev shell tooling. Sourced by scripts/install-shellcheck.sh. +SHELLCHECK_VERSION="0.11.0" + +SHELLCHECK_LINUX_X86_64_SHA256="8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198" +SHELLCHECK_LINUX_AARCH64_SHA256="12b331c1d2db6b9eb13cfca64306b1b157a86eb69db83023e261eaa7e7c14588" + +SHELLCHECK_DARWIN_X86_64_SHA256="3c89db4edcab7cf1c27bff178882e0f6f27f7afdf54e859fa041fca10febe4c6" +SHELLCHECK_DARWIN_AARCH64_SHA256="56affdd8de5527894dca6dc3d7e0a99a873b0f004d7aabc30ae407d3f48b0a79" diff --git a/docs/doctrine/12-testing-and-validation.md b/docs/doctrine/12-testing-and-validation.md index 3212368..98b3c39 100644 --- a/docs/doctrine/12-testing-and-validation.md +++ b/docs/doctrine/12-testing-and-validation.md @@ -33,6 +33,10 @@ If available: shellcheck bin/agently lib/*.sh tests/*.sh ``` +CI/dev shell tooling is pinned through `ci/toolchain.env`. +To upgrade ShellCheck, update the version and platform SHA256 values there, +then verify with `scripts/install-shellcheck.sh` and the normal validation suite. + ## Smoke Test Doctrine The smoke test SHOULD cover: diff --git a/lib/agently.sh b/lib/agently.sh index 25cb7d5..687e4b1 100644 --- a/lib/agently.sh +++ b/lib/agently.sh @@ -4,6 +4,7 @@ set -euo pipefail if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then echo "FAIL: agently lib must be executed, not sourced" >&2 + # shellcheck disable=SC2317 return 1 2>/dev/null || exit 1 fi diff --git a/lib/cmd-inspect.sh b/lib/cmd-inspect.sh index 1e5a25f..3b29d76 100644 --- a/lib/cmd-inspect.sh +++ b/lib/cmd-inspect.sh @@ -46,6 +46,10 @@ inspect_emit_log_bounded() { omitted=$((lines - max_lines)) printf '\n[TRUNCATED %s LINES - full log: %s]\n' "$omitted" "$logfile" warn "full output logged at $logfile" + if (( status != 0 )); then + warn "command exited with status $status; log tail:" + tail -n 5 "$logfile" >&2 + fi else cat "$logfile" fi @@ -156,7 +160,7 @@ inspect_grep_run() { if has_cmd rg && [[ "$(agently_bool "$(agently_config_get "$root" inspect prefer_ripgrep)" 2>/dev/null || printf true)" == "true" ]]; then rg --line-number --no-heading --color never -- "$pattern" "$target" > "$log" 2>&1 else - grep -R -n -- "$pattern" "$target" > "$log" 2>&1 + grep -R -n --exclude-dir=.agently --exclude-dir=.git -- "$pattern" "$target" > "$log" 2>&1 fi status=$? set -e diff --git a/lib/cmd-packet.sh b/lib/cmd-packet.sh index 7b016fa..683b92d 100644 --- a/lib/cmd-packet.sh +++ b/lib/cmd-packet.sh @@ -241,7 +241,8 @@ packet_doctrine_manifest_section() { } packet_agent_rules_section() { - local root="$1" budget="$2" file="$root/AGENTS.md" + local root="$1" budget="$2" file + file="$root/AGENTS.md" printf '\n' if [[ ! -f "$file" ]]; then printf '_(AGENTS.md missing)_\n' diff --git a/lib/cmd-serena.sh b/lib/cmd-serena.sh index 09a4dbb..0c7b84a 100644 --- a/lib/cmd-serena.sh +++ b/lib/cmd-serena.sh @@ -318,7 +318,8 @@ serena_mcp_claude_state() { } serena_memory_snapshot_lines() { - local root="$1" dir="$root/.serena/memories" file rel size mtime + local root="$1" dir file rel size mtime + dir="$root/.serena/memories" [[ -d "$dir" ]] || return 0 while IFS= read -r file; do rel="$(rel_to_root "$root" "$file")" diff --git a/lib/cmd-tooling.sh b/lib/cmd-tooling.sh index eef6c88..5fa648b 100644 --- a/lib/cmd-tooling.sh +++ b/lib/cmd-tooling.sh @@ -110,7 +110,8 @@ profile_value_from_file_or_default() { } profile_ensure_gitignore() { - local root="$1" file="$root/.agently/.gitignore" tmp + local root="$1" file tmp + file="$root/.agently/.gitignore" mkdir -p "$root/.agently" if [[ ! -f "$file" ]]; then printf 'local.yml\ncache/\n' > "$file" diff --git a/lib/cmd-ws.sh b/lib/cmd-ws.sh index dc4c22b..978cfcc 100644 --- a/lib/cmd-ws.sh +++ b/lib/cmd-ws.sh @@ -321,7 +321,8 @@ workstream_branch_prepare() { } workstream_state_branch_get() { - local dir="$1" key="$2" file="$dir/state.yml" + local dir="$1" key="$2" file + file="$dir/state.yml" [[ -f "$file" ]] || return 0 awk -v key="$key" ' /^[^[:space:]#][^:]*:/ { diff --git a/lib/common.sh b/lib/common.sh index e0a87a5..f047106 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -780,7 +780,8 @@ extract_skeleton_for_file() { } structural_digest_for_file() { - local file="$1" rel="${2:-$file}" lang lines bytes sha + local file="$1" rel lang lines bytes sha + rel="${2:-$file}" lang="$(detect_language_for_file "$file")" lines="$(line_count "$file")" bytes="$(byte_count "$file")" diff --git a/scripts/install-shellcheck.sh b/scripts/install-shellcheck.sh new file mode 100755 index 0000000..0915dbb --- /dev/null +++ b/scripts/install-shellcheck.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -euo pipefail + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +shellcheck_version() { + shellcheck --version 2>/dev/null | awk '/^version:/ { print $2; exit }' || true +} + +install_shellcheck_binary() { + local binary + local install_dir + local target + + binary="$1" + install_dir="$2" + target="$install_dir/shellcheck" + + if [ -d "$install_dir" ] && [ -w "$install_dir" ]; then + mv "$binary" "$target" || fail "failed to install ShellCheck to $target" + chmod +x "$target" || fail "failed to mark ShellCheck executable at $target" + return + fi + + command -v sudo >/dev/null 2>&1 || fail "install dir is not writable and sudo is unavailable: $install_dir" + sudo mkdir -p "$install_dir" || fail "failed to create install dir: $install_dir" + sudo mv "$binary" "$target" || fail "failed to install ShellCheck to $target" + sudo chmod +x "$target" || fail "failed to mark ShellCheck executable at $target" +} + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/.." && pwd)" +toolchain_env="$repo_root/ci/toolchain.env" + +[ -f "$toolchain_env" ] || fail "missing toolchain manifest: $toolchain_env" + +# shellcheck source=../ci/toolchain.env +# shellcheck disable=SC1091 +. "$toolchain_env" + +: "${SHELLCHECK_VERSION:?}" +: "${SHELLCHECK_LINUX_X86_64_SHA256:?}" +: "${SHELLCHECK_LINUX_AARCH64_SHA256:?}" +: "${SHELLCHECK_DARWIN_X86_64_SHA256:?}" +: "${SHELLCHECK_DARWIN_AARCH64_SHA256:?}" + +if command -v shellcheck >/dev/null 2>&1; then + installed_version="$(shellcheck_version)" + if [ "$installed_version" = "$SHELLCHECK_VERSION" ]; then + echo "ShellCheck $SHELLCHECK_VERSION already installed; skipping." >&2 + shellcheck --version + exit 0 + fi +fi + +os="$(uname -s)" +arch="$(uname -m)" + +case "$os:$arch" in + Linux:x86_64 | Linux:amd64) + platform="linux.x86_64" + sha256="$SHELLCHECK_LINUX_X86_64_SHA256" + ;; + Linux:aarch64 | Linux:arm64) + platform="linux.aarch64" + sha256="$SHELLCHECK_LINUX_AARCH64_SHA256" + ;; + Darwin:x86_64 | Darwin:amd64) + platform="darwin.x86_64" + sha256="$SHELLCHECK_DARWIN_X86_64_SHA256" + ;; + Darwin:aarch64 | Darwin:arm64) + platform="darwin.aarch64" + sha256="$SHELLCHECK_DARWIN_AARCH64_SHA256" + ;; + *) + fail "unsupported ShellCheck platform: $os $arch" + ;; +esac + +asset="shellcheck-v${SHELLCHECK_VERSION}.${platform}.tar.xz" +url="https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/${asset}" +install_dir="${INSTALL_DIR:-/usr/local/bin}" +tmp_dir="$(mktemp -d)" +asset_path="$tmp_dir/$asset" +checksum_file="$tmp_dir/${asset}.sha256" + +cleanup() { + rm -rf "$tmp_dir" +} +trap cleanup EXIT + +echo "Installing ShellCheck $SHELLCHECK_VERSION for $platform." >&2 + +if command -v curl >/dev/null 2>&1; then + curl -fsSL -o "$asset_path" "$url" || fail "failed to download $url" +elif command -v wget >/dev/null 2>&1; then + wget -q -O "$asset_path" "$url" || fail "failed to download $url" +else + fail "curl or wget is required to download ShellCheck" +fi + +printf '%s %s\n' "$sha256" "$asset_path" > "$checksum_file" +if command -v sha256sum >/dev/null 2>&1; then + sha256sum -c "$checksum_file" >/dev/null || fail "SHA256 verification failed for $asset" +elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 -c "$checksum_file" >/dev/null || fail "SHA256 verification failed for $asset" +else + fail "sha256sum or shasum is required to verify ShellCheck" +fi + +tar -xJf "$asset_path" -C "$tmp_dir" || fail "failed to extract $asset" +binary="$tmp_dir/shellcheck-v${SHELLCHECK_VERSION}/shellcheck" +[ -x "$binary" ] || fail "ShellCheck binary not found in $asset" + +install_shellcheck_binary "$binary" "$install_dir" +hash -r 2>/dev/null || true + +shellcheck --version diff --git a/tests/doctrine-snapshot.sh b/tests/doctrine-snapshot.sh index 8f1c145..34b80d4 100755 --- a/tests/doctrine-snapshot.sh +++ b/tests/doctrine-snapshot.sh @@ -52,7 +52,7 @@ if grep -RIlq "docs/doctrine" "$ROOT/templates"; then fail "templates/ must not reference docs/doctrine (use .agently/doctrine runtime snapshot)" fi -printf "%s\n" "$readme_doctrine_section" | grep -Fq "Agently source repository" \ +printf "%s\n" "$readme_doctrine_section" | grep -Fq "Agently source-repo-only" \ || fail "README Doctrine section must qualify docs/doctrine as Agently source-repo-only" printf "%s\n" "$readme_doctrine_section" | grep -Fq ".agently/doctrine" \ diff --git a/tests/inspect.sh b/tests/inspect.sh index ff46a46..a8b7647 100755 --- a/tests/inspect.sh +++ b/tests/inspect.sh @@ -85,6 +85,19 @@ done assert_contains "$TMP/grep.out" "[TRUNCATED" assert_contains "$TMP/grep.out" "full log:" +# Mask rg so inspect grep takes the grep -R fallback, which must not fail +# on its own log file under .agently/cache. +mkdir -p "$TMP/norg-bin" +ln -s /usr/bin/* "$TMP/norg-bin/" 2>/dev/null || true +ln -sf /bin/* "$TMP/norg-bin/" 2>/dev/null || true +rm -f "$TMP/norg-bin/rg" +if PATH="$TMP/norg-bin" command -v rg >/dev/null 2>&1; then + fail "rg still resolvable; grep fallback not exercised" +fi +PATH="$TMP/norg-bin" "${AGENTLY[@]}" inspect grep alpha . --max 3 > "$TMP/grep-fallback.out" +assert_contains "$TMP/grep-fallback.out" "[TRUNCATED" +assert_contains "$TMP/grep-fallback.out" "full log:" + "${AGENTLY[@]}" inspect tree fixtures --depth 2 > "$TMP/tree.out" assert_contains "$TMP/tree.out" "sample.sh"