diff --git a/README.md b/README.md index 0f81198..6d5c367 100644 --- a/README.md +++ b/README.md @@ -234,10 +234,15 @@ output-parent binding, and returns one out-of-band completion digest. ## Verification +See the [operational readiness guide](docs/readiness.md) for first-run setup and +remaining evidence gaps, and the +[suite Conductor guide](https://herdr.structupath.ai/docs/conductor/) +for cross-project documentation. + ```bash npm run check npm run test:stage2 -bash -n scripts/*.sh +npm run check:shell shellcheck --shell=bash scripts/*.sh python3 -m py_compile scripts/harness-fs-helper.py go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 diff --git a/docs/herdr-plugins-cheatsheet.md b/docs/herdr-plugins-cheatsheet.md index 773c854..e59c6f0 100644 --- a/docs/herdr-plugins-cheatsheet.md +++ b/docs/herdr-plugins-cheatsheet.md @@ -2,7 +2,8 @@ The canonical cross-plugin guide lives in the [`herdr-suite-site` documentation](https://github.com/StructuPath/herdr-suite-site/tree/main/docs-src). -That separate site is not changed or promoted by Conductor Stage 2. +The suite guide summarizes the plugins; this repository defines Conductor's +runtime contract. For Conductor `0.4.0`, use this repository's [README](../README.md) as the operational authority. Its bounded support is exactly Herdr `0.7.5`, protocol @@ -19,11 +20,17 @@ Key boundaries: - reviewer/validator sources are distinct exact-integration-SHA snapshots with separate writable outboxes and empty artifacts/source outputs; - stand-down closes an exact deterministic pane prefix and retains all product - resources; and -- same-UID races, final-check/pane-close TOCTOU, cumulative retention, SHA-1-width - support, and no ambiguous recovery remain explicit. + resources; +- same-UID races, final-check/pane-close TOCTOU, cumulative retention, and + SHA-1-width support remain explicit; +- configuration v3 optionally names one existing local apply target ref; +- preview binds the exact integration and gates, an operator records the + approval receipt, and apply consumes it before any single-ref fast-forward; +- only an uncertain Stage 3 apply publication can be resolved by exact target + re-observation; ambiguous pane/agent operations still refuse recovery. Historical designs under [`docs/history/`](history/) and retained Stage 1 evidence -are lineage records, not current operational guidance. Stage 2 does not add -preview, approval/apply, adapters, unattended launch, product cleanup, Browser or -site promotion, push, tag, or release automation. +are lineage records, not current operational guidance. Conductor does not add +suite adapters, unattended launch, product cleanup, Browser promotion, push, +tag, or release automation. See [operational readiness](readiness.md) for setup, +validation, and the remaining evidence gaps. diff --git a/docs/readiness.md b/docs/readiness.md new file mode 100644 index 0000000..001000b --- /dev/null +++ b/docs/readiness.md @@ -0,0 +1,56 @@ +# Conductor operational readiness + +Conductor 0.4.0 supports attended task/report coordination and single-ref local +apply on exactly Herdr 0.7.5 (protocol 17, API schema 1). The manifest minimum +does not imply compatibility with newer Herdr releases. Seven installed actions +share one runtime; there is no compiled bundle or dependency installation step. + +## Prepare a first run + +1. Check `node --version`, `python3 --version`, `git --version`, and + `herdr --version` against the [README requirements](../README.md#supported-contract). +2. Run `npm run check` in this checkout. It validates scripts, the manifest, + documentation, historical evidence, and automated behavior. Shell validation + checks each entrypoint individually without executing it. +3. Follow the [configuration contract](../README.md#configuration) in the + intended invoking repository. Commit the configuration before assembly. + Start with configuration v2 or v3 with `"apply": null` when local apply is + not required. Choose explicit producer ownership and required commands. +4. Invoke the installed actions from the intended Herdr workspace. Inspect the + returned task/source/outbox paths and give workers their exact task-bound + publisher command. Every transition is attended. +5. Collect reports with harvest and inspect the exact integrated result. If + applying, use preview's exact approval command and receipt contract before + invoking apply. The target must be an existing local branch at the integration + base and not checked out in any worktree. +6. Stand down after inspection. Archive retains all product resources; plan + capacity for worktrees, branches, reports, and logs. + +## Validation and remaining work + +`npm run check` is the routine build/validation equivalent for this script-based +plugin. Before review also run `npm run test:stage2` (includes Stage 3), +`npm run check:shellcheck`, and `npm run check:workflow`. The latter validates +workflow syntax; it does not change CI configuration. + +Automated tests use isolated Git repositories and a fake Herdr executable, +including real subprocess crashes, lock contention, rejected reports, gate +snapshots, and apply consumption/re-observation. Passing them is not a new live +installation or release attestation. Retained Stage 1/2 evidence remains bound +to its historical source commits. The opt-in live evidence workflow requires +the independent exact-source review described in [CONTRIBUTING](../CONTRIBUTING.md). + +Priorities for subsequent work: + +- Obtain candidate-specific live evidence for Stage 3 before making broader + deployment claims; preserve the separate independent review requirement. +- Test additional Herdr versions explicitly before widening the exact runtime + contract or the website's compatibility claims. +- Improve operator guidance for retained resources and uncertain pane/agent + operations without introducing automatic deletion or inferred recovery. +- Evaluate suite adapters as a separate design. Conductor currently does not + invoke Swarm, and Guard observations do not grant apply approval. + +The [security boundaries](../SECURITY.md) continue to apply: coordination and +receipts are cooperative same-UID records, worker reports are assertions, and +only the Stage 3 apply publication has bounded uncertainty resolution. diff --git a/package.json b/package.json index 6092026..92cd5c1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "test": "node --test", "test:stage2": "node --test tests/task-report-schema.test.mjs tests/report-publisher.test.mjs tests/stage2-*.test.mjs tests/stage3-*.test.mjs tests/state-kernel.test.mjs tests/stage1-runtime-*.test.mjs", - "check:shell": "bash -n scripts/*.sh", + "check:shell": "for script in scripts/*.sh; do bash -n \"$script\" || exit; done", "check:shellcheck": "shellcheck --shell=bash scripts/*.sh", "check:python": "python3 -m py_compile scripts/harness-fs-helper.py", "check:workflow": "go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7", diff --git a/tests/shell-validation.test.mjs b/tests/shell-validation.test.mjs new file mode 100644 index 0000000..8413d5d --- /dev/null +++ b/tests/shell-validation.test.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { + mkdtempSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; + +const command = JSON.parse( + readFileSync(new URL("../package.json", import.meta.url), "utf8"), +).scripts["check:shell"]; + +test("shell validation checks later scripts, handles spaces, and never executes them", () => { + const root = mkdtempSync(join(tmpdir(), "conductor-shell-check-")); + try { + mkdirSync(join(root, "scripts")); + writeFileSync(join(root, "scripts", "a.sh"), "exit 42\n"); + writeFileSync(join(root, "scripts", "z later.sh"), "if then\n"); + const invalid = spawnSync("/bin/sh", ["-c", command], { + cwd: root, + encoding: "utf8", + }); + assert.notEqual(invalid.status, 0); + assert.match(invalid.stderr, /z later\.sh/); + writeFileSync(join(root, "scripts", "z later.sh"), "exit 43\n"); + const valid = spawnSync("/bin/sh", ["-c", command], { + cwd: root, + encoding: "utf8", + }); + assert.equal(valid.status, 0, valid.stderr); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/tests/stage1-runtime-reconcile.test.mjs b/tests/stage1-runtime-reconcile.test.mjs index 517b5ba..235efa9 100644 --- a/tests/stage1-runtime-reconcile.test.mjs +++ b/tests/stage1-runtime-reconcile.test.mjs @@ -58,6 +58,7 @@ test("report-first reconcile and stand-down retain Git and product inventory", a "refs/heads", ); const reconciled = await reconcile({ + herdrBin: "fake", contextJson: context(fixture.repository, fixture.workspace), stateRoot: fixture.stateRoot, exec: fixture.fake.exec, @@ -177,6 +178,7 @@ test("source, target, journal, registration, and lock failures perform zero CAS" const before = updateRefCount(fixture); const invocation = () => reconcile({ + herdrBin: "fake", contextJson: context(fixture.repository, fixture.workspace), stateRoot: fixture.stateRoot, exec: reconcileExec, @@ -199,6 +201,7 @@ test("source and target races immediately before CAS perform zero CAS", async () let injected = false; await assert.rejects(() => reconcile({ + herdrBin: "fake", contextJson: context(fixture.repository, fixture.workspace), stateRoot: fixture.stateRoot, exec: fixture.fake.exec, @@ -229,6 +232,7 @@ test("post-CAS ref, tracked-worktree, and staged-index drift never publish recon await assert.rejects( () => reconcile({ + herdrBin: "fake", contextJson: context(fixture.repository, fixture.workspace), stateRoot: fixture.stateRoot, exec: fixture.fake.exec, @@ -273,6 +277,7 @@ test("post-CAS ref, tracked-worktree, and staged-index drift never publish recon const attempts = updateRefCount(fixture); await assert.rejects(() => reconcile({ + herdrBin: "fake", contextJson: context(fixture.repository, fixture.workspace), stateRoot: fixture.stateRoot, exec: fixture.fake.exec, @@ -296,6 +301,7 @@ test("post-CAS synchronization checkpoints preserve concurrent tracked edits and await assert.rejects( () => reconcile({ + herdrBin: "fake", contextJson: context(fixture.repository, fixture.workspace), exec: fixture.fake.exec, fault(name) { @@ -349,6 +355,7 @@ test("CAS failure cannot synchronize the target worktree", async () => { await assert.rejects( () => reconcile({ + herdrBin: "fake", contextJson: context(fixture.repository, fixture.workspace), stateRoot: fixture.stateRoot, exec: racingExec, @@ -373,6 +380,7 @@ test("cross-scope callers and conflicting complete producer sets perform zero CA const before = updateRefCount(isolated); await assert.rejects(() => reconcile({ + herdrBin: "fake", contextJson: context(repository, workspace), stateRoot: isolated.stateRoot, exec: isolated.fake.exec, @@ -404,6 +412,7 @@ test("cross-scope callers and conflicting complete producer sets perform zero CA } await assert.rejects(() => reconcile({ + herdrBin: "fake", contextJson: context(fixture.repository, fixture.workspace), stateRoot: fixture.stateRoot, exec: fixture.fake.exec, diff --git a/tests/stage1-runtime-stand-down-archive.test.mjs b/tests/stage1-runtime-stand-down-archive.test.mjs index 4ef1082..1376e0a 100644 --- a/tests/stage1-runtime-stand-down-archive.test.mjs +++ b/tests/stage1-runtime-stand-down-archive.test.mjs @@ -643,6 +643,7 @@ test("pre-intent integration failure is retryable while failed pane close is nev operation === "merge" ? () => reconcile({ + herdrBin: "fake", contextJson: context(fixture.repository, fixture.workspace), stateRoot: fixture.stateRoot, exec: failingExec,