diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index ae91f64..561a569 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -37,6 +37,7 @@ jobs: - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 - run: ${{ matrix.timeout }} -k 5 270 cargo +1.95 nextest run --locked - run: ${{ matrix.timeout }} -k 5 60 cargo +1.95 test --manifest-path fixtures/handoff/Cargo.toml --locked --tests + - run: ${{ matrix.timeout }} -k 5 60 cargo +1.95 test --manifest-path examples/spendcheck/Cargo.toml --locked --tests - if: matrix.os == 'macos-14' run: shasum -a256 target/debug/allstack > target/debug/allstack.sha256 - if: matrix.os == 'macos-14' @@ -64,5 +65,7 @@ jobs: tool: cargo-deny@0.19.2 - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 - run: timeout -k 5 60 cargo +1.95 fmt --all -- --check + - run: timeout -k 5 60 cargo +1.95 fmt --manifest-path examples/spendcheck/Cargo.toml -- --check - run: timeout -k 5 90 cargo +1.95 deny check - run: timeout -k 5 270 cargo +1.95 clippy --all-targets --locked -- -D warnings + - run: timeout -k 5 60 cargo +1.95 clippy --manifest-path examples/spendcheck/Cargo.toml --all-targets --locked -- -D warnings diff --git a/README.md b/README.md index da3b7b1..85fef16 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Allstack -Plan, build, verify, and ship across coding agents, with durable history and evidence tied to the delivered change. +Opinionated product and engineering skills for Codex and Claude Code, backed by durable project memory and delivery evidence. -**Status: implementation preview.** The Rust CLI implements local run storage, task coordination, host invocation, immutable handoffs, verification, and GitHub publication. Source-level support is distinct from release proof: use the [acceptance scenarios](docs/reference/release-acceptance.md) to audit actual host journeys, interruptions, and delivered artifacts. +**Status: implementation preview.** Invoke native skills to shape an idea, plan a change, review an existing branch, exercise an application, or ship authorized work. AllSource, Chronis, and Prime support durable continuation underneath. Source support and real-host acceptance are tracked separately in the [documentation](docs/README.md). The [verification report](docs/reports/2026-10-24-cross-host-workflow-verification.md) records authenticated host execution, delivered forward-run evidence, reverse handoff observations, and supported limits. Private gate receipts remain readable through `allstack evidence `; source-linked Prime context through `allstack recall `. @@ -10,11 +10,37 @@ Allstack embeds [AllSource Core](https://github.com/all-source-os/all-source) 0. ## First complete job -A developer plans a change in one host, continues implementation in another, resumes after an interruption, verifies the exact resulting commit, and publishes one reviewable pull request. The next host recovers the plan, workspace snapshot, evidence, and outstanding actions without copying a conversation transcript. +A developer shapes an application change, reviews its architecture, implements it, checks concrete failure risks, exercises the real user flow, and publishes one verified PR. Each specialist also works independently. An explicit host change preserves the accepted plan, candidate, ownership, and outstanding checks without copying a conversation transcript. In this workflow, **ship means a verified commit pushed to a feature branch and an open pull request**. Merge and deployment are separate actions. -## Component boundaries +## Start in your coding host + +Install the CLI from a reviewed checkout with Rust 1.95 or later: + +```sh +cargo install --path . --locked +allstack -C /path/to/project init --skills-only --check +allstack -C /path/to/project init --skills-only +``` + +Then invoke a skill by name. Codex accepts `$allstack-plan`; Claude Code accepts `/allstack-plan`. Existing sessions may need reopening for skill discovery. + +| Skill | Complete job | +| --- | --- | +| `allstack-shape` | Challenge demand, alternatives, and the outcome worth building. | +| `allstack-plan` | Resolve architecture, failure behavior, compatibility, and acceptance. | +| `allstack-review` | Find evidenced defects in an existing branch, diff, or PR. | +| `allstack-qa` | Exercise the actual UI, API, CLI, or job using available tools. | +| `allstack-ship` | Verify and publish the authorized change through managed delivery. | +| `allstack-recall` | Recover relevant project knowledge and inspect its sources. | +| `allstack-handoff` | Explicitly continue an active delivery in another host. | + +Skills-only setup needs no Rust project, Chronis executable, task, or delivery run. Independent shaping, planning, review, and QA use the native host's tools and permissions. Shared original methods live in [skills/](skills); Rust packages them for both hosts with capability overlays. `allstack skills` inspects the bundled interface without opening stores. + +See the [native-skills guide](docs/reference/native-skills.md) for installation, standalone memory, and enrollment into durable shipping. The [Spendcheck application](examples/spendcheck) demonstrates a concrete expense-report change through these methods. + +## Ecosystem underneath | Component | Owns | | --- | --- | @@ -27,12 +53,11 @@ In this workflow, **ship means a verified commit pushed to a feature branch and Workflow methods and repository policies are versioned data. Generic mechanics belong in Rust. Optional browser, research, evaluation, and hosted-service integrations follow after the core workflow is proven. -## Use the preview +## Durable delivery and recovery -Install from a reviewed checkout with Rust 1.95 or later: +For managed delivery, initialize a real project policy and Chronis: ```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 @@ -47,7 +72,7 @@ The [installed-bootstrap verification report](docs/reports/2026-10-24-repository 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. +Managed delivery needs an authorized live feature branch, a GitHub `origin`, and an explicit verification policy. Use `start` for a clean managed journey or `adopt` to enroll completed native work with its existing accepted plan. `current` discovers the active job behind the skill interface. Both coding hosts retain normal authentication and permissions. `doctor` probes availability without authenticating them. The [CLI guide](docs/reference/cli.md) documents the underlying recovery protocol. Select `delivery` for an accepted change or `bugfix` for evidence-led diagnosis and repair with regression coverage. Pass `start --workflow bugfix`; omitting the option retains `delivery`. Both share the same durable lifecycle and publication gates. See the [workflow catalog](docs/reference/workflow-catalog.md). @@ -60,6 +85,7 @@ Snapshots require explicit `git add -- ` for new files. They reject symlin ## Documentation - [Documentation index](docs/README.md) +- [Native skills](docs/reference/native-skills.md) - [Cross-host workflow design](docs/plans/2026-10-24-cross-host-workflow-design.md) - [Release acceptance scenarios](docs/reference/release-acceptance.md) - [CLI and recovery guide](docs/reference/cli.md) @@ -72,7 +98,7 @@ Acceptance checkboxes remain open until retained evidence establishes each claim ## Relationship to gstack -[gstack](https://github.com/garrytan/gstack) demonstrates opinionated planning, review, QA, and delivery workflows. Allstack uses that workflow shape as a reference and focuses its first release on durable cross-host continuation. Allstack's workflow text is original. Any future reuse must retain the applicable upstream and third-party notices. +[gstack](https://github.com/garrytan/gstack) demonstrates opinionated planning, review, QA, and delivery workflows. Allstack follows that specialist interaction model with original methods. AllSource, Chronis, and Prime provide durable history, ownership, and source-linked continuation underneath the native skills. Any future reuse must retain the applicable upstream and third-party notices. ## License diff --git a/docs/README.md b/docs/README.md index ed7ec4f..0e79aaa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,10 @@ ## Design +- [2026-10-25: Spendcheck category plan](plans/2026-10-25-spendcheck-category-plan.md) — real Claude native shaping/planning output for the expense-report application change. + +- [2026-10-25: Realignment with gstack's specialist workflow model](plans/2026-10-25-gstack-realignment-design.md) — product direction proposal, retained ecosystem strengths, native skills, and application-level acceptance. + - [2026-10-24: Selectable workflow catalog and bug fixes](plans/2026-10-24-workflow-catalog-design.md) — data-defined selection, retained-run compatibility, additive initialization, and authenticated bug-fix acceptance. - [2026-10-24: Installed CLI repository bootstrap](plans/2026-10-24-repository-bootstrap-design.md) — fresh-repository setup and installed-artifact verification. @@ -10,6 +14,12 @@ ## Reference +- [Native skills and complete application verification](reports/2026-10-25-native-skills-application-verification.md) — installed interface, real Codex/Claude use, frozen expense acceptance, and durable publication evidence. + +- [Native specialist skills](reference/native-skills.md) — direct Codex/Claude methods, standalone project memory, and enrollment into durable delivery. +- [Spendcheck application request](reference/native-application-request.md) — category-filtering behavior and frozen executable acceptance. +- [Source-linked expense decision](reference/native-recall-context.md) — project Prime readback and retained plan digest for incoming-host source inspection. + - [Workflow catalog and authenticated bug-fix verification](reports/2026-10-25-workflow-catalog-verification.md) — installed selection, legacy history, real host recovery, Prime repair, and retained proof identities. - [Workflow catalog](reference/workflow-catalog.md) — selectable delivery and bug-fix methods, installation, and retained-run compatibility. @@ -36,3 +46,5 @@ The repository is an implementation preview. Source-level contracts and fixture - [Allstack delivery plan 750de2d1-ec0b-4327-be96-4f0e1ba5c5f5](plans/allstack-750de2d1-ec0b-4327-be96-4f0e1ba5c5f5.md) - [Allstack delivery plan f2c4e41f-7616-4b34-aa56-4347fd1617fc](plans/allstack-f2c4e41f-7616-4b34-aa56-4347fd1617fc.md) + +- [Allstack delivery plan 9dc4c81f-73ca-4b92-89a2-be3c2ca44deb](plans/allstack-9dc4c81f-73ca-4b92-89a2-be3c2ca44deb.md) diff --git a/docs/plans/2026-10-25-gstack-realignment-design.md b/docs/plans/2026-10-25-gstack-realignment-design.md new file mode 100644 index 0000000..41a27b2 --- /dev/null +++ b/docs/plans/2026-10-25-gstack-realignment-design.md @@ -0,0 +1,84 @@ +# Allstack realignment: specialist workflows over durable ecosystem tools + +Date: 2026-10-25. Status: implementation preview on PR #5. Native specialist methods are the primary interface; see the [application verification report](../reports/2026-10-25-native-skills-application-verification.md) for installed and authenticated evidence. + +## Product promise + +Allstack gives a builder opinionated product and engineering skills inside their existing coding host, with project memory, task ownership, and portable continuation supplied by AllSource, Chronis, and Prime. + +A complete human job is: shape an application change, challenge its design, implement it, review its failure risks, exercise the real user flow, and publish a verified PR. A builder can invoke any specialist independently. If they change coding hosts, the next host can continue from source-linked decisions and evidence without copying a transcript. + +## Evidence of drift + +The current Allstack README leads with durable cross-host execution. Its normal journey exposes run IDs, epochs, clean indexed inputs, a frozen verification policy, managed worktrees, and explicit handoffs before a user reaches the intended engineering work. Those mechanics serve recovery and publication, but currently define the product interface. + +The four current method files contain 5, 7, 5, and 7 lines respectively. They specify planning/building duties and coordinator restrictions. They do not yet supply the substantive product challenge, architecture tradeoffs, review heuristics, or application QA methods that distinguish a specialist toolkit. Line counts are a symptom; the missing decision procedures and independent entry points are the gap. + +Upstream gstack presents specialist skills that a builder invokes directly for product thinking, planning, review, QA, and delivery. Its architecture combines opinionated skill text with concrete browser tools. Its current installation documentation also covers multiple coding hosts and outside-model review. Supporting Codex and Claude alone therefore does not establish Allstack's differentiation. + +Sources inspected on 2026-10-25: + +- [gstack README](https://github.com/garrytan/gstack#readme): specialist entry points, installation, and supported hosts. +- [gstack architecture](https://github.com/garrytan/gstack/blob/main/ARCHITECTURE.md): skill templates, shared generated contracts, and browser execution. +- [gstack builder ethos](https://github.com/garrytan/gstack/blob/main/ETHOS.md): user-directed decisions, existing solutions as inputs, and proof through executed behavior. +- [Current Allstack README](../../README.md), [CLI](../reference/cli.md), and [method files](../../workflows/methods/): current product surface and implementation boundary. + +## What to retain and change + +| Concern | Proposed direction | +| --- | --- | +| Specialist methodology | Make it the primary product: substantive, original methods with explicit inputs, judgment, outputs, and stop conditions. | +| Native host invocation | Install directly invocable Codex and Claude skills from shared method sources. Keep capability differences visible. | +| AllSource | Retain authoritative managed-run events and operation receipts; add skill-boundary records only where they support continuation or a user decision. | +| Chronis | Retain tasks, dependencies, and claims for work the user commits to doing. A standalone review must not require creating a delivery task. | +| Prime | Recall relevant decisions and incidents with source inspection; capture accepted decisions and useful outcomes with provenance. Keep execution state and permission outside memory. | +| Managed coordinator | Retain exact-commit verification, reconciliation, process ownership, and handoff safety. Offer it for complete delivery and explicit continuation rather than requiring it for every specialist invocation. | +| Catalog | Organize by complete human jobs and useful specialists. A new ID pointing at two generic prompts is insufficient acceptance. | +| Browser and ecosystem tools | Route through available host/browser/research tools. State actual capabilities; add integrations when a complete job needs them. | + +## Approaches + +1. **Native specialist skills backed by Allstack, recommended.** Keep the existing engine and add the missing methodology and invocation surface. Users work within their host; the engine supports durable delivery when selected. +2. **Maintain a gstack fork with ecosystem replacements.** This retains more upstream methodology but requires ongoing reconciliation of assumptions, generated content, tools, and host behavior. Choose it only if continuing upstream compatibility becomes a product requirement. +3. **Continue expanding coordinator workflow IDs.** This preserves the current trajectory but does not address the user's concern about intention and design. + +Adopt gstack's emphasis on specialist judgment, direct invocation, and actual execution. Preserve Allstack's original implementation and method text. Any deliberate upstream text or code reuse must carry its applicable attribution and notices. Do not claim a clean-room rewrite is inherently better than adapting an existing method; choose against concrete fit and maintenance cost. + +## Initial user-facing skills + +These names define the installed specialist interface; real-host and application acceptance must be established by retained execution evidence: + +| Skill | Complete job and output | +| --- | --- | +| `allstack-shape` | Challenge the requested outcome, users, evidence, constraints, and alternatives; produce an indexed decision-ready brief. | +| `allstack-plan` | Review architecture, failure cases, tests, and dependencies; produce an actionable implementation plan with explicit tradeoffs. | +| `allstack-review` | Inspect an existing branch or PR for concrete correctness and safety risks; report actionable source locations and supporting evidence. | +| `allstack-qa` | Exercise the application's actual UI, API, CLI, or job flow using available tools; report reproducible defects and retained execution evidence. | +| `allstack-ship` | Run applicable review and verification, then publish the exact authorized commit through existing managed publication. | +| `allstack-recall` / `allstack-handoff` | Recover relevant source-linked knowledge or explicitly transfer an active job to another supported host. | + +Plan → build → verify → ship remains a supported end-to-end journey. Users may also ask for a review of an existing branch, investigate a defect, or plan a change without starting that journey. Skills must support these partial jobs honestly without inventing successful delivery state. + +The first implementation should select the smallest complete application journey that exercises the intended specialist set. Add design, security, performance, or retrospective specialists when their own methods and acceptance scenarios are defined. Do not enumerate unsupported commands merely to resemble upstream breadth. + +## Shared methodology and host adapters + +Maintain original method sources with a compact capability and result contract. Render or package repository-local host skills from those sources, using Rust tooling where generation is necessary. Shared methodology includes the questions to answer, risks to inspect, required evidence, allowed actions, and expected result. Host overlays bind actual tools and invocation conventions; they must not duplicate the whole method. + +Direct skill execution uses the native host's tools and current repository policy. It cannot pretend the current coordinator's plan/build permissions allow compilation or browser QA. Keep managed-run restrictions intact; introduce the required adapter capability explicitly when integrating a specialist into a managed journey. A prose instruction is not a permission grant or an execution fence. + +Separate independently invocable specialist results from managed delivery transitions. A review finding can be retained with a source reference without fabricating a run stage, task completion, or successful publication. Existing AllSource run/event compatibility remains a requirement. + +## Next end-to-end proof + +Choose a concrete application change, with an observable before/after user flow, rather than using a catalog extraction as the primary demonstration. Reuse an authorized repository and publication destination; do not create another repository or branch without the required user instruction. + +The proof must show a builder invoking the installed skills naturally in Codex and Claude, receiving substantive product and engineering feedback, implementing the application change, exercising its actual runtime, and publishing one verified PR. Include an intentional host change within that useful job. Inspect actual host calls, changed files, runtime behavior, and destination state before asserting success. + +Acceptance also includes independent use: invoke review against an existing branch without a delivery run, and invoke planning without granting commit or publication authority. Recall should recover an accepted source-linked decision in a new host session. Durable continuation must retain the applicable work and evidence while keeping execution details behind the normal skill interface. + +## Implementation and acceptance + +The implementation packages the seven original methods for both native hosts, adds skills-only setup, retains standalone source-linked knowledge in Prime, and enrolls accepted native work into the existing delivery engine. AllSource and Chronis retain their existing authority boundaries. Application acceptance uses Spendcheck category filtering, independent native planning/review/recall, and exact-commit publication. + +Use the already authorized `feat/refactor-workflow` branch and one PR for this design review and any accepted implementation. No branch rename or additional PR is implied by this proposal. diff --git a/docs/plans/2026-10-25-spendcheck-category-plan.md b/docs/plans/2026-10-25-spendcheck-category-plan.md new file mode 100644 index 0000000..7185d31 --- /dev/null +++ b/docs/plans/2026-10-25-spendcheck-category-plan.md @@ -0,0 +1,131 @@ +Write/ExitPlanMode disabled. Plan delivered as message artifact below. + +--- + +# Spendcheck `--category` filter — engineering plan + +## Context + +Expense-report users need a travel-only report while refunds and separate currency totals stay correct. Today the `spendcheck` CLI reports every record; there is no way to scope a report to one category. The accepted change adds `spendcheck --category ` with exact, case-sensitive matching, leaving the unfiltered path byte-identical. + +Confidence: **rock**. The contract is already pinned by frozen tests (`tests/category.rs`, `tests/baseline.rs`) and `docs/reference/native-application-request.md`. Standard library only; the change is one validation loop plus the argument grammar. + +Bounded to the application change. The separate source review, executable QA run, Prime decision retention, the Claude↔Codex candidate transfer, and PR #5 publication are owned by their own methods and are **not** part of this plan. + +## Complete job (shaped) + +A human runs the CLI against the strict three-column CSV (`category,currency,amount`) and gets a report scoped to one category, with correct per-currency, refund-aware totals. + +- **Trigger:** `spendcheck --category `. +- **Input:** the existing CSV subset — unquoted fields, uppercase three-letter currency, signed decimal amount with exactly two fraction digits. +- **Output:** `records: N\n` then per-currency totals sorted by currency, counting and summing only rows whose category equals `` exactly. +- **Failure behavior:** every row is validated before any filtering; a malformed row fails with its source line and no partial stdout, even when the filter would exclude it. Missing value, empty name, unknown flag, and trailing argument are rejected. A valid category with no matches prints `records: 0` and exits 0. + +## Observed current behavior + +`src/lib.rs` +- `report(input: &str) -> Result` (lib.rs:3). Checks the header; per row validates field count (3), non-empty trimmed category (lib.rs:12), 3-char ASCII-uppercase currency (lib.rs:16), and amount via `minor_units` — exactly two fraction digits, i64 minor units, `checked_mul`/`checked_add`/`checked_neg` (lib.rs:38–58). +- Accumulates per-currency totals in a `BTreeMap<&str, i64>` with `checked_add` plus an overflow error (lib.rs:22); counts every valid row. +- Emits `records: {count}\n` then `{CCY} {sign}{whole}.{frac:02}\n` per currency in `BTreeMap` (currency-sorted) order (lib.rs:25–34). An empty map prints only the count line. + +`src/main.rs` +- Accepts exactly one argument (the path); any other count yields `usage: spendcheck ` (main.rs:3–9). On `Ok` it `print!`s the whole report; on `Err` it `eprintln!`s and `exit(1)` (main.rs:10–16). stdout is written once, after the full report string exists — so "no partial stdout" is already structural. + +Frozen contract (stays byte-identical): `tests/baseline.rs` calls `spendcheck::report(input)` with a **single argument** (baseline.rs:4,11), so the `report` signature cannot change. `tests/category.rs` drives the **binary** and fixes every observable string (`records: 3\nEUR 10.00\nUSD 7.00\n` for `--category travel`; `records: 0\n` for `rent` and for `Travel`). `expenses.csv` is the shared fixture. + +## Alternatives considered + +- **Change `report`'s signature to take the category** — rejected. Frozen `baseline.rs` calls `report(input)` with one argument; adding a parameter breaks a frozen file's compilation. +- **Duplicate the parse/validate loop in a second function** — rejected. Validation (currency, amount, field count, overflow) is the correctness core; duplicating it invites skew between filtered and unfiltered paths. +- **Chosen:** keep `report(input)` as a thin wrapper and add one new public function carrying an optional category. Single validation loop, frozen signature preserved, extends the existing seam rather than introducing an abstraction. + +## Proposed behavior and exact files + +### `examples/spendcheck/src/lib.rs` +- Add `pub fn report_filtered(input: &str, category: Option<&str>) -> Result` holding the current loop body. +- Reduce `report` to `report_filtered(input, None)` — preserves the frozen one-argument signature and behavior. +- Validation stays **unconditional, in the same order as today**, computed for every row before any filtering. Only the count and total accumulation become gated: + ```rust + if category.map_or(true, |wanted| fields[0] == wanted) { + let total = totals.entry(currency).or_default(); + *total = total.checked_add(amount).ok_or("total overflow")?; + count += 1; + } + ``` + Match on the raw `fields[0]` (exact, case-sensitive). `map_or(true, …)` keeps the no-filter path identical and avoids an MSRV assumption about `Option::is_none_or`. Output formatting, `BTreeMap` ordering, and `minor_units` are untouched. The overflow check still applies to included rows only, matching the frozen baseline overflow test (which runs unfiltered). + +### `examples/spendcheck/src/main.rs` +- Replace the `args.len() == 1` branch with a slice match over the collected args, dispatching into `report` / `report_filtered`: + ```rust + let result = match args.as_slice() { + [path] => read_report(path, None), + [path, flag, value] if flag == "--category" && !value.is_empty() => + read_report(path, Some(value)), + _ => Err("usage: spendcheck [--category ]".into()), + }; + ``` + `read_report` keeps the existing `read_to_string(...).map_err(...).and_then(|t| spendcheck::report_filtered(&t, category))`. The `Ok` → `print!`, `Err` → `eprintln!` + `exit(1)` tail is unchanged, preserving the single atomic stdout write. +- This grammar yields exactly the frozen rejections: `[path,"--category"]` (2 elements, no arm), `[path,"--unknown","travel"]` (guard false), `[path,"--category",""]` (empty value, guard false), and `[path,"--category","travel","extra"]` (4 elements) all fall to `_` → usage error, exit 1, empty stdout. + +### `examples/spendcheck/README.md` +- Document the `--category ` flag: exact case-sensitive match; a valid category with no matches prints `records: 0` and exits 0; every row is validated before filtering so a malformed excluded row still fails. Keep the existing CSV-subset and currency/rounding description. Prose only. + +**Unchanged:** `Cargo.toml` (std only, no new dependencies), `expenses.csv`, `tests/baseline.rs`, `tests/category.rs`. + +## Failure handling + +| Condition | Detected by | User sees | +|---|---|---| +| Malformed row (field count / currency / amount), even one excluded by filter | `report_filtered` validation loop, before filtering | stderr `invalid … at line N`, exit 1, empty stdout | +| Total overflow on an included row | `checked_add` in the loop | stderr `total overflow`, exit 1, empty stdout | +| Valid category, zero matches | empty `totals` map | `records: 0\n`, exit 0 | +| Missing value / empty name / unknown flag / trailing arg | `main` slice match `_` arm | stderr usage line, exit 1, empty stdout | +| Partial write | unreachable — report string fully built before the single `print!` | never partial | +| Retry / interruption / duplicate run | pure read→compute→print, no persisted state | idempotent, identical output each run | + +The subtle invariant — validate-before-filter — is owned by keeping validation unconditional and gating only accumulation. The likely defect is moving the filter above validation so an excluded malformed row passes; the negative acceptance case below catches it. + +## Executable acceptance + +All commands execute real code (`cargo test` runs the suites; `cargo run` runs the built binary). None run in this plan phase. + +Frozen suites (must stay green, unchanged): +```sh +cargo test --manifest-path examples/spendcheck/Cargo.toml --test baseline +cargo test --manifest-path examples/spendcheck/Cargo.toml --test category +``` + +Runtime checks: +```sh +# unfiltered unchanged -> records: 4\nEUR 14.50\nUSD 7.00\n , exit 0 +cargo run --manifest-path examples/spendcheck/Cargo.toml -- examples/spendcheck/expenses.csv +# filtered, refund-aware -> records: 3\nEUR 10.00\nUSD 7.00\n , exit 0 +cargo run --manifest-path examples/spendcheck/Cargo.toml -- examples/spendcheck/expenses.csv --category travel +# no matches: empty success -> records: 0\n , exit 0 +cargo run --manifest-path examples/spendcheck/Cargo.toml -- examples/spendcheck/expenses.csv --category rent +# case-sensitive: no match -> records: 0\n , exit 0 +cargo run --manifest-path examples/spendcheck/Cargo.toml -- examples/spendcheck/expenses.csv --category Travel +``` + +Negative cases (defect-catching): +```sh +# malformed EXCLUDED row still fails: stderr "line 3", exit 1, stdout empty +printf 'category,currency,amount\ntravel,EUR,1.00\nfood,EUR,bad\n' > /tmp/sc.csv +cargo run --manifest-path examples/spendcheck/Cargo.toml -- /tmp/sc.csv --category travel +# arg rejections: each exit 1, empty stdout +cargo run --manifest-path examples/spendcheck/Cargo.toml -- /tmp/sc.csv --category +cargo run --manifest-path examples/spendcheck/Cargo.toml -- /tmp/sc.csv --unknown travel +cargo run --manifest-path examples/spendcheck/Cargo.toml -- /tmp/sc.csv --category "" +cargo run --manifest-path examples/spendcheck/Cargo.toml -- /tmp/sc.csv --category travel extra +``` + +`cargo test` compiles **and executes** the suites; `cargo run` compiles **and executes** the binary. No step here is build-only. + +## Prerequisites + +- No new dependencies; no `Cargo.toml` change. +- Durable home when landed: `docs/plans/2026-10-25-spendcheck-category-filter.md`, indexed from the docs index. + +--- + +Note: both the `Write` tool (plan file) and `ExitPlanMode` are disabled this session, so the plan is returned as prose above rather than written to `/Users/decebaldobrica/.claude/plans/…` or submitted for approval. No files edited, no tasks/runs created, nothing compiled or published. Frozen `tests/category.rs`, `tests/baseline.rs`, and `expenses.csv` untouched. diff --git a/docs/plans/allstack-9dc4c81f-73ca-4b92-89a2-be3c2ca44deb.md b/docs/plans/allstack-9dc4c81f-73ca-4b92-89a2-be3c2ca44deb.md new file mode 100644 index 0000000..7185d31 --- /dev/null +++ b/docs/plans/allstack-9dc4c81f-73ca-4b92-89a2-be3c2ca44deb.md @@ -0,0 +1,131 @@ +Write/ExitPlanMode disabled. Plan delivered as message artifact below. + +--- + +# Spendcheck `--category` filter — engineering plan + +## Context + +Expense-report users need a travel-only report while refunds and separate currency totals stay correct. Today the `spendcheck` CLI reports every record; there is no way to scope a report to one category. The accepted change adds `spendcheck --category ` with exact, case-sensitive matching, leaving the unfiltered path byte-identical. + +Confidence: **rock**. The contract is already pinned by frozen tests (`tests/category.rs`, `tests/baseline.rs`) and `docs/reference/native-application-request.md`. Standard library only; the change is one validation loop plus the argument grammar. + +Bounded to the application change. The separate source review, executable QA run, Prime decision retention, the Claude↔Codex candidate transfer, and PR #5 publication are owned by their own methods and are **not** part of this plan. + +## Complete job (shaped) + +A human runs the CLI against the strict three-column CSV (`category,currency,amount`) and gets a report scoped to one category, with correct per-currency, refund-aware totals. + +- **Trigger:** `spendcheck --category `. +- **Input:** the existing CSV subset — unquoted fields, uppercase three-letter currency, signed decimal amount with exactly two fraction digits. +- **Output:** `records: N\n` then per-currency totals sorted by currency, counting and summing only rows whose category equals `` exactly. +- **Failure behavior:** every row is validated before any filtering; a malformed row fails with its source line and no partial stdout, even when the filter would exclude it. Missing value, empty name, unknown flag, and trailing argument are rejected. A valid category with no matches prints `records: 0` and exits 0. + +## Observed current behavior + +`src/lib.rs` +- `report(input: &str) -> Result` (lib.rs:3). Checks the header; per row validates field count (3), non-empty trimmed category (lib.rs:12), 3-char ASCII-uppercase currency (lib.rs:16), and amount via `minor_units` — exactly two fraction digits, i64 minor units, `checked_mul`/`checked_add`/`checked_neg` (lib.rs:38–58). +- Accumulates per-currency totals in a `BTreeMap<&str, i64>` with `checked_add` plus an overflow error (lib.rs:22); counts every valid row. +- Emits `records: {count}\n` then `{CCY} {sign}{whole}.{frac:02}\n` per currency in `BTreeMap` (currency-sorted) order (lib.rs:25–34). An empty map prints only the count line. + +`src/main.rs` +- Accepts exactly one argument (the path); any other count yields `usage: spendcheck ` (main.rs:3–9). On `Ok` it `print!`s the whole report; on `Err` it `eprintln!`s and `exit(1)` (main.rs:10–16). stdout is written once, after the full report string exists — so "no partial stdout" is already structural. + +Frozen contract (stays byte-identical): `tests/baseline.rs` calls `spendcheck::report(input)` with a **single argument** (baseline.rs:4,11), so the `report` signature cannot change. `tests/category.rs` drives the **binary** and fixes every observable string (`records: 3\nEUR 10.00\nUSD 7.00\n` for `--category travel`; `records: 0\n` for `rent` and for `Travel`). `expenses.csv` is the shared fixture. + +## Alternatives considered + +- **Change `report`'s signature to take the category** — rejected. Frozen `baseline.rs` calls `report(input)` with one argument; adding a parameter breaks a frozen file's compilation. +- **Duplicate the parse/validate loop in a second function** — rejected. Validation (currency, amount, field count, overflow) is the correctness core; duplicating it invites skew between filtered and unfiltered paths. +- **Chosen:** keep `report(input)` as a thin wrapper and add one new public function carrying an optional category. Single validation loop, frozen signature preserved, extends the existing seam rather than introducing an abstraction. + +## Proposed behavior and exact files + +### `examples/spendcheck/src/lib.rs` +- Add `pub fn report_filtered(input: &str, category: Option<&str>) -> Result` holding the current loop body. +- Reduce `report` to `report_filtered(input, None)` — preserves the frozen one-argument signature and behavior. +- Validation stays **unconditional, in the same order as today**, computed for every row before any filtering. Only the count and total accumulation become gated: + ```rust + if category.map_or(true, |wanted| fields[0] == wanted) { + let total = totals.entry(currency).or_default(); + *total = total.checked_add(amount).ok_or("total overflow")?; + count += 1; + } + ``` + Match on the raw `fields[0]` (exact, case-sensitive). `map_or(true, …)` keeps the no-filter path identical and avoids an MSRV assumption about `Option::is_none_or`. Output formatting, `BTreeMap` ordering, and `minor_units` are untouched. The overflow check still applies to included rows only, matching the frozen baseline overflow test (which runs unfiltered). + +### `examples/spendcheck/src/main.rs` +- Replace the `args.len() == 1` branch with a slice match over the collected args, dispatching into `report` / `report_filtered`: + ```rust + let result = match args.as_slice() { + [path] => read_report(path, None), + [path, flag, value] if flag == "--category" && !value.is_empty() => + read_report(path, Some(value)), + _ => Err("usage: spendcheck [--category ]".into()), + }; + ``` + `read_report` keeps the existing `read_to_string(...).map_err(...).and_then(|t| spendcheck::report_filtered(&t, category))`. The `Ok` → `print!`, `Err` → `eprintln!` + `exit(1)` tail is unchanged, preserving the single atomic stdout write. +- This grammar yields exactly the frozen rejections: `[path,"--category"]` (2 elements, no arm), `[path,"--unknown","travel"]` (guard false), `[path,"--category",""]` (empty value, guard false), and `[path,"--category","travel","extra"]` (4 elements) all fall to `_` → usage error, exit 1, empty stdout. + +### `examples/spendcheck/README.md` +- Document the `--category ` flag: exact case-sensitive match; a valid category with no matches prints `records: 0` and exits 0; every row is validated before filtering so a malformed excluded row still fails. Keep the existing CSV-subset and currency/rounding description. Prose only. + +**Unchanged:** `Cargo.toml` (std only, no new dependencies), `expenses.csv`, `tests/baseline.rs`, `tests/category.rs`. + +## Failure handling + +| Condition | Detected by | User sees | +|---|---|---| +| Malformed row (field count / currency / amount), even one excluded by filter | `report_filtered` validation loop, before filtering | stderr `invalid … at line N`, exit 1, empty stdout | +| Total overflow on an included row | `checked_add` in the loop | stderr `total overflow`, exit 1, empty stdout | +| Valid category, zero matches | empty `totals` map | `records: 0\n`, exit 0 | +| Missing value / empty name / unknown flag / trailing arg | `main` slice match `_` arm | stderr usage line, exit 1, empty stdout | +| Partial write | unreachable — report string fully built before the single `print!` | never partial | +| Retry / interruption / duplicate run | pure read→compute→print, no persisted state | idempotent, identical output each run | + +The subtle invariant — validate-before-filter — is owned by keeping validation unconditional and gating only accumulation. The likely defect is moving the filter above validation so an excluded malformed row passes; the negative acceptance case below catches it. + +## Executable acceptance + +All commands execute real code (`cargo test` runs the suites; `cargo run` runs the built binary). None run in this plan phase. + +Frozen suites (must stay green, unchanged): +```sh +cargo test --manifest-path examples/spendcheck/Cargo.toml --test baseline +cargo test --manifest-path examples/spendcheck/Cargo.toml --test category +``` + +Runtime checks: +```sh +# unfiltered unchanged -> records: 4\nEUR 14.50\nUSD 7.00\n , exit 0 +cargo run --manifest-path examples/spendcheck/Cargo.toml -- examples/spendcheck/expenses.csv +# filtered, refund-aware -> records: 3\nEUR 10.00\nUSD 7.00\n , exit 0 +cargo run --manifest-path examples/spendcheck/Cargo.toml -- examples/spendcheck/expenses.csv --category travel +# no matches: empty success -> records: 0\n , exit 0 +cargo run --manifest-path examples/spendcheck/Cargo.toml -- examples/spendcheck/expenses.csv --category rent +# case-sensitive: no match -> records: 0\n , exit 0 +cargo run --manifest-path examples/spendcheck/Cargo.toml -- examples/spendcheck/expenses.csv --category Travel +``` + +Negative cases (defect-catching): +```sh +# malformed EXCLUDED row still fails: stderr "line 3", exit 1, stdout empty +printf 'category,currency,amount\ntravel,EUR,1.00\nfood,EUR,bad\n' > /tmp/sc.csv +cargo run --manifest-path examples/spendcheck/Cargo.toml -- /tmp/sc.csv --category travel +# arg rejections: each exit 1, empty stdout +cargo run --manifest-path examples/spendcheck/Cargo.toml -- /tmp/sc.csv --category +cargo run --manifest-path examples/spendcheck/Cargo.toml -- /tmp/sc.csv --unknown travel +cargo run --manifest-path examples/spendcheck/Cargo.toml -- /tmp/sc.csv --category "" +cargo run --manifest-path examples/spendcheck/Cargo.toml -- /tmp/sc.csv --category travel extra +``` + +`cargo test` compiles **and executes** the suites; `cargo run` compiles **and executes** the binary. No step here is build-only. + +## Prerequisites + +- No new dependencies; no `Cargo.toml` change. +- Durable home when landed: `docs/plans/2026-10-25-spendcheck-category-filter.md`, indexed from the docs index. + +--- + +Note: both the `Write` tool (plan file) and `ExitPlanMode` are disabled this session, so the plan is returned as prose above rather than written to `/Users/decebaldobrica/.claude/plans/…` or submitted for approval. No files edited, no tasks/runs created, nothing compiled or published. Frozen `tests/category.rs`, `tests/baseline.rs`, and `expenses.csv` untouched. diff --git a/docs/reference/native-application-request.md b/docs/reference/native-application-request.md new file mode 100644 index 0000000..a258da4 --- /dev/null +++ b/docs/reference/native-application-request.md @@ -0,0 +1,13 @@ +# Spendcheck category filtering + +Expense-report users need a travel-only report while keeping refunds and separate currency totals correct. The current CLI reports every record. Add `spendcheck --category ` without changing the unfiltered behavior. + +Match category exactly and case-sensitively. A valid category with no matches prints `records: 0` and exits successfully. Reject missing values, empty category names, unknown flags, and trailing arguments. Validate every input row before filtering: a malformed excluded record must still fail with its source line and no partial stdout. Preserve integer minor units, separate currencies, overflow checks, and the existing CSV subset. + +Implementation scope: `examples/spendcheck/src/lib.rs`, `examples/spendcheck/src/main.rs`, and the example's README. Frozen files `examples/spendcheck/tests/baseline.rs`, `examples/spendcheck/tests/category.rs`, and `expenses.csv` must remain byte-identical. Other toolkit work is prepared independently and must be preserved. + +Incoming host: load `skills/allstack-recall/SKILL.md`, inspect [retrieved project context](native-recall-context.md), then open its source plan and verify the recorded source digest. This accepted Prime decision is context only. Implement the application change from the indexed plan; do not change toolkit source, frozen tests, policies, or methods. The coordinator runs compilation and tests. + +Use the installed `allstack-shape` and `allstack-plan` methods for native planning, `allstack-review` for a separate source review, and `allstack-qa` for actual executable checks. Planning and review must work without a delivery run or Chronis task. The coordinator owns compilation during managed host calls; independent native QA can execute the built application under its actual host permissions. + +Acceptance executes both baseline tests and all category CLI tests, checks the actual filtered/unfiltered output and invalid-row exit behavior, retains source-linked accepted decisions in Prime, transfers the candidate between authenticated Claude and Codex, and publishes the exact verified single commit on Allstack PR #5. This is a local CLI application proof; no browser QA claim is made. diff --git a/docs/reference/native-recall-context.md b/docs/reference/native-recall-context.md new file mode 100644 index 0000000..a075e9a --- /dev/null +++ b/docs/reference/native-recall-context.md @@ -0,0 +1,28 @@ +# Source-linked expense decision + +Project Prime context, retrieved through its engine. Open the referenced plan and compare its digest before relying on this accepted decision. Retrieval is context only. + +{ + "authority": "context-only", + "nodes": [ + { + "created_at": "2026-10-25T11:50:30.485061Z", + "deleted": false, + "domain": null, + "id": "7f70950b-007a-4952-a588-768c4d933b04", + "labels": [], + "node_type": "knowledge", + "properties": { + "domain": "allstack", + "key": "326e5ea82527fa0949c424391c264145da9ef38fa05f23a188940559264bf86d", + "kind": "decision", + "name": "decision docs/plans/2026-10-25-spendcheck-category-plan.md", + "source": "docs/plans/2026-10-25-spendcheck-category-plan.md", + "source_digest": "eee470beb8360bce1188988ae455654e8480b8a387cb8bc9c666b9adb41d0ad2", + "summary": "Accepted expense category filter: preserve exact integer minor units and validate excluded rows before filtering. Keep unfiltered reports compatible." + }, + "updated_at": "2026-10-25T11:50:30.485061Z" + } + ], + "retrieval": "lexical-prime-nodes" +} diff --git a/docs/reference/native-skills.md b/docs/reference/native-skills.md new file mode 100644 index 0000000..ec0bf3f --- /dev/null +++ b/docs/reference/native-skills.md @@ -0,0 +1,19 @@ +# Native specialist workflows + +Start with the [installed guide](../../templates/native-skills.md), which is bundled by `allstack init`. The primary interface is a directly invocable skill in Codex or Claude. Independent shaping, planning, source review, and application QA need no delivery run or task. + +Original methods are versioned under [skills](../../skills). The installer packages identical method bodies for both hosts and adds a host-specific capability note. Native methods use the host's current tools and permissions. Existing managed plan/build calls retain their stricter tool scope; their restrictions do not apply to an independent native invocation. + +## Durable services + +AllSource continues to validate managed-run event chains and exact-commit receipts. Chronis owns delivery claims and completion. Prime retains source-linked context. Standalone `remember` records a bounded summary plus the digest of an existing Markdown document under docs, then reads the actual Prime node back. Repeated identical writes reuse the binding. Source changes create a distinct historical binding rather than silently replacing earlier evidence. + +`recall` reads the repository's local Prime memory, combines standalone knowledge with retained delivery outcomes, and returns context only. An absent local memory returns an empty result without creating a store. Open the cited source and compare its recorded digest before treating a historical note as current. Hosted synchronization and vector search are outside this local preview. + +## Native work to managed shipping + +`adopt` accepts the completed native candidate, indexed request, existing accepted plan, and frozen policy. It requires commit authority, an authorized live feature branch, real initialized Chronis, and explicit staging of new files. It uses the existing snapshot, task setup, accepted-plan, handoff, verification, and publication paths without invoking a model again. Adoption does not imply that the candidate passed any checks. + +`current` folds and validates AllSource histories to discover one active job. It refuses ambiguous or oversized discovery; explicit run commands remain available. Skills keep those protocol identifiers inside tool calls. Changing hosts preserves immutable scope and advances ownership; native tools outside managed calls remain under host policy rather than epoch fencing. + +See the [application request](native-application-request.md) for the concrete expense-filter job used to verify native planning, real implementation, source review, actual CLI QA, source-linked recall, and exact-commit delivery. diff --git a/docs/reports/2026-10-25-native-skills-application-verification.md b/docs/reports/2026-10-25-native-skills-application-verification.md new file mode 100644 index 0000000..b5c593f --- /dev/null +++ b/docs/reports/2026-10-25-native-skills-application-verification.md @@ -0,0 +1,75 @@ +# Native skills and application verification + +Date: 2026-10-25. Publication target: [Allstack PR #5](https://github.com/all-source-os/allstack/pull/5). Scope: original native methods, installed Codex/Claude skills, standalone source-linked recall, and a real expense-report change carried through durable delivery. + +## Product behavior + +The primary interface is `allstack-shape`, `allstack-plan`, `allstack-review`, `allstack-qa`, `allstack-ship`, `allstack-recall`, and `allstack-handoff` inside the coding host. Shared method sources are packaged with small host capability overlays. Independent planning, review, QA, and recall require no delivery run or Chronis task. + +`allstack init --skills-only` installs that interface without a project profile or Chronis initialization. `skills` works outside Git and opens no stores. `remember` retains a bounded summary and document digest in local Prime; `recall` retrieves standalone knowledge and existing delivery outcomes. `adopt` enrolls an existing candidate and accepted plan without repeating model work. `current` discovers one active job through validated AllSource histories and refuses ambiguity. + +Managed delivery keeps immutable scope, expected revisions, Chronis claims, explicit handoff, process ownership, exact-commit gates, and remote/PR readback. No new workflow ID or weaker publication path was introduced. + +## Installed artifact + +- Final isolated CLI SHA-256: `9d18314eff02b59ef78e12507db46c85f24821351070c71c935f8fc9f7b11d34`. +- Skills-only setup installed seven native skills for each host, reran unchanged, and created no Allstack, Chronis, or Prime stores before host execution. +- A real installation produced by the previous verified CLI (`576a673ebc9975e09e50f06445131d1ebff2ef7ce0b3df60aa751217bc19d969`) upgraded through the final CLI. Fifteen existing resources, including the policy, remained byte-identical; the documentation index gained its native guide link. Full setup contains the seven native skills plus the two existing delivery/catalog skills per host. +- Upgrade and idempotent rerun succeeded while macOS denied reads of both source checkouts. Final bundled skill discovery also succeeded outside Git. + +The isolated executable was installed from the built binary. The global Allstack installation was not replaced. Resource and executable manifests remain with the private qualification evidence. + +## Authenticated native work + +All hosts used their normal authentication and configured models. No model override or permission bypass was used. The Rust qualification harness uses the production owner-pipe supervisor with one bounded deadline and retained process receipts. + +| Operation | Observed result | Receipt SHA-256 | +| --- | --- | --- | +| Claude native shaping/planning | Read both installed methods and application source; returned the engineering plan; exit 0, 221985 ms | `be6cc7a969b2cc5c61938a6d593a9df7eec5bff4ea1dbfc65a5c095004433cbb` | +| Codex native recall | Discovered `allstack-recall`, called project `allstack recall`, opened the source, and verified its digest; exit 0, 72702 ms | `862a047de050623fe5a815606e770471cf342771d1f36117ea2f862c3d5b16df` | +| Claude native review/QA | Invoked `/allstack-review` and loaded `allstack-qa` through the native Skill tool; four actual CLI commands; exit 0, 110933 ms | `d52a1ad508dc1cf20ec527f6bb948a3e3a5ab4d03b37d87d6a6752b19062d690` | + +The native review project remained without Chronis or Allstack delivery events. Its Prime decision node `d7299784-9196-46c2-8cd9-4a93af3a63c3` retained source `docs/plans/2026-10-25-spendcheck-category-plan.md` with digest `eee470beb8360bce1188988ae455654e8480b8a387cb8bc9c666b9adb41d0ad2`. The fresh Codex session's actual query returned that node, and its actual SHA command matched the document bytes. + +Initial probe launches failed because the command environment omitted the installed Claude directory. Retained supervisor stderr identified the spawn failure; the successful probes used the observed executable and explicit PATH. The constrained planning probe also attempted one unavailable Write tool. Its tool result reported that Write was disabled, the attempted file does not exist, and the plan was returned for indexing. Both completed Claude responses reported zero permission denials. These observations are retained rather than counted as successful writes. + +## Complete application change + +[Spendcheck](../../examples/spendcheck) summarizes strict CSV expense records in exact minor units and keeps currencies separate. The change adds `--category ` with exact, case-sensitive matching. Every row is validated before filtering, including excluded rows. + +Before implementation, both baseline tests passed; category acceptance executed six tests with two passing and four failing. After real Codex implementation, the unchanged two baseline tests and six category tests all passed. Their source digests remained fixed: + +| Frozen input | SHA-256 | +| --- | --- | +| `tests/baseline.rs` | `36278b65d0cec35d8f5b277f02453528ed11c0552a9db6786123b7e97c11c393` | +| `tests/category.rs` | `0b7baddea9c2a0728117fafbddcf3537e41d9b8143ed988ceac34793dc3ddafc` | +| `expenses.csv` | `3089824068939b1858f768f14f27cf557118183833aa0843da3802c0ff1bfde8` | + +The inspected Codex manifest changed exactly `src/lib.rs`, `src/main.rs`, and the example README. The managed host receipt recorded exit 0 and 101721 ms. Its actual commands read the recall method and retrieved project context, then checked the accepted plan and frozen input hashes. + +Claude's actual QA tool results matched these outcomes. The coordinator independently repeated the four executable cases and inspected separate exit status, stdout, and stderr: + +| Input | Exit | stdout | stderr | +| --- | --- | --- | --- | +| Unfiltered sample | 0 | `records: 4`, `EUR 14.50`, `USD 7.00` | empty | +| `--category travel` | 0 | `records: 3`, `EUR 10.00`, `USD 7.00` | empty | +| `--category rent` | 0 | `records: 0` | empty | +| Malformed excluded record | 1 | empty | `invalid amount at line 3` | + +QA executable digest: `d4b396482b8f65e8fae85178f324d568db2da5571bab1586026d57a11fdb9f51`. A source manifest proved the standalone review copy matched the managed candidate. The native review reported no actionable findings; this does not establish absence of all defects. No browser verification is claimed. + +## Durable delivery and final verification + +AllSource run `9dc4c81f-73ca-4b92-89a2-be3c2ca44deb` adopted the native plan and prepared candidate. Claude owned epoch 1, real Codex implemented at epoch 2, and the candidate returned to Claude at epoch 3. Chronis task `t-16f532` claims were read back at both handoffs. The coordinator Prime decision node is `7f70950b-007a-4952-a588-768c4d933b04`. + +The frozen [verification policy](../../workflows/native-gates.toml), digest `82cce6955bfc9dcd50b850f5905245400b2752504e14c5bdf524f4a8b2c7c387`, executes runtime/native tests, application tests, existing host fixtures, formatting, lint, and source checks. Its final receipts bind the candidate, environment, installed CLI, and exact single commit. Shipping must confirm that same commit at the remote branch and PR head before task completion and the Prime outcome. + +The complete policy executed 53 runtime/native tests, eight application tests, and four existing host-fixture tests successfully. Both actual-Chronis qualification cases are excluded from the portable suite; the staged-deletion qualification ran separately and passed one executed case. Formatting, lint, and source checks also passed. The final publication receipt identifies the exact tested commit. + +Native adoption also has an explicit staged-deletion regression qualification. The pre-repair native CLI (`962c61e52fa24860b45559c47043fc91d8cf1997d703288c1dae39e0f57d9db0`) failed with `pathspec 'obsolete.md' did not match any files`; the repaired CLI passed the same qualification with actual Git index changes, signed commits, real Chronis, and executed project gates. Only remote discovery/forge reads were protocol fixtures. The coordinator now stages unstaged paths and separately checks the cached candidate, preserving files already staged for deletion. The qualification executes separately because it requires configured Chronis and Git identity; CI reports it as skipped rather than treating it as a live success. + +Use the run's validated history/evidence exports and PR head/checks to audit final publication. Private proof includes native tool ledgers, actual process results, source/resource manifests, before/after test logs, and engine readbacks. Linux/macOS CI also executes the application suite. A test runner's empty unit targets are not counted as executed acceptance tests. + +Supported limits remain sequential hosts on one machine, local lexical Prime context, and native tools governed by host policy outside managed calls. Hosted memory synchronization, merge, deployment, and browser QA are outside this proof. + +The owner-loss test waits for a complete newline-terminated PID before revoking the owner pipe. Its cleanup closes that pipe and reaps the supervisor on failure. The child-reaped assertion and original deadlines remain intact. diff --git a/examples/skill_probe.rs b/examples/skill_probe.rs new file mode 100644 index 0000000..91905ac --- /dev/null +++ b/examples/skill_probe.rs @@ -0,0 +1,20 @@ +//! Real-host qualification reuses the production owner-pipe supervisor and receipts. +use allstack::infrastructure::process::{self, Spec}; +use anyhow::{Context, Result}; + +fn main() -> Result<()> { + let argument = std::env::args() + .nth(1) + .context("supply a private Spec JSON file")?; + if argument == "__supervise" { + return process::supervise(); + } + let spec: Spec = serde_json::from_slice(&std::fs::read(argument)?)?; + let outcome = process::run(spec)?; + println!("{}", serde_json::to_string(&outcome)?); + anyhow::ensure!( + outcome.success(), + "native skill probe did not complete successfully" + ); + Ok(()) +} diff --git a/examples/spendcheck/Cargo.lock b/examples/spendcheck/Cargo.lock new file mode 100644 index 0000000..d61ff73 --- /dev/null +++ b/examples/spendcheck/Cargo.lock @@ -0,0 +1,5 @@ +version = 4 + +[[package]] +name = "spendcheck" +version = "0.1.0" diff --git a/examples/spendcheck/Cargo.toml b/examples/spendcheck/Cargo.toml new file mode 100644 index 0000000..ad920a4 --- /dev/null +++ b/examples/spendcheck/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "spendcheck" +version = "0.1.0" +edition = "2024" +publish = false + +[workspace] diff --git a/examples/spendcheck/README.md b/examples/spendcheck/README.md new file mode 100644 index 0000000..a7837df --- /dev/null +++ b/examples/spendcheck/README.md @@ -0,0 +1,17 @@ +# Spendcheck + +Summarize expense records without combining currencies or rounding floating-point money. Input uses a strict three-column CSV subset: `category,currency,amount`, unquoted fields, uppercase three-letter currencies, and signed decimal amounts with exactly two fractional digits. Commas inside fields are unsupported. + +```sh +cargo run --manifest-path examples/spendcheck/Cargo.toml -- examples/spendcheck/expenses.csv +``` + +The report prints the record count followed by totals sorted by currency. Invalid records and overflowing totals fail with a nonzero exit and no partial report. + +Add `--category ` after the CSV path to report only records whose category matches exactly, including case: + +```sh +cargo run --manifest-path examples/spendcheck/Cargo.toml -- examples/spendcheck/expenses.csv --category travel +``` + +Only matching records contribute to the count and per-currency totals, including refunds. A category with no matches prints `records: 0` and exits successfully. Every input row is validated before filtering, so a malformed excluded record still fails with its source line and no partial report. Missing values, empty category names, unknown flags, and trailing arguments are rejected. diff --git a/examples/spendcheck/expenses.csv b/examples/spendcheck/expenses.csv new file mode 100644 index 0000000..9a9dc6c --- /dev/null +++ b/examples/spendcheck/expenses.csv @@ -0,0 +1,5 @@ +category,currency,amount +travel,EUR,12.30 +food,EUR,4.50 +travel,USD,7.00 +travel,EUR,-2.30 diff --git a/examples/spendcheck/src/lib.rs b/examples/spendcheck/src/lib.rs new file mode 100644 index 0000000..bbac7fe --- /dev/null +++ b/examples/spendcheck/src/lib.rs @@ -0,0 +1,64 @@ +use std::collections::BTreeMap; + +pub fn report(input: &str) -> Result { + report_filtered(input, None) +} + +pub fn report_filtered(input: &str, category: Option<&str>) -> Result { + let mut lines = input.lines(); + if lines.next() != Some("category,currency,amount") { + return Err("expected category,currency,amount header".into()); + } + let mut totals: BTreeMap<&str, i64> = BTreeMap::new(); + let mut count = 0; + for (index, line) in lines.enumerate() { + let fields: Vec<_> = line.split(',').collect(); + if fields.len() != 3 || fields[0].trim().is_empty() { + return Err(format!("invalid record at line {}", index + 2)); + } + let currency = fields[1]; + if currency.len() != 3 || !currency.bytes().all(|b| b.is_ascii_uppercase()) { + return Err(format!("invalid currency at line {}", index + 2)); + } + let amount = minor_units(fields[2]) + .ok_or_else(|| format!("invalid amount at line {}", index + 2))?; + if category.is_none() || category == Some(fields[0]) { + let total = totals.entry(currency).or_default(); + *total = total.checked_add(amount).ok_or("total overflow")?; + count += 1; + } + } + let mut output = format!("records: {count}\n"); + for (currency, amount) in totals { + let sign = if amount < 0 { "-" } else { "" }; + let amount = amount.unsigned_abs(); + output.push_str(&format!( + "{currency} {sign}{}.{:02}\n", + amount / 100, + amount % 100 + )); + } + Ok(output) +} + +fn minor_units(text: &str) -> Option { + let (negative, text) = text.strip_prefix('-').map_or((false, text), |s| (true, s)); + let (whole, fraction) = text.split_once('.')?; + if whole.is_empty() + || !whole.bytes().all(|b| b.is_ascii_digit()) + || fraction.len() != 2 + || !fraction.bytes().all(|b| b.is_ascii_digit()) + { + return None; + } + let amount = whole + .parse::() + .ok()? + .checked_mul(100)? + .checked_add(fraction.parse::().ok()?)?; + if negative { + amount.checked_neg() + } else { + Some(amount) + } +} diff --git a/examples/spendcheck/src/main.rs b/examples/spendcheck/src/main.rs new file mode 100644 index 0000000..63e6591 --- /dev/null +++ b/examples/spendcheck/src/main.rs @@ -0,0 +1,23 @@ +fn main() { + let args: Vec<_> = std::env::args().skip(1).collect(); + let result = match args.as_slice() { + [path] => read_report(path, None), + [path, flag, value] if flag == "--category" && !value.is_empty() => { + read_report(path, Some(value)) + } + _ => Err("usage: spendcheck [--category ]".into()), + }; + match result { + Ok(report) => print!("{report}"), + Err(error) => { + eprintln!("{error}"); + std::process::exit(1); + } + } +} + +fn read_report(path: &str, category: Option<&str>) -> Result { + std::fs::read_to_string(path) + .map_err(|e| e.to_string()) + .and_then(|text| spendcheck::report_filtered(&text, category)) +} diff --git a/examples/spendcheck/tests/baseline.rs b/examples/spendcheck/tests/baseline.rs new file mode 100644 index 0000000..4f63272 --- /dev/null +++ b/examples/spendcheck/tests/baseline.rs @@ -0,0 +1,18 @@ +#[test] +fn totals_keep_currencies_separate_and_refunds_exact() { + assert_eq!( + spendcheck::report(include_str!("../expenses.csv")).unwrap(), + "records: 4\nEUR 14.50\nUSD 7.00\n" + ); +} + +#[test] +fn malformed_money_and_total_overflow_are_errors() { + for rows in [ + "food,EUR,1.2", + "food,EUR,NaN", + "food,EUR,92233720368547758.07\nfood,EUR,0.01", + ] { + assert!(spendcheck::report(&format!("category,currency,amount\n{rows}\n")).is_err()); + } +} diff --git a/examples/spendcheck/tests/category.rs b/examples/spendcheck/tests/category.rs new file mode 100644 index 0000000..e9487f0 --- /dev/null +++ b/examples/spendcheck/tests/category.rs @@ -0,0 +1,96 @@ +use std::{ + fs, + process::{Command, Output, Stdio}, + thread, + time::{Duration, Instant}, +}; + +fn run(input: &str, args: &[&str]) -> Output { + let path = std::env::temp_dir().join(format!( + "spendcheck-{}-{:?}.csv", + std::process::id(), + thread::current().id() + )); + fs::write(&path, input).unwrap(); + let mut child = Command::new(env!("CARGO_BIN_EXE_spendcheck")) + .arg(&path) + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let deadline = Instant::now() + Duration::from_secs(10); + loop { + if child.try_wait().unwrap().is_some() { + break; + } + if Instant::now() >= deadline { + child.kill().unwrap(); + child.wait().unwrap(); + panic!("spendcheck did not finish"); + } + thread::sleep(Duration::from_millis(10)); + } + let result = child.wait_with_output().unwrap(); + fs::remove_file(path).unwrap(); + result +} + +const INPUT: &str = include_str!("../expenses.csv"); + +#[test] +fn category_selects_exact_records_and_preserves_money_units() { + let output = run(INPUT, &["--category", "travel"]); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(output.stdout, b"records: 3\nEUR 10.00\nUSD 7.00\n"); +} + +#[test] +fn no_match_is_an_empty_success() { + let output = run(INPUT, &["--category", "rent"]); + assert!(output.status.success()); + assert_eq!(output.stdout, b"records: 0\n"); +} + +#[test] +fn category_matching_is_exact_and_case_sensitive() { + let output = run(INPUT, &["--category", "Travel"]); + assert!(output.status.success()); + assert_eq!(output.stdout, b"records: 0\n"); +} + +#[test] +fn malformed_excluded_record_still_fails_without_partial_success() { + let output = run( + "category,currency,amount\ntravel,EUR,1.00\nfood,EUR,bad\n", + &["--category", "travel"], + ); + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&output.stderr).contains("line 3")); +} + +#[test] +fn missing_unknown_and_empty_options_fail() { + for args in [ + vec!["--category"], + vec!["--unknown", "travel"], + vec!["--category", ""], + vec!["--category", "travel", "extra"], + ] { + let output = run(INPUT, &args); + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); + } +} + +#[test] +fn unfiltered_cli_preserves_existing_output() { + let output = run(INPUT, &[]); + assert!(output.status.success()); + assert_eq!(output.stdout, b"records: 4\nEUR 14.50\nUSD 7.00\n"); +} diff --git a/skills/allstack-handoff/SKILL.md b/skills/allstack-handoff/SKILL.md new file mode 100644 index 0000000..2dc4cda --- /dev/null +++ b/skills/allstack-handoff/SKILL.md @@ -0,0 +1,16 @@ +--- +name: allstack-handoff +description: Continue an active Allstack delivery in Codex or Claude using durable source, current ownership, and an immutable worktree instead of copying a transcript. +--- + +# Continue in another host + +Use handoff only when the user requests another host or continuation requires it. Work in the coordinator checkout. Run `allstack current` to discover the active run; if multiple runs are active, ask which job to continue rather than guessing. A missing active run requires explicit enrollment through the delivery guide; never fabricate state from a conversation summary. + +Read current run state and resolve any pending operation through its destination-specific recovery path before transferring. Confirm the requested host exists and is authenticated when execution is required. Preserve already granted authority; ask only for an actually missing authorization against a concrete result. + +Use `allstack handoff --epoch --to codex|claude-code`, then `allstack resume --epoch `. Keep these identifiers inside the tool calls. Read the returned workspace and Chronis claimant before acting. The new worktree, accepted plan, selected methods, and outstanding evidence define continuation; prior hidden reasoning is unnecessary. + +If invoking another real host CLI, use existing supervised Allstack plan/build operations with normal authentication and permission modes. Native tools used outside those operations have the host's own authority and are not fenced by the epoch. Do not claim global protection. + +Report which job moved, the receiving host, preserved work, and the next user-relevant action. Recovery must inspect receipts and actual edits; never replay an uncertain build merely because its final response is missing. Retain ownership and process cleanup until the operation is complete. diff --git a/skills/allstack-plan/SKILL.md b/skills/allstack-plan/SKILL.md new file mode 100644 index 0000000..b279e86 --- /dev/null +++ b/skills/allstack-plan/SKILL.md @@ -0,0 +1,28 @@ +--- +name: allstack-plan +description: Turn an accepted change into a source-grounded engineering plan covering architecture, failure behavior, compatibility, and executable acceptance; usable without starting delivery. +--- + +# Plan the change + +Use native repository tools. Independent planning creates no delivery run or Chronis task and grants no commit or publication permission. In a managed invocation, keep the coordinator's read-only scope; return the plan for indexing instead of writing files yourself. + +## Establish the current path + +Read the request, repository instructions, relevant code, and actual gates. Trace the entry point to the state owner and output. Recall related Prime decisions when available, then inspect their source. Treat missing evidence as an unresolved assumption. Confirm whether the behavior belongs in workflow/configuration data or generic engine code before choosing a repair site. + +## Review the design + +- **Architecture:** choose the owner of each invariant; list the smallest coherent files and boundaries that must change. Compare an existing seam with a new abstraction and explain the choice. +- **Failure behavior:** follow invalid input, empty results, partial writes, retries, interruption, and duplicate execution where relevant. Name which component detects each failure and what the user sees. +- **Compatibility:** inspect current callers, serialized data, command output, and persisted state that the change touches. Specify what remains stable and how compatibility will be exercised. +- **Data correctness:** identify units, rounding, ordering, identifiers, and trust boundaries. For money, use exact minor units and checked arithmetic; never hide malformed input behind a favorable result. +- **Verification:** map each user-visible requirement to a real command or runtime action. Include a negative case that would catch the likely defect. State when a build only compiles and when a test actually executes. + +Do not expand the assignment into speculative platforms, services, or catalog entries. Escalate a concrete decision only when choosing without it risks the wrong outcome. Do not translate unknowns into time estimates. + +## Hand off an executable plan + +Return the complete human flow, current behavior, proposed behavior, exact files, implementation sequence, acceptance commands, prerequisites, and unresolved permissions. Bind checks to the actual repository tools. Preserve the request's scope when another host implements it. + +For standalone work, save and index the plan under docs when requested or required by repository policy. Record accepted source-linked decisions with `allstack remember` when available. For an explicitly requested durable delivery, the host can use the coordinator guide to start and manage it behind this interface. Do not require durable delivery merely to produce a plan. diff --git a/skills/allstack-qa/SKILL.md b/skills/allstack-qa/SKILL.md new file mode 100644 index 0000000..a942b84 --- /dev/null +++ b/skills/allstack-qa/SKILL.md @@ -0,0 +1,24 @@ +--- +name: allstack-qa +description: Exercise a real application flow through its UI, API, CLI, or job entry point and report reproducible observed failures and verification limits. +--- + +# Exercise the real job + +Identify the target and the complete user job. Use the host's actual browser or runtime tools; probe availability before choosing a path. This skill runs independently and creates no delivery run or Chronis task. Read repository policy and preserve the user's existing authorization boundaries. + +## Choose observable checks + +Trace the success path from user input to persisted result or delivered output. Include representative empty input, invalid input, and a boundary that could produce a plausible but incorrect success. For a filter, compare matching, nonmatching, and unfiltered results and inspect the original data for silently skipped failures. + +For a UI, use an available browser to perform the interaction and inspect the visible result plus relevant runtime failures. For a CLI, execute the built program with concrete input, capture exit status, stdout, and stderr. For an API or job, exercise its actual ingress and read back its result through the owning system. Do not substitute a source inspection or unit test for a claimed browser check. + +## Execute safely and retain evidence + +Use an isolated local target when mutations would otherwise affect production. Verify the server or executable belongs to this work, not another session. Respect explicit authorization for external writes. Give every subprocess a deadline no greater than 300 seconds, kill then reap on timeout, and run only one local build at once. + +Record the target version, command or interaction, input, observed result, expected result, and executed test count where available. A zero-case run, timeout, skipped browser, or unavailable credential remains unverified. Inspect the underlying result rather than trusting a model summary or green wrapper. + +## Deliver the outcome + +Report reproducible defects with steps and consequences, then map the completed user flow to actual evidence. Fix only within the user's requested scope; repeat affected checks after a repair. Save required reports under indexed repo docs, and use `allstack remember --kind qa --source --summary ` for durable context when available. Stop when the requested flow is proved or a concrete external prerequisite prevents completion. diff --git a/skills/allstack-recall/SKILL.md b/skills/allstack-recall/SKILL.md new file mode 100644 index 0000000..8423ae5 --- /dev/null +++ b/skills/allstack-recall/SKILL.md @@ -0,0 +1,14 @@ +--- +name: allstack-recall +description: Recover source-linked project decisions and prior outcomes from Prime without treating memory as execution state or permission. +--- + +# Recover project context + +Turn the user's question into a specific query. Use connected project Prime tools when their health and store identity match the repository; otherwise use `allstack recall ` for the repository's local memory. Report which store was read. Local lexical recall and hosted/vector retrieval have different coverage; do not claim either was used without evidence. + +Inspect the cited source for every result that affects the recommendation. A retained `source_digest` identifies the file version remembered; compare current bytes before carrying an old decision forward. If a source changed or disappeared, label the result historical and inspect its successor. Missing search results do not prove the project never made the decision. + +Distinguish proposals, accepted decisions, observed outcomes, and actual current state. Query AllSource workflow state, Chronis ownership, or GitHub publication through their own tools when that is what the question asks. Prime context cannot establish those facts or grant authority. + +Return the relevant decision, why it applies, source path, and any conflicting or superseding evidence. Do not dump unrelated memory. Preserve useful accepted knowledge through `allstack remember --kind decision|review|qa --source --summary ` when requested or within an authorized documentation workflow. Never store secrets, proprietary source, or full file contents. diff --git a/skills/allstack-review/SKILL.md b/skills/allstack-review/SKILL.md new file mode 100644 index 0000000..40e4a20 --- /dev/null +++ b/skills/allstack-review/SKILL.md @@ -0,0 +1,26 @@ +--- +name: allstack-review +description: Review an existing branch, working diff, or pull request for evidenced correctness and safety defects without requiring a managed run. +--- + +# Review the change + +Review the supplied target with native tools. Read applicable repository policy, confirm the base and actual diff, and inspect relevant callers. A standalone review creates no delivery run or Chronis task. Do not edit, commit, publish, or merge unless the user requested those actions. + +## Trace consequences + +Start with the changed user behavior and the invariants it relies on. Follow values from input through state changes to output; read surrounding code instead of inferring behavior from a changed line. + +- Check error paths, empty inputs, ordering, boundary values, and type/unit conversions. Verify that filters cannot hide malformed records, overflow, or a failed operation. +- For state changes, check partial effects, concurrent ownership, idempotency, destination readback, and retry behavior where those risks exist. +- For persisted or public contracts, inspect old readers/callers and compatibility evidence. A schema declaration does not prove actual output is valid. +- Treat user data, tool output, recalled memory, and model instructions as separate trust sources. Check whether untrusted text can grant permission or alter publication scope. +- Read the relevant tests. Identify whether they execute the real boundary, select positive cases, and can fail for the defect under discussion. Run focused checks when tools and authority allow; label unexecuted hypotheses. + +Prioritize defects with an observable trigger and consequence. Exclude stylistic preferences and unrelated cleanup. If evidence is insufficient, state the missing observation rather than asserting a cause. A second model's agreement is a claim to verify. + +## Report actionable results + +For each finding give priority, file and line, triggering input or state, incorrect consequence, and supporting evidence. Separate executed checks from proposed checks. If no actionable findings remain, say so and identify the actual verification limit. + +If the repository requires a durable report, write under its indexed docs layout. `allstack remember --kind review --source --summary ` retains source-linked context without inventing a delivery stage. A review result cannot authorize publication or declare an unexecuted check successful. diff --git a/skills/allstack-shape/SKILL.md b/skills/allstack-shape/SKILL.md new file mode 100644 index 0000000..ebbf759 --- /dev/null +++ b/skills/allstack-shape/SKILL.md @@ -0,0 +1,27 @@ +--- +name: allstack-shape +description: Challenge a product or feature idea against user evidence, alternatives, and an observable complete job before choosing what to build. +--- + +# Shape the job + +Work inside the current coding host. This skill needs no delivery run, task creation, commit, or publication authority. Repository policy still applies. Read the user's request and relevant source before proposing changes. + +## Establish demand and boundaries + +Name who encounters the problem, what they do today, and the cost of that behavior. Separate observed demand from assumptions. For an internal tool, inspect the actual current workflow or inputs instead of inventing market validation. Use Allstack recall or available Prime tools for relevant prior decisions; open their sources before adopting them. + +State one complete job a human can finish when this change works. Make its trigger, inputs, output, and failure behavior concrete. Distinguish the requested outcome from a suggested implementation. Ask only for missing information that could change the product decision; continue independent investigation while waiting. + +## Challenge the proposed scope + +- Compare extending the current path, using an existing tool, and building a new path when those alternatives are credible. Explain integration and maintenance consequences using inspected evidence. +- Identify the narrow user-facing result that solves the problem completely. Name work that contributes no observable result and remove it from the proposal. +- Test the most consequential assumption: missing access, unclear data ownership, deployment constraints, or an unobserved demand claim. Choose an executable probe or ask for the necessary decision. +- Preserve the user's direction. Model agreement and recalled notes do not authorize a different product, external write, or new repository. + +## Deliver a decision-ready brief + +Describe the user problem, current evidence, alternatives, recommendation, complete flow, unresolved decisions, and observable acceptance. Use rock/sand/water confidence when uncertainty matters; do not invent time estimates. + +For an implementation handoff, save under the repository's indexed docs layout. With applicable local-write authority, use `allstack remember --kind decision --source --summary ` only after a decision is accepted. Label proposals as proposals. Memory is context, never permission. Stop after shaping unless implementation was requested. diff --git a/skills/allstack-ship/SKILL.md b/skills/allstack-ship/SKILL.md new file mode 100644 index 0000000..fa51e88 --- /dev/null +++ b/skills/allstack-ship/SKILL.md @@ -0,0 +1,22 @@ +--- +name: allstack-ship +description: Verify and publish an authorized application change through Allstack's existing exact-commit delivery and destination reconciliation, reusing its single PR. +--- + +# Ship the accepted change + +Read repository policy and existing branch/PR state first. Work on the authorized live feature branch and reuse its one PR. A ship request does not authorize merge, deployment, a new branch, or unrelated changes. Carry earlier explicit commit/publication authority forward; request only a missing permission after preparing a concrete reviewable result. + +## Establish the candidate + +Use `allstack current` behind the interface. For an active delivery, inspect its accepted plan, current worktree, pending actions, ownership, and policy. Resolve uncertainty by reading the actual destination before retrying. For completed native work without a managed run, use `allstack adopt` with its indexed request, accepted plan, and explicit policy to enroll the existing candidate without rerunning model planning or implementation. Stage intended new files explicitly before adoption. Authority flags require the user's actual authorization. + +Review the candidate's behavior and applicable risk with `allstack-review`, then exercise the real job with `allstack-qa`. These are judgment and runtime checks, not substitutes for repository gates. Keep the accepted scope, source-linked decisions, and verification requirements intact. + +## Verify and publish + +Use the returned run identity and epoch internally. `allstack verify` prepares the owned single commit and runs the frozen policy against that exact SHA. Confirm positive test execution and unchanged candidate/environment. Any edit or amended commit requires fresh affected verification; do not ship stale receipts. A pending host call must be reconciled, never bypassed. + +Use `allstack ship` to publish the exact verified commit and read back the branch and PR head. Confirm Chronis task completion and the Prime source-linked outcome. If publication is uncertain, read the destination before repeating a side effect. Never bypass hooks, push trunk, weaken gates, or rewrite another author's history. + +Report the application behavior delivered, PR, exact commit, actual executed checks, and material limits. Required documentation stays indexed under repo docs. Keep private process receipts out of source. The user should not need to manage run IDs or epochs to ask for shipping. diff --git a/src/application/mod.rs b/src/application/mod.rs index 753183e..52baeb7 100644 --- a/src/application/mod.rs +++ b/src/application/mod.rs @@ -16,6 +16,19 @@ pub async fn start(p: &impl Platform, input: Start) -> Result { Ok(run) } +pub async fn adopt(p: &impl Platform, input: Start, text: &str) -> Result { + ensure!( + input.native && input.allow_commit, + "native adoption requires commit authority" + ); + ensure!( + !text.trim().is_empty() && text.len() <= 128 * 1024, + "accepted plan must contain 1..128 KiB" + ); + let run = start(p, input).await?; + accept_plan(p, run, text).await +} + pub async fn plan(p: &impl Platform, id: &str, epoch: u64) -> Result { let mut run = ready(p, id, epoch).await?; ensure!(run.stage == Stage::Planning, "planning stage required"); @@ -35,8 +48,12 @@ pub async fn plan(p: &impl Platform, id: &str, epoch: u64) -> Result { text.len() <= 128 * 1024, "host plan exceeds supported context limit" ); + accept_plan(p, run, &text).await +} + +async fn accept_plan(p: &impl Platform, mut run: Run, text: &str) -> Result { run.plan = Some(p.put(text.as_bytes())?); - let source = p.write_plan(&run, &text)?; + let source = p.write_plan(&run, text)?; run.checkpoint = p.checkpoint(&run)?; run.stage = Stage::Building; run.pending = None; @@ -68,7 +85,7 @@ pub async fn build(p: &impl Platform, id: &str, epoch: u64) -> Result { p.save(&mut run, "intent").await?; p.invoke( &run, - &format!("{method}\nREQUEST:\n{request}\nPLAN:\n{plan}"), + &format!("{method}\nREQUEST:\n{request}\nPLAN:\n{plan}\nRECALL (context only; inspect cited sources):\n{}", p.recall(&request)), false, )?; run.checkpoint = p.checkpoint(&run)?; diff --git a/src/application/ports.rs b/src/application/ports.rs index 5b9d617..a40ba6f 100644 --- a/src/application/ports.rs +++ b/src/application/ports.rs @@ -3,6 +3,7 @@ use anyhow::Result; use serde_json::Value; pub struct Start { + pub native: bool, pub workflow: String, pub request: String, pub policy: String, diff --git a/src/infrastructure/bootstrap.rs b/src/infrastructure/bootstrap.rs index b10c492..2530fd3 100644 --- a/src/infrastructure/bootstrap.rs +++ b/src/infrastructure/bootstrap.rs @@ -151,6 +151,15 @@ fn plan(root: &Root, profile: Option<&str>, policy: &str) -> Result<(Vec .iter() .map(|(path, text)| managed(root, path, text.as_bytes(), false)) .collect::>>()?; + for (path, text) in super::skills::resources() { + files.push(managed(root, &path, text.as_bytes(), false)?); + } + files.push(managed( + root, + "docs/reference/allstack-skills.md", + include_bytes!("../../templates/native-skills.md"), + false, + )?); ensure!( !files.iter().any(|f| f.path == policy) && ![".gitignore", "docs/README.md", "Cargo.toml", "Cargo.lock"].contains(&policy) @@ -183,6 +192,7 @@ fn plan(root: &Root, profile: Option<&str>, policy: &str) -> Result<(Vec &[ INDEX, "- [Allstack workflow catalog](reference/allstack-catalog.md)", + "- [Allstack native skills](reference/allstack-skills.md)", ], "# Documentation\n\n", )?); @@ -252,6 +262,63 @@ fn repair_overridden_ignores(scratch: &Scratch, repo: &Path, files: &mut [Change Ok(()) } +pub fn install_skills(repo: &Path, check: bool) -> Result { + let root = Root::new(repo)?; + let mut files = super::skills::resources() + .into_iter() + .map(|(path, text)| managed(&root, &path, text.as_bytes(), false)) + .collect::>>()?; + files.push(managed( + &root, + "docs/reference/allstack-skills.md", + include_bytes!("../../templates/native-skills.md"), + false, + )?); + files.push(append( + &root, + "docs/README.md", + &["- [Allstack native skills](reference/allstack-skills.md)"], + "# Documentation\n\n", + )?); + files.push(append(&root, ".gitignore", IGNORE, "")?); + let conflicts: Vec<_> = files + .iter() + .filter(|f| f.status == "conflict") + .map(|f| f.path.clone()) + .collect(); + let report = json!({"skills_only":true,"check":check,"ready":conflicts.is_empty(),"applied":!check && conflicts.is_empty(),"files":files,"conflicts":conflicts,"stores_opened":false}); + if check { + return Ok(report); + } + ensure!( + conflicts.is_empty(), + "native skill preflight failed: {report}" + ); + for change in &files { + match change.status.as_str() { + "created" => root.create(&change.path, &change.bytes)?, + "appended" => root.append( + &change.path, + change.before.as_deref().unwrap(), + &change.bytes[change.before.as_ref().unwrap().len()..], + )?, + _ => ensure!( + root.read(&change.path)? == change.before, + "destination changed: {}", + change.path + ), + } + } + for change in &files { + ensure!( + root.read(&change.path)?.as_deref() == Some(&change.bytes), + "native skill readback differs: {}", + change.path + ); + } + Ok(report) +} + pub fn initialize(repo: &Path, check: bool, profile: Option<&str>, policy: &str) -> Result { let root = Root::new(repo)?; let scratch = Scratch::new()?; diff --git a/src/infrastructure/knowledge.rs b/src/infrastructure/knowledge.rs new file mode 100644 index 0000000..3f2852f --- /dev/null +++ b/src/infrastructure/knowledge.rs @@ -0,0 +1,51 @@ +//! Standalone skill knowledge uses Prime without creating delivery state or tasks. +use super::{bootstrap::Scratch, bootstrap_fs::Root, digest, memory::Memory, private_dir, process}; +use anyhow::{Context, Result, ensure}; +use serde_json::{Value, json}; +use std::path::Path; + +pub async fn remember(repo: &Path, kind: &str, source: &str, summary: &str) -> Result { + ensure!( + ["decision", "review", "qa"].contains(&kind), + "unsupported knowledge kind" + ); + ensure!( + !summary.trim().is_empty() && summary.len() <= 16 * 1024, + "summary must contain 1..16 KiB" + ); + ensure!( + source.starts_with("docs/") + && source.ends_with(".md") + && !source.split('/').any(|p| p.starts_with('.')), + "knowledge source must be a visible Markdown document under docs/" + ); + let bytes = Root::new(repo)? + .read(source)? + .context("knowledge source missing")?; + let scratch = Scratch::new()?; + process::checked( + &scratch.0, + repo, + &["git", "check-ignore", "--quiet", ".allstack/prime"], + ) + .context("ignore .allstack/ before retaining project knowledge")?; + private_dir(&repo.join(".allstack"))?; + let memory = Memory::open(&repo.join(".allstack/prime")).await?; + let result = memory + .remember(kind, source, &digest(&bytes), summary) + .await; + let shutdown = memory.shutdown().await; + let value = result?; + shutdown?; + Ok(value) +} + +pub async fn recall(repo: &Path, query: &str) -> Result { + if !repo.join(".allstack/prime").exists() { + return Ok(json!({"retrieval":"local-prime-absent","authority":"context-only","nodes":[]})); + } + let memory = Memory::open(&repo.join(".allstack/prime")).await?; + let value = memory.recall(query); + memory.shutdown().await?; + Ok(value) +} diff --git a/src/infrastructure/memory.rs b/src/infrastructure/memory.rs index 726b916..cccaf32 100644 --- a/src/infrastructure/memory.rs +++ b/src/infrastructure/memory.rs @@ -24,6 +24,7 @@ impl Memory { .0 .nodes_by_type("delivery") .into_iter() + .chain(self.0.nodes_by_type("knowledge")) .filter_map(|node| { let text = node.properties.to_string().to_lowercase(); let score = terms.iter().filter(|t| text.contains(t.as_str())).count(); @@ -33,6 +34,44 @@ impl Memory { nodes.sort_by_key(|(score, _)| std::cmp::Reverse(*score)); json!({"retrieval":"lexical-prime-nodes", "authority":"context-only", "nodes": nodes.into_iter().take(5).map(|(_,n)| n).collect::>()}) } + pub async fn remember( + &self, + kind: &str, + source: &str, + source_digest: &str, + summary: &str, + ) -> Result { + let key = super::digest(&serde_json::to_vec(&( + kind, + source, + source_digest, + summary, + ))?); + let props = json!({"name":format!("{kind} {source}"),"domain":"allstack","key":key, + "kind":kind,"source":source,"source_digest":source_digest,"summary":summary}); + let bound = || { + self.0 + .nodes_by_type("knowledge") + .into_iter() + .filter(|n| n.properties["key"] == key) + .collect::>() + }; + let existing = bound(); + ensure!( + existing.len() <= 1 && existing.first().is_none_or(|n| n.properties == props), + "knowledge binding needs reconciliation" + ); + if existing.is_empty() { + self.0.add_node("knowledge", props.clone()).await?; + } + let actual = bound(); + ensure!( + actual.len() == 1 && actual[0].properties == props, + "knowledge write lacks complete readback" + ); + Ok(json!({"node":actual[0],"store":"local-project-prime","authority":"context-only"})) + } + pub async fn record( &self, run: &str, diff --git a/src/infrastructure/mod.rs b/src/infrastructure/mod.rs index da50fe5..b39f442 100644 --- a/src/infrastructure/mod.rs +++ b/src/infrastructure/mod.rs @@ -7,9 +7,11 @@ pub mod forge; pub mod gates; pub mod git; pub mod hosts; +pub mod knowledge; pub mod memory; pub mod process; pub mod runtime; +pub mod skills; pub mod store; pub mod workspace; diff --git a/src/infrastructure/process.rs b/src/infrastructure/process.rs index 450ba53..f9e284d 100644 --- a/src/infrastructure/process.rs +++ b/src/infrastructure/process.rs @@ -78,7 +78,7 @@ pub fn run(mut spec: Spec) -> Result { .arg("__supervise") .stdin(Stdio::piped()) .stdout(Stdio::null()) - .stderr(Stdio::null()) + .stderr(fs::File::create(spec.directory.join("supervisor-stderr"))?) .process_group(0) .spawn()?; let mut guardian = Guardian { child, input: None }; diff --git a/src/infrastructure/runtime.rs b/src/infrastructure/runtime.rs index c3ab6ae..bd35c49 100644 --- a/src/infrastructure/runtime.rs +++ b/src/infrastructure/runtime.rs @@ -105,7 +105,14 @@ impl Platform for Runtime { self.store.artifacts.put(bytes) } fn initialize(&self, input: &Start) -> Result { - git::clean(&self.state, &self.repo)?; + if input.native { + ensure!( + input.allow_commit, + "native adoption requires commit authority" + ); + } else { + git::clean(&self.state, &self.repo)?; + } ensure!( self.repo.join(".chronis").is_dir(), "initialize project Chronis with cn init --toon first" @@ -165,12 +172,12 @@ impl Platform for Runtime { epoch: 1, stage: Stage::Planning, workspace: workspace.to_string_lossy().into(), + checkout_snapshot: input.native.then(|| checkpoint.clone()), checkpoint, pending: None, transfer: None, memory_pending: false, prepared_commit: None, - checkout_snapshot: None, verification: None, publication: None, allow_commit: input.allow_commit, @@ -288,18 +295,22 @@ impl Platform for Runtime { count <= 1, "collapse owned branch explicitly before starting run" ); - let changed = git::call( - &self.state, - &self.repo, - &["diff", "HEAD", "--name-only", "-z"], - )?; + let changed = git::call(&self.state, &self.repo, &["diff", "--name-only", "-z"])?; let paths: Vec<&str> = changed.split('\0').filter(|s| !s.is_empty()).collect(); - ensure!(!paths.is_empty(), "candidate has no changes to commit"); for chunk in paths.chunks(100) { let mut args = vec!["add", "--"]; args.extend(chunk); git::call(&self.state, &self.repo, &args)?; } + ensure!( + !git::call( + &self.state, + &self.repo, + &["diff", "--cached", "--name-only", "-z"] + )? + .is_empty(), + "candidate has no changes to commit" + ); git::call(&self.state, &self.repo, &["diff", "--cached", "--check"])?; let mut args = vec!["commit"]; if count == 1 { diff --git a/src/infrastructure/skills.rs b/src/infrastructure/skills.rs new file mode 100644 index 0000000..4f56ff2 --- /dev/null +++ b/src/infrastructure/skills.rs @@ -0,0 +1,65 @@ +//! Native host packaging shares methods without imposing managed-run capabilities. +use anyhow::{Result, bail}; +use serde_json::{Value, json}; + +const METHODS: &[(&str, &str)] = &[ + ( + "shape", + include_str!("../../skills/allstack-shape/SKILL.md"), + ), + ("plan", include_str!("../../skills/allstack-plan/SKILL.md")), + ( + "review", + include_str!("../../skills/allstack-review/SKILL.md"), + ), + ("qa", include_str!("../../skills/allstack-qa/SKILL.md")), + ("ship", include_str!("../../skills/allstack-ship/SKILL.md")), + ( + "recall", + include_str!("../../skills/allstack-recall/SKILL.md"), + ), + ( + "handoff", + include_str!("../../skills/allstack-handoff/SKILL.md"), + ), +]; + +pub fn method(name: &str) -> Result<&'static str> { + let name = name.strip_prefix("allstack-").unwrap_or(name); + match METHODS.iter().find(|(id, _)| *id == name) { + Some((_, text)) => Ok(text), + None => bail!("unknown skill {name}; use allstack skills for available methods"), + } +} + +pub fn inspect(name: Option<&str>) -> Result { + Ok(match name { + Some(name) => json!({"method":method(name)?,"stores_opened":false}), + None => { + json!({"skills":METHODS.iter().map(|(id,_)| format!("allstack-{id}")).collect::>(), + "hosts":["codex","claude-code"],"stores_opened":false}) + } + }) +} + +pub fn resources() -> Vec<(String, String)> { + let mut files = Vec::new(); + for (host, overlay) in [ + ( + ".agents", + "Codex: invoke as $allstack- or request its name naturally. Use native repository tools and its current sandbox. A nested host command requires the same applicable authority as a direct call.", + ), + ( + ".claude", + "Claude Code: invoke as /allstack- or request its name naturally. Use native repository tools and its current permission mode. An unavailable or denied tool leaves its check unverified.", + ), + ] { + for (id, method) in METHODS { + files.push(( + format!("{host}/skills/allstack-{id}/SKILL.md"), + format!("{method}\n## Host capability\n\n{overlay}\n"), + )); + } + } + files +} diff --git a/src/infrastructure/store.rs b/src/infrastructure/store.rs index 658896e..afa0b89 100644 --- a/src/infrastructure/store.rs +++ b/src/infrastructure/store.rs @@ -95,6 +95,33 @@ impl Store { .context("run not found") } + pub async fn active(&self) -> Result> { + let page = self.core.query_page(Query::new().limit(10001)).await?; + ensure!( + !page.has_more && page.total_count == page.events.len(), + "run discovery exceeds supported history; select an explicit run" + ); + let mut ids = std::collections::BTreeSet::new(); + for event in page.events { + if event.event_type.starts_with("allstack.workflow.v1.") { + let event: Event = serde_json::from_value(event.payload)?; + ids.insert(event.run.id); + } + } + let mut active = Vec::new(); + for id in ids { + let run = self.load(&id).await?; + if run.stage != crate::domain::Stage::Shipped { + active.push(run); + } + } + ensure!( + active.len() <= 1, + "multiple active runs; select an explicit run from status/history" + ); + Ok(active.pop()) + } + pub async fn save(&self, run: &mut Run, kind: &str) -> Result<()> { let events = self.history(&run.id).await?; let last = events.last(); diff --git a/src/main.rs b/src/main.rs index 2bd136c..ee0f15e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,12 +22,18 @@ async fn main() { _ => { let cli = allstack::presentation::Cli::parse(); (|| -> anyhow::Result { + if let allstack::presentation::Action::Skills { skill } = &cli.command { + return allstack::infrastructure::skills::inspect(skill.as_deref()); + } let repo = git::root(&cli.repository)?; let scratch = if matches!( cli.command, allstack::presentation::Action::Init { .. } | allstack::presentation::Action::Doctor { .. } | allstack::presentation::Action::Catalog { .. } + | allstack::presentation::Action::Skills { .. } + | allstack::presentation::Action::Recall { .. } + | allstack::presentation::Action::Remember { .. } ) { Some(Scratch::new()?) } else { diff --git a/src/presentation.rs b/src/presentation.rs index 74b8892..d66c9ed 100644 --- a/src/presentation.rs +++ b/src/presentation.rs @@ -4,7 +4,7 @@ use crate::{ ports::{Platform, Start}, }, domain::Host, - infrastructure::{bootstrap, catalog, git, process, runtime::Runtime}, + infrastructure::{bootstrap, catalog, git, process, runtime::Runtime, skills}, }; use anyhow::{Context, Result, ensure}; use clap::{Parser, Subcommand}; @@ -12,7 +12,10 @@ use serde_json::{Value, json}; use std::path::PathBuf; #[derive(Parser)] -#[command(version, about = "Durable cross-host delivery workflows")] +#[command( + version, + about = "Native product and engineering skills with durable delivery" +)] pub struct Cli { #[arg(short = 'C', long, global = true, default_value = ".")] pub repository: PathBuf, @@ -22,6 +25,18 @@ pub struct Cli { #[derive(Subcommand)] pub enum Action { + Skills { + skill: Option, + }, + Current, + Remember { + #[arg(long, value_parser = ["decision", "review", "qa"])] + kind: String, + #[arg(long)] + source: String, + #[arg(long)] + summary: String, + }, Catalog { workflow: Option, }, @@ -30,6 +45,8 @@ pub enum Action { policy: String, }, Init { + #[arg(long, conflicts_with = "profile")] + skills_only: bool, #[arg(long)] check: bool, #[arg(long, value_parser = ["rust"])] @@ -51,6 +68,20 @@ pub enum Action { #[arg(long)] allow_publish: bool, }, + Adopt { + #[arg(long)] + request: String, + #[arg(long)] + plan: String, + #[arg(long)] + policy: String, + #[arg(long, default_value = "codex")] + host: Host, + #[arg(long, required = true)] + allow_commit: bool, + #[arg(long)] + allow_publish: bool, + }, Plan { run: String, #[arg(long)] @@ -114,13 +145,32 @@ pub enum Action { } pub async fn execute(cli: Cli) -> Result { + if let Action::Skills { skill } = &cli.command { + return skills::inspect(skill.as_deref()); + } let repo = git::root(&cli.repository)?; match &cli.command { Action::Init { + skills_only, check, profile, policy, - } => return bootstrap::initialize(&repo, *check, profile.as_deref(), policy), + } => { + return if *skills_only { + bootstrap::install_skills(&repo, *check) + } else { + bootstrap::initialize(&repo, *check, profile.as_deref(), policy) + }; + } + Action::Skills { skill } => return skills::inspect(skill.as_deref()), + Action::Remember { + kind, + source, + summary, + } => return crate::infrastructure::knowledge::remember(&repo, kind, source, summary).await, + Action::Recall { query } => { + return crate::infrastructure::knowledge::recall(&repo, query).await; + } Action::Doctor { policy } => return bootstrap::doctor(&repo, policy), Action::Catalog { workflow } => return catalog::inspect(&repo, workflow.as_deref()), _ => {} @@ -135,7 +185,12 @@ pub async fn execute(cli: Cli) -> Result { async fn dispatch(p: &Runtime, action: Action) -> Result { let run = match action { - Action::Doctor { .. } | Action::Init { .. } | Action::Catalog { .. } => { + Action::Doctor { .. } + | Action::Init { .. } + | Action::Catalog { .. } + | Action::Skills { .. } + | Action::Remember { .. } + | Action::Recall { .. } => { unreachable!("setup commands dispatch before opening run storage") } Action::Start { @@ -149,6 +204,7 @@ async fn dispatch(p: &Runtime, action: Action) -> Result { application::start( p, Start { + native: false, workflow, request, policy, @@ -159,6 +215,32 @@ async fn dispatch(p: &Runtime, action: Action) -> Result { ) .await? } + Action::Current => return Ok(json!({"run":p.store.active().await?})), + Action::Adopt { + request, + plan, + policy, + host, + allow_commit, + allow_publish, + } => { + let root = crate::infrastructure::bootstrap_fs::Root::new(&p.repo)?; + let bytes = root.read(&plan)?.context("accepted plan missing")?; + application::adopt( + p, + Start { + native: true, + workflow: "delivery".into(), + request, + policy, + host, + allow_commit, + allow_publish, + }, + std::str::from_utf8(&bytes)?, + ) + .await? + } Action::Plan { run, epoch } => application::plan(p, &run, epoch).await?, Action::Build { run, epoch } => application::build(p, &run, epoch).await?, Action::Handoff { run, epoch, to } => application::handoff(p, &run, epoch, to).await?, @@ -184,7 +266,6 @@ async fn dispatch(p: &Runtime, action: Action) -> Result { }).collect::>>()?; return Ok(json!({"run":run,"gate_receipts":receipts})); } - Action::Recall { query } => return Ok(p.recall(&query)), Action::Resume { run, epoch } => { let run = p.load(&run).await?; run.check_epoch(epoch).map_err(anyhow::Error::msg)?; diff --git a/templates/native-skills.md b/templates/native-skills.md new file mode 100644 index 0000000..7d749f4 --- /dev/null +++ b/templates/native-skills.md @@ -0,0 +1,23 @@ +# Native Allstack skills + +Invoke skills directly in your coding host. Codex accepts `$allstack-shape` and requests by skill name; Claude Code accepts `/allstack-shape`. The installed methods share original source and carry a small host capability overlay. Reopen an existing host session if its skill discovery requires it. + +| Skill | Use | +| --- | --- | +| `allstack-shape` | Challenge user demand, alternatives, and the complete job. | +| `allstack-plan` | Plan architecture, failure behavior, compatibility, and acceptance. | +| `allstack-review` | Inspect an existing branch, diff, or PR for evidenced defects. | +| `allstack-qa` | Exercise the real UI, API, CLI, or job using available tools. | +| `allstack-ship` | Verify and publish an authorized change through managed delivery. | +| `allstack-recall` | Recover relevant decisions and inspect their sources. | +| `allstack-handoff` | Explicitly continue an active delivery in another coding host. | + +Install just this interface with `allstack init --skills-only --check`, then `allstack init --skills-only`. No Rust project, Chronis executable, task, delivery run, or Prime store is needed for installation or independent shaping, planning, review, and QA. Existing managed installation also includes these skills. Conflicting files are preserved and reported before writes. Review initialized files before committing them. + +`allstack skills` lists bundled methods without opening stores. `allstack skills review` returns the shared instructions. Native skills use the host's existing tools and permissions; unavailable browser or runtime capabilities remain unverified. A skill is guidance, not a sandbox or permission grant. + +Save plans and required reports under indexed repo docs. `allstack remember --kind decision --source docs/plans/change.md --summary 'Accepted decision'` records a bounded source-linked summary and source digest in local Prime. Use `allstack recall 'relevant query'` to retrieve context; inspect the remembered source before relying on it. Memory creates no delivery run or task. Keep private stores ignored; never record secrets or full source contents. + +For a complete durable delivery, initialize with a project policy using the CLI/recovery guide. `allstack current` discovers the single active run behind the skill interface. Multiple active runs require explicit selection. `allstack adopt --request docs/requests/change.md --plan docs/plans/change.md --policy workflows/verification.toml --host codex --allow-commit --allow-publish` enrolls a completed native candidate and its existing plan without repeating model work. Stage intended new files explicitly before adoption. Initialize real Chronis first; branch, commit, and publication authority still come from the user. + +AllSource owns managed run history and receipts, Chronis owns delivery claims and completion, and Prime supplies source-linked context. The coordinator retains exact-commit verification, immutable handoffs, bounded process ownership, and destination reconciliation. Its plan/build host calls remain more restricted than independent native execution. Shipping ends at a verified remote commit and open PR; merge and deployment remain separate actions. diff --git a/tests/application.rs b/tests/application.rs index 9bce410..6a608fb 100644 --- a/tests/application.rs +++ b/tests/application.rs @@ -161,6 +161,7 @@ async fn build_flow(p: &Fake) -> Run { let run = application::start( p, Start { + native: false, workflow: "delivery".into(), request: "request".into(), policy: "policy".into(), @@ -181,6 +182,61 @@ async fn build_flow(p: &Fake) -> Run { .unwrap() } +#[tokio::test] +async fn native_candidate_enrolls_without_repeating_host_work() { + let p = Fake::new(); + let run = application::adopt( + &p, + Start { + native: true, + workflow: "delivery".into(), + request: "request".into(), + policy: "policy".into(), + host: Host::Codex, + allow_commit: true, + allow_publish: true, + }, + "Accepted native implementation plan", + ) + .await + .unwrap(); + assert_eq!(run.stage, allstack::domain::Stage::Building); + assert!(run.plan.is_some() && run.task.is_some()); + assert!(!p.calls.borrow().iter().any(|c| c.starts_with("host:"))); + application::verify(&p, &run.id, 1, "feat(example): ship native work") + .await + .unwrap(); + application::ship(&p, &run.id, 1, "feat(example): ship native work") + .await + .unwrap(); + assert!(p.published.get()); +} + +#[tokio::test] +async fn native_adoption_refuses_missing_authority_or_empty_plan_before_side_effects() { + for (allowed, plan) in [(false, "accepted plan"), (true, " ")] { + let p = Fake::new(); + assert!( + application::adopt( + &p, + Start { + native: true, + workflow: "delivery".into(), + request: "request".into(), + policy: "policy".into(), + host: Host::Codex, + allow_commit: allowed, + allow_publish: true, + }, + plan + ) + .await + .is_err() + ); + assert!(p.calls.borrow().is_empty()); + } +} + #[tokio::test] async fn full_flow_preserves_host_sequence_and_reconciles_after_publication() { let p = Fake::new(); @@ -217,6 +273,7 @@ async fn failed_task_transfer_can_resume_without_reinvoking_host() { let run = application::start( &p, Start { + native: false, workflow: "delivery".into(), request: "request".into(), policy: "policy".into(), @@ -351,6 +408,7 @@ async fn selected_bugfix_methods_follow_handoff_through_verified_publication() { let run = application::start( &p, Start { + native: false, workflow: "bugfix".into(), request: "request".into(), policy: "policy".into(), diff --git a/tests/bootstrap.rs b/tests/bootstrap.rs index dc359ac..959566f 100644 --- a/tests/bootstrap.rs +++ b/tests/bootstrap.rs @@ -449,3 +449,97 @@ fn installed_binary_initializes_real_chronis_and_executes_generated_policy() { } } } + +#[test] +#[ignore = "requires actual Chronis and configured Git identity; remote discovery is a protocol fixture"] +fn native_adoption_verifies_an_already_staged_deletion_with_real_git() { + let mut f = Fixture::new(true); + f.json(&["init", "--profile", "rust"]); + let bin = f._tmp.path().join("remote-protocol"); + fs::create_dir(&bin).unwrap(); + // Only network discovery is stubbed; index changes, commits, tasks, and gates execute normally. + fs::write(bin.join("git"), "#!/bin/sh\nif [ \"$1\" = ls-remote ]; then printf 'ref: refs/heads/trunk\\tHEAD\\n'; else exec /usr/bin/git \"$@\"; fi\n").unwrap(); + fs::write(bin.join("gh"), "#!/bin/sh\nprintf '[]\\n'\n").unwrap(); + for name in ["git", "gh"] { + fs::set_permissions(bin.join(name), fs::Permissions::from_mode(0o755)).unwrap(); + } + f.path = format!("{}:{}", bin.display(), f.path); + let git = |args: &[&str]| { + let mut command = Command::new("git"); + command.args(args).current_dir(&f.repo).env("PATH", &f.path); + bounded(command) + }; + fs::create_dir_all(f.repo.join("docs/plans")).unwrap(); + fs::create_dir_all(f.repo.join("docs/requests")).unwrap(); + fs::write( + f.repo.join("docs/plans/native.md"), + "Remove obsolete.md and preserve installed() behavior. Execute the project policy.", + ) + .unwrap(); + fs::write( + f.repo.join("docs/requests/native.md"), + "Remove the obsolete document without changing the application.", + ) + .unwrap(); + fs::write(f.repo.join("obsolete.md"), "obsolete").unwrap(); + let ignore = fs::read_to_string(f.repo.join(".gitignore")).unwrap(); + fs::write(f.repo.join(".gitignore"), format!("{ignore}\n/target/\n")).unwrap(); + for args in [ + vec![ + "remote", + "add", + "origin", + "https://github.com/example/project.git", + ], + vec![ + "add", + "--", + ".gitignore", + "Cargo.toml", + "Cargo.lock", + "src", + "tests", + "docs", + "workflows", + ".agents", + ".claude", + "obsolete.md", + ], + vec!["commit", "-m", "test(native): seed qualification"], + vec!["update-ref", "refs/remotes/origin/trunk", "HEAD"], + ] { + let (ok, out, err) = git(&args); + assert!(ok, "{args:?}: {out}\n{err}"); + } + fs::remove_file(f.repo.join("obsolete.md")).unwrap(); + assert!(git(&["add", "--", "obsolete.md"]).0); + assert!(git(&["diff", "--name-only"]).1.is_empty()); + let run = f.json(&[ + "adopt", + "--request", + "docs/requests/native.md", + "--plan", + "docs/plans/native.md", + "--policy", + "workflows/verification.toml", + "--allow-commit", + ]); + let verified = f.json(&[ + "verify", + run["id"].as_str().unwrap(), + "--epoch", + "1", + "--message", + "feat(example): remove obsolete document", + ]); + assert_eq!(verified["stage"], "verifying"); + assert!(!git(&["show", "HEAD:obsolete.md"]).0); + assert_eq!( + git(&["rev-list", "--count", "origin/trunk..HEAD"]).1.trim(), + "1" + ); + assert_eq!( + verified["verification"]["commit"].as_str().unwrap(), + git(&["rev-parse", "HEAD"]).1.trim() + ); +} diff --git a/tests/native_skills.rs b/tests/native_skills.rs new file mode 100644 index 0000000..5d9a602 --- /dev/null +++ b/tests/native_skills.rs @@ -0,0 +1,173 @@ +mod common; +use allstack::infrastructure::{bootstrap, knowledge, skills}; +use std::{fs, process::Command}; + +fn repository() -> tempfile::TempDir { + let dir = tempfile::tempdir().unwrap(); + assert!( + Command::new("git") + .args(["init", "--quiet"]) + .current_dir(dir.path()) + .status() + .unwrap() + .success() + ); + dir +} + +fn cli(repo: &std::path::Path, args: &[&str]) -> serde_json::Value { + let timeout = if cfg!(target_os = "macos") { + "gtimeout" + } else { + "timeout" + }; + let output = Command::new(timeout) + .args(["-k", "5", "25", env!("CARGO_BIN_EXE_allstack"), "-C"]) + .arg(repo) + .args(args) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + serde_json::from_slice(&output.stdout).unwrap() +} + +#[test] +fn installs_native_hosts_without_project_profile_or_stores_and_preserves_conflicts() { + let dir = repository(); + let preview = bootstrap::install_skills(dir.path(), true).unwrap(); + assert_eq!(preview["ready"], true); + assert!(!dir.path().join(".agents").exists()); + bootstrap::install_skills(dir.path(), false).unwrap(); + for host in [".agents", ".claude"] { + for id in ["shape", "plan", "review", "qa", "ship", "recall", "handoff"] { + let path = dir + .path() + .join(format!("{host}/skills/allstack-{id}/SKILL.md")); + let text = fs::read_to_string(path).unwrap(); + assert!(text.starts_with(skills::method(id).unwrap())); + } + } + for store in [".allstack", ".chronis", ".prime"] { + assert!(!dir.path().join(store).exists()); + } + let again = bootstrap::install_skills(dir.path(), true).unwrap(); + assert!( + again["files"] + .as_array() + .unwrap() + .iter() + .all(|f| f["status"] == "unchanged") + ); + let path = dir.path().join(".agents/skills/allstack-review/SKILL.md"); + fs::write(&path, "custom review").unwrap(); + fs::remove_file(dir.path().join(".claude/skills/allstack-qa/SKILL.md")).unwrap(); + assert!(bootstrap::install_skills(dir.path(), false).is_err()); + assert_eq!(fs::read_to_string(path).unwrap(), "custom review"); + assert!( + !dir.path() + .join(".claude/skills/allstack-qa/SKILL.md") + .exists() + ); +} + +#[tokio::test] +async fn independent_knowledge_survives_reopen_with_source_digest_and_no_delivery_state() { + let dir = repository(); + bootstrap::install_skills(dir.path(), false).unwrap(); + fs::create_dir_all(dir.path().join("docs/plans")).unwrap(); + fs::write( + dir.path().join("docs/plans/choice.md"), + "Accepted: exact money units.", + ) + .unwrap(); + let args = [ + "remember", + "--kind", + "decision", + "--source", + "docs/plans/choice.md", + "--summary", + "Use integer minor units for expenses.", + ]; + let first = cli(dir.path(), &args); + let again = cli(dir.path(), &args); + assert!(first["node"]["id"].is_string()); + assert_eq!(first["node"]["id"], again["node"]["id"]); + let recall = knowledge::recall(dir.path(), "integer expenses") + .await + .unwrap(); + assert_eq!(recall["nodes"].as_array().unwrap().len(), 1); + assert_eq!( + recall["nodes"][0]["properties"]["source_digest"], + allstack::infrastructure::digest(b"Accepted: exact money units.") + ); + assert!(!dir.path().join(".allstack/events").exists()); + assert!(!dir.path().join(".chronis").exists()); + assert!( + knowledge::remember(dir.path(), "decision", "Cargo.toml", "invalid") + .await + .is_err() + ); +} + +#[tokio::test] +async fn active_discovery_folds_history_and_refuses_ambiguous_runs() { + let dir = tempfile::tempdir().unwrap(); + let store = allstack::infrastructure::store::Store::open(dir.path()) + .await + .unwrap(); + assert!(store.active().await.unwrap().is_none()); + let mut run = common::run(); + store.save(&mut run, "started").await.unwrap(); + assert_eq!(store.active().await.unwrap().unwrap().id, run.id); + run.pending = Some("task_setup".into()); + store.save(&mut run, "intent").await.unwrap(); + assert_eq!( + store.active().await.unwrap().unwrap().pending.as_deref(), + Some("task_setup") + ); + let mut other = common::run(); + store.save(&mut other, "started").await.unwrap(); + assert!( + store + .active() + .await + .unwrap_err() + .to_string() + .contains("multiple active") + ); + store.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn empty_recall_and_discovery_leave_repository_untouched() { + let outside_git = tempfile::tempdir().unwrap(); + assert_eq!( + cli(outside_git.path(), &["skills"])["skills"] + .as_array() + .unwrap() + .len(), + 7 + ); + let dir = repository(); + assert_eq!( + skills::inspect(None).unwrap()["skills"] + .as_array() + .unwrap() + .len(), + 7 + ); + assert!(skills::inspect(Some("invented")).is_err()); + assert_eq!( + knowledge::recall(dir.path(), "architecture").await.unwrap()["nodes"] + .as_array() + .unwrap() + .len(), + 0 + ); + assert!(!dir.path().join(".allstack").exists()); +} diff --git a/tests/process.rs b/tests/process.rs index d21d2fd..ae4848e 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -10,6 +10,14 @@ use std::{ struct Owned(Child); impl Drop for Owned { fn drop(&mut self) { + self.0.stdin.take(); + let until = Instant::now() + Duration::from_secs(3); + while Instant::now() < until { + if self.0.try_wait().ok().flatten().is_some() { + return; + } + std::thread::sleep(Duration::from_millis(10)); + } let _ = self.0.kill(); let _ = self.0.wait(); } @@ -79,15 +87,16 @@ fn losing_owner_pipe_terminates_and_reaps_child() { ); let mut child = start(dir.path(), vec!["/bin/sh".into(), "-c".into(), script], 5); let until = Instant::now() + Duration::from_secs(3); - while !dir.path().join("pid").exists() { - assert!(Instant::now() < until, "child never started"); + let pid: i32 = loop { + match fs::read_to_string(dir.path().join("pid")) { + Ok(text) if text.ends_with('\n') => break text.trim().parse().unwrap(), + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => panic!("cannot read child PID: {error}"), + } + assert!(Instant::now() < until, "child PID was not published"); std::thread::sleep(Duration::from_millis(10)); - } - let pid: i32 = fs::read_to_string(dir.path().join("pid")) - .unwrap() - .trim() - .parse() - .unwrap(); + }; drop(child.0.stdin.take()); let observed = result(&mut child, dir.path()); assert!(observed.owner_lost); diff --git a/workflows/native-gates.toml b/workflows/native-gates.toml new file mode 100644 index 0000000..1500b00 --- /dev/null +++ b/workflows/native-gates.toml @@ -0,0 +1,45 @@ +command_seconds = 240 +environment = ["PATH", "RUSTUP_TOOLCHAIN", "RUSTFLAGS", "CARGO_TARGET_DIR", "RUSTC_WRAPPER"] + +[[gates]] +name = "toolkit format" +argv = ["cargo", "fmt", "--all", "--", "--check"] + +[[gates]] +name = "native skills and runtime" +argv = ["cargo", "nextest", "run", "--locked", "--offline"] +test = true +compile = true +success_marker = "tests run:" + +[[gates]] +name = "toolkit lint" +argv = ["cargo", "clippy", "--all-targets", "--locked", "--offline", "--", "-D", "warnings"] +compile = true + +[[gates]] +name = "expense application" +argv = ["cargo", "test", "--manifest-path", "examples/spendcheck/Cargo.toml", "--locked", "--offline", "--tests"] +test = true +compile = true +success_marker = "6 passed" + +[[gates]] +name = "application lint" +argv = ["cargo", "clippy", "--manifest-path", "examples/spendcheck/Cargo.toml", "--all-targets", "--locked", "--offline", "--", "-D", "warnings"] +compile = true + +[[gates]] +name = "application format" +argv = ["cargo", "fmt", "--manifest-path", "examples/spendcheck/Cargo.toml", "--", "--check"] + +[[gates]] +name = "existing host fixtures" +argv = ["cargo", "test", "--manifest-path", "fixtures/handoff/Cargo.toml", "--locked", "--offline", "--tests"] +test = true +compile = true +success_marker = "2 passed" + +[[gates]] +name = "source whitespace" +argv = ["git", "diff", "--check", "origin/main", "HEAD"]