diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5aef9ae..df3abc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: '1.26.x' @@ -36,6 +38,8 @@ jobs: go test ./cmd/loopexec -run '^TestDocumentationContract' -count=1 go test ./tools/docs-continuity -count=1 go run ./tools/docs-continuity bundle "$RUNNER_TEMP/loopexec-documentation" + - name: Reject new machine-specific audit paths + run: go run ./tools/audit-privacy - name: Format run: test -z "$(gofmt -l cmd internal examples tools)" - name: Vet diff --git a/.small/handoff.small.yml b/.small/handoff.small.yml index b696c37..0bce6e9 100644 --- a/.small/handoff.small.yml +++ b/.small/handoff.small.yml @@ -1,6 +1,6 @@ small_version: "1.0.0" owner: agent -summary: 'SMALL audit blocker resolved: installed stable1.1.2, root/site CI configs and prose updated, exact retained QA proof unchanged. Task41 completed; task36 is the deployment continuation. Cache-free root/site strict pass and missing proof remains fail-closed; CLI/docs contract tests, canonical tests/build and refreshed desktop/mobile QA pass. Source-bound bundle sourcee0d04802 docscb4c8adb in build/deployment/small112-documentation and docs-checkout. Rebuild older platform packages before publication. Next commit paired PRs, verify hosted CI/protections, authorized deployment, official-origin docs readback and release; recipes remain after release. No remote publication or production action.' +summary: 'Prepared canonical documentation correction and enforced audit privacy: full Go tests, vet, privacy checks, strict SMALL and seven package checksums pass. Source bundle includes 22 canonical files; new tracking is portable and historical receipts remain unchanged. Hosted CI adoption, paired docs deployment, official-origin readback and v0.4.1 tag publication remain to finish. SMALL1.1.2 lacks a global pre-recording path guard; a separate upstream correction is needed. No new path-bearing captures, private caches or private handoffs are in this delivery.' resume: current_task_id: task-36 next_steps: diff --git a/.small/plan.small.yml b/.small/plan.small.yml index c90acdb..1e2b9e8 100644 --- a/.small/plan.small.yml +++ b/.small/plan.small.yml @@ -108,10 +108,10 @@ tasks: status: completed - id: task-36 title: Adopt canonical docs-site handshake and hosted release protections before publication - status: pending + status: blocked - id: task-37 title: Prepare v0.4.1 release and source-bound docs deployment artifacts with verified CI handoff - status: pending + status: completed - id: task-38 title: Add tested configurable software, game-development and audio-engineering recipes after deployment status: blocked @@ -127,3 +127,6 @@ tasks: - id: task-41 title: Adopt stable SMALL1.1.2 for preserved QA command validation in local and CI tooling status: completed + - id: task-42 + title: Enforce portable audit privacy and preserve local-only validation history + status: completed diff --git a/.small/progress.small.yml b/.small/progress.small.yml index 74f55ec..0d9bd1e 100644 --- a/.small/progress.small.yml +++ b/.small/progress.small.yml @@ -4500,3 +4500,298 @@ entries: status: in_progress task_id: task-36 timestamp: "2026-10-09T14:21:09.461009000Z" + - evidence: Added task task-42 via small plan --add + notes: Enforce portable audit privacy and preserve local-only validation history + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: pending + task_id: task-42 + timestamp: "2026-10-14T06:27:11.925012000Z" + - evidence: Preparing audited documentation correction and executable privacy gate from the published baseline. Original private validation captures remain unchanged outside this delivery branch; new tracking uses portable commands and relative paths. + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:27:12.056766000Z" + - command: go get gopkg.in/yaml.v3@v3.0.1 + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T062712.783809000Z.txt + command_sha256: 79a31d94fd01588acdff4b67e36ee23571f35fefd5983538cbceea74f9f458bf + command_summary: go get gopkg.in/yaml.v3@v3.0.1 + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:27:12.783809000Z" + - command: gofmt -w tools/audit-privacy + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T063014.867152000Z.txt + command_sha256: b3a72158596426b603ea786a32b3a30bebe0f5a2fe7a6637a80493b9c06ba8ab + command_summary: gofmt -w tools/audit-privacy + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:30:14.867152000Z" + - command: go test ./tools/audit-privacy -count=1 + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T063019.238509000Z.txt + command_sha256: 485bf811b6c8a062db277a5f7ef514bd370f74314849d76d0fe52ba777f13cd3 + command_summary: go test ./tools/audit-privacy -count=1 + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:30:19.238509000Z" + - command: go run ./tools/audit-privacy + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T063245.131319000Z.txt + command_sha256: 0c2bb4fddd6d7917f7457560c3f5639ae0ea6c8f8a3bfa6c74845558d08f177d + command_summary: go run ./tools/audit-privacy + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:32:45.131319000Z" + - command: go test ./... + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T064031.644757000Z.txt + command_sha256: 1bb497e3e13a1105cf24e3359fa3ef75de08b66ff8a2839cd7f9ea97824d9eb3 + command_summary: go test ./... + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:40:31.644757000Z" + - command: go vet ./... + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T064132.986079000Z.txt + command_sha256: 3d4b424f6fcd11f530525ddea9a46aeaffec7c883b6c04b6baf7df36bf47800b + command_summary: go vet ./... + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:41:32.986079000Z" + - command: go run ./tools/docs-continuity bundle build/portable-documentation + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T064133.508201000Z.txt + command_sha256: 6fa1fa3adbafbbe01ad6cac0ae96657753a4fe147bfbad04a6dfc28657069a7f + command_summary: go run ./tools/docs-continuity bundle build/portable-documentation + evidence: + exit_code: 1 + kind: cli_execution + outcome: failed + strength: cli_captured + notes: 'apply: command outcome failed; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:41:33.508201000Z" + - command: mkdir -p build + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T064450.858989000Z.txt + command_sha256: 9c48652a43dfb2ad72c56b34f93b16b22b7321806a89ccf615ba545deecf1a68 + command_summary: mkdir -p build + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:44:50.858989000Z" + - command: go run ./tools/docs-continuity bundle build/portable-documentation + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T064451.870099000Z.txt + command_sha256: 6fa1fa3adbafbbe01ad6cac0ae96657753a4fe147bfbad04a6dfc28657069a7f + command_summary: go run ./tools/docs-continuity bundle build/portable-documentation + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:44:51.870099000Z" + - command: gofmt -w tools/audit-privacy + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T064655.537245000Z.txt + command_sha256: b3a72158596426b603ea786a32b3a30bebe0f5a2fe7a6637a80493b9c06ba8ab + command_summary: gofmt -w tools/audit-privacy + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:46:55.537245000Z" + - command: go run ./tools/docs-continuity bundle build/portable-documentation-v2 + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T064656.976940000Z.txt + command_sha256: fc36b40def90f190ff736294a470354b1939803051dc7d2a1d7967b0b3e06e31 + command_summary: go run ./tools/docs-continuity bundle build/portable-documentation-v2 + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:46:56.976940000Z" + - command: go test ./tools/audit-privacy ./tools/docs-continuity -count=1 + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T064657.588452000Z.txt + command_sha256: 181711a740c052f61334348f1d493185952c495a61beb6d8243646b9aaddc87c + command_summary: go test ./tools/audit-privacy ./tools/docs-continuity -count=1 + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:46:57.588452000Z" + - command: gofmt -w tools/audit-privacy + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T065227.521608000Z.txt + command_sha256: b3a72158596426b603ea786a32b3a30bebe0f5a2fe7a6637a80493b9c06ba8ab + command_summary: gofmt -w tools/audit-privacy + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:52:27.521608000Z" + - command: go run ./tools/docs-continuity bundle build/portable-documentation-v3 + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T065229.013029000Z.txt + command_sha256: 484406c01aa699da54dfa571c4413bc125cb5e36b90c517e45db741e9c9650a7 + command_summary: go run ./tools/docs-continuity bundle build/portable-documentation-v3 + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:52:29.013029000Z" + - command: go test ./tools/audit-privacy -count=1 + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T065229.557400000Z.txt + command_sha256: 485bf811b6c8a062db277a5f7ef514bd370f74314849d76d0fe52ba777f13cd3 + command_summary: go test ./tools/audit-privacy -count=1 + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:52:29.557400000Z" + - evidence: Added structured canonical audit privacy checking, explicit immutable historical baseline, tests, hard agent policy, allowlisted guide and changelog. Full Go tests and vet passed. Focused tests cover escaped YAML, Unix and Windows paths, relative references, original progress/proof preservation, untracked files, deletion and private diagnostic keys. Clean delivery passes; original path-bearing unpublished candidate is rejected without disclosing values. + notes: This is repository publication enforcement, not a built-in SMALL1.1.2 guarantee. Hosted CI adoption and release remain separate gates; no original receipts were rewritten. + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: completed + task_id: task-42 + timestamp: "2026-10-14T06:56:11.050874000Z" + - command: go run ./tools/audit-privacy + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T065611.537116000Z.txt + command_sha256: 0c2bb4fddd6d7917f7457560c3f5639ae0ea6c8f8a3bfa6c74845558d08f177d + command_summary: go run ./tools/audit-privacy + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T06:56:11.537116000Z" + - command: ./scripts/build-release.sh v0.4.1 build/privacy-release + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T070630.476126000Z.txt + command_sha256: 04b2ed95c133ef5c530407e2fb95df5c03b25c1aa98cf64595eeca616e762cad + command_summary: ./scripts/build-release.sh v0.4.1 build/privacy-release + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T07:06:30.476126000Z" + - evidence: Fresh preparation passed full Go tests, vet, privacy regression tests and strict SMALL validation. Built six trimmed-path platform binaries plus the standalone source-bound documentation archive; all seven checksums passed. Bundle includes 22 canonical files matching the privacy guide and exact source. No private caches, new path-bearing captures or handoffs are included in publication. + notes: Hosted adoption, official-origin deployment and stable release publication are task36 gates, not implied by local preparation. + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: completed + task_id: task-37 + timestamp: "2026-10-14T07:09:17.569470000Z" + - evidence: Prepared safe source and metadata; publication awaits exact-head CI, paired docs deployment, official-origin readback and tag workflow. No GitHub authentication, paid-plan or repository visibility blocker exists. Existing public audit prefix is preserved; new machine-specific captures remain private and excluded. + notes: The privacy gate passes this clean delivery branch and rejects the previous unpublished candidate. Do not relax source/docs or privacy checks to publish. + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: blocked + task_id: task-36 + timestamp: "2026-10-14T07:09:17.829923000Z" + - command: go run ./tools/docs-continuity bundle build/portable-documentation-final + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T071537.902434000Z.txt + command_sha256: 8b277df3b57be97c2290bd1528193ec1250ba249e8cbd04224b487e4ba3610f0 + command_summary: go run ./tools/docs-continuity bundle build/portable-documentation-final + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T07:15:37.902434000Z" + - command: go test ./tools/audit-privacy -count=1 + command_ref: .small-cache/logs/f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96/commands/20261014T071538.124582000Z.txt + command_sha256: 485bf811b6c8a062db277a5f7ef514bd370f74314849d76d0fe52ba777f13cd3 + command_summary: go test ./tools/audit-privacy -count=1 + evidence: + exit_code: 0 + kind: cli_execution + outcome: succeeded + strength: cli_captured + notes: 'apply: command outcome succeeded; task acceptance unchanged' + replayId: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 + status: in_progress + task_id: task-42 + timestamp: "2026-10-14T07:15:38.124582000Z" diff --git a/.small/workspace.small.yml b/.small/workspace.small.yml index 1948ce8..eaf1087 100644 --- a/.small/workspace.small.yml +++ b/.small/workspace.small.yml @@ -2,6 +2,6 @@ small_version: "1.0.0" owner: agent kind: repo-root created_at: "2026-01-20T23:56:04Z" -updated_at: "2026-10-09T14:21:09.527594Z" +updated_at: "2026-10-14T07:15:38.197771Z" run: replay_id: f6bbda15d790df6d023dea340edd1b85ccac89c82db362652a1b656981e82d96 diff --git a/AGENTS.md b/AGENTS.md index c60095a..19ef702 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,16 @@ # HARD RULES Never directly edit .env files +## Canonical Audit Privacy + +- New `.small` records and `.small-command-proofs` MUST use repository-relative + paths or logical aliases, never machine-specific absolute paths. +- Do not write host paths into commands, summaries, evidence, notes or handoffs. + Keep host mappings and private captures outside tracked canonical state. +- Preserve historical receipts and exact proof bytes; never scrub them in place. +- Run `go run ./tools/audit-privacy` before publication. Follow + `docs/audit-privacy.md`; SMALL v1.1.2 strict alone does not enforce this rule. + ## Code and documentation continuity - LoopExec is a general-purpose bounded agentic loop governor. Project-specific diff --git a/CHANGELOG.md b/CHANGELOG.md index 78cfe1a..d99b45a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,19 @@ ## Unreleased +- Added canonical audit privacy policy and a publication gate rejecting new + absolute host paths in SMALL metadata and command proofs. Historical receipts + remain unchanged; unpublished private captures are excluded from delivery. + Named-home references are rejected along with absolute paths. + +- Corrected architecture to describe the shipped external-check runtime; marked + the historical SMALL task-list design as planned. +- Reconciled source/publication status, canonical Getting Started and conceptual + homepage prose; removed obsolete rollout claims and corrected historical releases. + Future recipes and workflow adapters will remain configurable, opt-in integrations. -## v0.4.1 +## v0.4.1 — current source See [release notes](docs/releases/v0.4.1.md). Publication status is authoritative on [GitHub Releases](https://github.com/justyn-clark/loopexec/releases). @@ -17,7 +27,7 @@ on [GitHub Releases](https://github.com/justyn-clark/loopexec/releases). - Hardened code/docs continuity: canonical prose and changelog co-change CI gate, generated CLI/JSON contract tests, source-bound allowlisted documentation in every platform archive, and fail-closed deployed-docs checks before publication. - The separate docs site must adopt the build handshake before the next release. + The docs site consumes the allowlisted bundle; release publication requires exact official-origin readback. - Added a non-publishing Release workflow rehearsal and a main-line ancestry gate before deployed-docs verification or tag publication. - Clarified general-purpose acceptance policy: original run contracts, candidate @@ -31,7 +41,8 @@ on [GitHub Releases](https://github.com/justyn-clark/loopexec/releases). presence from verified attestation. The legacy JSON `attested` field still describes file presence; use `attestation_status` and explicit verification. - Added an evidence-capture guide for creative verification and a release/status - roadmap. These changes are local until a subsequent release is published. + roadmap. These changes are merged in source. The Releases page establishes whether +matching binary packages have been published. ## v0.4.0 @@ -52,5 +63,15 @@ proofs, and binary release packages. Existing convergence exit code 10 is unchan ## v0.2.0 -Previous published release. Later core command and deterministic demo work is -included in v0.3.0; this changelog does not retroactively attribute it to v0.2.0. +Published June 27, 2026 (UTC). Introduced the real bounded loop, computed halts, +typed receipts, determinism probing, regression/integrity guards, replay, +attestation, context slicing and isolation orchestration. The deterministic +demo and unattended workflow extensions arrived in v0.3.0. See +[the historical release](https://github.com/justyn-clark/loopexec/releases/tag/v0.2.0). + +## v0.1.0 + +Published February 22, 2026. Established the standalone execution harness, +single-object JSON contract, stable exit classes, contract tests and +format/vet/test CI. See +[the historical release](https://github.com/justyn-clark/loopexec/releases/tag/v0.1.0). diff --git a/README.md b/README.md index 851aadc..339728a 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,29 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Go version](https://img.shields.io/badge/go-1.26.0-00ADD8?logo=go&logoColor=white)](https://go.dev) -`loopexec` is a deterministic runtime for loop engineering: it drives bounded, stateless, auditable execution loops that progress through an external check and stop on a computed reason, never on an agent's say-so. It is the runtime companion to SMALL Protocol: SMALL describes state, constraints, plans, progress, and handoff; loopexec drives one bounded loop against that state and reports machine-readable outcomes. +## Overview + +LoopExec is a deterministic runtime for bounded agentic execution loops. Work progresses through an external check and stops on a computed reason — never because an agent says it is done. + +Supply a work command and an external check. LoopExec bounds execution, detects regression, no progress and oscillation, applies integrity guards, and records replayable receipts. Opt-in workflows add usage evidence, scoped model-call budgets and candidate restoration. Isolation orchestration composes with operator-provided container, egress and credential hooks. + +The runtime governs execution; adapters supply models, checks and infrastructure. Synthetic creative examples prove runtime mechanics, not production model quality. Observed spending cannot enforce a provider-side cap, a signature file requires explicit verification, and retaining a candidate does not establish owner acceptance. + +[Getting Started](docs/getting-started.md) begins with an offline deterministic proof. +SMALL describes plans, constraints, progress and handoff; LoopExec drives one bounded loop. SMALL is optional for consumers. The normative contract is `SPEC.md`. The current source implements bounded execution, determinism probing, integrity and regression guards, offline-verifiable receipts, and opt-in unattended workflows. See `SPEC.md` section 11 for the implemented scope and remaining sub-parts. ## Current status -Source identity: **v0.4.1**. This patch adds evidence-complete reports and enforced +Source identity: **v0.4.1**. Documentation describes current source; the public +binary version is established by GitHub Releases, not this source badge. This patch adds evidence-complete reports and enforced code/docs release continuity. See [v0.4.1 notes](docs/releases/v0.4.1.md); the [Releases page](https://github.com/justyn-clark/loopexec/releases) establishes publication status. Workflow thresholds remain supplied by each integration. +## Implemented capabilities + Implemented through v0.4.0: - Real bounded `run` loop: iterates `--exec` then `--check` until the check passes (`success_condition_met`), a bound trips (`max_iterations_reached`), or work fails (`execution_failure`); "no check, no loop"; computed halt reasons mapped to a stable exit-code class; typed JSONL receipts (`.loopexec/run-.jsonl`) and atomic durable state. @@ -36,12 +48,14 @@ Implemented through v0.4.0: - `isolate` - two-zone orchestration: detached-clone sandbox + per-run minted/revoked credential + rendered/launched exec/agent zones. - Global `--json` output, explicit exit-code contract, contract and unit tests, GitHub Actions CI (`gofmt`, `go vet`, `go test ./...`). -Not implemented yet (named sub-parts; see `SPEC.md` section 11): +## Integration boundaries and planned sub-parts + +See `SPEC.md` section 11: - The operator-provided infra `isolate` composes with: the container engine, the auditing egress proxy, and the provider key API (`--runtime` / `--egress-proxy` / `--mint-cmd` + `--revoke-cmd`). - Provider-specific live usage wrappers; the deeper metric-integrity layers (assertion-count / coverage-floor); `probe-check` adversarial perturbation + the in-loop sequential monitor. - The `import_closure` / `dep_graph` context-relevance tiers; github/slack escalation channels; the kill-the-PID watchdog actuator. -- Full SMALL-driven `task_list` loop execution and `small` CLI integration; container, Nix, or remote substrates and multi-worker orchestration. +- Full SMALL-driven `task_list` loop execution; container, Nix, or remote substrates and multi-worker orchestration. ## Install @@ -49,24 +63,25 @@ Not implemented yet (named sub-parts; see `SPEC.md` section 11): go install github.com/justyn-clark/loopexec/cmd/loopexec@latest ``` -Requires Go 1.26 or newer. For a pinned v0.4.1 install, use `@v0.4.1` once that -release is published. Until then, v0.4.0 remains the prior published stable release. +Requires Go 1.26 or newer. The command resolves the latest public stable release. Source identity and +publication are distinct: use a pinned tag only when it is listed as public on GitHub Releases. Prebuilt archives and SHA256 checksums are linked from [GitHub Releases](https://github.com/justyn-clark/loopexec/releases). `loopexec --version` prints the installed version; `loopexec version --json` emits its machine-readable identity. See the [release notes](docs/releases/v0.4.1.md) for compatibility and platform limits. -SMALL is optional for LoopExec consumers. This repository uses the tracked `.small/` files for its own development audit; CI requires a canonical progress update with source or documentation changes and validates the tracked state. The [SMALL integration guide](docs/small-integration.md) covers SMALL CLI v1.1.0 with legacy v1 artifacts and opt-in collaborative v2 sessions; LoopExec remains the loop governor. +SMALL is optional for LoopExec consumers. This repository uses the tracked `.small/` files for its own development audit; CI requires a canonical progress update with source or documentation changes and validates the tracked state. The [SMALL integration guide](docs/small-integration.md) covers SMALL CLI v1.1.2 with legacy v1 artifacts and opt-in collaborative v2 sessions; LoopExec remains the loop governor. ## 60-second proof -Run the built-in deterministic demo with no API key, model, or network dependency: +Run the built-in deterministic demo with no model, API key, Python runner, or network access: ```bash loopexec demo ``` -The command creates a red local fixture, sends it through the real bounded loop, repairs it, observes the external check turn green, writes a typed JSONL receipt, and verifies the recorded check fingerprint through the same verifier used by `loopexec replay`. It proves LoopExec's execution-integrity contract; it does not run an agent. +The command creates a red local fixture, sends it through the real bounded loop, repairs it, observes the external check turn green, writes a typed JSONL receipt, and verifies the recorded check fingerprint through the same verifier used by `loopexec replay`. It proves the execution/check/receipt/replay contract, not agent intelligence. +For your own work command and check, follow [Getting Started](docs/getting-started.md). The receipt path in the output identifies the retained temporary workdir. To keep the proof at a known path, pass a fresh directory with `--workdir `; `demo` refuses to overwrite an existing `.loopexec` directory or `status.txt` fixture. @@ -94,7 +109,7 @@ Code/tooling changes also require canonical documentation and changelog updates. The generated [CLI and JSON contract](docs/cli-reference.generated.md) is checked against the runtime. Every release packages source-bound docs and publication requires matching deployed content. See [continuity policy](docs/documentation-continuity.md) -for enforcement and the pending website rollout boundary. +for enforced source-bound release gates and the operator/account boundary. ## CLI @@ -115,7 +130,7 @@ See [acceptance and evidence](docs/acceptance-policy.md) for general-purpose objective, retention, convergence and owner-decision boundaries. The [creative verification guide](docs/creative-verification.md) is an optional example. The [status and roadmap](docs/status-and-roadmap.md) distinguishes published -capabilities, local improvements, and remaining work. +capabilities, source changes, and remaining work. ### Global flag @@ -136,7 +151,7 @@ Example JSON response (a converged run): ```json { "tool": "loopexec", - "version": "0.4.0", + "version": "0.4.1", "status": "halted", "run_id": "local", "iteration": 3, @@ -172,7 +187,7 @@ The `halt_reason` string is the stable contract; the exit code is its coarse cla ```text SPEC.md canonical, normative contract (source of truth) cmd/loopexec/ loopexec CLI -docs/architecture.md planned loop architecture +docs/architecture.md implemented runtime architecture docs/cli.md current loopexec command contract docs/integrations.md integration guidance for the implemented CLI docs/loop-contract.md task_list mode profile of SPEC.md @@ -189,7 +204,7 @@ Read the docs in this order: 0. `SPEC.md` - the canonical, normative contract (source of truth for the binary and the docs site) 1. `docs/cli.md` - what the current CLI actually does 2. `docs/integrations.md` - how to integrate with the current implementation -3. `docs/architecture.md` - target architecture and planned boundaries +3. `docs/architecture.md` - implemented architecture and integration boundaries 4. `docs/loop-contract.md` - the `task_list` mode profile ## Links diff --git a/SPEC.md b/SPEC.md index f7c0f02..c20bc5d 100644 --- a/SPEC.md +++ b/SPEC.md @@ -161,6 +161,9 @@ A live-LLM trajectory is not reproducible; only the **verdict** is. Marketing co ## 11. Capability status (normative source for the docs matrix) +This matrix describes current source. Publication status comes from GitHub Releases; +new v0.4.1 report behavior requires a matching public binary. + Each capability is **Shipped** (in `cmd/loopexec` with tests), **In progress** (on a feature branch), or **Planned** (specified here, not yet built). The docs site MUST render this distinction and MUST NOT present Planned capability as Shipped. | Capability | Status | @@ -179,6 +182,7 @@ Each capability is **Shipped** (in `cmd/loopexec` with tests), **In progress** ( | Receipt pinning: model-identity tuple + sampling + context manifest (sha256) + cost + check fingerprint (section 8) | Shipped (legacy flags record metadata; workflow usage hooks support exact in-loop strict/observed/unmetered accounting) | | `replay`: verify a receipt offline by re-running the check and matching the fingerprint (section 8) | Shipped | | `attest`: HMAC-sign a receipt and `--verify` it (section 8) | Shipped | +| `report`: read-only event/failure/missing-evidence reporting | Shipped (v0.4.1 source; signature presence is unverified, candidate retention is not acceptance) | | `reexecute`: live re-run of the recorded config N times, halt-reason distribution (section 8) | Shipped | | `escalate` / `watch` / `ack` + comprehension gate (section 9) | Shipped (file/stdout channels, heartbeat + staleness detection, comprehension `--comprehension-every`; github/slack channels + kill-the-PID actuator Planned) | | `build-context`: budgeted relevant-file slice with workdir-confined, symlink-safe file resolution (section 4) | Shipped (stacktrace + last-diff + untracked relevance; import_closure / dep_graph tiers Planned) | @@ -190,7 +194,16 @@ Each capability is **Shipped** (in `cmd/loopexec` with tests), **In progress** ( This table is the contract between the binary and the site. When a capability moves status, update it here first; the binary tests and the docs matrix both reference this section. -Every row above now has a Shipped core. The remaining work is named, inline sub-parts (the operator-provided infra hooks, provider-specific metering wrappers, the deeper metric-integrity layers, the `import_closure`/`dep_graph` context tiers, github/slack escalation channels, the kill-the-PID watchdog actuator) -- not whole capabilities. +Every row above has a Shipped core. The remaining work is named, inline sub-parts (the operator-provided infra hooks, provider-specific metering wrappers, the deeper metric-integrity layers, the `import_closure`/`dep_graph` context tiers, github/slack escalation channels, the kill-the-PID watchdog actuator) -- not whole capabilities. + +Additional integration and topology status: + +| Capability | Status | +| --- | --- | +| SMALL CLI validation and v1/v2 compatibility proofs | Shipped (repository/adapter integration; SMALL remains an optional external CLI) | +| Full SMALL-driven `task_list` topology | Planned | +| Provider-specific live usage wrappers | Planned (adapters must supply provider enforcement for strict monetary caps) | +| Container/Nix/remote substrates and multi-worker orchestration | Planned (existing isolation hooks compose with operator infrastructure) | ## 12. Unattended workflow extension diff --git a/docs/architecture.md b/docs/architecture.md index e21b390..48d3e01 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,250 +1,91 @@ -Note: This document describes planned behavior. Some commands may not yet be implemented. - -# loopexec Architecture - -This document describes the high-level architecture of loopexec. - ---- - -## Overview - -loopexec is a state-driven execution CLI. It reads state from SMALL artifacts, executes bounded work, and records outcomes atomically. - -loopexec does not: -- Define state (SMALL does) -- Orchestrate workflows -- Replace CI or task runners -- Manage agents or models - -loopexec does: -- Drive execution loops -- Enforce state gates -- Record evidence -- Enable safe resume - ---- - -## Execution Loop - -loopexec implements a controlled execution loop (Ralph loop) with hard state boundaries. - -``` -+-----------------+ -| Validate State |<---------+ -| (strict check) | | -+--------+--------+ | - | | - v | -+--------+--------+ | -| Select Task | | -| (pending, deps)| | -+--------+--------+ | - | | - v | -+--------+--------+ | -| Mark In Progress| | -| (progress add) | | -+--------+--------+ | - | | - v | -+--------+--------+ | -| Execute Step | | -| (substrate) | | -+--------+--------+ | - | | - v | -+--------+--------+ | -| Checkpoint | | -| (atomic record)| | -+--------+--------+ | - | | - v | -+--------+--------+ | -| Re-validate |-----------+ -| (strict check) | -+-----------------+ +# LoopExec architecture + +This guide describes current source. [SPEC.md](../SPEC.md) is the normative +behavioral contract; [its capability matrix](../SPEC.md#11-capability-status-normative-source-for-the-docs-matrix) +distinguishes shipped cores from planned sub-parts. + +## Runtime and adapter boundary + +LoopExec is a deterministic Go runtime for bounded agentic execution loops. +An adapter supplies the work command and external check. LoopExec owns the +iteration loop, computed halts, durable runtime state, receipts and configured +guards. Models, quality oracles and infrastructure remain integration responsibilities. + +SMALL is an optional external state/audit CLI. LoopExec does not select SMALL +tasks, invoke SMALL checkpoints automatically, or require a `.small/` directory +for an ordinary run. See [SMALL integration](small-integration.md) for tested +composition; the [historical task-list design](loop-contract.md) remains planned. + +## External-check execution loop + +```text +configuration + external check + | + validate workspace/policy + | + bounded work command <----------------+ + | | + external check + configured guards | + | | + record evidence and durable state | + | | + compute halt or continue -------------+ ``` -Each iteration: -1. Validates workspace state via `small check --strict` -2. Selects the next actionable task (status: pending, dependencies met) -3. Marks the task in progress with evidence -4. Executes exactly one bounded command via a substrate -5. Records the outcome atomically via `small checkpoint` -6. Re-validates state before continuing - -The loop terminates when no actionable tasks remain or a halt condition is triggered. - ---- - -## State Gates - -loopexec enforces state validity at defined boundaries. - -### Strict Check - -`small check --strict` is the primary gate. It validates: -- All SMALL artifacts are present and well-formed -- Schema compliance for each artifact -- Invariant rules (non-empty intent, valid constraints) -- ReplayId presence in handoff - -Strict check runs: -- Before any loop iteration -- After each checkpoint -- Before handoff - -If strict check fails, execution halts. The failure must be resolved before work continues. - -### Checkpoint - -`small checkpoint` atomically updates plan and progress. A checkpoint: -- Sets task status (completed or blocked) -- Records evidence of what changed -- Appends to the progress audit trail - -Checkpoints are the only valid way to mark work complete. Plan-only toggles are forbidden. - ---- - -## Substrate Boundary - -A substrate is an execution environment where commands run. - -### Supported Substrates - -| Substrate | Description | -|-----------|-------------| -| Local shell | Default. Commands run in the current shell environment. | -| Container | Commands run in an isolated container. Environment is explicit. | -| Reproducible shell | Commands run in a reproducible environment (e.g., Nix devshell). | - -### Substrate Contract - -Substrates must: -- Accept a command string -- Execute the command -- Return exit code, stdout, stderr -- Not modify SMALL artifacts directly - -The substrate is recorded as part of execution evidence. This makes the environment explicit and reproducible. - -### Substrate Selection - -loopexec selects the substrate based on: -- Workspace configuration -- Command-line flags -- Constraint directives - -The default is local shell. Other substrates require explicit configuration. - ---- - -## Failure Modes - -loopexec halts execution under these conditions: - -### Gate Failures - -| Failure | Cause | Resolution | -|---------|-------|------------| -| Strict check fails | Invalid SMALL state | Fix artifacts, re-run check | -| Missing replayId | Handoff not initialized | Run `small handoff` | -| Schema violation | Malformed artifact | Fix schema errors | - -### Execution Failures - -| Failure | Cause | Resolution | -|---------|-------|------------| -| Command exits non-zero | Substrate command failed | Checkpoint as blocked, investigate | -| Constraint violation | Command violates constraint | Checkpoint as blocked, revise approach | -| Timeout | Command exceeded limit | Checkpoint as blocked, adjust or split task | - -### Task Failures - -| Failure | Cause | Resolution | -|---------|-------|------------| -| No actionable tasks | All tasks completed or blocked | Run handoff | -| Dependency cycle | Tasks depend on each other | Fix plan structure | -| Missing task | Referenced task not in plan | Add task via `small plan --add` | - -When execution halts, loopexec records the halt reason. The next session can resume from the recorded state. - ---- - -## Resume Semantics - -loopexec supports safe resume via SMALL handoff and replayId. - -### Handoff - -`small handoff` generates a resume context containing: -- Summary of current state -- Current task (if in progress) -- Next steps (pending tasks) -- ReplayId for session continuity - -Handoff runs only after strict check passes. This ensures the recorded state is valid. - -### ReplayId - -The replayId is a unique identifier for the current execution session. It: -- Links progress entries to a specific session -- Enables detection of state drift between sessions -- Provides an audit anchor for lineage tracking - -ReplayId is generated automatically when handoff runs. All subsequent progress and checkpoint operations reference this replayId. - -### Resume Flow - -1. New session starts -2. Read handoff.small.yml to get replayId and context -3. Run `small check --strict` to validate state -4. Continue loop from current_task_id or next pending task -5. All new progress entries inherit the replayId - -If the workspace state has changed outside the session (manual edits, other agents), strict check will fail. The state must be reconciled before resuming. - ---- - -## Component Boundaries - -``` -+---------------------+ -| loopexec CLI | -+----------+----------+ - | - v -+----------+----------+ -| Loop Driver | -| (iteration logic) | -+----------+----------+ - | - +-----+-----+ - | | - v v -+----+----+ +----+----+ -| Selector| | Emitter | -| (task) | | (json) | -+---------+ +---------+ - | - v -+----+----+ -|Substrate| -| (exec) | -+---------+ - | - v -+----+----+ -| SMALL | -| (state) | -+---------+ -``` - -- **Loop Driver**: Orchestrates the execution loop. Calls selector, substrate, and SMALL CLI. -- **Selector**: Chooses the next actionable task based on status and dependencies. -- **Substrate**: Executes commands in the configured environment. -- **Emitter**: Produces JSON output for integrations. -- **SMALL**: External authority for all state operations. - -loopexec never bypasses SMALL. All state reads and writes go through the SMALL CLI. +`cmd/loopexec/run.go` implements this loop. Missing checks fail closed. +The work step runs before the check. Integrity, metering and acceptance guards +dominate a green check; an agent's completion statement is not a halt input. +The legacy check fingerprint records exit code and normalized output hash. +The external check exits 0 when green; verified governor convergence exits 10. + +Set-based failure collectors enable the no-regression ratchet and +no-progress/oscillation detection. Opt-in workflow configuration adds argv +hooks, exact usage evidence, numeric progress/patience and candidate manifests. +See the [workflow contract](workflows.md) for ordering and resume rules. + +## Components and evidence + +| Component | Implementation and responsibility | +| --- | --- | +| CLI/configuration | `cmd/loopexec/main.go`; command tree, flags, JSON identity and exit classes | +| Loop/guards | `run.go`, `progress.go`, `numeric.go`; execution, check, integrity and progress decisions | +| Subprocess lifecycle | `internal/subprocess` and `lifecycle.go`; deadlines, cancellation, process groups and bounded output | +| Workflow/budget | `workflow.go`, `budget.go`, `scoped_budget.go`; pinned policy, exact usage and durable pre-call reservations | +| Candidates | `candidate.go`, `internal/workflow`; manifest-bound snapshots, validation and restoration | +| Receipts/reporting | `receipt.go`, `report.go`; typed JSONL events, saved state, replay, attestation and read-only reports | +| Isolation hooks | `isolate.go`; detached clone, credential lifecycle and two-zone launch orchestration | + +Runtime state lives under `.loopexec/`; it is independent of SMALL audit state. +Receipts carry observed outcomes and check fingerprints. `replay` re-runs the +saved check and compares its fingerprint; it does not run the work command. +`report` reads evidence without execution. Signature-file presence is +unverified until explicit `attest --verify`; retaining a candidate is not +owner acceptance. See [acceptance policy](acceptance-policy.md). + +## Bounds and isolation + +Iteration bounds always govern runs. Total/per-command deadlines default to +disabled (zero) and must be configured when time bounds are required. +Linux/macOS cancellation owns a process group; other platforms bound the direct +child only. Native Windows runtime behavior is not verified by current CI. + +Shared scoped budgets count adapter-reserved model calls across run IDs, +independently of token/dollar accounting. Strict monetary guarantees require +trusted adapter/provider enforcement. Observed metering stops later work; +it cannot prevent a provider from exceeding an in-flight allowance. + +`isolate` orchestrates operator-provided container runtime, egress proxy and +credential APIs. LoopExec itself does not supply full OS/container containment. +Container/Nix/remote substrates and multi-worker orchestration remain planned. + +## Resume and source verification + +Workflow resume revalidates pinned configuration, policy and saved candidates; +it preserves cumulative usage instead of resetting allowances. Interrupted +work is not evidence of completion. A workflow runtime has one governor. + +The generated [CLI/JSON reference](cli-reference.generated.md) is checked +against the runtime. [Documentation continuity](documentation-continuity.md) +binds allowlisted guides to the release source and requires official deployed +readback. Hash provenance establishes which source was presented; behavioral +tests and semantic review establish what it means. diff --git a/docs/audit-privacy.md b/docs/audit-privacy.md new file mode 100644 index 0000000..d71268b --- /dev/null +++ b/docs/audit-privacy.md @@ -0,0 +1,49 @@ +# Audit Privacy + +New canonical tracking must not disclose machine-specific filesystem paths. +This applies to every `.small` YAML file, including progress, handoff, plan, +intent, constraints and workspace metadata, and to `.small-command-proofs`. +It covers commands, summaries, evidence, notes and embedded scripts. + +Use repository-relative paths such as `build/proofs/run.json`, or logical +aliases such as `[workspace]` and `[local-cache]`. Keep alias-to-machine +mappings in untracked private configuration. Prefer helper scripts invoked +by relative path rather than capturing large inline scripts. Do not expose +usernames, home directories, temporary host directories, absolute tool +locations, Windows drives, network shares or local file URIs. +Named-home shell references are machine-specific too; use a logical alias instead. + +## Enforcement + +Run `go run ./tools/audit-privacy` before publication. CI and the release +workflow run the same check. It inspects tracked and untracked canonical files, +parses YAML values (including escaped values), and rejects new absolute paths. +Failures identify only the relative record location, not the offending value. +This privacy lint complements secret scanning and human semantic review. + +The historical baseline in `scripts/audit-privacy-policy.json` pins existing +published history. Progress entries must remain unchanged; historical proof +bytes must remain identical. Baseline changes require an explicit owner policy +decision, never an agent exception to clear a failing check. + +## Historical Records + +Never obfuscate an original receipt or hash-bound proof in place. Changing +command bytes invalidates its digest and provenance. Unpublished path-bearing +captures stay private and are not imported into public delivery branches. +Preserve them locally; record fresh validation with portable commands through +SMALL on a clean delivery branch retaining the published history. + +Already published historical records need a separate owner-approved cleanup +or protocol migration. This gate does not remove old public Git history. + +## SMALL Boundary + +SMALL v1.1.2 does not enforce this privacy property by itself. A fixture with a +synthetic absolute path in progress evidence passed its strict check. A written +constraint must not be presented as executable enforcement. LoopExec adds a +publication gate; upstream SMALL should reject unsafe new authored fields +before recording them or executing a command. A future redacted export must +preserve private original bytes and hashes with a separate public provenance +contract. Never silently sanitize receipts, weaken exact-byte proof, or publish +the private command cache. diff --git a/docs/cli.md b/docs/cli.md index 3173127..6d2af29 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -92,7 +92,7 @@ Example: ```json { "tool": "loopexec", - "version": "0.4.0", + "version": "0.4.1", "status": "ok", "run_id": "local", "iteration": 1, diff --git a/docs/documentation-continuity.md b/docs/documentation-continuity.md index a1987b2..32cbced 100644 --- a/docs/documentation-continuity.md +++ b/docs/documentation-continuity.md @@ -89,8 +89,10 @@ An administrator or direct manual publication can bypass repository automation; account rules and human semantic review are still required. Do not claim they are configured without live verification. -These gates are local unreleased work until committed and adopted by CI. The -separate website currently needs the handshake integration and deployment; the -new release workflow intentionally blocks until it is ready. Do not weaken the -gate because the current website predates it. No release or site deployment is -authorized merely by preparing or testing a bundle. +The gates are committed on main and run in CI. The separate website consumes +the canonical bundle and exposes the manifest, raw documents and homepage marker. +A successful previous deployment does not prove that a new bundle is deployed: +verify the exact intended release bundle again after every import/deployment. +Account protections must be verified separately; committed workflows do not +make unprotected branch checks mandatory. Preparing a bundle alone does not +authorize release or deployment. diff --git a/docs/documentation-files.json b/docs/documentation-files.json index ab964b0..e05a948 100644 --- a/docs/documentation-files.json +++ b/docs/documentation-files.json @@ -5,6 +5,7 @@ "SPEC.md", "CHANGELOG.md", "docs/documentation-files.json", + "docs/getting-started.md", "docs/cli.md", "docs/cli-reference.generated.md", "docs/architecture.md", @@ -14,6 +15,7 @@ "docs/small-integration.md", "docs/acceptance-policy.md", "docs/documentation-continuity.md", + "docs/audit-privacy.md", "docs/creative-verification.md", "docs/status-and-roadmap.md", "docs/releases/v0.3.0.md", diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..bb9796a --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,58 @@ +# Getting Started + +## Install and prove the runtime + +With Go 1.26 or newer, install the latest published stable release. Ensure +Go's binary directory (`go env GOBIN`, or `$(go env GOPATH)/bin` when GOBIN +is empty) is on PATH. + +```sh +go install github.com/justyn-clark/loopexec/cmd/loopexec@latest +loopexec --version +loopexec demo +``` + +The command resolves the latest public stable release. [GitHub Releases](https://github.com/justyn-clark/loopexec/releases) +is authoritative; documentation source identity does not establish publication. +For a pinned install, substitute a tag listed there as public for `latest`. Prebuilt archives and +checksums are available there if Go is unavailable. + +After installation, `loopexec demo` needs no model, API key, Python runner, +or network access. It creates a red local fixture, executes the real bounded +loop, repairs the fixture, observes an external green check, writes a receipt, +and replays the check fingerprint. This proves the execution/check/receipt/replay +contract, not agent intelligence. Its successful process exit is 0. + +No clone or hidden fixture is needed. The output gives the retained temporary +workdir and receipt path. For a known location, use `loopexec demo --workdir `; +it refuses to overwrite existing `.loopexec` or `status.txt` fixtures. + +## Run your own bounded loop + +In your own project, supply a real work command and a real check: + +```sh +loopexec run \ + --check "go test ./..." \ + --exec "make fix" \ + --max-iterations 10 +``` + +This example assumes that your project has Go tests and a `make fix` target +that performs one repair attempt. Replace them with your project's commands. +LoopExec runs the work command, then the check, until the check passes, a bound +trips, or execution fails. Halt reasons are computed from observed state. + +An external check exits 0 when green. A converged `loopexec run` exits **10**, +so shell/CI wrappers must handle that successful convergence class explicitly. +Use the reported receipt and run ID with `loopexec replay` and `loopexec report`; +see the [CLI guide](cli.md) for exact flags and halt classes. + +## Go deeper + +- [Capabilities](../SPEC.md#11-capability-status-normative-source-for-the-docs-matrix): shipped cores and planned sub-parts. +- [Workflows](workflows.md): deadlines, usage accounting, numeric progress and candidate restoration. +- [Integrations](integrations.md): adapter and operator responsibilities, including isolation hooks. +- [Shared model-call budgets](../examples/scoped-budget/README.md): call limits across run IDs. +- [SMALL](small-integration.md): optional state/audit integration. +- [Acceptance and evidence](acceptance-policy.md): retention, convergence and owner decisions. diff --git a/docs/integrations.md b/docs/integrations.md index d60a359..c9834c1 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -52,7 +52,7 @@ and the creative proof script. Hidden receipt files are included in proof artifacts; private signing keys and diagnostic tails are excluded. See [CLI reference](cli.md) for the full command surface. -For SMALL CLI v1.1.0 and collaborative v2 sessions, see the +For SMALL CLI v1.1.2 and collaborative v2 sessions, see the [SMALL integration guide](small-integration.md). The optional real-CLI proof keeps task acceptance separate from a command exit and preserves session attribution without introducing a second loop controller. diff --git a/docs/loop-contract.md b/docs/loop-contract.md index df21786..4bd0ea1 100644 --- a/docs/loop-contract.md +++ b/docs/loop-contract.md @@ -1,13 +1,14 @@ Historical design: this document specifies the planned SMALL v1 `task_list` topology. -That topology is not implemented by LoopExec v0.3.0. The shipped external-check +That topology remains unimplemented in current LoopExec source. The shipped external-check contract is [SPEC.md](../SPEC.md); current SMALL v1/v2 CLI composition is documented in [the SMALL integration guide](small-integration.md). The five-file and single-in-progress requirements below apply to this historical v1 design, not to SMALL v2 collaborative sessions. -# Loop Contract Specification +# Historical SMALL task-list design (planned) -This document defines the normative contract for loopexec execution loops. +This document records a proposed contract for the unimplemented SMALL task-list topology. +It does not govern the shipped external-check loop. Version: 1.0.0 diff --git a/docs/releases/v0.4.1.md b/docs/releases/v0.4.1.md index 6ff8e9e..4184756 100644 --- a/docs/releases/v0.4.1.md +++ b/docs/releases/v0.4.1.md @@ -1,5 +1,8 @@ # LoopExec v0.4.1 +Current source. [GitHub Releases](https://github.com/justyn-clark/loopexec/releases) +establishes publication status; these notes alone do not establish a release. + This patch improves evidence reporting and couples implementation publication to matching canonical documentation. LoopExec remains a general-purpose bounded agentic loop governor, not a project-specific quality certifier. @@ -36,16 +39,18 @@ state, handoff notes, logs and secrets are excluded. ## Installation and verification -After publication, with Go 1.26 or newer: +With Go 1.26 or newer, install the latest public stable release: ```sh -go install github.com/justyn-clark/loopexec/cmd/loopexec@v0.4.1 +go install github.com/justyn-clark/loopexec/cmd/loopexec@latest +loopexec --version loopexec version --json loopexec demo ``` +For a pinned install, substitute a public release tag for `latest`. Or select the OS/architecture archive from -[the release](https://github.com/justyn-clark/loopexec/releases/tag/v0.4.1), verify +[GitHub Releases](https://github.com/justyn-clark/loopexec/releases), verify `checksums.txt`, extract, and inspect `documentation/manifest.json`. The [Releases page](https://github.com/justyn-clark/loopexec/releases) is the authority for whether publication has completed; prepared notes are not proof. diff --git a/docs/small-integration.md b/docs/small-integration.md index cec367c..e106106 100644 --- a/docs/small-integration.md +++ b/docs/small-integration.md @@ -1,6 +1,11 @@ # SMALL integration -LoopExec v0.3.0 composes with SMALL CLI v1.1.0, supporting both v1 artifact +New canonical tracking must use relative paths or logical aliases, not absolute +host paths. The repository's [audit privacy gate](audit-privacy.md) enforces this +before publication; SMALL v1.1.2 strict alone does not. Preserve existing receipts +and exact command hashes rather than rewriting them to remove paths. + +Current LoopExec source composes with SMALL CLI v1.1.2, supporting both v1 artifact workspaces and explicitly migrated v2 session workspaces. SMALL is an optional external CLI, not a runtime dependency. diff --git a/docs/status-and-roadmap.md b/docs/status-and-roadmap.md index 2694170..da93054 100644 --- a/docs/status-and-roadmap.md +++ b/docs/status-and-roadmap.md @@ -1,27 +1,27 @@ # LoopExec status and roadmap -Snapshot verified October 7, 2026. This is a dated audit, not a live release feed. -This audit covers the general-purpose LoopExec governor and published `loopexec` -binary. The home project's quality verifier is one consumer, not its product scope. +Historical audit verified October 13, 2026, before v0.4.1 publication. Release and deployment status can change; +[GitHub Releases](https://github.com/justyn-clark/loopexec/releases) remains authoritative. +LoopExec is a general-purpose bounded loop governor. The documentation site +presents its contract; it does not host the runtime. ## Published and deployed | Surface | Verified status | Evidence | | --- | --- | --- | -| Latest published release | v0.4.0, September 26, 2026; public, not a draft or prerelease | [Release](https://github.com/justyn-clark/loopexec/releases/tag/v0.4.0) | -| Release artifacts | Six macOS/Linux/Windows amd64/arm64 archives plus SHA256 checksums | Release assets and [successful release workflow](https://github.com/justyn-clark/loopexec/actions/runs/36276149282) | -| Release commit | `c0e4cc2653b428faa2fceb6485f94a98aa3167d5` | Annotated v0.4.0 tag resolves to this commit | -| Published main | `4c96ad49d9d57199824fc44c7eeee8108d292dbd`; local main matched at audit start | [Main](https://github.com/justyn-clark/loopexec/tree/main) | -| Main CI | Passed on the exact published main commit | [CI run](https://github.com/justyn-clark/loopexec/actions/runs/36288545259) | -| Open GitHub backlog | No open issues or PRs at audit time; not evidence that the product has no remaining work | Repository issues/PR API reads | -| Public documentation | https://loopexec.dev is accessible; its homepage/changelog describe v0.2.0 | Public page read and matching source in the separate `loopexec.dev` repository | +| Source identity | v0.4.1, merged on main | [Source](https://github.com/justyn-clark/loopexec/tree/main), [notes](releases/v0.4.1.md) | +| Public release at audit | v0.4.0, September 26, 2026 | [Release](https://github.com/justyn-clark/loopexec/releases/tag/v0.4.0) | +| Release assets | Six macOS/Linux/Windows amd64/arm64 archives and SHA256 checksums | v0.4.0 release assets | +| Canonical documentation flow | Source-bound bundle imported by the site; official-origin readback required for each release source | [Continuity policy](documentation-continuity.md) | +| Hosted core gates | CI and non-publishing Release rehearsal passed on merged source `6b027d0` | [CI](https://github.com/justyn-clark/loopexec/actions/runs/37960688842), [rehearsal](https://github.com/justyn-clark/loopexec/actions/runs/37961116072) | +| Hosted site gate | Canonical Docs passed on docs source `595e4b0` | [CI](https://github.com/justyn-clark/loopexec.dev/actions/runs/38295817979) | +| Account enforcement | Required Linux/macOS CI checks enabled on core main, including administrators; force pushes and deletion disabled | GitHub branch-protection API, October 13, 2026 | -LoopExec ships as a locally installed CLI; the deployed website is documentation, -not a hosted LoopExec runtime. The docs repository is configured for Vercel, but -this audit did not verify the provider deployment ID or control-plane settings. -Linux/macOS CI exercises runtime behavior. Windows is cross-built; native -Windows runtime remains unverified. Asset metadata was checked live; the binary -archives were not downloaded again during this audit. +The live documentation manifest already describes source v0.4.1. This is a +documentation source identity, not evidence of a public binary release. +Every subsequent documentation change needs a new bundle and deployment +readback. Linux/macOS CI covers runtime behavior; Windows is cross-built +and native Windows runtime remains unverified. ## Available capabilities @@ -33,10 +33,9 @@ replay/attestation, halt explanations and subprocess deadlines remain available. See [SPEC section 11](../SPEC.md#11-capability-status-normative-source-for-the-docs-matrix) for the capability-level contract. -The current local patch improves reports, enforces code/docs continuity gates, +The v0.4.1 source improves reports, enforces code/docs continuity gates, packages source-bound docs and adds general-purpose [acceptance guidance](acceptance-policy.md). -Those changes are unreleased and do not alter v0.4.0 packages or existing consumer -installations. No new version, push, tag, deployment or publication was performed. +Those source changes do not alter existing v0.4.0 packages or consumer installations. The source and site integration are merged; v0.4.1 has not been published at this audit. An optional Jev semantic judge exists in a separate local `codex/jev-judge` worktree based on the older v0.3.0 development lineage. It is uncommitted and @@ -49,8 +48,8 @@ before considering it for a release. | Priority | Work | Acceptance evidence | | --- | --- | --- | -| P0 | Adopt canonical docs/build handshake in the site, then sync home/install/CLI/capability/changelog | Source-bound manifest/content readback, rendered-page checks and correct published/unreleased boundaries; publication remains blocked until matching | -| P0 | Commit continuity gates and require CI/account release protections | Exact-commit CI plus live branch/tag/release permission verification; local tests do not establish hosted enforcement | +| P0 | Keep each new canonical bundle and rendered site synchronized | Source-bound manifest/content readback, rendered-page checks and correct published/unreleased boundaries; publication remains blocked until matching | +| P0 | Require committed CI/account release protections | Exact-commit CI plus live branch/tag/release permission verification; local tests do not establish hosted enforcement | | P0 | Release these report fixes after review and CI | Exact-commit CI, offline proof receipts, packages/checksums and fresh install verification | | P1 | Migrate existing adapters from v0.3.0/custom budget guards deliberately | Historic and failed reservations preserved; no restored allowance; parity and concurrency checks before model launch | | P1 | Add general-purpose read-only objective history across run IDs | Objective/candidate/policy/evidence/decision/budget/promotion history; explicit owner revisions and missing-evidence warnings; no work or model calls | diff --git a/go.mod b/go.mod index f83650e..b8ac48c 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,5 @@ require github.com/spf13/cobra v1.8.1 require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 912390a..76db3b1 100644 --- a/go.sum +++ b/go.sum @@ -7,4 +7,5 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/scripts/audit-privacy-policy.json b/scripts/audit-privacy-policy.json new file mode 100644 index 0000000..236bae0 --- /dev/null +++ b/scripts/audit-privacy-policy.json @@ -0,0 +1,3 @@ +{ + "legacy_base": "6b027d00863534318c24d724238422f0a6d94be3" +} diff --git a/tools/audit-privacy/main.go b/tools/audit-privacy/main.go new file mode 100644 index 0000000..42376a4 --- /dev/null +++ b/tools/audit-privacy/main.go @@ -0,0 +1,190 @@ +// audit-privacy rejects new host paths without rewriting original receipts. +package main + +import ( + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "reflect" + "regexp" + "sort" + "strings" + + "gopkg.in/yaml.v3" +) + +var uri = regexp.MustCompile(`(?i)\b[a-z][a-z0-9+.-]*://[^\s\x22\x27\x60<>]+`) +var fileURI = regexp.MustCompile(`(?i)\bfile:`) +var hostRoot = regexp.MustCompile(`(?i)(^|[\s\x22\x27\x60=(:,\[])/(Users|home|private|tmp|var|Volumes|root|opt|usr|etc)/`) +var absolutePath = regexp.MustCompile("(?m)(^|[\\s\\x22\\x27\\x60=(:,\\[])(/[^/\\s\\x22\\x27\\x60;,)}\\]]|[a-zA-Z]:[/\\\\]|\\\\\\\\|//[^/\\s]|~[^/\\\\\\s\\x22\\x27\\x60]*[/\\\\])") + +func hostPath(value string) bool { + return fileURI.MatchString(value) || hostRoot.MatchString(value) || absolutePath.MatchString(uri.ReplaceAllString(value, "")) +} + +func newPaths(current, previous any, at string) []string { + if reflect.DeepEqual(current, previous) { + return nil + } + var found []string + switch value := current.(type) { + case string: + if hostPath(value) { + found = append(found, at) + } + case map[string]any: + old, _ := previous.(map[string]any) + keys := make([]string, 0, len(value)) + for key := range value { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + location := at + "." + key + if hostPath(key) { + location = at + "." + found = append(found, location) + } + found = append(found, newPaths(value[key], old[key], location)...) + } + case []any: + old, _ := previous.([]any) + for index, item := range value { + var before any + if index < len(old) { + before = old[index] + } + found = append(found, newPaths(item, before, fmt.Sprintf("%s[%d]", at, index))...) + } + default: + if current != nil && hostPath(fmt.Sprint(current)) { + found = append(found, at) + } + } + return found +} + +func inspect(path string, current, previous []byte) ([]string, error) { + if bytes.Equal(current, previous) { + return nil, nil + } + if strings.HasPrefix(path, ".small-command-proofs/") { + if previous != nil { + return nil, errors.New("historical command proof was modified") + } + if hostPath(string(current)) { + return []string{"command"}, nil + } + return nil, nil + } + var now, before map[string]any + if err := yaml.Unmarshal(current, &now); err != nil { + return nil, errors.New("invalid canonical YAML") + } + if previous != nil { + if err := yaml.Unmarshal(previous, &before); err != nil { + return nil, errors.New("invalid baseline YAML") + } + } + if path == ".small/progress.small.yml" { + old, _ := before["entries"].([]any) + entries, _ := now["entries"].([]any) + if len(entries) < len(old) { + return nil, errors.New("historical progress was removed") + } + for i := range old { + if !reflect.DeepEqual(entries[i], old[i]) { + return nil, errors.New("historical progress was modified") + } + } + } + return newPaths(now, before, "record"), nil +} + +func git(root string, args ...string) ([]byte, error) { + return exec.Command("git", append([]string{"-C", root}, args...)...).Output() +} + +func check(root, base string) error { + commit, err := git(root, "rev-parse", "--verify", "--end-of-options", base+"^{commit}") + if err != nil { + return errors.New("audit baseline is not a local commit") + } + base = strings.TrimSpace(string(commit)) + if _, err := git(root, "merge-base", "--is-ancestor", base, "HEAD"); err != nil { + return errors.New("audit baseline is not an ancestor") + } + paths, err := git(root, "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ".small", ".small-command-proofs") + if err != nil { + return errors.New("cannot enumerate canonical audit files") + } + baselinePaths, err := git(root, "ls-tree", "-r", "--name-only", "-z", base, "--", ".small", ".small-command-proofs") + if err != nil { + return errors.New("cannot enumerate historical audit files") + } + paths = append(paths, baselinePaths...) + seen, failed := map[string]bool{}, false + for _, path := range strings.Split(string(paths), "\x00") { + if path == "" || seen[path] { + continue + } + seen[path] = true + info, err := os.Lstat(filepath.Join(root, path)) + if err != nil || !info.Mode().IsRegular() || info.Size() > 4<<20 { + return errors.New("canonical audit file missing, linked or oversized") + } + current, err := os.ReadFile(filepath.Join(root, path)) + if err != nil { + return errors.New("cannot read canonical audit file") + } + previous, err := git(root, "show", base+":"+path) + if err != nil { + previous = nil + } + locations, err := inspect(path, current, previous) + if err != nil { + fmt.Fprintf(os.Stderr, "%s: %s\n", path, err) + failed = true + } + for _, location := range locations { + // Report only the record location, not the offending private value. + fmt.Fprintf(os.Stderr, "%s: %s: non-portable host path\n", path, location) + failed = true + } + } + if failed { + return errors.New("new audit records require relative paths or logical aliases") + } + return nil +} + +func main() { + root := flag.String("repo", ".", "repository to inspect") + base := flag.String("base", "", "explicit historical baseline for a local check") + flag.Parse() + if *base == "" { + data, err := os.ReadFile(filepath.Join(*root, "scripts/audit-privacy-policy.json")) + var policy struct { + LegacyBase string `json:"legacy_base"` + } + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + var extra any + if err != nil || decoder.Decode(&policy) != nil || decoder.Decode(&extra) != io.EOF || !regexp.MustCompile(`^[0-9a-f]{40}$`).MatchString(policy.LegacyBase) { + fmt.Fprintln(os.Stderr, "AUDIT_PRIVACY_FAILED: invalid explicit historical policy") + os.Exit(1) + } + *base = policy.LegacyBase + } + if err := check(*root, *base); err != nil { + fmt.Fprintln(os.Stderr, "AUDIT_PRIVACY_FAILED:", err) + os.Exit(1) + } + fmt.Println("AUDIT_PRIVACY_OK") +} diff --git a/tools/audit-privacy/main_test.go b/tools/audit-privacy/main_test.go new file mode 100644 index 0000000..e423121 --- /dev/null +++ b/tools/audit-privacy/main_test.go @@ -0,0 +1,132 @@ +package main + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestHostPath(t *testing.T) { + for _, value := range []string{ + "cd /Users/example/repo", "GOCACHE=/private/tmp/cache go test ./...", + "/home/example/repo", "(/var/tmp/output)", "'/opt/homebrew/bin/small'", + `C:\Users\example\repo`, `D:/temp/output`, `\\server\share\file`, + "file:///workspace/output", "~/private-file", "//server/share/file", + "~example/private-file", + "https://example.org/?path=/Users/example/repo", + } { + if !hostPath(value) { + t.Errorf("missed host path: %q", value) + } + } + for _, value := range []string{ + "go test ./...", "build/proofs/run.json", ".small-cache/logs/replay/commands/proof.txt", + "build/tmp/output", "examples/home/model.json", + "https://github.com/justyn-clark/loopexec/pull/7", "s3://public-bucket/object", + "$WORKSPACE/build/output", "[workspace]/build/output", "builder/checker", "score 4/5", + } { + if hostPath(value) { + t.Errorf("rejected portable value: %q", value) + } + } +} + +func TestCanonicalHistoryAndEncoding(t *testing.T) { + base := []byte("entries:\n - evidence: legacy /Users/example/work\n") + good := append(append([]byte{}, base...), []byte(" - evidence: checked build/proofs\n")...) + if hits, err := inspect(".small/progress.small.yml", good, base); err != nil || len(hits) != 0 { + t.Fatalf("preserved history rejected: %v %v", hits, err) + } + bad := append(append([]byte{}, base...), []byte(" - evidence: \"\\u002fUsers\\u002fexample\\u002fwork\"\n")...) + if hits, err := inspect(".small/progress.small.yml", bad, base); err != nil || len(hits) != 1 { + t.Fatalf("encoded new path not rejected: %v %v", hits, err) + } + if _, err := inspect(".small/progress.small.yml", []byte("entries: []\n"), base); err == nil { + t.Fatal("removed history accepted") + } + if _, err := inspect(".small/progress.small.yml", []byte("entries:\n - evidence: changed\n"), base); err == nil { + t.Fatal("rewritten history accepted") + } + if hits, err := inspect(".small/handoff.small.yml", []byte("summary: /Users/example/work\n"), []byte("summary: safe\n")); err != nil || len(hits) != 1 { + t.Fatal("handoff path accepted") + } + if _, err := inspect(".small/handoff.small.yml", []byte("summary: [\n"), nil); err == nil { + t.Fatal("invalid YAML accepted") + } +} + +func TestExactCommandProof(t *testing.T) { + path := ".small-command-proofs/logs/replay/commands/proof.txt" + legacy := []byte("cd /Users/example/work") + if hits, err := inspect(path, legacy, legacy); err != nil || len(hits) != 0 { + t.Fatal("unchanged legacy proof rejected") + } + if _, err := inspect(path, []byte("cd [workspace]"), legacy); err == nil { + t.Fatal("altered proof accepted") + } + if hits, err := inspect(path, legacy, nil); err != nil || len(hits) != 1 { + t.Fatal("new unsafe proof accepted") + } + if hits, err := inspect(path, []byte("go test ./..."), nil); err != nil || len(hits) != 0 { + t.Fatal("portable proof rejected") + } +} + +func TestPrivateKeysStayOutOfDiagnostics(t *testing.T) { + locations := newPaths(map[string]any{"/Users/example/private": "/home/example/work"}, nil, "record") + if len(locations) != 2 { + t.Fatalf("expected key and value violations: %v", locations) + } + for _, location := range locations { + if strings.Contains(location, "/Users/") || strings.Contains(location, "/home/") { + t.Fatal("private key exposed in diagnostic location") + } + } +} + +func TestChecksUntrackedFiles(t *testing.T) { + root := t.TempDir() + run := func(args ...string) string { + t.Helper() + cmd := exec.Command("git", append([]string{"-C", root}, args...)...) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git failed: %v %s", err, out) + } + return strings.TrimSpace(string(out)) + } + write := func(name, data string) { + t.Helper() + path := filepath.Join(root, name) + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(data), 0600); err != nil { + t.Fatal(err) + } + } + run("init", "--quiet") + run("config", "user.name", "Privacy fixture") + run("config", "user.email", "fixture@example.invalid") + write(".small/progress.small.yml", "entries:\n - evidence: legacy /Users/example/work\n") + run("add", ".small") + run("commit", "--quiet", "-m", "Legacy fixture") + base := run("rev-parse", "HEAD") + if err := check(root, base); err != nil { + t.Fatal(err) + } + write(".small-command-proofs/new.txt", "cd /Users/example/work") + if err := check(root, base); err == nil { + t.Fatal("untracked unsafe proof accepted") + } + write(".small-command-proofs/new.txt", "go test ./...") + if err := check(root, base); err != nil { + t.Fatal(err) + } + run("rm", ".small/progress.small.yml") + if err := check(root, base); err == nil { + t.Fatal("deleted historical progress accepted") + } +}