From 41f85c16742f35ea0f98a39e30fcce30bb280ba2 Mon Sep 17 00:00:00 2001 From: Decebal Dobrica <352761+decebal@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:17:34 +0100 Subject: [PATCH] feat(bootstrap): initialize repositories from installed cli Bundle workflow data, explicit verification policy, indexed documentation and host skills so an installed executable can prepare a repository safely. Preserve custom files, defer check-mode engine access and validate Chronis through its CLI. --- README.md | 10 +- docs/README.md | 7 + .../2026-10-24-repository-bootstrap-design.md | 51 +++ docs/reference/cli.md | 12 +- .../fresh-repository-delivery-request.md | 15 + ...-fresh-repository-delivery-verification.md | 79 ++++ ...10-24-repository-bootstrap-verification.md | 78 ++++ src/infrastructure/bootstrap.rs | 388 +++++++++++++++++ src/infrastructure/bootstrap_fs.rs | 189 +++++++++ src/infrastructure/mod.rs | 2 + src/infrastructure/runtime.rs | 9 - src/main.rs | 15 +- src/presentation.rs | 28 +- templates/SKILL.md | 18 + templates/allstack.md | 47 +++ templates/verification-rust.toml | 17 + tests/bootstrap.rs | 395 ++++++++++++++++++ 17 files changed, 1342 insertions(+), 18 deletions(-) create mode 100644 docs/plans/2026-10-24-repository-bootstrap-design.md create mode 100644 docs/reference/fresh-repository-delivery-request.md create mode 100644 docs/reports/2026-10-24-fresh-repository-delivery-verification.md create mode 100644 docs/reports/2026-10-24-repository-bootstrap-verification.md create mode 100644 src/infrastructure/bootstrap.rs create mode 100644 src/infrastructure/bootstrap_fs.rs create mode 100644 templates/SKILL.md create mode 100644 templates/allstack.md create mode 100644 templates/verification-rust.toml create mode 100644 tests/bootstrap.rs diff --git a/README.md b/README.md index d5ea8ac..cc2dc6e 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,18 @@ Install from a reviewed checkout with Rust 1.95 or later: ```sh cargo install --path . --locked allstack --help +allstack -C /path/to/target-repository init --check --profile rust +allstack -C /path/to/target-repository init --profile rust allstack -C /path/to/target-repository doctor ``` -The target needs an existing authorized feature branch, a GitHub `origin`, initialized Chronis, indexed documentation, workflow methods, and a frozen verification policy. Both coding hosts need their normal authentication and permissions. Follow the [CLI guide](docs/reference/cli.md) for setup and the complete Codex → Claude Code → Codex sequence. +`init` bundles workflow methods, indexed CLI documentation, repository-local Codex/Claude guidance, private-store ignores, and Chronis initialization. The explicit Rust profile requires `Cargo.toml` and `Cargo.lock`; an existing valid `--policy ` supports custom verification. Conflicting managed files are preserved and block writes. Reruns fill missing resources without replacing existing policies or repository instructions. Review initialized inputs before committing them. + +The [installed-bootstrap verification report](docs/reports/2026-10-24-repository-bootstrap-verification.md) records fresh setup with actual Chronis and generated-policy execution while source-checkout reads were denied. + +The [fresh-repository delivery report](docs/reports/2026-10-24-fresh-repository-delivery-verification.md) extends that proof through authenticated Codex → Claude Code → Codex handoffs, five executed acceptance tests, and a newly published [example PR](https://github.com/all-source-os/allstack-example/pull/1). + +Starting a run needs an existing authorized feature branch, a GitHub `origin`, clean indexed inputs, and a frozen verification policy. Both coding hosts need their normal authentication and permissions. `doctor` reads setup and executable availability without opening mutable run stores; it does not authenticate hosts. Follow the [CLI guide](docs/reference/cli.md) for setup and the complete Codex → Claude Code → Codex sequence. Each command holds an exclusive kernel lock on the local store. There is no persistent coordinator daemon or IPC service. Epoch checks fence Allstack-managed transitions; native tools used outside Allstack remain outside that boundary. diff --git a/docs/README.md b/docs/README.md index cf93a24..0d52ecb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,14 +2,21 @@ ## Design +- [2026-10-24: Installed CLI repository bootstrap](plans/2026-10-24-repository-bootstrap-design.md) — fresh-repository setup and installed-artifact verification. + - [2026-10-24: Cross-host plan → build → verify → ship](plans/2026-10-24-cross-host-workflow-design.md) — product boundary, component ownership, state transitions, recovery, and implementation sequence. ## Reference +- [Fresh repository authenticated delivery verification](reports/2026-10-24-fresh-repository-delivery-verification.md) — installed initialization, real Codex/Claude execution, five passing tests, new remote publication, and Chronis/Prime readback. + +- [Installed repository bootstrap verification](reports/2026-10-24-repository-bootstrap-verification.md) — source-denied installed executable, real Chronis, idempotent setup, and generated-policy execution. + - [Cross-host workflow verification](reports/2026-10-24-cross-host-workflow-verification.md) — authenticated host runs, exact-commit gates, recovery observations, and supported limits. - [First-release acceptance scenarios](reference/release-acceptance.md) — complete host journeys, failure cases, and required release evidence. - [Forward handoff request](reference/handoff-forward-request.md) and [reverse handoff request](reference/handoff-reverse-request.md) — real-host fixture requirements. +- [Fresh repository delivery request](reference/fresh-repository-delivery-request.md) — installed initialization followed by an authenticated host journey and exact-commit publication. - [CLI and recovery guide](reference/cli.md) — prerequisites, frozen verification policy, host handoffs, and publication recovery. - [Delivery workflow definition](../workflows/delivery.toml) — versioned workflow data and method references. - [Codex host guidance](../hosts/codex/SKILL.md) — managed Codex execution boundary. diff --git a/docs/plans/2026-10-24-repository-bootstrap-design.md b/docs/plans/2026-10-24-repository-bootstrap-design.md new file mode 100644 index 0000000..4d54252 --- /dev/null +++ b/docs/plans/2026-10-24-repository-bootstrap-design.md @@ -0,0 +1,51 @@ +# Installed CLI repository bootstrap + +Status: accepted implementation design on `feat/repository-bootstrap`. + +Confidence: rock. The existing runtime and actual host journeys are proven; the missing path is preparing a different repository from the installed executable without copying files from the Allstack checkout. + +## Outcome + +A developer installs Allstack, enters an existing Git repository, and runs `allstack init` with an explicit supported verification profile or an existing verification policy. The executable installs its bundled workflow methods and repository-local guidance for both hosts, indexes documentation, ignores private runtime stores, and initializes Chronis. `doctor` identifies remaining prerequisites. No hosted AllSource account or Allstack source checkout is required. + +Bootstrap never creates branches, commits, pushes, publishes, authenticates hosts, or installs global skills. Starting a managed run still requires the existing authority and branch checks. + +## Approach + +1. **Bundled Rust bootstrap command, recommended.** Ship authored templates with the executable using compile-time resources. Preflight every destination before writing. This reuses the installed CLI, path safety checks, bounded process supervisor, and existing workflow data. +2. **Downloadable template archive.** Keeps the binary smaller but requires a second artifact, version matching, and a separate conflict-safe installer. +3. **Documented manual copy.** Adds no runtime behavior but retains the missing installed-user path and leaves host-guidance links dependent on their original checkout layout. + +Use the first approach. Add no dependency or configuration layer unless the accepted behavior requires one. + +## Interface and ownership + +- `allstack init --check` returns the proposed files, missing prerequisites, and conflicts without repository writes or Chronis initialization. +- Applying initialization requires an explicit Rust/Cargo test profile or an existing valid verification policy. Do not guess a project's required checks or emit a test gate that cannot establish execution. +- Preserve existing custom policies and documentation. Refuse conflicting managed templates before applying the batch. Identical files are an idempotent success. +- Install workflow data and methods under `workflows/`, the CLI reference under indexed `docs/reference/`, and guidance under repository-local Codex and Claude skill directories. Installed links must resolve from those locations. +- Append only missing private-store ignore rules and documentation index entries. Preserve unrelated bytes and existing repository instructions. +- Initialize Chronis through `cn init --toon` only after successful preflight, and only when the project store is absent. Surface uncertain outcomes and support rerunning initialization. +- Report template version/digests and actual created, unchanged, appended, or conflicting paths. Do not record initialization as a successful delivery run. + +Presentation parses bootstrap arguments. Infrastructure owns bundled resources and filesystem/process effects. Bootstrap does not open the mutable run engine before preflight; dry-run process evidence must not create an unignored `.allstack/` inside the target. The existing run domain and transition history remain unchanged. + +`doctor` reports setup completeness separately from executable availability and authentication. Read actual files, policy validity, ignore behavior, Chronis presence, and component state. Executable version probes continue to mean availability only. + +## Safety and recovery + +Reject path traversal and symlink destinations. Preflight all managed paths, then use guarded atomic writes that cannot overwrite a file introduced after preflight. A partial filesystem or Chronis failure must name completed and remaining work; rerunning converges without overwriting user changes. Never add a force-overwrite or permission-bypass mode to make a fixture pass. + +The target may have existing documentation, ignore rules, and policies. Initialization must work there without replacing user-owned content. Configuration changes remain reviewable and uncommitted until the developer follows their repository's normal Git process. + +## Acceptance and proof + +- An executable installed into an isolated prefix initializes a fresh Rust fixture repository without access to the Allstack source tree at runtime. +- Check mode creates no repository files; conflicts preserve destination bytes and do not initialize Chronis. +- Applying initialization creates complete indexed resources, valid policy data, correctly located host guidance, ignored runtime storage, and a real Chronis project store. +- A second initialization is idempotent. A changed managed file is preserved and reported as a conflict. Symlink/path-escape attempts fail. +- `doctor` reports actual missing prerequisites before initialization and setup readiness after it, without claiming host authentication. +- Black-box tests invoke the real CLI and real Git/Chronis boundaries where available. Retain an indexed report with installed binary digest, exact commands, and observed results. +- Existing runtime tests, the four handoff fixture tests, formatting, Clippy, dependency checks, and Linux/macOS CI remain green on the one implementation commit. + +Deliver through one new PR from `feat/repository-bootstrap`, based on current `main`, after explicit branch permission. No release tag, package-registry publication, new catalog workflow, or merge is part of this continuation. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d91a1ed..6006727 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -8,15 +8,19 @@ Use one Unix machine with Git, GitHub CLI, Rust/Cargo, Chronis `cn`, Codex CLI, Install from the reviewed Allstack checkout with `cargo install --path . --locked`. The Rust package requires Rust 1.95 or later. `allstack --help` and each subcommand's `--help` expose the installed interface. +Put the installed executable directory on the `PATH` inherited by both coding hosts. A custom `cargo install --root ` needs `/bin` there as well. Keep the selected policy environment stable from verification through shipping. The [fresh-repository verification](../reports/2026-10-24-fresh-repository-delivery-verification.md) records the complete authenticated journey and actual destination readbacks. + Prepare the target repository before starting: 1. Use an existing feature branch whose creation the user authorized. Confirm its PR has not merged. Allstack does not create branches. -2. Initialize project task storage with `cn init --toon` when `.chronis/` does not exist. Allstack uses this repository's Chronis store through `cn --toon`. -3. Provide `docs/README.md`, a request document, `workflows/delivery.toml`, and the referenced `workflows/methods/plan.md` and `workflows/methods/build.md` documents. Use the versioned delivery definition and methods from this repository as the starting point. -4. Provide the verification policy described below. Commit the prepared inputs through the repository's normal authorized process so the starting tree is clean. Ignore `.allstack/` and machine-local Chronis storage; do not commit either store. +2. Inspect bundled setup with `allstack init --check --profile rust`, then apply with `allstack init --profile rust`. The explicit Rust profile requires `Cargo.toml` and `Cargo.lock`. For existing verification data, use `allstack init --policy ` instead. Check mode creates no target files or project store. +3. Initialization installs `workflows/delivery.toml`, referenced methods, `docs/reference/allstack.md`, its documentation index entry, and repository-local skills in `.agents/skills/allstack-delivery/` and `.claude/skills/allstack-delivery/`. It appends private-store ignores and initializes Chronis with `cn init --toon` only when the target store is absent. Existing custom policies, instructions, and documentation remain intact. Different managed resources are conflicts; resolve them explicitly before rerunning. +4. Review the policy described below against the repository's required checks. Add and index a request document. Commit the prepared inputs through the repository's normal authorized process so the starting tree is clean. Initialization does not run gates, create a branch, commit, publish, authenticate hosts, or install global skills. Partial failures name completed paths; inspect their effects before rerunning. There is no force-overwrite mode. 5. Ensure repository setup works in a new detached worktree. Gitignored dependencies and build output are not included in source snapshots. Prepare required local inputs explicitly before invoking managed work. -Run `allstack -C /path/to/target-repository doctor`. Its JSON reports the actual repository and state directory, local durability mode, coordinator model, Prime scope, and probed tool versions. A successful `--version` probe establishes executable availability; it does not establish authentication, model access, or host behavior. +Run `allstack -C /path/to/target-repository doctor` (add `--policy ` for a custom policy). Its JSON separates `setup_ready` from `executables_ready`, reports missing/conflicting resources, actual ignore behavior, tracked private files, Chronis engine readback, and tool versions. It does not open mutable AllSource/Prime run stores. A successful `--version` probe establishes executable availability; it does not establish authentication, model access, or host behavior. + +`init --check` returns `ready: false` with planned paths, digests, conflicts, and missing prerequisites when setup cannot be applied. Existing Chronis engine readback is explicitly deferred until apply; opening its engine could write storage metadata. Check-mode readiness covers resource and executable preflight, with that readback still outstanding. Applying failed preflight stops before writing managed resources or initializing Chronis. Symlinks, traversal, reserved policy destinations, and nonregular/multiply-linked files are refused. File creation cannot replace a destination introduced after preflight. Index/ignore updates append bytes after comparing the original contents. Concurrent edits can require inspection and a rerun. ## Frozen verification policy diff --git a/docs/reference/fresh-repository-delivery-request.md b/docs/reference/fresh-repository-delivery-request.md new file mode 100644 index 0000000..785356b --- /dev/null +++ b/docs/reference/fresh-repository-delivery-request.md @@ -0,0 +1,15 @@ +# Fresh repository delivery exercise + +Implement `handoff_receipt` in the initialized example repository's `src/lib.rs`: + +```rust +pub fn handoff_receipt(from: &str, to: &str) -> Result +``` + +Accept exactly `codex` and `claude-code`. A transfer must change hosts. Return `FROM -> TO` for either supported direction. Reject empty, unknown, unchanged, whitespace-padded, and differently cased host names. Do not normalize input. + +Keep the public signature and existing test assertions. Change only `src/lib.rs`; preserve the frozen verification policy, workflow methods, documentation, repository instructions, host skills, and dependency configuration. The coordinator owns Chronis, commits, branches, verification, and publication. Do not change those from a managed host call. Use normal host permissions without bypass flags. + +Acceptance: all five integration tests execute and pass through the generated `workflows/verification.toml`; formatting and whitespace gates pass on the exact delivered commit. Retained evidence must show authenticated Codex planning, an immutable handoff to Claude Code for implementation, a fresh Codex epoch recovering authoritative context, verified source, and a matching remote commit/open PR. Chronis task completion and source-linked Prime outcome must be read back after publication. + +The example begins with a failing valid-transfer assertion. A planning response, compilation, manually edited result, or zero selected tests cannot establish completion. diff --git a/docs/reports/2026-10-24-fresh-repository-delivery-verification.md b/docs/reports/2026-10-24-fresh-repository-delivery-verification.md new file mode 100644 index 0000000..1c01219 --- /dev/null +++ b/docs/reports/2026-10-24-fresh-repository-delivery-verification.md @@ -0,0 +1,79 @@ +# Fresh repository authenticated delivery verification + +Date: 2026-10-24. [Example repository](https://github.com/all-source-os/allstack-example), [delivered proof PR #1](https://github.com/all-source-os/allstack-example/pull/1), and [toolkit PR #3](https://github.com/all-source-os/allstack/pull/3). + +## Delivered result + +An installed Allstack executable initialized a new Rust repository, invoked authenticated Codex planning, handed the retained plan and immutable source to authenticated Claude Code, recovered the candidate in a fresh Codex epoch, verified its exact commit, pushed a new remote branch, and created an open proof PR. Chronis completion and an embedded Prime outcome were subsequently read back through their own tools. + +The delivered implementation accepts exactly the two supported host transfers and rejects unknown, empty, unchanged, whitespace-padded, and differently cased names. Claude changed only `src/lib.rs`; the five frozen integration tests remained byte-identical. The initial fixture executed four passing tests and one failing valid-transfer test. The delivered candidate executed all five successfully. + +| Evidence | Observed result | +| --- | --- | +| Installed executable SHA-256 | `4e794ece7b2cd8ec06a99e8914cf811af6d3808be6081167bdb9685159d4d3d7` | +| Managed run | `9f981bf4-cb9d-477e-8c5d-8864e1296834` | +| Host sequence | Codex epoch 1 → Claude Code epoch 2 → Codex epoch 3 | +| Example branch | `feat/handoff-receipt` | +| Exact verified and published commit | `ccb3f4fe96d7b8c3dda7c4e8f39a0f1aa381a540` | +| PR destination | `all-source-os/allstack-example#1`, open, one commit, head equal to verified SHA | +| Chronis project task | `t-f05a7f`, `done`, claimant `allstack:9f981bf4-cb9d-477e-8c5d-8864e1296834:codex:3` | +| AllSource history | 22 validated events; reopened final revision 22, stage `shipped`, no pending operation | +| Prime outcome | Delivery node `55fcbdde-dc83-4d85-96bc-c95389c8da8f`, key `:shipped`, source equal to proof PR URL | +| Cleanup | All three run-owned detached worktrees removed; example primary checkout clean | + +The executable is the same installed artifact qualified in the [bootstrap report](2026-10-24-repository-bootstrap-verification.md). This journey required no toolkit runtime change. Its evidence and reproduction request are versioned on toolkit PR #3. + +## Initialization and authenticated execution + +The example began as a new local Git repository with a dependency-free Cargo package, lockfile, stub function, and five acceptance tests. `init --check --profile rust` reported nine planned resources with no conflicts and created neither project store. Applying `init --profile rust` created those resources and initialized the target's actual Chronis store. Doctor returned `setup_ready: true`, `executables_ready: true`, and `chronis: readable`. + +The public GitHub repository started with a server-generated README/license commit. Its default `main` was fetched, and the explicitly authorized feature branch retained the local scaffold. No trunk push occurred. The forge was configured and read back as rebase-only. + +Codex CLI `0.163.0` reported a ChatGPT login. Claude Code `2.1.310` reported `loggedIn: true`, `authMethod: claude.ai`, and `apiProvider: firstParty`. Actual model calls then completed through the installed coordinator: + +| Host receipt | Retained observation | +| --- | --- | +| Codex planning | Exit 0, `turn.completed`, 15 recorded command-tool completions, 75,325 ms | +| Claude implementation | Exit 0, `subtype: success`, `is_error: false`, zero permission denials, 19,163 ms | +| Claude receipt model label | `claude-opus-4-8` | + +Codex planning receipt SHA-256: `f3d99fe919328525d64282d35b131d3b2f49da61a11399adb9d923eeaa1b0039`. Claude implementation receipt SHA-256: `05b8b0f1f74f121f846f5e58750026ecddaa3cf026499865a2852446455a2ba1`. Raw transcripts and account identity fields remain private. + +The initial Codex plan recorded that the custom installation prefix was absent from its `PATH`. The prefix's `bin` directory was added to the inherited environment before handoff and build. The accepted plan remained unchanged. Operators using a custom prefix must expose that executable directory to their hosts. Rust 1.95 and the selected environment were then held constant through verification and shipping. + +## Handoff, verification, and publication + +The plan was indexed at `docs/plans/allstack-.md` and retained with digest `0dceaacea7ddf27ccbde2acc2a6b8d545bda74e668763f585b62db97a9cfeedf`. The first handoff restored a distinct Claude worktree from checkpoint `d6f0388aaa06bd657a2069c94ff423cbbf85353066c31e92e66ae4b93953ca45` and advanced the Chronis claimant to epoch 2. + +After Claude edited the implementation, its checkpoint was `2493f542f03c42be47317a2a12cb7ea203b5e923922037322acae13b3c5fe639`. A second handoff restored that candidate in Codex epoch 3. A separate `resume` process returned authoritative request, plan, run state, and source-linked Prime recall. The incoming source was inspected, and its test-file digest was compared with the original frozen fixture. + +The unchanged generated policy digest was `9b39285fd803258d73450d1a2e1fcccd2a8f591dc53c4a684ec57431b80358a3`. Managed verification created the owned one-commit candidate and executed: + +| Gate | Actual result | Retained receipt digest | +| --- | --- | --- | +| `cargo fmt --check` | Exit 0 | `5d1fd303ccd458b9607b961aae6041a91ceebfe43b0dfdea9d8db7b822d50376` | +| `cargo test --locked` | Five integration tests passed, zero failed | `f6cb2cd058fe2b3d0b3c0af457bc755c2ff03d5c4519dd3c5b99e39e118940d8` | +| `git diff --check` | Exit 0 | `0855960220f15bd59fc22b2ab38462ca0839a480bed8b506d24206a8bbf0f53e` | + +The test output also contained zero-test library/doc units. The positive five-test integration summary supplied execution proof. The frozen test-file SHA-256 was `6dd68840773b48a07aafa1f219e68eb4347880a69d19e6ae56c1c2a8b55ec46f`. The delivered implementation SHA-256 was `e060c77c059cdd3fe70192ac0a4a0494ab3afda0d59175a60d18bd4fbc4885da`. + +Managed shipping performed a real new-branch push and real PR creation. Their retained process receipt digests were `234f66e65041b354de9e81e1869a29fdf7a0f999e262cb79470b38266fd2e21e` and `73d08c1685cc866f44ee6938f408e8bffa5294724cbfa7eaf03aca0b5a4224db`, both exit 0. Independent GitHub API reads confirmed the actual source bytes, open PR, one commit, and head SHA; `git ls-remote` confirmed the same branch SHA. + +Chronis was read from the example repository's project store, rather than the toolkit tracker. Embedded Prime was read through the example's `allstack recall` command, rather than the global Prime MCP store. Reopened run state confirmed `publication.task_completed: true` and `publication.memory_indexed: true`. + +## Inspect and reproduce + +Use the [fresh-repository request](../reference/fresh-repository-delivery-request.md), an installed executable on the inherited `PATH`, normal host authentication, and an explicitly authorized repository/feature branch. Follow the [CLI guide](../reference/cli.md). The [delivered example plan](https://github.com/all-source-os/allstack-example/blob/ccb3f4fe96d7b8c3dda7c4e8f39a0f1aa381a540/docs/plans/allstack-9f981bf4-cb9d-477e-8c5d-8864e1296834.md) retains the actual Codex response. + +With the retained example checkout and private stores: + +```sh +allstack -C /path/to/allstack-example history 9f981bf4-cb9d-477e-8c5d-8864e1296834 +allstack -C /path/to/allstack-example evidence 9f981bf4-cb9d-477e-8c5d-8864e1296834 +allstack -C /path/to/allstack-example recall 'verified commit' +cn show t-f05a7f --toon +``` + +Run the final Chronis command from the example checkout. A fresh clone contains public source and the indexed plan, while original private stores remain local. + +This proves a fresh installed setup through authenticated model execution, immutable host handoffs, final-commit verification, new remote publication, task completion, and source-linked outcome readback. Scope remains sequential hosts on one Unix machine. Merge, deployment, hosted synchronization, and fresh-fixture crash injection were not exercised. diff --git a/docs/reports/2026-10-24-repository-bootstrap-verification.md b/docs/reports/2026-10-24-repository-bootstrap-verification.md new file mode 100644 index 0000000..ccd5339 --- /dev/null +++ b/docs/reports/2026-10-24-repository-bootstrap-verification.md @@ -0,0 +1,78 @@ +# Installed repository bootstrap verification + +Date: 2026-10-24. Scope: `allstack init`, repository-local guidance, verification policy setup, and component inspection from an installed executable. [Design](../plans/2026-10-24-repository-bootstrap-design.md). [Implementation PR](https://github.com/all-source-os/allstack/pull/3). + +## Installed artifact and isolation + +The executable was installed into an isolated prefix using Rust 1.95 and `cargo install --path . --locked --debug --force --root /install`. Its SHA-256 matched the executable produced by the final local test build: + +```text +4e794ece7b2cd8ec06a99e8914cf811af6d3808be6081167bdb9685159d4d3d7 +``` + +The compiled bootstrap test harness was copied outside the checkout. The installed executable and copied harness ran from the qualification prefix under a macOS sandbox that denied reads beneath the entire Allstack checkout, including its implementation worktree. A direct attempt to read the checkout's `Cargo.toml` raised `PermissionError` before qualification. The fixture repository was created outside that denied tree. + +The sandbox profile allowed ordinary operations and added this restriction, with the actual checkout substituted for the placeholder: + +```scheme +(version 1) +(allow default) +(deny file-read* (subpath "/absolute/allstack-checkout")) +``` + +The selected test was `installed_binary_initializes_real_chronis_and_executes_generated_policy` in `tests/bootstrap.rs`. `ALLSTACK_TEST_BINARY` pointed at the installed executable. The invocation used the copied harness, rather than a Cargo process that would need to read the denied source tree: + +```sh +gtimeout -k 5 120 sandbox-exec -f /source-denied.sb \ + /bootstrap-tests --ignored --exact \ + installed_binary_initializes_real_chronis_and_executes_generated_policy --nocapture +``` + +The qualification environment put the user's installed host binary directory first on `PATH`. With inherited app precedence, the Claude version probe failed; its resolved absolute executable and the explicit installed-host precedence both returned `2.1.310 (Claude Code)`. No host authentication or permission settings were changed. + +## Observed complete job + +| Boundary | Observed result | +| --- | --- | +| `init --check --profile rust` | Ready; no target resources or project store created; existing source bytes unchanged | +| `init --profile rust` | Nine managed resources created and read back by digest; no conflicts or missing prerequisites | +| Chronis | Actual `cn 0.9.1` initialized the target `.chronis/`; `cn list --all --toon` read the project through its engine | +| Host guidance | Both repository-local skill links resolved to the installed CLI guide | +| Private storage | Git confirmed `.allstack/`, `.chronis/`, and `.prime/` ignored; setup did not create `.allstack/` | +| Doctor | `setup_ready: true`, `executables_ready: true`, `chronis: readable`, `stores_opened: false`, `authentication: not-probed` | +| Rerun | All nine resources unchanged; versioned bytes unchanged | +| Changed managed method | Initialization failed with a conflict and preserved its contents | +| Generated Rust policy | Formatting passed, Cargo executed one integration test, and whitespace checks passed | + +The installed resources were the delivery workflow, two method documents, CLI guide, two host skills, verification policy, `.gitignore`, and documentation index. The generated policy digest was `9b39285fd803258d73450d1a2e1fcccd2a8f591dc53c4a684ec57431b80358a3`. + +Retained output included: + +```text +GATE format success=true +GATE tests success=true +running 1 test +test installed ... ok +test result: ok. 1 passed; 0 failed +GATE diff success=true +test installed_binary_initializes_real_chronis_and_executes_generated_policy ... ok +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 9 filtered out +``` + +The complete private qualification log has SHA-256 `505e97fceacef47cd5033a0987c6fb88a8249604b7c36fde30e1cda5fd115a52`. It includes the actual bootstrap resource digests, doctor response, Cargo execution output, and selected harness result. Zero-test library/doc units were also present; the positive integration-test summary supplied execution proof. + +Doctor observed Codex CLI `0.163.0`, Claude Code `2.1.310`, Chronis `0.9.1`, Git `2.46.1`, GitHub CLI `2.101.0`, Cargo `1.97.1`, and rustc `1.97.1` on the qualified runtime `PATH`. The Allstack build and quality checks used the explicit Rust 1.95 toolchain; the generated fixture policy used that observed runtime Cargo/rustc pair. + +## Safety and regression evidence + +The default suite executed **29 tests**, all passing. Nine bootstrap tests cover real Git and executable process boundaries, with a temporary shell executable modeling only the Chronis protocol. They exercise existing instruction/index preservation, custom policy validation, effective ignore order, overridden ignores, dry-run engine deferral, preflight conflicts, missing prerequisites, traversal/symlink refusal, concurrent destination protection, and recovery after a failed Chronis initialization. The separate installed-artifact test above uses actual Chronis and is explicitly selected; the default suite's one skipped test is not counted as executed proof. + +The four existing forward/reverse handoff fixture tests also passed locally. Formatting, Clippy with warnings denied, and dependency policy passed. No dependency or lockfile change was required; existing duplicate-version and unused-license-allowance warnings remain advisory. + +[CI run 40124667431](https://github.com/all-source-os/allstack/actions/runs/40124667431) executed 29 tests plus the four handoff fixture tests on both Ubuntu 24.04 and macOS 14. Actual runner summaries were read from its logs. The source/dependency job passed formatting, Clippy, advisory, license, ban, and source checks. [PR checks](https://github.com/all-source-os/allstack/pull/3/checks) expose publication-SHA validation. CI visibility does not establish branch-protection enforcement. + +## Supported limits + +This qualifies fresh repository preparation and generated-policy execution. It does not establish host authentication, remote publication from the fresh fixture, hosted synchronization, or new model execution. The prior [cross-host verification report](2026-10-24-cross-host-workflow-verification.md) retains the actual Codex/Claude delivery evidence. + +Existing Chronis engine validation is deferred in check mode to preserve its no-write contract. Apply and doctor validate through `cn`. Interrupted writes can leave completed resources that require inspection and a rerun. File creation refuses replacement; appends preserve existing bytes and compare the preflight contents, but do not create an authenticated boundary against arbitrary native filesystem mutation. The supported process and host scope remains sequential execution on one Unix machine. diff --git a/src/infrastructure/bootstrap.rs b/src/infrastructure/bootstrap.rs new file mode 100644 index 0000000..0783043 --- /dev/null +++ b/src/infrastructure/bootstrap.rs @@ -0,0 +1,388 @@ +//! Repository setup runs before mutable workflow stores can be opened. +use super::{bootstrap_fs::Root, digest, private_dir, process}; +use crate::domain::Policy; +use anyhow::{Context, Result, ensure}; +use serde::Serialize; +use serde_json::{Value, json}; +use std::{ + fs, + path::{Path, PathBuf}, +}; + +const RESOURCES: &[(&str, &str)] = &[ + ( + "workflows/delivery.toml", + include_str!("../../workflows/delivery.toml"), + ), + ( + "workflows/methods/plan.md", + include_str!("../../workflows/methods/plan.md"), + ), + ( + "workflows/methods/build.md", + include_str!("../../workflows/methods/build.md"), + ), + ( + "docs/reference/allstack.md", + include_str!("../../templates/allstack.md"), + ), + ( + ".agents/skills/allstack-delivery/SKILL.md", + include_str!("../../templates/SKILL.md"), + ), + ( + ".claude/skills/allstack-delivery/SKILL.md", + include_str!("../../templates/SKILL.md"), + ), +]; +const RUST_POLICY: &str = include_str!("../../templates/verification-rust.toml"); +const IGNORE: &[&str] = &["/.allstack/", "/.chronis/", "/.prime/"]; +const INDEX: &str = "- [Allstack CLI and recovery](reference/allstack.md)"; + +pub struct Scratch(pub PathBuf); +impl Scratch { + pub fn new() -> Result { + let path = std::env::temp_dir().join(format!("allstack-setup-{}", uuid::Uuid::new_v4())); + fs::create_dir(&path)?; + private_dir(&path)?; + Ok(Self(path)) + } +} +impl Drop for Scratch { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +#[derive(Serialize)] +struct Change { + path: String, + status: String, + digest: String, + #[serde(skip)] + before: Option>, + #[serde(skip)] + bytes: Vec, +} + +fn managed(root: &Root, path: &str, bytes: &[u8], preserve: bool) -> Result { + let before = root.read(path)?; + let (status, bytes) = match &before { + None => ("created", bytes.to_vec()), + Some(old) if preserve || old == bytes => ("unchanged", old.clone()), + Some(_) => ("conflict", bytes.to_vec()), + }; + Ok(Change { + path: path.into(), + status: status.into(), + digest: digest(&bytes), + before, + bytes, + }) +} + +fn append(root: &Root, path: &str, entries: &[&str], prefix: &str) -> Result { + let before = root.read(path)?; + let mut bytes = before.clone().unwrap_or_else(|| prefix.as_bytes().to_vec()); + let text = std::str::from_utf8(&bytes).context("index/ignore must be UTF-8")?; + let missing: Vec<_> = entries + .iter() + .filter(|entry| !text.lines().any(|line| line == **entry)) + .collect(); + if !missing.is_empty() { + if !bytes.is_empty() && !bytes.ends_with(b"\n") { + bytes.push(b'\n'); + } + for entry in missing { + bytes.extend_from_slice(entry.as_bytes()); + bytes.push(b'\n'); + } + } + let status = if before.as_ref() == Some(&bytes) { + "unchanged" + } else if before.is_some() { + "appended" + } else { + "created" + }; + Ok(Change { + path: path.into(), + status: status.into(), + digest: digest(&bytes), + before, + bytes, + }) +} + +fn plan(root: &Root, profile: Option<&str>, policy: &str) -> Result<(Vec, Vec)> { + ensure!( + profile.is_none_or(|p| p == "rust"), + "unsupported profile; use rust or an existing policy" + ); + let mut files = RESOURCES + .iter() + .map(|(path, text)| managed(root, path, text.as_bytes(), false)) + .collect::>>()?; + ensure!( + !files.iter().any(|f| f.path == policy) + && ![".gitignore", "docs/README.md", "Cargo.toml", "Cargo.lock"].contains(&policy) + && !Path::new(policy) + .components() + .any(|c| c.as_os_str().to_str().is_some_and(|s| s.starts_with('.'))), + "policy path overlaps a managed or source input" + ); + let mut missing = Vec::new(); + if let Some(bytes) = root.read(policy)? { + let parsed: Policy = toml::from_str(std::str::from_utf8(&bytes)?)?; + parsed.validate().map_err(anyhow::Error::msg)?; + files.push(managed(root, policy, &bytes, true)?); + } else if profile == Some("rust") { + for path in ["Cargo.toml", "Cargo.lock"] { + if root.read(path)?.is_none() { + missing.push(format!("Rust profile requires {path}")); + } + } + files.push(managed(root, policy, RUST_POLICY.as_bytes(), false)?); + } else { + missing.push(format!( + "provide --profile rust or an existing valid policy at {policy}" + )); + } + files.push(append(root, ".gitignore", IGNORE, "")?); + files.push(append( + root, + "docs/README.md", + &[INDEX], + "# Documentation\n\n", + )?); + for path in [".allstack", ".prime", ".chronis"] { + root.directory(path)?; + } + Ok((files, missing)) +} + +fn call(scratch: &Scratch, repo: &Path, argv: &[&str]) -> Result { + process::checked(&scratch.0, repo, argv) +} + +fn repair_overridden_ignores(scratch: &Scratch, repo: &Path, files: &mut [Change]) -> Result<()> { + let outcome = process::run(process::Spec { + argv: [ + "git", + "check-ignore", + "--no-index", + "--", + ".allstack/bootstrap-probe", + ".chronis/bootstrap-probe", + ".prime/bootstrap-probe", + ] + .map(String::from) + .to_vec(), + cwd: repo.into(), + seconds: 30, + environment: Default::default(), + input: None, + directory: scratch.0.join("process"), + })?; + ensure!( + !outcome.timed_out && !outcome.owner_lost && matches!(outcome.code, Some(0 | 1)), + "cannot inspect private-store ignore state: {}", + outcome.stderr + ); + let change = files + .iter_mut() + .find(|f| f.path == ".gitignore") + .context("missing ignore plan")?; + for (rule, path) in IGNORE.iter().zip([ + ".allstack/bootstrap-probe", + ".chronis/bootstrap-probe", + ".prime/bootstrap-probe", + ]) { + let pending = &change.bytes[change.before.as_ref().map_or(0, Vec::len)..]; + if !outcome.stdout.lines().any(|line| line == path) + && !std::str::from_utf8(pending)? + .lines() + .any(|line| line == *rule) + { + if !change.bytes.ends_with(b"\n") { + change.bytes.push(b'\n'); + } + change.bytes.extend_from_slice(rule.as_bytes()); + change.bytes.push(b'\n'); + change.status = if change.before.is_some() { + "appended" + } else { + "created" + } + .into(); + change.digest = digest(&change.bytes); + } + } + Ok(()) +} + +pub fn initialize(repo: &Path, check: bool, profile: Option<&str>, policy: &str) -> Result { + let root = Root::new(repo)?; + let scratch = Scratch::new()?; + let (mut files, mut missing) = plan(&root, profile, policy)?; + repair_overridden_ignores(&scratch, repo, &mut files)?; + if call(&scratch, repo, &["cn", "--version", "--toon"]).is_err() { + missing.push("Chronis cn executable unavailable".into()); + } + let tracked = call( + &scratch, + repo, + &["git", "ls-files", "--", ".allstack", ".chronis", ".prime"], + )?; + if !tracked.is_empty() { + missing.push("private store files are tracked; resolve before initialization".into()); + } + let chronis_exists = root.directory(".chronis")?; + if !check && chronis_exists && call(&scratch, repo, &["cn", "list", "--all", "--toon"]).is_err() + { + missing.push("existing Chronis store unreadable through cn".into()); + } + let conflicts: Vec<_> = files + .iter() + .filter(|f| f.status == "conflict") + .map(|f| &f.path) + .collect(); + let ready = missing.is_empty() && conflicts.is_empty(); + let mut report = json!({"template_version":env!("CARGO_PKG_VERSION"), "repository":repo, + "check":check,"ready":ready,"applied":false,"files":files,"missing":missing,"conflicts":conflicts, + "chronis":if chronis_exists && check { "existing; engine readback deferred until apply" } else if chronis_exists { "existing" } else { "initialize" }}); + if check { + return Ok(report); + } + ensure!(ready, "bootstrap preflight failed: {report}"); + let mut completed = Vec::new(); + for change in &files { + let result = match change.status.as_str() { + "created" => root.create(&change.path, &change.bytes), + "appended" => root.append( + &change.path, + change.before.as_deref().unwrap_or_default(), + &change.bytes[change.before.as_ref().map_or(0, Vec::len)..], + ), + _ => root.read(&change.path).and_then(|actual| { + ensure!( + actual == change.before, + "destination changed after preflight: {}; completed: {completed:?}", + change.path + ); + Ok(()) + }), + }; + result.with_context(|| { + format!( + "bootstrap failed at {}; completed: {completed:?}; rerun after inspection", + change.path + ) + })?; + completed.push(change.path.clone()); + } + if !chronis_exists { + ensure!( + !root.directory(".chronis")?, + "Chronis store appeared after preflight; inspect and rerun; completed: {completed:?}" + ); + call(&scratch, repo, &["cn", "init", "--toon"]).with_context(|| { + format!("Chronis initialization uncertain; completed: {completed:?}; inspect and rerun") + })?; + } + ensure!( + root.directory(".chronis")?, + "cn did not initialize the target repository; completed: {completed:?}" + ); + call(&scratch, repo, &["cn", "list", "--all", "--toon"]).with_context(|| { + format!("Chronis readback failed; completed: {completed:?}; inspect and rerun") + })?; + check_ignored(&scratch, repo).with_context(|| { + format!("private-store ignore readback failed; completed: {completed:?}; inspect and rerun") + })?; + for change in &files { + ensure!( + root.read(&change.path) + .with_context(|| format!( + "readback failed; completed: {completed:?}; inspect and rerun" + ))? + .as_deref() + == Some(change.bytes.as_slice()), + "destination readback changed: {}; completed: {completed:?}; inspect and rerun", + change.path + ); + } + report["applied"] = json!(true); + report["chronis"] = json!(if chronis_exists { + "existing" + } else { + "initialized" + }); + Ok(report) +} + +fn check_ignored(scratch: &Scratch, repo: &Path) -> Result<()> { + for path in [ + ".allstack/bootstrap-probe", + ".chronis/bootstrap-probe", + ".prime/bootstrap-probe", + ] { + let actual = call( + scratch, + repo, + &["git", "check-ignore", "--no-index", "--", path], + )?; + ensure!( + actual.trim() == path, + "private store is not ignored: {path}" + ); + } + Ok(()) +} + +pub fn doctor(repo: &Path, policy: &str) -> Result { + let root = Root::new(repo)?; + let scratch = Scratch::new()?; + let (files, mut missing) = plan(&root, None, policy)?; + for f in &files { + if f.status != "unchanged" { + missing.push(format!("{}: {}", f.path, f.status)); + } + } + let chronis = if root.directory(".chronis")? { + match call(&scratch, repo, &["cn", "list", "--all", "--toon"]) { + Ok(_) => "readable", + Err(_) => { + missing.push("Chronis store unreadable through cn".into()); + "unreadable" + } + } + } else { + missing.push("Chronis project store absent".into()); + "absent" + }; + if check_ignored(&scratch, repo).is_err() { + missing.push("private stores are not all ignored".into()); + } + let tracked = call( + &scratch, + repo, + &["git", "ls-files", "--", ".allstack", ".chronis", ".prime"], + )?; + if !tracked.is_empty() { + missing.push("private store files are tracked".into()); + } + let tools: Vec<_> = ["codex", "claude", "cn", "git", "gh", "cargo", "rustc"].iter().map(|name| { + let args = if *name == "cn" { vec![*name,"--version","--toon"] } else { vec![*name,"--version"] }; + let result = call(&scratch, repo, &args); + json!({"tool":name,"available":result.is_ok(),"version":result.ok().map(|s| s.lines().next().unwrap_or("").to_string())}) + }).collect(); + let available = tools.iter().all(|t| t["available"] == true); + Ok( + json!({"repository":repo,"store":repo.join(".allstack"),"setup_ready":missing.is_empty(), + "executables_ready":available,"authentication":"not-probed","missing":missing,"files":files,"chronis":chronis, + "stores_opened":false,"allsource":"embedded; fsync-per-event when run storage opens", + "prime":"embedded graph; lexical recall; local-only; not opened by doctor", + "coordinator":"exclusive-kernel-lock-per-run-command","host_scope":"sequential-same-machine","tools":tools}), + ) +} diff --git a/src/infrastructure/bootstrap_fs.rs b/src/infrastructure/bootstrap_fs.rs new file mode 100644 index 0000000..709012d --- /dev/null +++ b/src/infrastructure/bootstrap_fs.rs @@ -0,0 +1,189 @@ +//! Descriptor-relative writes keep bootstrap destinations behind symlink checks. +use anyhow::{Context, Result, ensure}; +use std::{ + ffi::CString, + fs::File, + io::{Read, Seek, Write}, + os::{ + fd::{AsRawFd, FromRawFd}, + unix::fs::MetadataExt, + }, + path::{Component, Path}, +}; + +pub struct Root(File); + +fn open(parent: &File, name: &str, flags: i32, mode: u32) -> std::io::Result { + let name = CString::new(name)?; + let fd = unsafe { + libc::openat( + parent.as_raw_fd(), + name.as_ptr(), + flags | libc::O_NOFOLLOW | libc::O_CLOEXEC, + mode, + ) + }; + if fd < 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(unsafe { File::from_raw_fd(fd) }) + } +} + +impl Root { + pub fn new(path: &Path) -> Result { + Ok(Self(File::open(path)?)) + } + + fn parent(&self, path: &str, create: bool) -> Result> { + let mut parts = Vec::new(); + for part in Path::new(path).components() { + match part { + Component::Normal(s) => parts.push(s.to_str().context("non-UTF8 destination")?), + _ => anyhow::bail!( + "destination must be repository-relative without traversal: {path}" + ), + } + } + let leaf = parts.pop().context("empty destination")?.to_string(); + let mut dir = self.0.try_clone()?; + for name in parts { + match open(&dir, name, libc::O_RDONLY | libc::O_DIRECTORY, 0) { + Ok(child) => dir = child, + Err(e) if e.kind() == std::io::ErrorKind::NotFound && create => { + let name_c = CString::new(name)?; + let result = unsafe { libc::mkdirat(dir.as_raw_fd(), name_c.as_ptr(), 0o755) }; + if result != 0 { + let error = std::io::Error::last_os_error(); + ensure!( + error.kind() == std::io::ErrorKind::AlreadyExists, + "create directory {name}: {error}" + ); + } + dir.sync_all()?; + dir = open(&dir, name, libc::O_RDONLY | libc::O_DIRECTORY, 0)?; + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(e) => { + return Err(e).with_context(|| format!("unsafe destination directory {path}")); + } + } + } + Ok(Some((dir, leaf))) + } + + pub fn read(&self, path: &str) -> Result>> { + let Some((parent, leaf)) = self.parent(path, false)? else { + return Ok(None); + }; + let mut file = match open(&parent, &leaf, libc::O_RDONLY | libc::O_NONBLOCK, 0) { + Ok(f) => f, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(e) => return Err(e).with_context(|| format!("unsafe destination {path}")), + }; + let metadata = file.metadata()?; + ensure!( + metadata.is_file() && metadata.nlink() == 1 && metadata.len() <= 4 * 1024 * 1024, + "destination must be a single regular file below 4 MiB: {path}" + ); + let mut bytes = Vec::new(); + (&mut file) + .take(4 * 1024 * 1024 + 1) + .read_to_end(&mut bytes)?; + ensure!( + bytes.len() <= 4 * 1024 * 1024, + "destination grew beyond limit: {path}" + ); + Ok(Some(bytes)) + } + + pub fn directory(&self, path: &str) -> Result { + let Some((parent, leaf)) = self.parent(path, false)? else { + return Ok(false); + }; + match open(&parent, &leaf, libc::O_RDONLY | libc::O_DIRECTORY, 0) { + Ok(_) => Ok(true), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false), + Err(e) => Err(e).with_context(|| format!("unsafe directory {path}")), + } + } + + pub fn create(&self, path: &str, bytes: &[u8]) -> Result<()> { + let (parent, leaf) = self + .parent(path, true)? + .context("missing destination parent")?; + let temporary = format!(".allstack-init-{}", uuid::Uuid::new_v4()); + let temporary_c = CString::new(temporary.as_str())?; + let leaf_c = CString::new(leaf)?; + let result: Result<()> = (|| { + let mut file = open( + &parent, + &temporary, + libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL, + 0o644, + )?; + file.write_all(bytes)?; + file.sync_all()?; + // linkat publishes complete bytes and refuses every existing destination. + let code = unsafe { + libc::linkat( + parent.as_raw_fd(), + temporary_c.as_ptr(), + parent.as_raw_fd(), + leaf_c.as_ptr(), + 0, + ) + }; + ensure!( + code == 0, + "destination appeared during initialization: {path}: {}", + std::io::Error::last_os_error() + ); + parent.sync_all()?; + Ok(()) + })(); + let removed = unsafe { libc::unlinkat(parent.as_raw_fd(), temporary_c.as_ptr(), 0) }; + result?; + ensure!( + removed == 0, + "temporary link cleanup failed for {path}: {}", + std::io::Error::last_os_error() + ); + parent.sync_all()?; + Ok(()) + } + + pub fn append(&self, path: &str, expected: &[u8], suffix: &[u8]) -> Result<()> { + let (parent, leaf) = self + .parent(path, false)? + .context("destination parent disappeared")?; + let mut file = open( + &parent, + &leaf, + libc::O_RDWR | libc::O_APPEND | libc::O_NONBLOCK, + 0, + )?; + ensure!( + unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } == 0, + "destination busy: {path}" + ); + let metadata = file.metadata()?; + ensure!( + metadata.is_file() && metadata.nlink() == 1 && metadata.len() == expected.len() as u64, + "destination changed during initialization: {path}" + ); + let mut actual = Vec::new(); + file.rewind()?; + (&mut file) + .take(expected.len() as u64 + 1) + .read_to_end(&mut actual)?; + ensure!( + actual == expected, + "destination changed during initialization: {path}" + ); + file.write_all(suffix)?; + file.sync_all()?; + parent.sync_all()?; + Ok(()) + } +} diff --git a/src/infrastructure/mod.rs b/src/infrastructure/mod.rs index 16da922..4549a76 100644 --- a/src/infrastructure/mod.rs +++ b/src/infrastructure/mod.rs @@ -1,3 +1,5 @@ +pub mod bootstrap; +pub mod bootstrap_fs; pub mod build_lock; pub mod chronis; pub mod forge; diff --git a/src/infrastructure/runtime.rs b/src/infrastructure/runtime.rs index 9713cc6..4feb64a 100644 --- a/src/infrastructure/runtime.rs +++ b/src/infrastructure/runtime.rs @@ -75,15 +75,6 @@ impl Runtime { } Ok(digest(&serde_json::to_vec(&data)?)) } - pub fn doctor(&self) -> Value { - let tools: Vec<_> = ["codex", "claude", "cn", "git", "gh"].iter().map(|name| { - let result = process::checked(&self.state, &self.repo, &[name, "--version"]); - json!({"tool":name,"available":result.is_ok(),"version":result.ok().map(|s| s.lines().next().unwrap_or("").to_string())}) - }).collect(); - json!({"repository":self.repo,"store":self.state,"durability":"fsync-per-event", - "coordinator":"exclusive-kernel-lock-per-command","prime":"embedded graph; lexical recall; local-only","prime_available":self.memory.is_some(), - "host_scope":"sequential-same-machine","tools":tools}) - } } impl Platform for Runtime { diff --git a/src/main.rs b/src/main.rs index 0041429..2a6f37c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use allstack::infrastructure::{ + bootstrap::Scratch, git, process::{self, Spec}, }; @@ -22,6 +23,15 @@ async fn main() { let cli = allstack::presentation::Cli::parse(); (|| -> anyhow::Result { let repo = git::root(&cli.repository)?; + let scratch = if matches!( + cli.command, + allstack::presentation::Action::Init { .. } + | allstack::presentation::Action::Doctor { .. } + ) { + Some(Scratch::new()?) + } else { + None + }; let mut argv = vec![ std::env::current_exe()?.to_string_lossy().into_owned(), "__operation".into(), @@ -33,7 +43,10 @@ async fn main() { seconds: 290, environment: BTreeMap::new(), input: None, - directory: repo.join(".allstack/process"), + directory: scratch + .as_ref() + .map(|s| s.0.join("process")) + .unwrap_or_else(|| repo.join(".allstack/process")), })?; anyhow::ensure!( output.success(), diff --git a/src/presentation.rs b/src/presentation.rs index da12e13..14a003d 100644 --- a/src/presentation.rs +++ b/src/presentation.rs @@ -4,7 +4,7 @@ use crate::{ ports::{Platform, Start}, }, domain::Host, - infrastructure::{git, process, runtime::Runtime}, + infrastructure::{bootstrap, git, process, runtime::Runtime}, }; use anyhow::{Context, Result, ensure}; use clap::{Parser, Subcommand}; @@ -22,7 +22,18 @@ pub struct Cli { #[derive(Subcommand)] pub enum Action { - Doctor, + Doctor { + #[arg(long, default_value = "workflows/verification.toml")] + policy: String, + }, + Init { + #[arg(long)] + check: bool, + #[arg(long, value_parser = ["rust"])] + profile: Option, + #[arg(long, default_value = "workflows/verification.toml")] + policy: String, + }, Start { #[arg(long)] request: String, @@ -99,6 +110,15 @@ pub enum Action { pub async fn execute(cli: Cli) -> Result { let repo = git::root(&cli.repository)?; + match &cli.command { + Action::Init { + check, + profile, + policy, + } => return bootstrap::initialize(&repo, *check, profile.as_deref(), policy), + Action::Doctor { policy } => return bootstrap::doctor(&repo, policy), + _ => {} + } let runtime = Runtime::open(&repo).await?; let result = dispatch(&runtime, cli.command).await; let shutdown = runtime.shutdown().await; @@ -109,7 +129,9 @@ pub async fn execute(cli: Cli) -> Result { async fn dispatch(p: &Runtime, action: Action) -> Result { let run = match action { - Action::Doctor => return Ok(p.doctor()), + Action::Doctor { .. } | Action::Init { .. } => { + unreachable!("setup commands dispatch before opening run storage") + } Action::Start { request, policy, diff --git a/templates/SKILL.md b/templates/SKILL.md new file mode 100644 index 0000000..03d80bb --- /dev/null +++ b/templates/SKILL.md @@ -0,0 +1,18 @@ +--- +name: allstack-delivery +description: Continue an Allstack plan, build, verify, and ship run across Codex CLI and Claude Code with retained evidence and explicit handoff. +--- + +# Allstack delivery + +Read repository instructions and the [installed CLI guide](../../../docs/reference/allstack.md). Use the installed `allstack` executable and the original repository as `-C`; edit only the active detached worktree returned by the run. + +Run `status ` and `resume --epoch ` before continuing. Read the request, indexed plan, source-linked Prime context, current stage, workspace, and pending operation. Inspect recalled sources before treating them as requirements. The coordinator owns the Chronis claim; do not change it independently. + +Use `plan` and `build` with the current epoch to invoke the selected host. Follow its ordinary authentication, repository rules, and permission controls. Never add bypass flags. Follow the frozen request and plan; explicitly stage intended new files before checkpointing. Do not commit, push, create branches, alter run state, or change verification policy from a managed build. + +Use `handoff --epoch --to codex|claude-code` for a sequential transfer. Read the returned epoch and workspace, then stop editing the outgoing workspace. Native tools remain outside epoch enforcement. + +After failure, inspect `status`, `history`, active edits, and private process evidence. Use `recover` for uncertain host/handoff work only after inspection. Recovery preserves edits without replaying the uncertain call. Use `verify` or `ship` for their destination-specific pending operations. + +Run `verify --epoch --message ` only under trusted user commit authority. Require positive executed Cargo/nextest test counts on the exact candidate commit. Run `ship --epoch --title ` only under trusted publication authority. Confirm remote/PR readbacks, Chronis completion, and Prime outcome before reporting completion. Merge and deployment are separate actions. Keep private stores, transcripts, credentials, and snapshots out of public documents. diff --git a/templates/allstack.md b/templates/allstack.md new file mode 100644 index 0000000..913019d --- /dev/null +++ b/templates/allstack.md @@ -0,0 +1,47 @@ +# Allstack CLI and recovery + +This repository uses the installed Allstack implementation preview. AllSource Core retains local workflow history, Chronis owns tasks and claims, and embedded Prime supplies source-linked lexical recall. Hosts execute sequentially on one Unix machine. No hosted account or Allstack source checkout is needed. + +## Repository setup + +Run `allstack init --check --profile rust` to inspect proposed changes without writing repository files. Apply with `allstack init --profile rust`. The Rust profile requires `Cargo.toml` and `Cargo.lock`; it installs formatting, positive executed Cargo tests, and whitespace checks. Review the policy against this repository's acceptance requirements before committing it. Initialization creates no execution proof. + +For a custom stack, supply an existing policy with `allstack init --policy workflows/custom.toml`. The policy needs at least one Cargo/nextest executed-test gate with a success marker and command deadlines within 300 seconds. Zero selected tests and compilation alone are inconclusive. Mark every compiling gate `compile = true`; Allstack checks competing compilers and serializes managed builds. Unsupported test-runner output remains inconclusive. + +Initialization preserves existing policies and repository instructions. Different managed files are conflicts. Ignore/index additions preserve existing bytes; reruns fill only missing resources. Check mode defers existing Chronis engine readback until apply because opening that engine can write storage metadata. Its readiness covers resource and executable preflight. A failure may leave named completed writes; inspect the result and rerun after resolving the cause. No force-overwrite mode is provided. + +Both host skills are repository-local under `.agents/skills/allstack-delivery/` and `.claude/skills/allstack-delivery/`. Host discovery and authentication follow each host's ordinary setup. Run `allstack doctor` to inspect setup completeness and executable availability. Version probes do not prove authentication or successful model execution. Doctor reads Chronis through `cn`; it does not create a run or open the mutable AllSource/Prime stores. + +Review and commit initialized inputs through the repository's normal process. Start from an existing, explicitly authorized feature branch with a GitHub `origin` and clean working tree. Check its PR is live. Prepare ignored dependencies and build output in new detached worktrees when needed. Initialization does not create branches or authorize commits/publication. + +## Complete delivery flow + +Create and index a request document with acceptance criteria and required evidence. Use the original repository as the CLI target throughout; substitute the returned run ID and current epoch at every step. + +```sh +allstack -C /path/to/repository start --request docs/requests/change.md \ + --policy workflows/verification.toml --host codex --allow-commit --allow-publish +allstack -C /path/to/repository plan <run> --epoch 1 +allstack -C /path/to/repository handoff <run> --epoch 1 --to claude-code +allstack -C /path/to/repository resume <run> --epoch 2 +allstack -C /path/to/repository build <run> --epoch 2 +allstack -C /path/to/repository handoff <run> --epoch 2 --to codex +allstack -C /path/to/repository verify <run> --epoch 3 --message "feat(example): implement accepted change" +allstack -C /path/to/repository ship <run> --epoch 3 --title "feat(example): implement accepted change" +``` + +Pass authority flags only when trusted user instructions authorize those actions. Reverse the hosts by starting with `--host claude-code` and handing off to `codex`. Allstack creates and reads back the Chronis claim. The incoming host receives the retained plan, request, immutable snapshot, and source-linked context. It does not need a copied conversation. + +Edit only the active worktree from `status` or `resume`. Explicitly stage each intended new file there with `git add -- <path>`, then use `checkpoint <run> --epoch <epoch>`. Untracked files block snapshots. Snapshots reject symlinks, recognized sensitive filenames, files above 4 MiB, totals above 64 MiB, and more than 10,000 files. Filename checks do not detect every secret. + +## Recovery and evidence + +Inspect `status <run>`, `history <run>`, active workspace, and private process receipts before retrying failures. An unresolved operation blocks managed execution. `recover <run> --epoch <epoch>` reconciles task setup, host calls, handoffs, and pending plan indexing. It preserves host edits without replaying uncertain calls; an absent accepted plan still requires deliberate planning. Always use the epoch returned after recovery. + +Repeat `verify` for pending commit/verification work. It materializes the accepted checkpoint into the original checkout, creates or amends the authorized owned commit, and runs frozen gates on that exact SHA. Do not edit or switch the original checkout while it runs. Changes to candidate content, workflow, methods, policy, or bound environment invalidate verification. + +Repeat `ship` for pending publication/task/outcome work. It reads the destination before retrying, confirms the exact remote commit and open PR head, completes Chronis, indexes Prime, and removes only unchanged run-owned worktrees. Unexpected edits preserve a workspace for inspection. Ship means one feature-branch commit and an open PR; merge and deployment remain separate. + +Use `evidence <run>` for digest-validated gate receipts and `recall <query>` for local source-linked Prime context. Recall cannot authorize actions or certify checks. Review excerpts before publishing evidence in indexed repository documents. + +`.allstack/`, `.chronis/`, and `.prime/` are private ignored stores. Read them through their engines or CLI tools, never by scraping WAL segments. Allstack holds an exclusive store lock per command, fsyncs workflow events, and owns/kills/reaps child processes within bounded deadlines. Epochs fence managed transitions; native tools remain outside that boundary. Missing objects, unknown host effects, or claim mismatches require inspection and reconciliation. diff --git a/templates/verification-rust.toml b/templates/verification-rust.toml new file mode 100644 index 0000000..6d5d47b --- /dev/null +++ b/templates/verification-rust.toml @@ -0,0 +1,17 @@ +command_seconds = 120 +environment = ["PATH", "RUSTUP_TOOLCHAIN", "RUSTFLAGS", "RUSTC_WRAPPER", "CARGO_TARGET_DIR"] + +[[gates]] +name = "format" +argv = ["cargo", "fmt", "--check"] + +[[gates]] +name = "tests" +argv = ["cargo", "test", "--locked"] +success_marker = "test result: ok." +test = true +compile = true + +[[gates]] +name = "diff" +argv = ["git", "diff", "--check"] diff --git a/tests/bootstrap.rs b/tests/bootstrap.rs new file mode 100644 index 0000000..2690e18 --- /dev/null +++ b/tests/bootstrap.rs @@ -0,0 +1,395 @@ +use allstack::infrastructure::bootstrap_fs::Root; +use serde_json::Value; +use std::{ + fs, + os::unix::fs::{PermissionsExt, symlink}, + path::{Path, PathBuf}, + process::{Child, Command, Stdio}, + time::{Duration, Instant}, +}; + +struct Owned(Child); +impl Drop for Owned { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } +} + +fn bounded(mut command: Command) -> (bool, String, String) { + let output = tempfile::tempdir().unwrap(); + let mut child = Owned( + command + .stdout(fs::File::create(output.path().join("out")).unwrap()) + .stderr(fs::File::create(output.path().join("err")).unwrap()) + .stdin(Stdio::null()) + .spawn() + .unwrap(), + ); + let deadline = Instant::now() + Duration::from_secs(45); + let status = loop { + if let Some(status) = child.0.try_wait().unwrap() { + break status; + } + assert!( + Instant::now() < deadline, + "bootstrap test child exceeded deadline" + ); + std::thread::sleep(Duration::from_millis(10)); + }; + ( + status.success(), + fs::read_to_string(output.path().join("out")).unwrap(), + fs::read_to_string(output.path().join("err")).unwrap(), + ) +} + +struct Fixture { + _tmp: tempfile::TempDir, + repo: PathBuf, + path: String, +} +impl Fixture { + fn new(real: bool) -> Self { + let tmp = tempfile::tempdir().unwrap(); + let repo = tmp.path().join("repository"); + fs::create_dir(&repo).unwrap(); + let mut git = Command::new("git"); + git.args(["init", "--quiet"]).current_dir(&repo); + assert!(bounded(git).0); + fs::write( + repo.join("Cargo.toml"), + "[package]\nname = \"bootstrap-fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ) + .unwrap(); + fs::write( + repo.join("Cargo.lock"), + "version = 4\n\n[[package]]\nname = \"bootstrap-fixture\"\nversion = \"0.1.0\"\n", + ) + .unwrap(); + fs::create_dir(repo.join("src")).unwrap(); + fs::write( + repo.join("src/lib.rs"), + "pub fn installed() -> bool {\n true\n}\n", + ) + .unwrap(); + fs::create_dir(repo.join("tests")).unwrap(); + fs::write( + repo.join("tests/installed.rs"), + "#[test]\nfn installed() {\n assert!(bootstrap_fixture::installed());\n}\n", + ) + .unwrap(); + let path = if real { + std::env::var("PATH").unwrap() + } else { + let bin = tmp.path().join("bin"); + fs::create_dir(&bin).unwrap(); + // A shell fixture models only the cn process protocol; real-store proof is separate. + fs::write(bin.join("cn"), "#!/bin/sh\ncase \"$1\" in\n--version) echo 'cn protocol-fixture';;\ninit) mkdir .chronis; printf fixture > .chronis/protocol;;\nlist) test -f .chronis/protocol;;\n*) exit 3;;\nesac\n").unwrap(); + fs::set_permissions(bin.join("cn"), fs::Permissions::from_mode(0o755)).unwrap(); + for tool in ["codex", "claude", "gh", "cargo", "rustc"] { + symlink("/usr/bin/false", bin.join(tool)).unwrap(); + } + format!("{}:{}", bin.display(), std::env::var("PATH").unwrap()) + }; + Self { + _tmp: tmp, + repo, + path, + } + } + fn run(&self, args: &[&str]) -> (bool, String, String) { + let binary = std::env::var("ALLSTACK_TEST_BINARY") + .unwrap_or_else(|_| env!("CARGO_BIN_EXE_allstack").into()); + let mut command = Command::new(binary); + command + .arg("-C") + .arg(&self.repo) + .args(args) + .env("PATH", &self.path) + .current_dir(&self.repo); + bounded(command) + } + fn json(&self, args: &[&str]) -> Value { + let (ok, out, err) = self.run(args); + assert!(ok, "{out}\n{err}"); + serde_json::from_str(&out).unwrap() + } + fn fingerprint(&self) -> Vec<(PathBuf, Vec<u8>)> { + fn walk(root: &Path, dir: &Path, out: &mut Vec<(PathBuf, Vec<u8>)>) { + for entry in fs::read_dir(dir).unwrap() { + let path = entry.unwrap().path(); + if [".git", ".chronis", ".allstack", ".prime"] + .iter() + .any(|name| path.file_name().unwrap() == *name) + { + continue; + } + if path.is_dir() { + walk(root, &path, out); + } else { + out.push(( + path.strip_prefix(root).unwrap().into(), + fs::read(path).unwrap(), + )); + } + } + } + let mut out = Vec::new(); + walk(&self.repo, &self.repo, &mut out); + out.sort(); + out + } +} + +#[test] +fn check_and_doctor_leave_fresh_repository_untouched() { + let f = Fixture::new(false); + let before = f.fingerprint(); + let doctor = f.json(&["doctor"]); + assert_eq!(doctor["setup_ready"], false); + assert_eq!(doctor["stores_opened"], false); + let check = f.json(&["init", "--check", "--profile", "rust"]); + assert_eq!(check["ready"], true); + assert_eq!(check["applied"], false); + assert_eq!(f.fingerprint(), before); + assert!(!f.repo.join(".allstack").exists()); + assert!(!f.repo.join(".chronis").exists()); +} + +fn assert_setup(f: &Fixture) { + let first = f.json(&["init", "--profile", "rust"]); + println!("BOOTSTRAP {first}"); + assert_eq!(first["applied"], true); + assert!(f.repo.join(".chronis").is_dir()); + assert!(!f.repo.join(".allstack").exists()); + let doctor = f.json(&["doctor"]); + println!("DOCTOR {doctor}"); + assert_eq!(doctor["setup_ready"], true, "{doctor}"); + assert_eq!(doctor["chronis"], "readable"); + assert_eq!(doctor["authentication"], "not-probed"); + let before = f.fingerprint(); + let second = f.json(&["init"]); + assert!( + second["files"] + .as_array() + .unwrap() + .iter() + .all(|f| f["status"] == "unchanged") + ); + assert_eq!(f.fingerprint(), before); + for host in [".agents", ".claude"] { + let skill = f.repo.join(host).join("skills/allstack-delivery/SKILL.md"); + let link = skill + .parent() + .unwrap() + .join("../../../docs/reference/allstack.md"); + assert!(link.canonicalize().unwrap().is_file()); + } + fs::write(f.repo.join("workflows/methods/build.md"), "custom method").unwrap(); + let before = f.fingerprint(); + let (ok, _, err) = f.run(&["init"]); + assert!(!ok && err.contains("conflict"), "{err}"); + assert_eq!(f.fingerprint(), before); +} + +#[test] +fn initializes_protocol_boundary_and_reruns_without_replacing_custom_files() { + let f = Fixture::new(false); + fs::write(f.repo.join("AGENTS.md"), "existing instructions").unwrap(); + fs::write( + f.repo.join(".gitignore"), + "/.allstack/\n!/.allstack/\ncustom/", + ) + .unwrap(); + fs::create_dir(f.repo.join("docs")).unwrap(); + fs::write(f.repo.join("docs/README.md"), "# Existing documentation").unwrap(); + assert_setup(&f); + assert_eq!( + fs::read_to_string(f.repo.join("AGENTS.md")).unwrap(), + "existing instructions" + ); + assert!( + fs::read_to_string(f.repo.join("docs/README.md")) + .unwrap() + .starts_with("# Existing documentation\n") + ); + assert!( + fs::read_to_string(f.repo.join(".gitignore")) + .unwrap() + .starts_with("/.allstack/\n!/.allstack/\ncustom/\n") + ); +} + +#[test] +fn effective_existing_ignores_keep_original_order_and_trailing_comments() { + let f = Fixture::new(false); + let original = "/.prime/\n/.allstack/\n/.chronis/\n# existing comment\n"; + fs::write(f.repo.join(".gitignore"), original).unwrap(); + f.json(&["init", "--profile", "rust"]); + assert_eq!( + fs::read_to_string(f.repo.join(".gitignore")).unwrap(), + original + ); + assert_eq!(f.json(&["doctor"])["setup_ready"], true); +} + +#[test] +fn check_mode_never_opens_existing_chronis_engine() { + let f = Fixture::new(false); + fs::create_dir(f.repo.join(".chronis")).unwrap(); + let cn = f._tmp.path().join("bin/cn"); + let script = fs::read_to_string(&cn).unwrap().replace( + "list) test -f .chronis/protocol", + "list) touch .chronis/engine-opened; exit 3", + ); + fs::write(cn, script).unwrap(); + let check = f.json(&["init", "--check", "--profile", "rust"]); + assert_eq!(check["ready"], true); + assert!(check["chronis"].as_str().unwrap().contains("deferred")); + assert!(!f.repo.join(".chronis/engine-opened").exists()); + assert!(!f.run(&["init", "--profile", "rust"]).0); + assert!(f.repo.join(".chronis/engine-opened").exists()); + assert!(!f.repo.join("workflows").exists()); +} + +#[test] +fn conflicts_and_missing_profile_fail_before_chronis_or_file_writes() { + let f = Fixture::new(false); + let before = f.fingerprint(); + assert!(!f.run(&["init"]).0); + assert_eq!(f.fingerprint(), before); + fs::create_dir(f.repo.join("workflows")).unwrap(); + fs::write(f.repo.join("workflows/delivery.toml"), "custom workflow").unwrap(); + let before = f.fingerprint(); + let check = f.json(&["init", "--check", "--profile", "rust"]); + assert_eq!(check["ready"], false); + assert_eq!(check["conflicts"][0], "workflows/delivery.toml"); + assert!(!f.run(&["init", "--profile", "rust"]).0); + assert_eq!(f.fingerprint(), before); + assert!(!f.repo.join(".chronis").exists()); +} + +#[test] +fn custom_policy_is_validated_preserved_and_used_by_doctor() { + let f = Fixture::new(false); + let custom = "command_seconds=60\n[[gates]]\nname='custom'\nargv=['cargo','test','--locked']\ntest=true\ncompile=true\nsuccess_marker='test result: ok.'\n"; + fs::write(f.repo.join("policy.toml"), custom).unwrap(); + f.json(&["init", "--policy", "policy.toml"]); + assert_eq!( + fs::read_to_string(f.repo.join("policy.toml")).unwrap(), + custom + ); + assert!(!f.repo.join("workflows/verification.toml").exists()); + assert_eq!( + f.json(&["doctor", "--policy", "policy.toml"])["setup_ready"], + true + ); + fs::write(f.repo.join("policy.toml"), "gates=[]").unwrap(); + let before = f.fingerprint(); + assert!(!f.run(&["init", "--policy", "policy.toml"]).0); + assert_eq!(f.fingerprint(), before); +} + +#[test] +fn symlink_traversal_and_no_clobber_races_preserve_destination() { + let tmp = tempfile::tempdir().unwrap(); + let outside = tempfile::tempdir().unwrap(); + let root = Root::new(tmp.path()).unwrap(); + symlink(outside.path(), tmp.path().join("docs")).unwrap(); + assert!(root.read("docs/README.md").is_err()); + assert!(root.create("docs/README.md", b"unsafe").is_err()); + assert!(root.create("../escape", b"unsafe").is_err()); + assert!(root.read("/absolute").is_err()); + assert!(!outside.path().join("README.md").exists()); + assert_eq!(root.read("new.txt").unwrap(), None); + fs::write(tmp.path().join("new.txt"), b"other writer").unwrap(); + assert!(root.create("new.txt", b"replacement").is_err()); + assert!(root.append("new.txt", b"old", b"suffix").is_err()); + assert_eq!( + fs::read(tmp.path().join("new.txt")).unwrap(), + b"other writer" + ); + symlink(outside.path().join("missing"), tmp.path().join("dangling")).unwrap(); + assert!(root.read("dangling").is_err()); +} + +#[test] +fn unsafe_paths_and_missing_chronis_fail_without_bootstrap_writes() { + let f = Fixture::new(false); + for path in ["../escape.toml", "/tmp/escape.toml", ".git/bootstrap.toml"] { + let before = f.fingerprint(); + assert!(!f.run(&["init", "--profile", "rust", "--policy", path]).0); + assert_eq!(f.fingerprint(), before); + } + let outside = tempfile::tempdir().unwrap(); + symlink(outside.path(), f.repo.join("docs")).unwrap(); + assert!(!f.run(&["init", "--profile", "rust"]).0); + assert!(!outside.path().join("reference").exists()); + assert!(!f.repo.join(".chronis").exists()); + fs::remove_file(f.repo.join("docs")).unwrap(); + let cn = f._tmp.path().join("bin/cn"); + fs::remove_file(&cn).unwrap(); + symlink("/usr/bin/false", cn).unwrap(); + let before = f.fingerprint(); + assert_eq!( + f.json(&["init", "--check", "--profile", "rust"])["ready"], + false + ); + assert!(!f.run(&["init", "--profile", "rust"]).0); + assert_eq!(f.fingerprint(), before); +} + +#[test] +fn failed_chronis_initialization_reports_completed_paths_and_rerun_converges() { + let f = Fixture::new(false); + let cn = f._tmp.path().join("bin/cn"); + let script = fs::read_to_string(&cn).unwrap().replace( + "init) mkdir", + "init) if test ! -f ../failed-once; then touch ../failed-once; exit 7; fi; mkdir", + ); + fs::write(cn, script).unwrap(); + let (ok, _, err) = f.run(&["init", "--profile", "rust"]); + assert!( + !ok && err.contains("completed:") && err.contains("Chronis initialization uncertain"), + "{err}" + ); + assert!(f.repo.join("workflows/verification.toml").is_file()); + assert!(!f.repo.join(".chronis").exists()); + let retry = f.json(&["init"]); + assert_eq!(retry["applied"], true); + assert_eq!(retry["chronis"], "initialized"); + assert!( + retry["files"] + .as_array() + .unwrap() + .iter() + .all(|f| f["status"] == "unchanged") + ); +} + +#[test] +#[ignore = "requires actual Chronis; run explicitly against installed ALLSTACK_TEST_BINARY"] +fn installed_binary_initializes_real_chronis_and_executes_generated_policy() { + let f = Fixture::new(true); + let before = f.fingerprint(); + assert_eq!( + f.json(&["init", "--check", "--profile", "rust"])["ready"], + true + ); + assert_eq!(f.fingerprint(), before); + assert_setup(&f); + let policy: allstack::domain::Policy = + toml::from_str(include_str!("../templates/verification-rust.toml")).unwrap(); + policy.validate().unwrap(); + for gate in policy.gates { + let mut command = Command::new(&gate.argv[0]); + command.args(&gate.argv[1..]).current_dir(&f.repo); + let (ok, out, err) = bounded(command); + println!("GATE {} success={ok}\n{out}\n{err}", gate.name); + assert!(ok, "{}: {out}\n{err}", gate.name); + if gate.test { + assert!(allstack::infrastructure::gates::executed_tests(&format!("{out}\n{err}")) > 0); + } + } +}