diff --git a/README.md b/README.md index cc2dc6e..da3b7b1 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ allstack --help allstack -C /path/to/target-repository init --check --profile rust allstack -C /path/to/target-repository init --profile rust allstack -C /path/to/target-repository doctor +allstack -C /path/to/target-repository catalog +allstack -C /path/to/target-repository catalog bugfix ``` `init` bundles workflow methods, indexed CLI documentation, repository-local Codex/Claude guidance, private-store ignores, and Chronis initialization. The explicit Rust profile requires `Cargo.toml` and `Cargo.lock`; an existing valid `--policy ` supports custom verification. Conflicting managed files are preserved and block writes. Reruns fill missing resources without replacing existing policies or repository instructions. Review initialized inputs before committing them. @@ -47,6 +49,10 @@ The [fresh-repository delivery report](docs/reports/2026-10-24-fresh-repository- 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. +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). + +The [catalog verification report](docs/reports/2026-10-25-workflow-catalog-verification.md) records installed selection, legacy-history readback, and an authenticated Codex/Claude bug-fix journey with recovered host calls and frozen regression coverage. + Each command holds an exclusive kernel lock on the local store. There is no persistent coordinator daemon or IPC service. Epoch checks fence Allstack-managed transitions; native tools used outside Allstack remain outside that boundary. Snapshots require explicit `git add -- ` for new files. They reject symlinks and recognized sensitive filenames, and allow at most 4 MiB per file, 64 MiB total, and 10,000 files. These checks do not detect every possible secret. Keep private stores and process evidence under ignored `.allstack/`. @@ -58,6 +64,7 @@ Snapshots require explicit `git add -- ` for new files. They reject symlin - [Release acceptance scenarios](docs/reference/release-acceptance.md) - [CLI and recovery guide](docs/reference/cli.md) - [Delivery workflow data](workflows/delivery.toml) +- [Workflow catalog](docs/reference/workflow-catalog.md) - [Codex host guidance](hosts/codex/SKILL.md) - [Claude Code host guidance](hosts/claude-code/SKILL.md) diff --git a/docs/README.md b/docs/README.md index 0d52ecb..ed7ec4f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,12 +2,19 @@ ## Design +- [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. - [2026-10-24: Cross-host plan → build → verify → ship](plans/2026-10-24-cross-host-workflow-design.md) — product boundary, component ownership, state transitions, recovery, and implementation sequence. ## Reference +- [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. +- [Catalog bug-fix request](reference/catalog-bugfix-request.md) — reproducible Prime retry conflict for the authenticated workflow journey. + - [Fresh repository authenticated delivery verification](reports/2026-10-24-fresh-repository-delivery-verification.md) — installed initialization, real Codex/Claude execution, five passing tests, new remote publication, and Chronis/Prime readback. - [Installed repository bootstrap verification](reports/2026-10-24-repository-bootstrap-verification.md) — source-denied installed executable, real Chronis, idempotent setup, and generated-policy execution. @@ -27,3 +34,5 @@ The repository is an implementation preview. Source-level contracts and fixture - [Allstack delivery plan 9acd9993-3a70-4a87-b4fe-f62b96eb2cbb](plans/allstack-9acd9993-3a70-4a87-b4fe-f62b96eb2cbb.md) - [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) diff --git a/docs/plans/2026-10-24-workflow-catalog-design.md b/docs/plans/2026-10-24-workflow-catalog-design.md new file mode 100644 index 0000000..f8fc68c --- /dev/null +++ b/docs/plans/2026-10-24-workflow-catalog-design.md @@ -0,0 +1,79 @@ +# Selectable workflow catalog and evidence-led bug fixes + +Date: 2026-10-24. Status: implementation preview on `feat/workflow-catalog`. See the [verification report](../reports/2026-10-25-workflow-catalog-verification.md) for installed resources, retained-run compatibility, and authenticated host evidence. + +## Outcome + +An installed Allstack executable lists available repository workflows, explains their methods and supported boundary, and starts either the existing `delivery` workflow or an evidence-led `bugfix` workflow. Both complete the same durable plan → build → verify → ship lifecycle across Codex CLI and Claude Code. Bug-fix planning establishes the reproduction and failed condition; implementation repairs that condition and supplies regression coverage. + +PR #3 merged as `add44a61aa454f6c11f2e055914c4c333de7a4fa`. Its [fresh installed delivery proof](../reports/2026-10-24-fresh-repository-delivery-verification.md) establishes authenticated execution, exact-commit publication, Chronis completion, and embedded Prime readback. Catalog expansion builds on that behavior. It does not declare every item in the broader [release acceptance](../reference/release-acceptance.md) complete. + +## Approaches considered + +1. **Repository catalog with shared lifecycle, selected.** Versioned catalog entries select workflow definitions and original method documents. Rust validates the supported lifecycle and safety contract. This provides a distinct new job while retaining existing handoff, verification, publication, and recovery mechanics. +2. **More standalone host skills.** Easy to discover inside individual hosts, but selection and evidence would remain disconnected from the durable run. This does not satisfy complete cross-host continuation. +3. **An arbitrary workflow graph engine.** Could express review-only or deployment jobs, but requires new transitions, authority rules, recovery semantics, and terminal outcomes. Current acceptance needs no such engine. + +The first expansion adds `bugfix`. Independent review and behavior-preserving refactoring need their own complete acceptance scenarios before becoming supported catalog entries. + +## User flow + +```sh +allstack init --check --profile rust +allstack init --profile rust +allstack catalog +allstack catalog bugfix +allstack start --workflow bugfix --request docs/requests/repair.md \ + --policy workflows/verification.toml --host codex --allow-commit --allow-publish +``` + +`catalog` lists repository-defined entries; supplying an ID returns its description, definition path, methods, supported hosts, lifecycle, and digests. Discovery validates files without opening mutable AllSource or Prime stores or creating Chronis tasks. An absent catalog in a previously initialized repository exposes legacy `delivery` only. Unknown IDs fail with available choices. + +Omitting `--workflow` preserves `delivery`. Explicit selection freezes the selected definition and method bytes before model execution. Normal `plan`, `handoff`, `resume`, `build`, `verify`, and `ship` commands continue using the returned run and epoch. Catalog entries cannot grant commit or publication authority. + +## Data and engine boundary + +- `workflows/catalog.toml` holds the versioned entry list, with unique IDs and contained definition paths. Verification policies are not discovered as workflows by scanning every TOML file. +- `workflows/bugfix.toml` declares the same supported lifecycle, hosts, storage ownership, handoff, and publication constraints as delivery. Its method references select diagnosis and repair instructions. +- `workflows/methods/diagnose.md` requires repository evidence, reproduction, observed versus expected behavior, the exact failed condition, the smallest responsible repair site, acceptance criteria, and unresolved evidence. Planning remains read-only and cannot compile. +- `workflows/methods/repair.md` requires inspection of that evidence, repair at its source, regression coverage, and preservation of unrelated work and frozen policy. The coordinator owns compilation, commit, and publication. +- Rust owns catalog loading, containment, supported-contract validation, and selection. Workflow-specific judgment belongs in method data. No dependency or service is added. + +Generic catalog validation refuses duplicate or mismatched IDs, unsupported schemas or lifecycle values, missing methods, unsafe paths, and unsupported safety declarations. Catalog metadata cannot silently claim a runtime capability that the coordinator does not implement. Files have bounded sizes and must be ordinary contained files under the existing snapshot boundary. + +## Durable state and compatibility + +AllSource remains the authority for the selected workflow and immutable methods. Add optional, defaulted selection metadata to retained runs, omitted during serialization when absent. Legacy events must deserialize and reproduce identical serialized bytes and event digests. Existing histories default to `workflows/delivery.toml`; they are never rewritten. + +Selection identity and definition path become immutable run scope. Verification reads the selected path rather than the current hardcoded delivery path. Changes to the selected definition or its methods invalidate the run's verification; changes to unrelated catalog entries do not redirect an accepted run. Recovery reads the retained selection, never chooses again from current catalog defaults. + +Chronis retains the existing exact run-based task identity and claimant format so uncertain creation remains reconcilable. New task descriptions include the workflow ID without changing ownership rules. Prime continues reading historical delivery nodes; new source-linked plan and outcome records include workflow identity. Recall remains context and cannot select a workflow, authorize publication, or certify a regression test. + +## Installed resources and existing repositories + +Bundle the catalog, bug-fix definition, methods, indexed catalog guide, and repository-local catalog skill for both supported hosts. Keep existing delivery resources byte-identical. Rerunning `init` on a PR #3 repository adds missing catalog resources and appends its documentation link without replacing its delivery guide, skills, instructions, custom methods, or policy. Conflicting new managed resources block writes before mutation, using the existing descriptor-based setup writer. + +Fresh initialization includes both workflows. Doctor reports catalog validity and missing resources without creating a run. Existing customized delivery files retain the established explicit-conflict behavior; this expansion supplies no overwrite or implicit migration mode. + +## Verification and publication + +Required proof covers selection through actual execution, not listing alone: + +1. Focused Rust tests exercise discovery without store creation, both selections, unknown IDs, malformed catalog contracts, unsafe paths, and method/definition mutation. A legacy serialized event fixture proves byte-for-byte hash compatibility. +2. Application tests exercise the bug-fix methods through handoff, final-commit verification, publication readback, Chronis completion, and Prime attribution. Synthetic host or forge fixtures are labeled as such. +3. Install the built executable into an isolated prefix. With source-checkout reads denied, prove fresh initialization and additive initialization over the previously bundled delivery resources. Inspect actual generated files and run positive regression tests. +4. Use real authenticated Codex and Claude for a deterministic bug-fix request on this implementation branch. Retain reproduction evidence, both process receipts, selected workflow and method digests, handoff epochs, actual changed files, and positive executed test counts. Fix any missing layer within this PR. +5. Publish the implementation through one live feature branch and one PR. The exact final commit must pass required gates, and independent remote/PR head reads must agree. Read actual Chronis and Prime outcomes through their engines before reporting completion. + +The authenticated journey may use this toolkit's own implementation PR as its publication destination. It must preserve the prepared catalog work and avoid creating a second proof repository or changing the existing example PR. New branch creation requires explicit user authorization before execution. If model execution is unavailable, record that limit and leave authenticated acceptance open rather than claiming completion. + +All commands and gates remain bounded by 300 seconds, compiling gates remain serialized, and private stores and raw transcripts remain ignored. Run focused failures before broad gates; rerun combined checks after the final commit changes. + +## Implementation sequence + +1. Add catalog data, loader, CLI discovery, and explicit start selection with legacy fallback. +2. Bind immutable selection through run state, environment checks, Chronis description, and Prime records; prove old histories remain valid. +3. Bundle additive resources and indexed operator guidance; qualify the installed artifact. +4. Exercise authenticated diagnosis and repair, verify the complete candidate, publish one commit/PR, and add an indexed evidence report. + +Completion means installed selection plus the real bug-fix journey reaches verified publication with task and memory readback. Merge, deployment, arbitrary graphs, review-only completion, hosted sync, and cross-machine transport remain outside this change. diff --git a/docs/plans/allstack-f2c4e41f-7616-4b34-aa56-4347fd1617fc.md b/docs/plans/allstack-f2c4e41f-7616-4b34-aa56-4347fd1617fc.md new file mode 100644 index 0000000..31fa8a7 --- /dev/null +++ b/docs/plans/allstack-f2c4e41f-7616-4b34-aa56-4347fd1617fc.md @@ -0,0 +1,94 @@ +1. **Diagnosis and evidence** + + `Memory::record` reports success when any `delivery` node has the requested `:` key. The early return at `src/infrastructure/memory.rs:45–51` never compares `workflow_id`, `run_id`, `source`, or `summary`. The post-write check at lines 60–65 repeats the same incomplete predicate. + + The retained raw baseline is: + + ```text + /Users/decebaldobrica/Projects/founder-mode/.allstack/workspaces/catalog-design/.allstack/proof/catalog/baseline-ci.log + ``` + + SHA-256: `68e23a944982a24b1f9aa0113a5c3824154fd9653fba2a4d674482d7a956701f`. + + Linux and macOS both executed the frozen tests against embedded Prime. Both recorded: + + ```text + conflicting retry must fail: delivery, https://github.com/example/project/pull/1, repair verified + Summary: 39 tests run: 38 passed, 1 failed, 1 skipped + Process completed with exit code 100. + ``` + + `identical_retry_keeps_one_source_linked_record` passed. The failing assertion is `tests/prime_retry.rs:57`. + + Proven reproduction inputs: first record `("run", "bugfix", "shipped", pull/1, "repair verified")`; retry with workflow `"delivery"` and all other inputs unchanged. Expected: explicit conflict error without mutation. Observed: success. + + The raw log records `cargo +1.95 nextest run --locked`, testing synthetic merge commit `9ed9103956578d97c2d66985a8d3b679d0f6b268`, incorporating feature head `fe7423c8f7deb0f6f93228cc764fbd8756fd66eb`. It does not retain a separate execution of the filtered reproduction command. The first assertion aborts the conflict loop, so source-only and summary-only conflicts remain experimentally unverified before repair. + +2. **Contract and responsible layer** + + Read requirements: `docs/reference/catalog-bugfix-request.md:11–24`, indexed catalog design at `docs/plans/2026-10-24-workflow-catalog-design.md:34–50`, and `workflows/bugfix.toml`. + + The workflow contract already requires retained workflow attribution and reconciliation before retrying uncertain operations. The request explicitly defines immutable retry bindings. This is a runtime violation of an existing contract. + + Repair belongs in the Allstack Prime adapter, `src/infrastructure/memory.rs`. `Runtime::remember` already supplies the selected workflow and complete caller inputs at `src/infrastructure/runtime.rs:217–222`. No catalog, method, policy, dependency, or upstream Prime change is indicated. + +3. **Complete affected flow** + + A user selects `bugfix`; the coordinator freezes the request, methods, workflow, and verification policy. Codex returns the diagnosis. The coordinator writes and indexes its plan, transfers an immutable checkpoint to Claude, verifies the repaired candidate, and publishes the exact verified commit. + + Plan indexing calls `remember` from `src/application/mod.rs:45–55`; recovery retries it at lines 164–169. Publication records the PR outcome at lines 265–276. Currently, a conflicting retained key can make those callers clear pending indexing or mark publication memory indexed despite retaining different attribution. + + Completion requires those success flags to follow a verified complete binding. A conflict must leave indexing unresolved. Existing application error propagation already supports that behavior. + +4. **Smallest repair** + + Change `Memory::record` while preserving its signature, existing node representation, and recall behavior. + + - Collect **all** `delivery` nodes matching the exact key. + - With no match, add one node using the existing property structure. + - With exactly one match, compare `key`, `run_id`, `workflow_id`, `source`, and `summary` against the request. + - Treat an **absent** `workflow_id` as `"delivery"` only. Explicit null, malformed metadata, or another workflow must not receive that fallback. + - Return success for an identical binding without writing. Return an explicit conflict error for any mismatch. + - Reject multiple matching nodes with a reconciliation-required error, including identical duplicates. Do not select, update, delete, or merge one automatically. + - After insertion, query again and require exactly one matching node with the complete requested binding before returning success. Missing visibility, mismatched properties, duplicates, or write errors must return an error. + + Share the binding validation between retry reconciliation and post-write verification. Preserve historical nodes and the existing `"delivery"` node type used by recall. + +5. **Regression coverage** + + Preserve `tests/prime_retry.rs` byte-for-byte. Its SHA-256 is: + + ```text + acd0d8725f9d82dab2d3863053637252c0385fb3f9bbfc21c36249b7b4226bd2 + ``` + + Add `tests/prime_binding.rs` for focused embedded-Prime coverage. Seed historical or malformed records through Prime’s API, shut down that writer, open `Memory`, exercise retries, then reopen Prime to verify node identities and complete properties remain unchanged. + + Cover: + + - Legacy node without `workflow_id`: identical delivery retry succeeds; bugfix retry fails; changed source or summary fails; historical recall still works. + - Matching key with incorrect or missing `run_id`: retry fails. + - Identical and conflicting duplicate nodes: both fail without mutation, regardless of insertion order. + - Matching retries across shutdown/reopen retain one unchanged record. + - Invalid workflow metadata does not receive the legacy fallback. + - Deterministic failed-readback cases: missing result, mismatched binding, and duplicates cannot produce success. Use a narrow validation/readback seam if necessary; retain actual embedded Prime for the primary regressions. + + Existing `tests/catalog.rs:153` creates its “legacy” record through current `Memory::record`, so it does not cover genuinely missing workflow metadata. Do not add inline test modules: `tests/architecture.rs:21–24` rejects them. + +6. **Coordinator verification and handoff** + + Retain the existing baseline unchanged. Index this plan under `docs/plans/allstack-f2c4e41f-7616-4b34-aa56-4347fd1617fc.md` and link it from `docs/README.md` through the coordinator. + + After Claude’s repair, execute the frozen reproduction: + + ```text + cargo +1.95 nextest run --locked --test prime_retry + ``` + + Require both tests to execute and pass. Successful completion of the conflict test must reach all three workflow/source/summary cases. Run the new binding tests separately, then execute every unchanged gate in `workflows/catalog-gates.toml`: formatting, complete toolkit regression suite, Clippy, both host fixtures, and source whitespace. Also satisfy CI dependency checks. + + Serialize compilation, retain actual output and positive execution counts, and bind final verification to the exact published commit. Verify frozen-file digests and review the final diff for preservation of prepared catalog work. + + Finish the authenticated Codex → Claude repair journey with immutable handoff receipts, selected `bugfix` identity, Chronis claimant/completion readback, remote/PR head agreement, and complete Prime plan/outcome attribution through their engines. + + This diagnosis performed read-only inspection. No files, task ownership, or run state changed; no compilation or tests executed. Duplicate-key, legacy, and failed-readback runtime behavior remains unproven until the coordinator executes the added coverage. diff --git a/docs/reference/catalog-bugfix-request.md b/docs/reference/catalog-bugfix-request.md new file mode 100644 index 0000000..2ac6281 --- /dev/null +++ b/docs/reference/catalog-bugfix-request.md @@ -0,0 +1,35 @@ +# Catalog bug-fix verification request + +## Observed defect + +Prime's recorder reconciles retries using only `:`. A pre-existing matching key causes `Memory::record` to return success without checking whether the caller supplied the same workflow, source, or summary. That permits a conflicting retry to claim successful indexing even though retained memory describes another outcome. + +The reproduction is `cargo +1.95 nextest run --locked --test prime_retry`. It uses actual embedded AllSource Prime, rather than a mocked graph. The identical-retry case must pass. Before repair, `conflicting_retry_cannot_claim_another_workflow_or_source` fails because a changed workflow is accepted. Retain the actual baseline command output with the workflow evidence. + +## Requested repair + +Repair `src/infrastructure/memory.rs` so a run/phase key binds one immutable workflow, source, summary, and run identity. Matching retries remain idempotent. Conflicting retries fail explicitly without adding or changing nodes. After a new write, read back the same complete binding before reporting success. Multiple nodes for the same key require reconciliation rather than selecting one arbitrarily. + +Keep historical delivery records compatible: a missing `workflow_id` on an older node means `delivery` only. It must not reconcile as another workflow. Historical nodes remain recallable. Add focused coverage for legacy delivery and duplicate-key behavior if needed, in a separate integration test file. Preserve the frozen reproduction file `tests/prime_retry.rs` byte-for-byte. + +Keep all prepared catalog implementation and unrelated source intact. Read the [catalog design](../plans/2026-10-24-workflow-catalog-design.md). Do not alter catalog definitions, method documents, frozen policies, or baseline evidence. Do not compile or create commits from the managed host call; the coordinator executes regression and complete gates afterward. + +## Acceptance + +- The retained baseline reproduces conflicting retry acceptance against actual embedded Prime. +- The same frozen two-test file passes after repair; all conflict combinations execute successfully. +- Missing legacy workflow metadata reconciles only with delivery; conflicting source or summary still fails. +- Duplicate keys and failed readback never produce a successful indexing claim. +- The complete toolkit suite, formatting, lint, and fixture checks pass on the exact published commit. +- Real Codex diagnosis and Claude repair run through selected `bugfix`, with immutable handoff and workflow-attributed Chronis/Prime readback. + +## Retained baseline observation + +[Baseline CI run 40143180893](https://github.com/all-source-os/allstack/actions/runs/40143180893) executed the frozen suite on Linux and macOS at `fe7423c8f7deb0f6f93228cc764fbd8756fd66eb`. Each platform reported 39 tests run: 38 passed, one failed, one skipped. The failure was: + +```text +thread 'conflicting_retry_cannot_claim_another_workflow_or_source' panicked at tests/prime_retry.rs:57:9: +conflicting retry must fail: delivery, https://github.com/example/project/pull/1, repair verified +``` + +The assertion expected an error but received success for a changed workflow under an existing key. Formatting, dependency policy, and Clippy passed in the same run. Raw CI logs are retained privately; this excerpt contains the observed reproduction. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 6006727..04f692c 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -22,6 +22,10 @@ Run `allstack -C /path/to/target-repository doctor` (add `--policy ` for a `init --check` returns `ready: false` with planned paths, digests, conflicts, and missing prerequisites when setup cannot be applied. Existing Chronis engine readback is explicitly deferred until apply; opening its engine could write storage metadata. Check-mode readiness covers resource and executable preflight, with that readback still outstanding. Applying failed preflight stops before writing managed resources or initializing Chronis. Symlinks, traversal, reserved policy destinations, and nonregular/multiply-linked files are refused. File creation cannot replace a destination introduced after preflight. Index/ignore updates append bytes after comparing the original contents. Concurrent edits can require inspection and a rerun. +## Workflow selection + +Use `allstack catalog` to list repository workflows or `allstack catalog bugfix` to inspect evidence-led diagnosis and repair. Pass `start --workflow bugfix` to select it; the default remains `delivery`. See the [catalog reference](workflow-catalog.md) for contracts and additive initialization. Catalog discovery opens no mutable run store. Existing repositories without a catalog and retained runs without selection metadata preserve legacy delivery behavior. + ## Frozen verification policy Create a repository-relative TOML file such as `workflows/verification.toml`: diff --git a/docs/reference/workflow-catalog.md b/docs/reference/workflow-catalog.md new file mode 100644 index 0000000..4432101 --- /dev/null +++ b/docs/reference/workflow-catalog.md @@ -0,0 +1,18 @@ +# Workflow catalog + +The [repository catalog](../../workflows/catalog.toml) selects versioned workflow definitions. `allstack catalog` lists validated entries, and `allstack catalog ` returns the selected definition path, method paths, digests, supported hosts, and stage order. Discovery opens no mutable project store. Catalog files are limited to 64 KiB and 32 entries; definitions and methods are each limited to 64 KiB and must be contained regular files under `workflows/` without symlinks or multiple hard links. + +| Workflow | Plan method | Build method | Complete outcome | +| --- | --- | --- | --- | +| [delivery](../../workflows/delivery.toml) | Shape an accepted change | Implement the complete request | Verified commit and open PR | +| [bugfix](../../workflows/bugfix.toml) | Diagnose an evidenced failed condition | Repair its cause and add regression coverage | Verified repair and open PR | + +Start a selected workflow with `allstack start --workflow bugfix --request docs/requests/repair.md --policy workflows/verification.toml --host codex`. Add commit/publication flags only under trusted user authority. Follow the [CLI guide](cli.md) for handoff, recovery, verification, and publication. Planning cannot compile; provide retained reproduction output when the diagnosis requires execution evidence. + +All entries use the supported plan → build → verify → ship lifecycle. Storage, host, handoff, process, and publication declarations must match the implemented contract. Unsupported declarations and unknown fields fail rather than implying support. Workflow-specific behavior lives in the plan/build method documents. The catalog does not supply permissions or an arbitrary graph engine. + +AllSource freezes selection identity, definition path, and definition/method bytes. Accepted runs do not reselect from a changed catalog. Verification binds the selected definition and methods, while the full candidate snapshot still covers repository content. Chronis uses the existing run identity and epoch claim; task descriptions include workflow identity. Prime records include workflow identity and source links. Historical `delivery` graph nodes remain discoverable. + +Run `allstack init --check`, then `allstack init` with an existing valid policy to add catalog resources to a previously initialized repository. Fresh Rust setup uses `--profile rust`. Existing delivery files stay byte-identical; missing catalog files, host skills, and indexed guidance are added. Conflicts block writes. Legacy repositories without a catalog expose only `delivery`, and old retained runs keep their original event serialization and default definition path. + +The [catalog design](../plans/2026-10-24-workflow-catalog-design.md) defines compatibility and authenticated acceptance. The [bug-fix reproduction request](catalog-bugfix-request.md) supplies the concrete repair used to verify this catalog expansion. diff --git a/docs/reports/2026-10-25-workflow-catalog-verification.md b/docs/reports/2026-10-25-workflow-catalog-verification.md new file mode 100644 index 0000000..dfc5c83 --- /dev/null +++ b/docs/reports/2026-10-25-workflow-catalog-verification.md @@ -0,0 +1,69 @@ +# Workflow catalog and authenticated bug-fix verification + +Date: 2026-10-25. [Implementation PR #4](https://github.com/all-source-os/allstack/pull/4), [catalog design](../plans/2026-10-24-workflow-catalog-design.md), and [repair request](../reference/catalog-bugfix-request.md). + +## Observed behavior + +The installed CLI lists `delivery` and `bugfix`, selects diagnosis and repair methods for `bugfix`, and retains that identity through AllSource events, Chronis claims, and source-linked Prime records. Actual Codex diagnosed a Prime retry conflict, actual Claude wrote its repair and compatibility tests, and an immutable handoff returned the candidate to Codex epoch 3. + +The repair binds each Prime run/phase key to one workflow, run identity, source, and summary. Identical retries remain idempotent. Conflicting retries and duplicate keys fail without mutation. Historical records missing workflow metadata reconcile as delivery only; explicit malformed metadata receives no fallback. + +| Retained identity | Value | +| --- | --- | +| Run | `f2c4e41f-7616-4b34-aa56-4347fd1617fc` | +| Selected workflow | `bugfix`, `workflows/bugfix.toml` | +| Workflow SHA-256 | `497e182314e07e811c8892d197aafb47f85ac2a4a91307e7cc6198896a217406` | +| Diagnosis method SHA-256 | `f33a8a24d74fd2c2a11c2e5e7b4f80937d23e59a23f8be7755249250c8631176` | +| Repair method SHA-256 | `0ca4e0b91f542774f9d2f9f192f593f6eb038f0f6d71257551dc7e0f9885c5d2` | +| Accepted Codex plan SHA-256 | `9d878a3844e0473443d1163882c342d77e10053fc6ebef4d9b19d40f4f8d2587` | +| Frozen policy SHA-256 | `4f1c98824edcc5708a7f61e1417c2485e15017ee35e5911dae376607eaa4c44b` | +| Frozen reproduction file SHA-256 | `acd0d8725f9d82dab2d3863053637252c0385fb3f9bbfc21c36249b7b4226bd2` | +| Final installed executable SHA-256 | `576a673ebc9975e09e50f06445131d1ebff2ef7ce0b3df60aa751217bc19d969` | +| Chronis run task | `t-f85189` | +| Publication destination | Existing implementation PR #4; one owned feature-branch commit | + +Raw process receipts, account metadata, source snapshots, and event-store files remain private. Retained private manifests bind executable digests to their compiled source inputs. The final installed executable was built from prepared code commit `bb06ce5c828feab6ad012272ff91db7f8e0c4d58`; this identifies the code input before inclusion of this report. + +## Baseline and regression evidence + +[Baseline CI run 40143180893](https://github.com/all-source-os/allstack/actions/runs/40143180893) tested feature head `fe7423c8f7deb0f6f93228cc764fbd8756fd66eb` through GitHub's synthetic merge checkout. Linux and macOS each executed 39 tests: 38 passed, one failed, one skipped. The conflicting-retry assertion expected an error but received success when workflow changed from `bugfix` to `delivery` under the same key. Formatting, dependency policy, and Clippy passed in that baseline run. + +The frozen two-test reproduction remained byte-identical after repair. A focused execution against actual embedded Prime passed both reproduction tests and seven new compatibility tests: nine executed, nine passed. The conflict loop reached workflow, source, and summary variations after repair. Compatibility cases seed historical, duplicate, and malformed records through Prime's own API and reopen the engine before inspecting them. + +An expanded focused run executed 18 catalog, architecture, and Prime tests successfully, including selected-definition/method mutation rejection and preservation of frozen paths when catalog entries change. A separate `cargo +1.95 clippy --all-targets --locked --offline -- -D warnings` completed successfully. Missing, mismatched, and duplicate readback validation also has deterministic helper coverage; that coverage does not simulate a real storage outage. + +## Installed resources and historical data + +An isolated executable prefix, rather than the source-tree binary path, supplied the CLI. A macOS sandbox denied reads of both Allstack source checkouts. Fresh initialization created 16 resources, listed both workflows, initialized actual Chronis, and returned `setup_ready: true` with `chronis: readable`. + +Additive initialization was tested over the previously bundled delivery files. It added catalog resources and the documentation link while preserving delivery definitions, methods, guide, both delivery skills, policy bytes, and existing index content. The rebuilt adapter artifact and final installed artifact passed source-denied bootstrap checks as well. Final installed doctor read the actual Chronis store and both catalog entries while source reads remained denied. + +The catalog binary also reopened the previous authenticated delivery run `9f981bf4-cb9d-477e-8c5d-8864e1296834` through AllSource's engine. All 22 events validated, revision 22 remained `shipped`, and selection metadata remained absent throughout the legacy serialization. The golden legacy-event test independently checks byte-for-byte serialization compatibility. + +## Real hosts, recovery, and permission handling + +Codex CLI reported its normal ChatGPT login; Claude reported its normal first-party login. No authentication or permission bypass flags were used. + +| Completed host call | Actual receipt | +| --- | --- | +| Codex diagnosis, epoch 1 | Exit 0, `turn.completed`, 29 command completions, 178,421 ms | +| Claude completion, epoch 2 | Exit 0, `subtype: success`, `is_error: false`, zero permission denials, 93,041 ms | +| Claude model label | `claude-opus-4-8` | + +The Codex receipt SHA-256 is `d37f468b09851b883062058edb6cff6ef8ba88d5a2bd34a03387be543c1d4a0a`. The successful Claude receipt SHA-256 is `c397f3abfa68583006c74b7a49bd49b6f6e511bd5c53af867af31b925a54509c`. + +Two earlier Claude calls reached the frozen 240-second deadline after retaining source changes: 240,021 ms and 240,014 ms, both with `timed_out: true` and `owner_lost: false`. The coordinator left each operation unresolved. Explicit inspection and recovery retained the repair and then the complete compatibility test file without replaying either call. + +A later Claude call returned a successful model result but included five denied shell attempts. Its actual receipts showed RTK-prefixed Git commands and file hashing outside the adapter's original allowlist. The coordinator refused to advance. The adapter now permits the corresponding narrow RTK forms and `shasum -a 256`, and explains current-directory shell scope to the host. `git -C`, compound commands, compilation, and publication remain outside that managed shell scope. A subsequent real call completed with zero denials; actual successful calls included file hashing, Git reads, and explicit staging. + +Each handoff restored a separate worktree and read back the Chronis claimant. Epoch 3 resumed the retained request, accepted plan, selected methods, and Prime context. The accepted Claude checkpoint was `b08898bddbc09b87f1fce918e72111f25501d30f29e6c32c9341932aa548f44a`. + +## Final verification and publication records + +The frozen [catalog policy](../../workflows/catalog-gates.toml) requires formatting, the complete positive-execution toolkit suite, Clippy, both host fixtures, and source whitespace checks on the prepared commit. Local compiler contention is checked before compilation. One verification attempt prepared the owned commit but refused to compete when another Cargo process started; focused checks and lint were then run before the final policy execution. + +Complete policy execution passed all five gates. The toolkit suite executed 47 tests successfully, with one explicitly ignored real-Chronis qualification. Both host fixtures executed two tests each, all passing; their empty library unit did not supply execution proof. Formatting, Clippy, and whitespace checks returned exit 0. The runtime-suite receipt digest was `33af847303a64ab362905c3bf95c5b473cee9a158df5cdc81fccd7eabeb61b49`. Report inclusion requires a fresh bound verification of the resulting commit before publication. + +Authoritative final commit, gate receipts, remote/PR agreement, Chronis completion, and Prime outcome are exposed by the run's `evidence`, `history`, and `recall` commands. The implementation PR links the delivered SHA and final checks. A fresh clone contains code and indexed documents; original private stores and validated exports remain local. + +The supported boundary remains sequential hosts on one Unix machine, local embedded storage and lexical Prime recall, and a verified feature commit with an open PR. Merge, deployment, hosted sync, arbitrary workflow graphs, cross-machine transport, and a new review-only lifecycle were not exercised. diff --git a/hosts/claude-code/SKILL.md b/hosts/claude-code/SKILL.md index 4091264..da449dd 100644 --- a/hosts/claude-code/SKILL.md +++ b/hosts/claude-code/SKILL.md @@ -17,6 +17,8 @@ Use the original repository as the CLI's `-C` target. Edit only the active detac `plan --epoch ` invokes real `claude --print` with planning permissions. `build --epoch ` invokes it with `acceptEdits` and the adapter's explicit tool allowlist. Host authentication and ordinary permission rules remain active. A denied operation must remain visible; never add bypass flags. +Build shell access covers `git diff`, `git status`, explicit `git add --`, their `rtk git` forms, and `shasum -a 256`. Run these from the active working directory. `git -C`, compound commands, compilation, and other shell programs require the coordinator's separate execution path. + Follow the frozen request and indexed plan. Explicitly stage each intended new file with `git add -- ` before checkpoint capture. Do not commit, push, create branches, alter run state, or modify the verification policy from a managed build. The coordinator owns final verification and publication. ## Transfer or recover diff --git a/src/application/ports.rs b/src/application/ports.rs index 86a29a0..5b9d617 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 workflow: String, pub request: String, pub policy: String, pub host: Host, diff --git a/src/domain.rs b/src/domain.rs index fbea199..b96bc53 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -129,6 +129,13 @@ pub struct Publication { pub memory_indexed: bool, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct WorkflowSelection { + pub id: String, + pub path: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Run { @@ -139,6 +146,8 @@ pub struct Run { pub base: String, pub trunk: String, pub workflow: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub selection: Option, pub methods: BTreeMap, pub method_paths: BTreeMap, pub policy: String, @@ -165,6 +174,18 @@ pub struct Run { } impl Run { + pub fn workflow_id(&self) -> &str { + self.selection + .as_ref() + .map_or("delivery", |s| s.id.as_str()) + } + + pub fn workflow_path(&self) -> &str { + self.selection + .as_ref() + .map_or("workflows/delivery.toml", |s| s.path.as_str()) + } + pub fn check_epoch(&self, epoch: u64) -> Result<(), String> { if self.epoch != epoch { Err(format!( @@ -231,6 +252,7 @@ pub fn validate_transition(previous: Option<&Run>, event: &Event) -> Result<(), if old.repository != event.run.repository || old.branch != event.run.branch || old.workflow != event.run.workflow + || old.selection != event.run.selection || old.methods != event.run.methods || old.method_paths != event.run.method_paths || old.policy != event.run.policy diff --git a/src/infrastructure/bootstrap.rs b/src/infrastructure/bootstrap.rs index 0783043..b10c492 100644 --- a/src/infrastructure/bootstrap.rs +++ b/src/infrastructure/bootstrap.rs @@ -10,6 +10,34 @@ use std::{ }; const RESOURCES: &[(&str, &str)] = &[ + ( + "workflows/catalog.toml", + include_str!("../../workflows/catalog.toml"), + ), + ( + "workflows/bugfix.toml", + include_str!("../../workflows/bugfix.toml"), + ), + ( + "workflows/methods/diagnose.md", + include_str!("../../workflows/methods/diagnose.md"), + ), + ( + "workflows/methods/repair.md", + include_str!("../../workflows/methods/repair.md"), + ), + ( + "docs/reference/allstack-catalog.md", + include_str!("../../templates/catalog.md"), + ), + ( + ".agents/skills/allstack-catalog/SKILL.md", + include_str!("../../templates/catalog-SKILL.md"), + ), + ( + ".claude/skills/allstack-catalog/SKILL.md", + include_str!("../../templates/catalog-SKILL.md"), + ), ( "workflows/delivery.toml", include_str!("../../workflows/delivery.toml"), @@ -152,7 +180,10 @@ fn plan(root: &Root, profile: Option<&str>, policy: &str) -> Result<(Vec files.push(append( root, "docs/README.md", - &[INDEX], + &[ + INDEX, + "- [Allstack workflow catalog](reference/allstack-catalog.md)", + ], "# Documentation\n\n", )?); for path in [".allstack", ".prime", ".chronis"] { @@ -378,10 +409,17 @@ pub fn doctor(repo: &Path, policy: &str) -> Result { json!({"tool":name,"available":result.is_ok(),"version":result.ok().map(|s| s.lines().next().unwrap_or("").to_string())}) }).collect(); let available = tools.iter().all(|t| t["available"] == true); + let catalog = match super::catalog::inspect(repo, None) { + Ok(value) => value, + Err(error) => { + missing.push(format!("workflow catalog invalid: {error:#}")); + json!({"error":format!("{error:#}")}) + } + }; Ok( json!({"repository":repo,"store":repo.join(".allstack"),"setup_ready":missing.is_empty(), "executables_ready":available,"authentication":"not-probed","missing":missing,"files":files,"chronis":chronis, - "stores_opened":false,"allsource":"embedded; fsync-per-event when run storage opens", + "catalog":catalog,"stores_opened":false,"allsource":"embedded; fsync-per-event when run storage opens", "prime":"embedded graph; lexical recall; local-only; not opened by doctor", "coordinator":"exclusive-kernel-lock-per-run-command","host_scope":"sequential-same-machine","tools":tools}), ) diff --git a/src/infrastructure/catalog.rs b/src/infrastructure/catalog.rs new file mode 100644 index 0000000..370e98d --- /dev/null +++ b/src/infrastructure/catalog.rs @@ -0,0 +1,222 @@ +//! Catalog discovery reads repository data without opening mutable run stores. +use super::{bootstrap_fs::Root, digest}; +use crate::domain::{Run, WorkflowSelection}; +use anyhow::{Context, Result, ensure}; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, path::Path}; + +const CATALOG: &str = "workflows/catalog.toml"; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Index { + schema_version: u32, + workflows: Vec, +} + +#[derive(Serialize)] +pub struct Entry { + pub id: String, + pub path: String, + pub description: String, + pub digest: String, + pub methods: BTreeMap, + pub hosts: Vec, + pub stage_order: Vec, + pub host_scope: String, + #[serde(skip)] + pub bytes: Vec, +} + +#[derive(Serialize)] +pub struct Method { + pub path: String, + pub digest: String, + #[serde(skip)] + pub bytes: Vec, +} + +fn read(root: &Root, path: &str) -> Result> { + ensure!( + path.starts_with("workflows/") && !path.split('/').any(|p| p.starts_with('.')), + "catalog inputs must be visible files under workflows/: {path}" + ); + let bytes = root + .read(path)? + .with_context(|| format!("catalog input missing: {path}"))?; + ensure!( + bytes.len() <= 64 * 1024, + "catalog input exceeds 64 KiB: {path}" + ); + Ok(bytes) +} + +fn valid_id(id: &str) -> bool { + !id.is_empty() + && id.len() <= 64 + && id.as_bytes()[0].is_ascii_lowercase() + && id + .bytes() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'-') +} + +fn load(root: &Root, selection: WorkflowSelection) -> Result { + ensure!(valid_id(&selection.id), "invalid workflow id"); + ensure!( + selection.path.ends_with(".toml"), + "workflow definition must be TOML" + ); + let bytes = read(root, &selection.path)?; + let definition: toml::Value = toml::from_str(std::str::from_utf8(&bytes)?)?; + let supported: toml::Value = toml::from_str(include_str!("../../workflows/delivery.toml"))?; + let table = definition.as_table().context("workflow must be a table")?; + ensure!( + table.keys().all(|key| supported.get(key).is_some()), + "unknown workflow field" + ); + ensure!( + definition.get("id").and_then(toml::Value::as_str) == Some(&selection.id), + "workflow id differs from catalog entry" + ); + for key in [ + "schema_version", + "hosts", + "host_scope", + "stage_order", + "dispositions", + "storage", + "handoff", + "execution", + "publication", + "stages", + ] { + ensure!( + definition.get(key) == supported.get(key), + "unsupported workflow contract: {key}" + ); + } + for key in ["version", "status", "description"] { + ensure!( + definition + .get(key) + .and_then(toml::Value::as_str) + .is_some_and(|s| !s.trim().is_empty()), + "workflow {key} must be nonempty text" + ); + } + let paths = definition + .get("methods") + .and_then(toml::Value::as_table) + .context("workflow methods missing")?; + ensure!( + paths.len() == 2 && paths.contains_key("plan") && paths.contains_key("build"), + "workflow requires exactly plan and build methods" + ); + let mut methods = BTreeMap::new(); + for (name, path) in paths { + let path = path.as_str().context("method path must be text")?; + let bytes = read(root, path)?; + ensure!( + !std::str::from_utf8(&bytes)?.trim().is_empty(), + "workflow method is empty" + ); + methods.insert( + name.clone(), + Method { + path: path.into(), + digest: digest(&bytes), + bytes, + }, + ); + } + Ok(Entry { + id: selection.id, + path: selection.path, + description: definition["description"].as_str().unwrap().into(), + digest: digest(&bytes), + methods, + hosts: vec!["codex".into(), "claude-code".into()], + stage_order: vec![ + "plan".into(), + "build".into(), + "verify".into(), + "ship".into(), + ], + host_scope: "sequential-same-machine".into(), + bytes, + }) +} + +pub fn entries(repo: &Path) -> Result> { + let root = Root::new(repo)?; + let selections = match root.read(CATALOG)? { + Some(bytes) => { + ensure!(bytes.len() <= 64 * 1024, "catalog exceeds 64 KiB"); + let index: Index = toml::from_str(std::str::from_utf8(&bytes)?)?; + ensure!( + index.schema_version == 1 + && !index.workflows.is_empty() + && index.workflows.len() <= 32, + "unsupported catalog schema or entry count" + ); + index.workflows + } + None => vec![WorkflowSelection { + id: "delivery".into(), + path: "workflows/delivery.toml".into(), + }], + }; + let mut ids = std::collections::BTreeSet::new(); + let mut paths = std::collections::BTreeSet::new(); + let mut entries = Vec::new(); + for selection in selections { + ensure!( + ids.insert(selection.id.clone()) && paths.insert(selection.path.clone()), + "duplicate catalog id or path" + ); + entries.push(load(&root, selection)?); + } + ensure!( + ids.contains("delivery"), + "catalog must retain default delivery workflow" + ); + entries.sort_by(|a, b| a.id.cmp(&b.id)); + Ok(entries) +} + +pub fn select(repo: &Path, id: &str) -> Result { + let entries = entries(repo)?; + let choices = entries + .iter() + .map(|e| e.id.as_str()) + .collect::>() + .join(", "); + entries + .into_iter() + .find(|e| e.id == id) + .with_context(|| format!("unknown workflow {id}; available: {choices}")) +} + +pub fn verify_scope(repo: &Path, run: &Run) -> Result<()> { + let root = Root::new(repo)?; + ensure!( + digest(&read(&root, run.workflow_path())?) == run.workflow, + "workflow definition changed" + ); + for (method, path) in &run.method_paths { + ensure!( + run.methods.get(method) == Some(&digest(&read(&root, path)?)), + "workflow method changed from accepted scope" + ); + } + Ok(()) +} + +pub fn inspect(repo: &Path, id: Option<&str>) -> Result { + match id { + Some(id) => Ok(serde_json::to_value(select(repo, id)?)?), + None => Ok( + serde_json::json!({"schema_version":1,"default":"delivery","workflows":entries(repo)?,"stores_opened":false}), + ), + } +} diff --git a/src/infrastructure/chronis.rs b/src/infrastructure/chronis.rs index 086266a..8409dfe 100644 --- a/src/infrastructure/chronis.rs +++ b/src/infrastructure/chronis.rs @@ -53,7 +53,11 @@ impl Tasks<'_> { "create", &title, "-d", - &format!("Allstack run {}; source-linked delivery", run.id), + &format!( + "Allstack run {}; source-linked workflow {}", + run.id, + run.workflow_id() + ), ], None, )?; diff --git a/src/infrastructure/hosts.rs b/src/infrastructure/hosts.rs index 896f20c..33bc530 100644 --- a/src/infrastructure/hosts.rs +++ b/src/infrastructure/hosts.rs @@ -11,6 +11,7 @@ pub fn invoke( prompt: &str, planning: bool, ) -> Result { + let claude_build_tools = "Read,Glob,Grep,Edit,Write,Bash(git diff *),Bash(git status *),Bash(git add -- *),Bash(rtk git diff *),Bash(rtk git status *),Bash(rtk git add -- *),Bash(shasum -a 256 *)"; let prompt = format!( "Managed Allstack run {}. Host {}, epoch {}, revision {}. Chronis task {} already claimed and read back by coordinator for this host. Active checkpoint {}. This detached worktree is intentional; original repository branch is {}. Do not change task ownership or run state yourself. Recover source context from the indexed plan; no prior conversation is assumed.\n\n{}", run.id, @@ -22,6 +23,13 @@ pub fn invoke( run.branch, prompt ); + let prompt = if run.host == Host::ClaudeCode && !planning { + format!( + "{prompt}\n\nShell capability scope: use git diff, git status, or git add -- from the current working directory. Matching rtk git forms and shasum -a 256 are allowed. Do not use git -C, compound shell commands, or other shell programs. Use Read, Glob, Grep, Edit, and Write for source work. The coordinator runs compilation, tests, and publication." + ) + } else { + prompt + }; let argv: Vec = match run.host { Host::Codex => vec![ "codex", @@ -57,7 +65,7 @@ pub fn invoke( if planning { "Read,Glob,Grep" } else { - "Read,Glob,Grep,Edit,Write,Bash(git diff *),Bash(git status *),Bash(git add -- *)" + claude_build_tools }, ] .into_iter() diff --git a/src/infrastructure/memory.rs b/src/infrastructure/memory.rs index 3c2d80f..726b916 100644 --- a/src/infrastructure/memory.rs +++ b/src/infrastructure/memory.rs @@ -33,34 +33,90 @@ 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 record(&self, run: &str, phase: &str, source: &str, summary: &str) -> Result<()> { + pub async fn record( + &self, + run: &str, + workflow: &str, + phase: &str, + source: &str, + summary: &str, + ) -> Result<()> { let key = format!("{run}:{phase}"); - if self - .0 - .nodes_by_type("delivery") - .iter() - .any(|n| n.properties["key"] == key) - { + let existing = self.bound(&key); + ensure!( + existing.len() <= 1, + "Prime key {key} is bound to {} nodes; reconciliation required before indexing", + existing.len() + ); + if let Some(node) = existing.first() { + ensure!( + binding_matches(node, &key, run, workflow, source, summary), + "Prime key {key} already binds a different workflow, source, summary, or run" + ); return Ok(()); } self.0 .add_node( "delivery", json!({"name":format!("{phase} {run}"),"domain":"allstack","key":key, - "source":source,"summary":summary,"run_id":run}), + "source":source,"summary":summary,"run_id":run,"workflow_id":workflow}), ) .await?; ensure!( - self.0 - .nodes_by_type("delivery") - .iter() - .any(|n| n.properties["key"] == key), - "Prime write not visible" + single_complete_binding(&self.bound(&key), &key, run, workflow, source, summary), + "Prime write not visible as a single complete binding for key {key}" ); Ok(()) } + fn bound(&self, key: &str) -> Vec { + self.0 + .nodes_by_type("delivery") + .into_iter() + .map(|node| node.properties) + .filter(|props| node_str(props, "key") == Some(key)) + .collect() + } pub async fn shutdown(self) -> Result<()> { self.0.shutdown().await?; Ok(()) } } + +fn node_str<'a>(props: &'a Value, field: &str) -> Option<&'a str> { + props.get(field).and_then(Value::as_str) +} + +// Legacy nodes omit workflow_id; malformed explicit values cannot use that fallback. +fn stored_workflow(props: &Value) -> Option<&str> { + match props.get("workflow_id") { + None => Some("delivery"), + Some(Value::String(workflow)) => Some(workflow.as_str()), + Some(_) => None, + } +} + +pub fn binding_matches( + props: &Value, + key: &str, + run: &str, + workflow: &str, + source: &str, + summary: &str, +) -> bool { + node_str(props, "key") == Some(key) + && node_str(props, "run_id") == Some(run) + && stored_workflow(props) == Some(workflow) + && node_str(props, "source") == Some(source) + && node_str(props, "summary") == Some(summary) +} + +pub fn single_complete_binding( + nodes: &[Value], + key: &str, + run: &str, + workflow: &str, + source: &str, + summary: &str, +) -> bool { + nodes.len() == 1 && binding_matches(&nodes[0], key, run, workflow, source, summary) +} diff --git a/src/infrastructure/mod.rs b/src/infrastructure/mod.rs index 4549a76..da50fe5 100644 --- a/src/infrastructure/mod.rs +++ b/src/infrastructure/mod.rs @@ -1,6 +1,7 @@ pub mod bootstrap; pub mod bootstrap_fs; pub mod build_lock; +pub mod catalog; pub mod chronis; pub mod forge; pub mod gates; diff --git a/src/infrastructure/runtime.rs b/src/infrastructure/runtime.rs index 4feb64a..c3ab6ae 100644 --- a/src/infrastructure/runtime.rs +++ b/src/infrastructure/runtime.rs @@ -1,4 +1,5 @@ use super::{ + catalog, chronis::Tasks, digest, forge, git, hosts, memory::Memory, @@ -8,7 +9,7 @@ use super::{ }; use crate::{ application::ports::{Platform, Start}, - domain::{Policy, Publication, Run, Stage, Verification}, + domain::{Policy, Publication, Run, Stage, Verification, WorkflowSelection}, }; use anyhow::{Context, Result, ensure}; use serde_json::{Value, json}; @@ -123,24 +124,12 @@ impl Platform for Runtime { let policy_bytes = fs::read(workspace::contained(&self.repo, &input.policy)?)?; let policy: Policy = toml::from_str(std::str::from_utf8(&policy_bytes)?)?; policy.validate().map_err(anyhow::Error::msg)?; - let workflow_bytes = fs::read(self.repo.join("workflows/delivery.toml"))?; - let definition: toml::Value = toml::from_str(std::str::from_utf8(&workflow_bytes)?)?; - ensure!( - definition["schema_version"].as_integer() == Some(1) - && definition["id"].as_str() == Some("delivery"), - "unsupported workflow schema or method" - ); + let selected = catalog::select(&self.repo, &input.workflow)?; let mut methods = BTreeMap::new(); let mut method_paths = BTreeMap::new(); - for method in ["plan", "build"] { - let path = definition["methods"][method] - .as_str() - .context("workflow method path missing")?; - methods.insert( - method.into(), - self.put(&fs::read(workspace::contained(&self.repo, path)?)?)?, - ); - method_paths.insert(method.into(), path.to_string()); + for (method, data) in &selected.methods { + methods.insert(method.clone(), self.put(&data.bytes)?); + method_paths.insert(method.clone(), data.path.clone()); } let base = git::call(&self.state, &self.repo, &["rev-parse", "HEAD"])?; let id = uuid::Uuid::new_v4().to_string(); @@ -160,7 +149,11 @@ impl Platform for Runtime { branch, base, trunk, - workflow: self.put(&workflow_bytes)?, + workflow: self.put(&selected.bytes)?, + selection: Some(WorkflowSelection { + id: selected.id, + path: selected.path, + }), methods, method_paths, policy: self.put(&policy_bytes)?, @@ -225,7 +218,7 @@ impl Platform for Runtime { self.memory .as_ref() .context("Prime unavailable; run state remains readable and indexing remains pending")? - .record(&run.id, phase, source, summary) + .record(&run.id, run.workflow_id(), phase, source, summary) .await } fn invoke(&self, run: &Run, prompt: &str, planning: bool) -> Result { @@ -329,18 +322,7 @@ impl Platform for Runtime { == workspace::content_digest(&self.store.artifacts, &run.checkpoint)?, "candidate content differs from accepted checkpoint" ); - let workflow = fs::read(self.repo.join("workflows/delivery.toml"))?; - ensure!( - digest(&workflow) == run.workflow, - "workflow definition changed" - ); - for (method, path) in &run.method_paths { - ensure!( - run.methods.get(method) - == Some(&digest(&fs::read(workspace::contained(&self.repo, path)?)?)), - "workflow method changed from accepted scope" - ); - } + catalog::verify_scope(&self.repo, run)?; ensure!( digest(&fs::read(workspace::contained( &self.repo, diff --git a/src/main.rs b/src/main.rs index 2a6f37c..2bd136c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ async fn main() { cli.command, allstack::presentation::Action::Init { .. } | allstack::presentation::Action::Doctor { .. } + | allstack::presentation::Action::Catalog { .. } ) { Some(Scratch::new()?) } else { diff --git a/src/presentation.rs b/src/presentation.rs index 14a003d..74b8892 100644 --- a/src/presentation.rs +++ b/src/presentation.rs @@ -4,7 +4,7 @@ use crate::{ ports::{Platform, Start}, }, domain::Host, - infrastructure::{bootstrap, git, process, runtime::Runtime}, + infrastructure::{bootstrap, catalog, git, process, runtime::Runtime}, }; use anyhow::{Context, Result, ensure}; use clap::{Parser, Subcommand}; @@ -22,6 +22,9 @@ pub struct Cli { #[derive(Subcommand)] pub enum Action { + Catalog { + workflow: Option, + }, Doctor { #[arg(long, default_value = "workflows/verification.toml")] policy: String, @@ -35,6 +38,8 @@ pub enum Action { policy: String, }, Start { + #[arg(long, default_value = "delivery")] + workflow: String, #[arg(long)] request: String, #[arg(long)] @@ -117,6 +122,7 @@ pub async fn execute(cli: Cli) -> Result { policy, } => return bootstrap::initialize(&repo, *check, profile.as_deref(), policy), Action::Doctor { policy } => return bootstrap::doctor(&repo, policy), + Action::Catalog { workflow } => return catalog::inspect(&repo, workflow.as_deref()), _ => {} } let runtime = Runtime::open(&repo).await?; @@ -129,10 +135,11 @@ pub async fn execute(cli: Cli) -> Result { async fn dispatch(p: &Runtime, action: Action) -> Result { let run = match action { - Action::Doctor { .. } | Action::Init { .. } => { + Action::Doctor { .. } | Action::Init { .. } | Action::Catalog { .. } => { unreachable!("setup commands dispatch before opening run storage") } Action::Start { + workflow, request, policy, host, @@ -142,6 +149,7 @@ async fn dispatch(p: &Runtime, action: Action) -> Result { application::start( p, Start { + workflow, request, policy, host, diff --git a/templates/catalog-SKILL.md b/templates/catalog-SKILL.md new file mode 100644 index 0000000..b3c389d --- /dev/null +++ b/templates/catalog-SKILL.md @@ -0,0 +1,12 @@ +--- +name: allstack-catalog +description: Select and continue installed Allstack delivery or evidence-led bugfix workflows across Codex CLI and Claude Code. +--- + +# Allstack catalog + +Read repository instructions and the [installed catalog guide](../../../docs/reference/allstack-catalog.md). Use `allstack catalog` and `allstack catalog ` to inspect versioned workflow data before choosing a workflow. Use `start --workflow delivery|bugfix` only on an already authorized live feature branch, with an indexed request and explicit verification policy. Commit and publication flags require trusted user authority. + +For bug fixes, include reproduction inputs, retained failure output, expected behavior, and regression acceptance in the request. Treat unproven causes as unresolved. Diagnose the responsible data contract or producer before changing generic mechanics. The selected plan and build methods supply workflow-specific instructions. + +Continue through the [CLI and recovery guide](../../../docs/reference/allstack.md). Read `status` and `resume` before acting; use the current epoch and active detached worktree. AllSource retains selected workflow and methods, the coordinator owns Chronis claims, and Prime recall requires source inspection. Use managed handoff, verification, and shipping. Do not create branches, commit, publish, change claims, or bypass permissions inside a managed host call. diff --git a/templates/catalog.md b/templates/catalog.md new file mode 100644 index 0000000..ff41e7b --- /dev/null +++ b/templates/catalog.md @@ -0,0 +1,16 @@ +# Allstack workflow catalog + +Run `allstack catalog` to inspect repository workflows without opening run stores. Run `allstack catalog bugfix` for its definition path, methods, supported hosts, lifecycle, and digests. The catalog is versioned at `workflows/catalog.toml`. Existing repositories without that file retain the default delivery workflow. + +`delivery` plans and implements an accepted change. `bugfix` diagnoses an evidenced defect and repairs its cause with regression coverage. Both use plan → build → verify → ship, sequential Codex CLI and Claude Code hosts, immutable handoff snapshots, Chronis claims, and source-linked Prime context. Planning is read-only; the coordinator executes compilation and final verification. Supply retained failure output in the indexed request when diagnosis needs execution evidence. + +```sh +allstack start --workflow bugfix --request docs/requests/repair.md \ + --policy workflows/verification.toml --host codex --allow-commit --allow-publish +``` + +Start only from an authorized live feature branch. Authority flags require trusted user instructions. Use the returned run ID and current epoch with the [CLI and recovery guide](allstack.md). Omitting `--workflow` selects `delivery`. Selected workflow and method bytes stay frozen through handoff, recovery, verification, and publication. Catalog edits cannot redirect an accepted run or grant permissions. + +Run `allstack init --check --profile rust`, then `allstack init --profile rust` to add bundled catalog resources. Existing delivery resources are preserved byte-for-byte. Conflicting managed resources block initialization before writes; resolve conflicts explicitly. Existing custom policies remain intact. Review and commit initialized files through normal repository policy. + +The catalog supports this fixed lifecycle only. Definitions cannot weaken supported host, handoff, storage, process, verification, or publication constraints. AllSource owns run history, Chronis owns task state, and embedded Prime remains local lexical context. Shipping confirms the verified remote commit and one open PR; merge and deployment are separate actions. diff --git a/tests/application.rs b/tests/application.rs index f9ada3d..9bce410 100644 --- a/tests/application.rs +++ b/tests/application.rs @@ -46,12 +46,26 @@ impl Platform for Fake { self.calls.borrow_mut().push(kind.into()); Ok(()) } - fn initialize(&self, _: &Start) -> Result { - Ok(common::run()) + fn initialize(&self, input: &Start) -> Result { + let mut run = common::run(); + if input.workflow == "bugfix" { + run.selection = Some(allstack::domain::WorkflowSelection { + id: "bugfix".into(), + path: "workflows/bugfix.toml".into(), + }); + run.methods = [ + ("plan".into(), "diagnose".into()), + ("build".into(), "repair".into()), + ] + .into(); + } + Ok(run) } fn object(&self, key: &str) -> Result> { Ok(if key == "policy" { b"command_seconds=10\n[[gates]]\nname='real tests'\nargv=['cargo','test','--lib']\ntest=true\nsuccess_marker='1 passed'\n".to_vec() + } else if key == "diagnose" || key == "repair" { + key.as_bytes().to_vec() } else { b"request and plan".to_vec() }) @@ -89,14 +103,20 @@ impl Platform for Fake { fn recall(&self, _: &str) -> Value { json!({"authority":"context-only"}) } - async fn remember(&self, _: &Run, _: &str, _: &str, _: &str) -> Result<()> { + async fn remember(&self, run: &Run, _: &str, source: &str, _: &str) -> Result<()> { ensure!(!self.fail_memory.replace(false), "memory unavailable"); + self.calls + .borrow_mut() + .push(format!("remember:{}:{source}", run.workflow_id())); Ok(()) } - fn invoke(&self, run: &Run, _: &str, planning: bool) -> Result { + fn invoke(&self, run: &Run, prompt: &str, planning: bool) -> Result { self.calls .borrow_mut() .push(format!("host:{}:{planning}", run.host.name())); + self.calls + .borrow_mut() + .push(format!("prompt:{}", prompt.lines().next().unwrap())); Ok("plan or completed build".into()) } fn write_plan(&self, _: &Run, _: &str) -> Result { @@ -141,6 +161,7 @@ async fn build_flow(p: &Fake) -> Run { let run = application::start( p, Start { + workflow: "delivery".into(), request: "request".into(), policy: "policy".into(), host: Host::Codex, @@ -196,6 +217,7 @@ async fn failed_task_transfer_can_resume_without_reinvoking_host() { let run = application::start( &p, Start { + workflow: "delivery".into(), request: "request".into(), policy: "policy".into(), host: Host::Codex, @@ -322,3 +344,43 @@ async fn changed_task_claim_blocks_first_publication() { ); assert!(!p.published.get()); } + +#[tokio::test] +async fn selected_bugfix_methods_follow_handoff_through_verified_publication() { + let p = Fake::new(); + let run = application::start( + &p, + Start { + workflow: "bugfix".into(), + request: "request".into(), + policy: "policy".into(), + host: Host::Codex, + allow_commit: true, + allow_publish: true, + }, + ) + .await + .unwrap(); + application::plan(&p, &run.id, 1).await.unwrap(); + application::handoff(&p, &run.id, 1, Host::ClaudeCode) + .await + .unwrap(); + application::build(&p, &run.id, 2).await.unwrap(); + application::handoff(&p, &run.id, 2, Host::Codex) + .await + .unwrap(); + application::verify(&p, &run.id, 3, "fix(example): repair observed defect") + .await + .unwrap(); + let shipped = application::ship(&p, &run.id, 3, "fix(example): repair observed defect") + .await + .unwrap(); + assert_eq!(shipped.workflow_id(), "bugfix"); + assert_eq!(shipped.stage, allstack::domain::Stage::Shipped); + let publication = shipped.publication.unwrap(); + assert!(publication.task_completed && publication.memory_indexed); + let calls = p.calls.borrow(); + assert!(calls.contains(&"prompt:diagnose".into())); + assert!(calls.contains(&"prompt:repair".into())); + assert!(calls.contains(&"remember:bugfix:https://github.com/example/project/pull/1".into())); +} diff --git a/tests/bootstrap.rs b/tests/bootstrap.rs index 2690e18..dc359ac 100644 --- a/tests/bootstrap.rs +++ b/tests/bootstrap.rs @@ -168,6 +168,9 @@ fn assert_setup(f: &Fixture) { assert_eq!(doctor["setup_ready"], true, "{doctor}"); assert_eq!(doctor["chronis"], "readable"); assert_eq!(doctor["authentication"], "not-probed"); + assert_eq!(doctor["catalog"]["workflows"].as_array().unwrap().len(), 2); + assert_eq!(f.json(&["catalog", "bugfix"])["id"], "bugfix"); + assert!(!f.repo.join(".allstack").exists()); let before = f.fingerprint(); let second = f.json(&["init"]); assert!( @@ -193,6 +196,59 @@ fn assert_setup(f: &Fixture) { assert_eq!(f.fingerprint(), before); } +#[test] +fn catalog_initialization_adds_resources_to_legacy_setup_without_replacing_bytes() { + let f = Fixture::new(false); + f.json(&["init", "--profile", "rust"]); + let preserved = [ + "workflows/delivery.toml", + "workflows/methods/plan.md", + "workflows/methods/build.md", + "docs/reference/allstack.md", + ".agents/skills/allstack-delivery/SKILL.md", + ".claude/skills/allstack-delivery/SKILL.md", + "workflows/verification.toml", + ]; + let bytes: Vec<_> = preserved + .iter() + .map(|p| fs::read(f.repo.join(p)).unwrap()) + .collect(); + for path in [ + "workflows/catalog.toml", + "workflows/bugfix.toml", + "workflows/methods/diagnose.md", + "workflows/methods/repair.md", + "docs/reference/allstack-catalog.md", + ".agents/skills/allstack-catalog/SKILL.md", + ".claude/skills/allstack-catalog/SKILL.md", + ] { + fs::remove_file(f.repo.join(path)).unwrap(); + } + let index = + "# Existing documentation\n\n- [Allstack CLI and recovery](reference/allstack.md)\n"; + fs::write(f.repo.join("docs/README.md"), index).unwrap(); + assert_eq!( + f.json(&["catalog"])["workflows"].as_array().unwrap().len(), + 1 + ); + let check = f.json(&["init", "--check"]); + assert_eq!(check["ready"], true); + assert!(!f.repo.join("workflows/catalog.toml").exists()); + f.json(&["init"]); + for (path, expected) in preserved.iter().zip(bytes) { + assert_eq!(fs::read(f.repo.join(path)).unwrap(), expected, "{path}"); + } + assert!( + fs::read_to_string(f.repo.join("docs/README.md")) + .unwrap() + .starts_with(index) + ); + assert_eq!( + f.json(&["catalog"])["workflows"].as_array().unwrap().len(), + 2 + ); +} + #[test] fn initializes_protocol_boundary_and_reruns_without_replacing_custom_files() { let f = Fixture::new(false); diff --git a/tests/catalog.rs b/tests/catalog.rs new file mode 100644 index 0000000..0706655 --- /dev/null +++ b/tests/catalog.rs @@ -0,0 +1,245 @@ +mod common; +use allstack::{ + domain::{Event, WorkflowSelection, validate_transition}, + infrastructure::{catalog, digest, memory::Memory}, +}; +use std::{fs, os::unix::fs::symlink, path::Path}; + +fn fixture() -> tempfile::TempDir { + let dir = tempfile::tempdir().unwrap(); + fs::create_dir_all(dir.path().join("workflows/methods")).unwrap(); + let source = Path::new(env!("CARGO_MANIFEST_DIR")); + for name in [ + "catalog.toml", + "delivery.toml", + "bugfix.toml", + "methods/plan.md", + "methods/build.md", + "methods/diagnose.md", + "methods/repair.md", + ] { + fs::copy( + source.join("workflows").join(name), + dir.path().join("workflows").join(name), + ) + .unwrap(); + } + dir +} + +#[test] +fn discovery_selects_distinct_methods_without_opening_stores() { + let dir = fixture(); + let all = catalog::inspect(dir.path(), None).unwrap(); + assert_eq!(all["stores_opened"], false); + assert_eq!(all["workflows"].as_array().unwrap().len(), 2); + let bugfix = catalog::select(dir.path(), "bugfix").unwrap(); + let delivery = catalog::select(dir.path(), "delivery").unwrap(); + assert!( + String::from_utf8(bugfix.methods["plan"].bytes.clone()) + .unwrap() + .contains("exact failed condition") + ); + assert_ne!( + bugfix.methods["plan"].digest, + delivery.methods["plan"].digest + ); + assert_ne!( + bugfix.methods["build"].digest, + delivery.methods["build"].digest + ); + let error = catalog::select(dir.path(), "review") + .err() + .unwrap() + .to_string(); + assert!(error.contains("available: bugfix, delivery")); + for name in [".allstack", ".prime", ".chronis"] { + assert!(!dir.path().join(name).exists()); + } +} + +#[test] +fn absent_catalog_keeps_legacy_delivery_available() { + let dir = fixture(); + fs::remove_file(dir.path().join("workflows/catalog.toml")).unwrap(); + let entries = catalog::entries(dir.path()).unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].id, "delivery"); + assert!(catalog::select(dir.path(), "bugfix").is_err()); +} + +#[test] +fn accepted_scope_uses_frozen_paths_and_rejects_definition_or_method_changes() { + let dir = fixture(); + let selected = catalog::select(dir.path(), "bugfix").unwrap(); + let mut run = common::run(); + run.selection = Some(WorkflowSelection { + id: selected.id, + path: selected.path, + }); + run.workflow = selected.digest; + run.methods = selected + .methods + .iter() + .map(|(k, v)| (k.clone(), v.digest.clone())) + .collect(); + run.method_paths = selected + .methods + .iter() + .map(|(k, v)| (k.clone(), v.path.clone())) + .collect(); + catalog::verify_scope(dir.path(), &run).unwrap(); + fs::write( + dir.path().join("workflows/catalog.toml"), + "invalid replacement catalog", + ) + .unwrap(); + fs::write( + dir.path().join("workflows/delivery.toml"), + "unrelated changed definition", + ) + .unwrap(); + catalog::verify_scope(dir.path(), &run).unwrap(); + let path = dir.path().join(run.workflow_path()); + fs::write(&path, b"changed selected definition").unwrap(); + assert!( + catalog::verify_scope(dir.path(), &run) + .unwrap_err() + .to_string() + .contains("workflow definition changed") + ); + fs::write(path, selected.bytes).unwrap(); + fs::write( + dir.path().join(&run.method_paths["plan"]), + "changed selected method", + ) + .unwrap(); + assert!( + catalog::verify_scope(dir.path(), &run) + .unwrap_err() + .to_string() + .contains("workflow method changed") + ); +} + +#[test] +fn invalid_catalogs_and_unsupported_contracts_fail_closed() { + for text in [ + "schema_version=2\nworkflows=[]", + "schema_version=1\nworkflows=[]", + "schema_version=1\nunknown=true\nworkflows=[]", + "schema_version=1\n[[workflows]]\nid='bugfix'\npath='workflows/bugfix.toml'", + "schema_version=1\n[[workflows]]\nid='delivery'\npath='workflows/delivery.toml'\n[[workflows]]\nid='delivery'\npath='workflows/bugfix.toml'", + "schema_version=1\n[[workflows]]\nid='delivery'\npath='workflows/bugfix.toml'", + "schema_version=1\n[[workflows]]\nid='delivery'\npath='workflows/../outside.toml'", + ] { + let dir = fixture(); + fs::write(dir.path().join("workflows/catalog.toml"), text).unwrap(); + assert!(catalog::entries(dir.path()).is_err(), "{text}"); + } + for (old, new) in [ + ("allow_trunk_push = false", "allow_trunk_push = true"), + ("schema_version = 1", "schema_version = 2"), + ( + "stage_order = [\"plan\", \"build\", \"verify\", \"ship\"]", + "stage_order = [\"plan\", \"ship\"]", + ), + ( + "plan = \"workflows/methods/diagnose.md\"", + "plan = \"/etc/passwd\"", + ), + ( + "build = \"workflows/methods/repair.md\"", + "build = \"workflows/methods/absent.md\"", + ), + ( + "status = \"implementation-preview\"", + "status = \"implementation-preview\"\nunknown = true", + ), + ] { + let dir = fixture(); + let path = dir.path().join("workflows/bugfix.toml"); + let source = fs::read_to_string(&path).unwrap(); + assert!(source.contains(old)); + fs::write(path, source.replace(old, new)).unwrap(); + assert!(catalog::entries(dir.path()).is_err(), "{new}"); + } +} + +#[test] +fn catalog_refuses_symlinks_hardlinks_and_oversized_inputs() { + let dir = fixture(); + let method = dir.path().join("workflows/methods/diagnose.md"); + fs::remove_file(&method).unwrap(); + symlink("plan.md", &method).unwrap(); + assert!(catalog::entries(dir.path()).is_err()); + fs::remove_file(&method).unwrap(); + fs::hard_link(dir.path().join("workflows/methods/plan.md"), &method).unwrap(); + assert!(catalog::entries(dir.path()).is_err()); + fs::remove_file(&method).unwrap(); + fs::write(&method, vec![b'x'; 65537]).unwrap(); + assert!(catalog::entries(dir.path()).is_err()); +} + +#[test] +fn selected_scope_is_immutable_and_legacy_event_bytes_survive() { + let bytes = include_bytes!("fixtures/legacy-event.json"); + let legacy: Event = serde_json::from_slice(bytes).unwrap(); + assert_eq!(legacy.run.workflow_id(), "delivery"); + assert_eq!(legacy.run.workflow_path(), "workflows/delivery.toml"); + assert_eq!(serde_json::to_vec(&legacy).unwrap(), bytes); + let mut changed = legacy.clone(); + changed.kind = "checkpointed".into(); + changed.previous = digest(bytes); + changed.run.revision += 1; + changed.run.selection = Some(WorkflowSelection { + id: "bugfix".into(), + path: "workflows/bugfix.toml".into(), + }); + assert!(validate_transition(Some(&legacy.run), &changed).is_err()); + let before = changed.run.clone(); + changed.run.selection.as_mut().unwrap().path = "workflows/delivery.toml".into(); + changed.run.revision += 1; + assert!(validate_transition(Some(&before), &changed).is_err()); +} + +#[tokio::test] +async fn prime_recall_retains_workflow_and_source_attribution() { + let dir = tempfile::tempdir().unwrap(); + let memory = Memory::open(dir.path()).await.unwrap(); + memory + .record( + "legacy", + "delivery", + "plan", + "docs/plans/legacy.md", + "legacy reproduction", + ) + .await + .unwrap(); + memory + .record( + "repair", + "bugfix", + "shipped", + "https://github.com/example/project/pull/1", + "reproduction repaired", + ) + .await + .unwrap(); + memory.shutdown().await.unwrap(); + let memory = Memory::open(dir.path()).await.unwrap(); + let recall = memory.recall("reproduction"); + let nodes = recall["nodes"].as_array().unwrap(); + assert_eq!(nodes.len(), 2); + let repair = nodes + .iter() + .find(|n| n["properties"]["run_id"] == "repair") + .unwrap(); + assert_eq!(repair["properties"]["workflow_id"], "bugfix"); + assert_eq!( + repair["properties"]["source"], + "https://github.com/example/project/pull/1" + ); + memory.shutdown().await.unwrap(); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 3c5d5c0..de3a402 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -10,6 +10,7 @@ pub fn run() -> Run { base: "base".into(), trunk: "main".into(), workflow: "workflow".into(), + selection: None, methods: [ ("plan".into(), "plan-method".into()), ("build".into(), "build-method".into()), diff --git a/tests/fixtures/legacy-event.json b/tests/fixtures/legacy-event.json new file mode 100644 index 0000000..9d7709d --- /dev/null +++ b/tests/fixtures/legacy-event.json @@ -0,0 +1 @@ +{"schema":1,"operation":"legacy-fixture","previous":"","kind":"started","run":{"id":"00000000-0000-4000-8000-000000000001","repository":"/fixture","remote":"https://github.com/example/project.git","branch":"feature","base":"base","trunk":"main","workflow":"workflow","methods":{"build":"build-method","plan":"plan-method"},"method_paths":{"build":"workflows/methods/build.md","plan":"workflows/methods/plan.md"},"policy":"policy","policy_path":"policy.toml","request":"request","plan":null,"task":"task-reference","host":"codex","epoch":1,"stage":"planning","workspace":"/workspace-1","checkpoint":"snapshot","pending":null,"transfer":null,"memory_pending":false,"prepared_commit":null,"verification":null,"publication":null,"allow_commit":true,"allow_publish":true,"revision":1}} \ No newline at end of file diff --git a/tests/prime_binding.rs b/tests/prime_binding.rs new file mode 100644 index 0000000..8be61f8 --- /dev/null +++ b/tests/prime_binding.rs @@ -0,0 +1,267 @@ +//! Legacy and malformed nodes cannot be produced through `Memory::record` +//! (it always writes a complete binding), so they are seeded through Prime's +//! own API, the writer is shut down, and `Memory` reopens the same data dir. + +use allsource_core::prime::Prime; +use allstack::infrastructure::memory::{Memory, binding_matches, single_complete_binding}; +use serde_json::{Value, json}; +use std::path::Path; + +const KEY: &str = "run:shipped"; +const SRC: &str = "https://github.com/example/project/pull/1"; +const SUM: &str = "legacy reproduction"; + +fn legacy(run: &str, source: &str, summary: &str) -> Value { + json!({ + "name": format!("shipped {run}"), + "domain": "allstack", + "key": KEY, + "source": source, + "summary": summary, + "run_id": run, + }) +} + +fn with_workflow(run: &str, workflow: Value, source: &str, summary: &str) -> Value { + let mut props = legacy(run, source, summary); + props["workflow_id"] = workflow; + props +} + +async fn seed(dir: &Path, nodes: &[Value]) { + let prime = Prime::open(dir).await.unwrap(); + for props in nodes { + prime.add_node("delivery", props.clone()).await.unwrap(); + } + prime.shutdown().await.unwrap(); +} + +async fn delivery_properties(dir: &Path) -> Vec { + let prime = Prime::open(dir).await.unwrap(); + let props = prime + .nodes_by_type("delivery") + .into_iter() + .map(|node| node.properties) + .collect(); + prime.shutdown().await.unwrap(); + props +} + +#[tokio::test] +async fn legacy_delivery_node_reconciles_only_as_delivery() { + let dir = tempfile::tempdir().unwrap(); + seed(dir.path(), &[legacy("run", SRC, SUM)]).await; + + let memory = Memory::open(dir.path()).await.unwrap(); + memory + .record("run", "delivery", "shipped", SRC, SUM) + .await + .expect("identical delivery retry matches a workflow-less legacy node"); + assert!( + memory + .record("run", "bugfix", "shipped", SRC, SUM) + .await + .is_err(), + "legacy delivery node must not reconcile as bugfix" + ); + assert!( + memory + .record("run", "delivery", "shipped", "https://example/pull/2", SUM) + .await + .is_err(), + "changed source must conflict against a legacy node" + ); + assert!( + memory + .record("run", "delivery", "shipped", SRC, "different result") + .await + .is_err(), + "changed summary must conflict against a legacy node" + ); + let recall = memory.recall("legacy reproduction"); + assert_eq!(recall["nodes"].as_array().unwrap().len(), 1); + memory.shutdown().await.unwrap(); + + let after = delivery_properties(dir.path()).await; + assert_eq!(after.len(), 1, "no node added or removed"); + assert!( + after[0].get("workflow_id").is_none(), + "legacy node keeps its missing workflow_id" + ); + assert_eq!(after[0]["source"], SRC); + assert_eq!(after[0]["summary"], SUM); +} + +#[tokio::test] +async fn matching_key_with_wrong_or_missing_run_id_fails() { + for seeded in [with_workflow("other", json!("delivery"), SRC, SUM), { + let mut props = with_workflow("run", json!("delivery"), SRC, SUM); + props.as_object_mut().unwrap().remove("run_id"); + props + }] { + let dir = tempfile::tempdir().unwrap(); + seed(dir.path(), &[seeded]).await; + let memory = Memory::open(dir.path()).await.unwrap(); + assert!( + memory + .record("run", "delivery", "shipped", SRC, SUM) + .await + .is_err(), + "a key match with a different or absent run_id must not reconcile" + ); + memory.shutdown().await.unwrap(); + assert_eq!(delivery_properties(dir.path()).await.len(), 1); + } +} + +#[tokio::test] +async fn duplicate_keys_fail_without_mutation_in_any_order() { + let identical = with_workflow("run", json!("bugfix"), SRC, SUM); + for pair in [ + vec![identical.clone(), identical.clone()], + vec![ + with_workflow("run", json!("bugfix"), SRC, SUM), + with_workflow("run", json!("delivery"), SRC, SUM), + ], + vec![ + with_workflow("run", json!("delivery"), SRC, SUM), + with_workflow("run", json!("bugfix"), SRC, SUM), + ], + ] { + let dir = tempfile::tempdir().unwrap(); + seed(dir.path(), &pair).await; + let memory = Memory::open(dir.path()).await.unwrap(); + assert!( + memory + .record("run", "bugfix", "shipped", SRC, SUM) + .await + .is_err(), + "duplicate key nodes require reconciliation, never an arbitrary pick" + ); + memory.shutdown().await.unwrap(); + assert_eq!( + delivery_properties(dir.path()).await.len(), + 2, + "a conflicting retry over duplicates adds and removes nothing" + ); + } +} + +#[tokio::test] +async fn invalid_workflow_metadata_is_not_given_the_legacy_fallback() { + for malformed in [Value::Null, json!(7)] { + let dir = tempfile::tempdir().unwrap(); + seed(dir.path(), &[with_workflow("run", malformed, SRC, SUM)]).await; + let memory = Memory::open(dir.path()).await.unwrap(); + assert!( + memory + .record("run", "delivery", "shipped", SRC, SUM) + .await + .is_err(), + "an explicit-null or non-string workflow_id binds no workflow" + ); + memory.shutdown().await.unwrap(); + assert_eq!(delivery_properties(dir.path()).await.len(), 1); + } +} + +#[tokio::test] +async fn matching_retry_across_reopen_keeps_one_unchanged_record() { + let dir = tempfile::tempdir().unwrap(); + let memory = Memory::open(dir.path()).await.unwrap(); + memory + .record("run", "bugfix", "shipped", SRC, "repair verified") + .await + .unwrap(); + memory.shutdown().await.unwrap(); + + let memory = Memory::open(dir.path()).await.unwrap(); + memory + .record("run", "bugfix", "shipped", SRC, "repair verified") + .await + .expect("an identical retry after reopen stays idempotent"); + memory.shutdown().await.unwrap(); + + let after = delivery_properties(dir.path()).await; + assert_eq!(after.len(), 1); + assert_eq!(after[0]["workflow_id"], "bugfix"); + assert_eq!(after[0]["source"], SRC); + assert_eq!(after[0]["summary"], "repair verified"); +} + +#[test] +fn readback_seam_rejects_missing_mismatch_and_duplicate() { + let good = with_workflow("run", json!("bugfix"), SRC, SUM); + assert!(single_complete_binding( + std::slice::from_ref(&good), + KEY, + "run", + "bugfix", + SRC, + SUM + )); + assert!( + !single_complete_binding(&[], KEY, "run", "bugfix", SRC, SUM), + "a missing node never proves a binding" + ); + let mismatched = with_workflow("run", json!("bugfix"), SRC, "different result"); + assert!( + !single_complete_binding( + std::slice::from_ref(&mismatched), + KEY, + "run", + "bugfix", + SRC, + SUM + ), + "a node that differs in one field never proves a binding" + ); + assert!( + !single_complete_binding(&[good.clone(), good], KEY, "run", "bugfix", SRC, SUM), + "duplicate nodes never prove a binding" + ); +} + +#[test] +fn binding_match_workflow_fallback_is_delivery_only() { + assert!(binding_matches( + &legacy("run", SRC, SUM), + KEY, + "run", + "delivery", + SRC, + SUM + )); + assert!(!binding_matches( + &legacy("run", SRC, SUM), + KEY, + "run", + "bugfix", + SRC, + SUM + )); + for malformed in [Value::Null, json!(7)] { + assert!( + !binding_matches( + &with_workflow("run", malformed, SRC, SUM), + KEY, + "run", + "delivery", + SRC, + SUM + ), + "malformed workflow_id never receives the legacy delivery fallback" + ); + } + assert!( + !binding_matches( + &with_workflow("other", json!("delivery"), SRC, SUM), + KEY, + "run", + "delivery", + SRC, + SUM + ), + "run identity is part of the immutable binding" + ); +} diff --git a/tests/prime_retry.rs b/tests/prime_retry.rs new file mode 100644 index 0000000..293b861 --- /dev/null +++ b/tests/prime_retry.rs @@ -0,0 +1,74 @@ +use allstack::infrastructure::memory::Memory; + +#[tokio::test] +async fn identical_retry_keeps_one_source_linked_record() { + let dir = tempfile::tempdir().unwrap(); + let memory = Memory::open(dir.path()).await.unwrap(); + for _ in 0..2 { + memory + .record( + "run", + "bugfix", + "shipped", + "https://github.com/example/project/pull/1", + "repair verified", + ) + .await + .unwrap(); + } + assert_eq!( + memory.recall("repair")["nodes"].as_array().unwrap().len(), + 1 + ); + memory.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn conflicting_retry_cannot_claim_another_workflow_or_source() { + for (workflow, source, summary) in [ + ( + "delivery", + "https://github.com/example/project/pull/1", + "repair verified", + ), + ( + "bugfix", + "https://github.com/example/project/pull/2", + "repair verified", + ), + ( + "bugfix", + "https://github.com/example/project/pull/1", + "different result", + ), + ] { + let dir = tempfile::tempdir().unwrap(); + let memory = Memory::open(dir.path()).await.unwrap(); + memory + .record( + "run", + "bugfix", + "shipped", + "https://github.com/example/project/pull/1", + "repair verified", + ) + .await + .unwrap(); + assert!( + memory + .record("run", workflow, "shipped", source, summary) + .await + .is_err(), + "conflicting retry must fail: {workflow}, {source}, {summary}" + ); + let recall = memory.recall("repair"); + let nodes = recall["nodes"].as_array().unwrap(); + assert_eq!(nodes.len(), 1); + assert_eq!(nodes[0]["properties"]["workflow_id"], "bugfix"); + assert_eq!( + nodes[0]["properties"]["source"], + "https://github.com/example/project/pull/1" + ); + memory.shutdown().await.unwrap(); + } +} diff --git a/workflows/bugfix.toml b/workflows/bugfix.toml new file mode 100644 index 0000000..aeabd7c --- /dev/null +++ b/workflows/bugfix.toml @@ -0,0 +1,81 @@ +schema_version = 1 +id = "bugfix" +version = "0.1.0-preview" +status = "implementation-preview" +description = "Diagnose an evidenced defect, repair its cause, verify regression coverage, and publish across sequential coding hosts." +hosts = ["codex", "claude-code"] +host_scope = "sequential-same-machine" +stage_order = ["plan", "build", "verify", "ship"] +dispositions = ["ready", "active", "awaiting_authority", "blocked", "outcome_unknown"] + +[methods] +plan = "workflows/methods/diagnose.md" +build = "workflows/methods/repair.md" + +[storage] +run_authority = "allsource-core" +task_authority = "chronis" +recall = "prime" +write_boundary = "single-local-coordinator" +event_namespace = "allstack.workflow.v1" +require_durable_acknowledgement = true +require_expected_revision = true + +[handoff] +mode = "immutable-snapshot-new-worktree" +require_new_epoch = true +require_artifact_readback = true +require_task_claim_readback = true +unresolved_action_policy = "reconcile-before-execute" + +[execution] +language = "rust" +max_command_seconds = 300 +build_concurrency = 1 +process_policy = "own-kill-reap" +authority_source = "trusted-user-instruction-and-host-policy" + +[publication] +destination = "feature-branch-and-open-pull-request" +max_open_prs_per_run = 1 +max_open_prs_per_session_repository = 1 +commits_per_pr = 1 +require_live_branch = true +require_explicit_branch_authority = true +require_commit_authority = true +require_publication_authority = true +require_final_commit_verification = true +require_clean_candidate = true +require_remote_ref_readback = true +require_pr_head_readback = true +allow_trunk_push = false +allow_hook_bypass = false +merge = false +deploy = false + +[[stages]] +id = "plan" +method = "recall-source-check-shape" +requires = ["repository-policy", "request", "component-health"] +produces = ["indexed-plan", "acceptance-map", "verification-policy", "task-reference", "authority-scope"] + +[[stages]] +id = "build" +method = "implement-complete-accepted-change" +requires = ["indexed-plan", "task-claim-readback", "active-host-epoch", "prepared-worktree"] +produces = ["candidate-snapshot", "change-manifest", "implementation-evidence"] + +[[stages]] +id = "verify" +method = "execute-required-checks-and-real-flow" +requires = ["candidate-snapshot", "acceptance-map", "verification-policy", "applicable-commit-authority"] +produces = ["final-commit", "bound-verification-receipts", "end-to-end-evidence"] +zero_selected_tests = "inconclusive" +candidate_change = "invalidate-affected-receipts" + +[[stages]] +id = "ship" +method = "publish-exact-verified-commit-and-reconcile" +requires = ["final-commit", "bound-verification-receipts", "applicable-authority", "live-feature-branch"] +produces = ["remote-commit-receipt", "pull-request-receipt", "task-completion-readback", "outcome-reference"] +unknown_side_effect = "read-destination-before-retry" diff --git a/workflows/catalog-gates.toml b/workflows/catalog-gates.toml new file mode 100644 index 0000000..56b7eb0 --- /dev/null +++ b/workflows/catalog-gates.toml @@ -0,0 +1,29 @@ +command_seconds = 240 +environment = ["PATH", "RUSTUP_TOOLCHAIN", "RUSTFLAGS", "CARGO_TARGET_DIR", "RUSTC_WRAPPER"] + +[[gates]] +name = "format" +argv = ["cargo", "fmt", "--all", "--", "--check"] + +[[gates]] +name = "runtime and catalog regression" +argv = ["cargo", "nextest", "run", "--locked", "--offline"] +success_marker = "tests run:" +test = true +compile = true + +[[gates]] +name = "lint" +argv = ["cargo", "clippy", "--all-targets", "--locked", "--offline", "--", "-D", "warnings"] +compile = true + +[[gates]] +name = "both host fixtures" +argv = ["cargo", "test", "--manifest-path", "fixtures/handoff/Cargo.toml", "--locked", "--offline", "--tests"] +success_marker = "2 passed" +test = true +compile = true + +[[gates]] +name = "source whitespace" +argv = ["git", "diff", "--check", "origin/main", "HEAD"] diff --git a/workflows/catalog.toml b/workflows/catalog.toml new file mode 100644 index 0000000..ced5272 --- /dev/null +++ b/workflows/catalog.toml @@ -0,0 +1,9 @@ +schema_version = 1 + +[[workflows]] +id = "delivery" +path = "workflows/delivery.toml" + +[[workflows]] +id = "bugfix" +path = "workflows/bugfix.toml" diff --git a/workflows/methods/diagnose.md b/workflows/methods/diagnose.md new file mode 100644 index 0000000..d7b5cf2 --- /dev/null +++ b/workflows/methods/diagnose.md @@ -0,0 +1,5 @@ +Diagnose the supplied defect from repository evidence. Read applicable instructions, the request, relevant code, and retained failure output. Open recalled sources before treating them as requirements. Identify observed versus expected behavior, reproduction inputs, the exact failed condition, and the responsible producer or layer. A model's explanation alone is not reproduction evidence. If required evidence is absent, name it explicitly and leave the diagnosis unproven. + +Produce a repair plan in your final response. Include the complete affected user flow, concrete source locations, evidence supporting the cause, the smallest responsible repair, regression tests that fail for the observed defect, and required final checks. For data-driven behavior, read the published data contract before proposing an engine change. Distinguish a missing contract from a runtime violating an existing contract. + +Do not edit files, create branches, commit, push, publish, send messages, spawn agents, or compile. The coordinator indexes the plan and runs later verification. Write normal artifact prose so the incoming host can act without this conversation. diff --git a/workflows/methods/repair.md b/workflows/methods/repair.md new file mode 100644 index 0000000..c58017b --- /dev/null +++ b/workflows/methods/repair.md @@ -0,0 +1,7 @@ +Repair the supplied defect in this managed worktree. Read applicable instructions, the indexed diagnosis, source-linked evidence, and the original request. Confirm which condition failed before editing. Preserve unrelated edits; you are not alone in this repository. + +Fix the responsible producer, data contract, or generic engine invariant identified by evidence. Add regression coverage that exercises the observed failure and checks the expected result. Do not replace a causal repair with a downstream masking guard. If evidence contradicts the diagnosis, inspect and report the unresolved condition rather than claiming a proven cause. Preserve the indexed plan and frozen verification policy. + +Do not create branches, commit, push, publish, send messages, spawn agents, or compile. The coordinator executes regression tests, verifies the final commit, and controls publication. Stage intended new source files explicitly with `git add -- ` so the checkpoint retains them. Never stage secrets or ignored files. Existing tracked edits need no staging. + +Return caveman: terse fragments, file:line, no prose.