diff --git a/ADOPTING.md b/ADOPTING.md index 631207b..9ceb445 100644 --- a/ADOPTING.md +++ b/ADOPTING.md @@ -2,7 +2,7 @@ Writwall is a document-controlled governance methodology with a self-hosting reference implementation and project-scaffolding toolkit. This is the complete on-ramp. If you do not yet know which agent to open, where it should run, or what to say first, begin with [`START-HERE.md`](START-HERE.md); it requires no prior Doctrine knowledge. -**Before anything else: do not unpack the distribution archive into your project.** `writwall-.zip` is a source distribution, not an overlay. Adoption instantiates a small set of project-side artifacts, listed in section 0. Writwall's own charter, governance directory, decisions, plan, state, and work history are its working records under Doctrine 5.1.4 and 5.1.5. They are readable as an example and are never copied into your project by any route below. +**Before anything else: do not unpack the distribution archive into your project.** `writwall-.zip` is a source distribution, not an overlay. Adoption instantiates a small set of project-side artifacts, listed in section 0. Writwall's own charter, governance directory, decisions, plan, state, and work history are its working records under Doctrine 5.1.4 and 5.1.5. They are readable as an example and are never copied into your project by any method below. If the archive may already have been unpacked into the target, stop before deleting or continuing. Follow `START-HERE.md`'s accidental-overlay recovery @@ -25,29 +25,43 @@ Owner ratifies an exact disposition. ├── history/ archive/ templates/ ``` -That is the whole footprint. Every route below produces exactly this, filled in to differing degrees. `DOCTRINE.md` itself is not copied into your project: after adoption your agents receive your charter and your routed records, never the methodology (Doctrine 1.2.4). +That is the whole footprint. The execution methods below contribute to this +single lifecycle at different stages. `DOCTRINE.md` itself is not copied into +your project: after adoption your agents receive your charter and your routed +records, never the methodology (Doctrine 1.2.4). `checks/check_work_order_dispatch.py` is a deterministic, read-only checker: it validates the lockout state (`--lockout`), a candidate work order before you create the activation pointer (`--work-order `), or the currently active pointer and the work order it names (`--active`). It catches a missing, malformed, or mistargeted pointer, CRLF/non-UTF-8 work-order bytes, and an unsafe or malformed grant, before you launch a mutating session. It is a convenience check, not enforcement: it never repairs anything, and passing it makes no claim about the capability wall, which is a separate, provider-specific mechanism. -Adoption is an Owner event with a defined boundary (Doctrine Part 6). Every route below ends the same way: a baseline commit is chosen, a governance directory exists, the charter is injected, the wall has been observed denying writes through every mutation channel, an adoption record (DR-001) is ratified, and an adoption commit contains it. Until DR-001 exists, nothing is governed and nothing counts. +Adoption is an Owner event with a defined boundary (Doctrine Part 6). The +single lifecycle ends with a chosen baseline commit, a governance directory, +an injected charter, honest birth-test evidence for every claimed enforcement +surface, a ratified adoption record (DR-001), and an adoption commit containing +it. Until DR-001 exists, nothing is governed and nothing counts. --- -## 1. Choose your route +## 1. Follow one canonical lifecycle -Choose the human operating model in `START-HERE.md` before choosing the -mechanical route below. In particular, make the self-contained adoption bundle -and these instructions local **before the wall is registered**. A correctly -locked session may deny the network request that would otherwise retrieve them. +There are not three competing adoption routes. Writwall has one canonical +lifecycle: the human Owner begins with a fresh Architect, explicitly promotes +an acceptable project sketch, materializes and ratifies adoption, then hands +the adopted repository to a fresh General. The General routes bounded +Operators and fresh Reviewers; new design or design-conformance questions go +back to a fresh Architect. -For a first adoption, the default is the day-zero coordinator: +The installed coordinator is the ordinary entry point. Prompt-only use, the +bundled skill, structured intake, and `init.sh` are execution methods or +fallbacks inside the same lifecycle. They do not change authority or role +separation. Make the self-contained adoption bundle and these instructions +local **before the wall is registered**. A correctly locked session may deny +the network request that would otherwise retrieve them. -Release `v0.10.0` packages the conversation-first Architect handoff, -canonical-root enforcement, and corrected lifecycle classification described -below. +Release `v0.11.0` packages the conversation-first Architect handoff, +canonical-root enforcement, corrected lifecycle classification, and the +repository-nonmutating `writwall inspect` entry described below. ```text -python -m pip install "https://github.com/HLLMR/writwall/archive/refs/tags/v0.10.0.zip" +python -m pip install "https://github.com/HLLMR/writwall/archive/refs/tags/v0.11.0.zip" # Installed command writwall start --project-root /path/to/your-project @@ -59,6 +73,21 @@ py -3 scripts/start_writwall.py --project-root C:\path\to\your-project python3 scripts/start_writwall.py --project-root /path/to/your-project ``` +For a zero-write inventory or an explicit safe role handoff, use: + +```text +writwall inspect --project-root /path/to/your-project --role auto +``` + +`--role architect` lets a fresh Architect re-enter a clean/new, incomplete, +adopted, or retired project without altering lifecycle state. `general` is +limited to adopted/retired lockout, `recovery` to partial bootstrap, and an +active work order remains Operator-only under `auto`. The command prints the +observed lifecycle, bounded evidence, selected role, and copyable prompt. It +creates no project, bootstrap, temporary, profile, privacy-screen, cache, or +bytecode state. The command still requires an installed Writwall package or a +local source tree; when neither exists, use section 2's prompt-only fallback. + It classifies the target from repository bytes, copies the complete skill bundle into a temporary `.writwall-bootstrap/` directory only for a clean/new target, and emits the exact next prompt. It is lifecycle-aware bootstrap and routing @@ -81,36 +110,59 @@ Operator packet behavior are documented in The idea-first qualification and identity gate are documented in [`docs/architect-interview.md`](docs/architect-interview.md). -| Route | Use when | Tooling | What it produces | -|---|---|---|---| -| A. Chat prompt | You want to think it through with a model before touching the repo, or your project has an existing document corpus that needs mapping | Any capable chat model | Draft DR-001, draft adoption mapping, draft charter kill list, a checklist of manual steps | -| B. Coding-agent skill | You are in Claude Code (or a coding agent that supports skills) inside the target repository | `skills/writwall-adopt/`, a self-contained bundle | Bootstrap: inventory, scaffolded `governance/`, installed adapter, installed pre-dispatch validator, birth-test evidence, proposals for the Owner, no commits. Then, only if you explicitly direct it, an Owner-directed recorder closeout that records the decisions you have ratified and makes the one local adoption commit. | -| C. Scaffolder | You already know the layout and only want the directories, templates, and dispatch checker | `init.sh` | Empty `governance/` structure, template copies, and a create-only pre-dispatch validator. Nothing project-specific. | +| Execution method | Use when | What it does | +|---|---|---| +| `writwall start` | A new idea or clean project may receive create-only bootstrap bytes | Emits the fresh Architect handoff and makes the complete temporary adoption bundle local | +| `writwall inspect --role architect` | An existing or workplace repository needs a zero-write first conversation, or an Architect must re-enter later | Prints bounded lifecycle evidence and a fresh Architect prompt without creating any state | +| Prompt-only fallback | The package and source tree are unavailable, or policy permits a model conversation but no local tool | Starts the same Architect function; repository mechanics wait until the bundle is local | +| Bundled `writwall-adopt` skill | The Owner has promoted the sketch and wants agent-assisted adoption mechanics | Inventories, proposes, and performs only separately ratified recorder actions | +| `--structured-intake` or `init.sh` | Deterministic intake or expert low-level scaffolding is specifically needed | Preserves compatibility and feeds the fresh Architect; neither creates a second lifecycle nor changes authority | -Routes combine. A common path for an existing project is A (mapping conversation) followed by B (mechanical bootstrap) followed by the Owner steps in section 5. +The coordinator handoff and bootstrap are temporary and must be removed before +the adoption commit. + +For an existing workplace repository, use the zero-write entry first: + +```text +writwall inspect --project-root /path/to/work-repository --role architect +``` -The coordinator selects among these routes; it does not replace them. Its -handoff is temporary and must be removed before the adoption commit. +Open the fresh Architect in an employer-approved account and interface, paste +the emitted handoff, and keep the first conversation read-only. Writwall does +not override employer policy or make an agent approved to receive source code, +customer data, or secrets. Infrastructure, DNS, mail, deployment, and other +account-bearing actions should be routed through separately bounded Operator +packets rather than giving the repository agent standing external authority. +The shipped mechanical wall currently covers only the documented Claude Code +adapter after project-local installation and a successful session-local birth +test; other agents remain instruction-bounded unless an equivalent adapter is +installed and tested. | Human command | Observed state | Fresh role receiving output | Prior session stops | Target bytes | |---|---|---|---|---| | `writwall start --project-root ` | Clean/new (ordinary invocation) | Architect (conversation-first) | Launcher returns; the Architect stops before adoption mechanics until the Owner promotes | Create-only bootstrap may be added | -| `... --structured-intake` | Clean/new | Adoption coordinator | Launcher returns; coordinator stops at adoption closeout | Create-only bootstrap may be added | +| `... --structured-intake` | Clean/new | Fresh Architect (prepared intake) | Launcher returns; Architect stops before adoption mechanics until explicit promotion | Create-only bootstrap may be added | | Same command | Partial/recovery | Recovery coordinator | Incomplete or locked session | Unchanged | | Same command | Adopted/retired lockout | Fresh General | Onboarding or prior work session | Unchanged | | Same command | Active work order | Bounded Operator/Implementer | Prior coordinator or Implementer context | Unchanged | | Same command | Malformed/contradictory | No role; fail-closed diagnostic | Invoking session | Unchanged | +| `writwall inspect ... --role auto` | Any valid lifecycle | Same lifecycle-derived role | Invoking session | Unchanged | +| `writwall inspect ... --role architect` | Clean/new, partial, adopted, or retired | Fresh Architect | Invoking session | Unchanged | --- -## 2. Route A: the adoption prompt +## 2. Prompt-only Architect fallback -Open your chat companion. Attach or paste `DOCTRINE.md`. Then paste this: +Open an employer-approved or otherwise appropriate capable agent outside any +already-locked Implementer session. Attach or paste `DOCTRINE.md`, then paste +this. This is still the fresh Architect stage; it is not an alternate adoption +process. ``` -You are helping me adopt the Doctrine (attached) into a software project as its -Owner. Act as a bootstrap interviewer, not as an implementer. You draft; I ratify. -Ask one question at a time. Do not summarize the doctrine back to me. +Act as the Writwall Architect for this project. Begin read-only. Listen to my +pitch, use repository evidence before asking me to restate visible facts, +challenge assumptions, and return a concise project sketch before any adoption +mechanics. You draft; I ratify. Do not summarize the doctrine back to me. Ground rules for you: - Everything you produce is a PROPOSAL until I say "ratified." Label drafts as such. @@ -120,7 +172,7 @@ Ground rules for you: - If I ask you to skip a step the doctrine requires (baseline commit, birth test, adoption mapping, DR-001), refuse and explain which clause requires it. -Walk me through, in order: +If I explicitly promote the sketch, walk me through, in order: 1. Baseline commit selection (Doctrine 2.25, 6.1.2). Ask what my last wholly pre-doctrine commit is. 2. Adoption mapping (6.3, Appendix E). Ask me to list every intent-bearing or @@ -147,11 +199,13 @@ repository operations each one implies listed beside it. Do not give me a checklist of edits to type. Then stop. ``` -That conversation produces drafts and a decision packet. Move them into your repository yourself, or hand the packet to Route B, which can record ratified decisions for you. +That conversation produces drafts and a decision packet. Move them into your +repository yourself, or give the exact ratified packet to the bundled skill's +recorder mode in section 3. --- -## 3. Route B: the coding-agent skill +## 3. Skill-assisted adoption materialization Copy `skills/writwall-adopt/` from this repository into your target project's skills location (Claude Code: `.claude/skills/writwall-adopt/`). Copy the whole directory: it is a self-contained bootstrap bundle carrying its own copy of the doctrine, the migration guides, the adapter and its README, the pre-dispatch validator, and templates A through E under `references/` and `assets/`. It reads nothing from this repository, so the target project never depends on Writwall (Doctrine 5.1.2). @@ -210,7 +264,7 @@ If your project already has partial governance artifacts from an earlier revisio --- -## 4. Route C: the scaffolder +## 4. Expert low-level scaffolder ```bash ./init.sh /path/to/your/project @@ -253,11 +307,11 @@ Every run ends with three lists: created, skipped, and refused. Read them. --- -## 5. The Owner's sequence (all routes end here) +## 5. The Owner's adoption sequence This is your decision sequence. Every step below is yours to decide and yours to ratify, and no agent may decide, sign, or infer any of it. That is what Doctrine 6.4.1 fixes: the order, and the authority. -It does not fix whose fingers move. Once you have made a decision and ratified it explicitly, an agent you have authorized may perform the repository mechanics that record it — materializing a file, archiving a superseded one, renaming a finished record, staging, and making the local adoption commit. Route B's recorder closeout is that path; doing it all yourself is equally correct and always available. What is never delegable is the deciding. +It does not fix whose fingers move. Once you have made a decision and ratified it explicitly, an agent you have authorized may perform the repository mechanics that record it — materializing a file, archiving a superseded one, renaming a finished record, staging, and making the local adoption commit. Section 3's recorder closeout is that path; doing it all yourself is equally correct and always available. What is never delegable is the deciding. In the order of Doctrine 6.4.1: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e59440..1e1f427 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ gate against the final checked public candidate on native Windows and native Ubuntu: ```text -python checks/check_coordinator_release.py --expected-tag v0.10.0 +python checks/check_coordinator_release.py --expected-tag v0.11.0 ``` The gate copies the candidate to temporary build space, builds and installs the diff --git a/PROJECTION-MANIFEST.sha256 b/PROJECTION-MANIFEST.sha256 index b228a71..5f74210 100644 --- a/PROJECTION-MANIFEST.sha256 +++ b/PROJECTION-MANIFEST.sha256 @@ -5,9 +5,9 @@ b2e36dfcfc6eb31570c9340640bcd73abc62f57c80b4794abf36e3d3e89ab34f .github/depend 9e90d43615b02a265b08692ef7a1c00a37477a5c6b1e8233a8fc7bedefaedea6 .github/pull_request_template.md 3c35b31bc2b80d101a3a549da68fec55587b6a6428ce6b32112670276490ce23 .github/workflows/ci.yml e544abe8ffd83c81c7b002cbd2e552f9d56f226ea20e1e0722c5d1bdec914fe0 .gitignore -32800f8e4b24c8bc96ad6ddb80ddb2c523d20719945a3042d81e09a74d3ce301 ADOPTING.md +3ab922d5b7962f571f0680224a52b55a84c0b0b9a6d1a5dfd775697172338582 ADOPTING.md 1179c999034f4ec1c1d44c1946bd2955c4625905e80767abe760c8c3ab01c493 CLAUDE.md -881dc7401aaaa3aa148be143bd3e191fc7f883466027cbc77e47ed4cc8f79b6a CONTRIBUTING.md +074908ecfc14027823851f9cea118a985c85dd947c17870a44f9c903cd28a348 CONTRIBUTING.md 664196054cd98585105be457afa09c788a482416ccb48a87bb269b2156e49ae6 DOCTRINE.md 9ba9550ad48438d0836ddab3da480b3b69ffa0aac7b7878b5a0039e7ab429411 LICENSE a38775f2d68b40577253ee48061ba67af3c75b7620dc506b34b40ce2a3b660ee LICENSE-MAP.md @@ -16,21 +16,21 @@ c274f80372d90c012937370f0e1f15087d22e308ef98b27cea5dc0d2d088366c LICENSES/Apach a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499 LICENSES/CC0-1.0.txt 59746d6285ffa44bfc7ecada352aa5d6a20dc8eab418a60ce091cc739012c135 LICENSES/MIT-0.txt 35e6d37b7c5fa0c1fc872315cbd362cd24bfa41e1b7dc3019fbcd31e99350f51 NAMING.md -a4322cd5b31594139c3f5bde2f7442aad89812ba56cdaa32818f976217149544 PROJECTION-PROVENANCE.md -925d0d79412b04c9a4f80d4adef2f6dd21d76b4492d37f3d1fcf40b20a70c235 PUBLICATION.md -d76060c70c361a64b52e9df88e400789c24fbc4fecd2b731dfe8a709a34e5e5e README.md +3bc783a329e09fb149c0c9f6e8da9187d8bf9a8a753350d54c78900431c7d7d7 PROJECTION-PROVENANCE.md +5dec4f02eeaef72ea93b66dd548520ef5af6c7d246b261a5c82804da94f71b63 PUBLICATION.md +bb9083c43def4a803c3f01e296ccbdb0402068ec39145e3ddd892ac3e922eea3 README.md 284a0862f3be77e8d867aa4d3ef92ed1a64ad4315d6d9074f6bb64771b6d1dd0 REUSE.toml ab75b39490b4db4e203f5b23b480a1c998d87cf07d760cb787cb260778b21d0a SECURITY.md 6a51c1211cc675599634d144ca24a705ec1696f84640a6464b14efb1d6c3a629 SELF-HOSTING.md -d9eb0445ef4da08bae1e3a7e81a03e92f2488eee1c1e68254f5c5fc640a99ff8 START-HERE.md +d4fc267f4ebacad09ef6c357aa339c0fb22e1a9b75392fcf80e3ec3ebff0caf7 START-HERE.md 75c7ae0f569148f489570d63df916a70b6ccf24b77db2076cdee29663f747428 adapters/claude-code/README.md aeb7f81d139e7ffa6de9a1782444b99eb6d549ac1c3a8b9c0f8bcb9c6addfa6d adapters/claude-code/SECURITY.md dd29af2a39d25e0270ad9acc23ee912f81e39c674e1179759f4a3010c6a0c1a0 adapters/claude-code/wo_capability_wall.py -ecc969827cb19e9d8cba3e7b369ff2b81d6407544f86a12a139b5c482624bbb7 checks/check_coordinator_release.py -b872c46f8a8ea2fc317b4c4fd0af00c9145012b11256c923987d878d79c726c2 checks/check_distribution.py +edfc5e7622a7a48c826e8bad5192d072ad3f0e3f29fa897f3e607cb04191cb9d checks/check_coordinator_release.py +20df8e937b6efeb7930894b7d4eba42761283b6d0166e78bcabaa2ab6dc75a7c checks/check_distribution.py 60fe377dac32b8d1697f859371ef40d26ed4e695fdceeda6d29ec0318d539504 checks/check_identity.py 30986c40ff7c9b29e2fba39ec04c18af3c1c351490410bd532a8391bcb92ed11 checks/check_licenses.py -e843892f24360174620fa02f3adf142a5eb5e2aac4bbfea786648f8b49375192 checks/check_name_clearance.py +6a5162c40e9df9bf198d391a937603907a019e04029298c3567f5e1df647c73b checks/check_name_clearance.py cf7e0c4523ba9335b78c1fe51dd744e5c4806c3f431705df3553f9b37591c2f8 checks/check_public_projection.py bb54d108d1dff56291169624d26eac4f44db00ebf98e38bf7e6d56cd9b8d9647 checks/check_work_order_dispatch.py b49e8c6fb8ef321e5a8f8b2012a77ab3870df2d4826ecb8fe0b33345d57d060a decisions/DR-001.md @@ -42,15 +42,15 @@ d9f01820bd45d8dad46e7fd307e0b6986a41d6cd2071da353530421aeea46f70 decisions/LICE fa88788242d920999b6e6737ea60b03dfe3f9ba2e0d90386b6bbcfeb6acd0509 docs/agents/domain.md 98305d69cb8ff9ebdd44d815248c4619aecc09295891c043e1423d1189888a49 docs/agents/issue-tracker.md 2177e1dbec58d14cb20e3b15fcb2cfb3ba6671025ba86a72e603c59d5cc09a06 docs/agents/triage-labels.md -3c7c89cbe276c65fddf565a743042a5c1a58f7dfe0208b3957260177eb327a27 docs/architect-interview.md +09038d48dfb92a83cc18cdd966dca5ebce8c870f5a74dcb7d47e9a08adefafe4 docs/architect-interview.md 5200a1511a781cfe49baf10177880cfc6e4e18eb8f9a55ca9e17ab4f9b35a312 docs/assets/writwall-og.png 55816569390363947ecc7735d7de13a3f59b8dae51f92383967e130b3a56d2a6 docs/assets/writwall-og.svg 0a5259d80265765aee16a421546e44458a1aabeecfa8c7f7dea8aead6a79655b docs/assets/writwall-readme-banner-0a5259d8.png ad0fb4f671b8da9e3ab9720af7b39ac9c93201e6131c1df996e090a2bb2acc8a docs/assets/writwall-readme-banner.svg 7cfd0ae28d07cdfbb367adc4f7e606a1538ebf61ff140a32f8e793028110cedf docs/bootstrap-charter-addendum.md -fdab8ed3cbb4f0ac24f9d39565baddea6b83984f698c2c48166b05590aff21ef docs/day-zero-coordinator.md +b961435c3e50093e590cdf151de8d4831d35dc7397c665c7fd9e5334d994d4ad docs/day-zero-coordinator.md e1214e3e6018642809339249bb091a6fd754847b4f77c4bc7a39c5c87e6769cc docs/identity-migration.md -4d54cd53db8c165b40af7eb11b97a7f4faf5f23479c0efc8238e5d463b159008 docs/name-clearance.md +b664a305cea2ea7de364df3aa05b9644c334c6ded7e4e0771ae11a512205d4d8 docs/name-clearance.md 1eef400dd2e12b109ceb9b30c107dbd7f25d3c64182346dd0f370bf9ccaa7088 docs/privacy-screen.md 55716ad256ad76dd355d10ab872ffcd29b03fbf9651763b21351e813ef89d0c5 examples/README.md f5c83009bcb248eccc8e861b5fc52b208a193c0b02293c72c0f4e96fcf3072de examples/name-clearance-incident-2026-08.md @@ -62,10 +62,10 @@ d2c5a8ca21edf842dfd17a83862024afa0a92349abf693a60e55ce454c8d78fa examples/name- 30cdb11fbeb2fd9bbf4048255331ccbbdd6e516fae5adfa5317af00ce53607c9 governance/ADOPTION-MAPPING.md 08b235351ab7799715b1e2df4fa3dd9fa88bb85084c8aade4d01039bf255b489 governance/LOG-denials-probes.md 5dba70f7d7263ecd10ecb2c18867b6c2c5870a3e001c25c7797d3d61feae680a governance/LOG-denials.jsonl -0720409460b68d93ae72010abab3162d1cc09cd275ddae6fa309ef7fe7b6eef3 governance/LOG.md -020bfe95a6b70da6a14dcc63087f1d90f1625fd6c08454ff8951044e04a2b0be governance/PLAN.md +ffa9029e76fbe660f9c4eced42b11960bfd96ef0451f7d18e874c349de264aba governance/LOG.md +ed0bdab770d734e83debbecc4d740f95018b39a290946d1941e2bbec09f57ee0 governance/PLAN.md dd445eb2994e0d9615bc61fe2ae157a6b14ba7b190c65b705d380b31c49fdfe0 governance/ROUTING.md -acea5eaf4e6dbee69d8e425c2848fd7f324fd67ea3a57f29a953efab46e28ce6 governance/STATE.md +646887c40607a97b418ec8f09be1c94c51f7eb053bca08908441007a397d31c7 governance/STATE.md e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 governance/archive/.gitkeep e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 governance/briefs/.gitkeep e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 governance/decisions/.gitkeep @@ -82,24 +82,24 @@ b567ce0c0867464328e81774d888f6491fa66b68ac73be01f993e5c4c66d3ed8 governance/tem d355e46f978f17de8824af805e05124e0f20b1c072523b518422044f88c6f079 governance/templates/D-adoption-record.md 2b586efadab716a59fcafb74312a45a05401a4787fee6ae18cb5c9dd14ef3a09 governance/templates/E-adoption-mapping.md e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 governance/work-orders/.gitkeep -51963137bf9d19220e4164072e7b3dee7d1312b714494b8728466e22ad8e63c9 identity/legacy-references.json +9e6cb17ec4a468bdd9e0f60333d9ea77b83579e4f3c7926e87d2ab9b34cc077f identity/legacy-references.json 345b7e962731c085a95aea66a344eae000b27c9bde13b0d790c76b73273dbe7a init.sh 5c90584642f405534b2071f27396ff293ab01632e4dbb8ccb6b8ec043dca4cc9 migration-guides/0.1-to-0.6.md ba4eff258ca5b9a45f3f9f1cbf646ba5bc5521fae812adacac65cd2e78698c9d migration-guides/0.6-to-0.7.md 7be9ff49c33830f929584e9e6756f06be8634b1c79ff7210bf5184b51bfc0769 migration-guides/0.7-to-0.8.md fafcbf659c40d7260dddaa8a64b6b59c3b7de484f77d879a90323bb4b3e1a4a7 projection/public-files.txt -1d9014a4540d81ecfd3b2bbe24577c5bc98999b53e3ca8158dd8600744772449 pyproject.toml +90a4b90139c1a23bbd98a2fb8b150a82cef0c17973d4c445c1206a26eeb4f8b6 pyproject.toml 455ca1ab3c9e7e78afbb9946e13b94497ba24003ab6f411ea96cce26d4ecc39e scripts/build_distribution.py -bdbe47e8ea246ceafb874229d5582a9cc18db7c66250c9873d50a787bd5d9fe2 scripts/build_public_projection.py +08fa88f3a1de7a26c14c383ea3f18e86838499d9eacb7cf57819fb27c2f20c30 scripts/build_public_projection.py 3cf88f936599e0e84bc2368bc0503a39b9f96e47b473e09b26569e5c3c9edbd9 scripts/collect_name_clearance.py c372f7f1736eb77bedaca43696ea0b060333733f912053479b363442022c4b24 scripts/privacy_screen.py -c367c7f4397c926701dda0a4118a2e1286787157d7de1d8f51fa207ec50021c4 scripts/start_writwall.py +61f8f3302322b704a090c6e4b5110c8189687705be244adcdd7d997e2bc0a9a9 scripts/start_writwall.py 374f4e8a80b7b9e162b9360a3907b6ffe12ce94ba0ed827058c7b3c9c0658b2c skills/writwall-adopt/LICENSE-MAP.md 02e75310dca3d528b4c5cd2ee9d8a57a89390365037c941010a6b2c1c9eb918e skills/writwall-adopt/SKILL.md 75c7ae0f569148f489570d63df916a70b6ccf24b77db2076cdee29663f747428 skills/writwall-adopt/assets/adapters/claude-code/README.md dd29af2a39d25e0270ad9acc23ee912f81e39c674e1179759f4a3010c6a0c1a0 skills/writwall-adopt/assets/adapters/claude-code/wo_capability_wall.py 7cfd0ae28d07cdfbb367adc4f7e606a1538ebf61ff140a32f8e793028110cedf skills/writwall-adopt/assets/bootstrap-charter-addendum.md -e843892f24360174620fa02f3adf142a5eb5e2aac4bbfea786648f8b49375192 skills/writwall-adopt/assets/checks/check_name_clearance.py +6a5162c40e9df9bf198d391a937603907a019e04029298c3567f5e1df647c73b skills/writwall-adopt/assets/checks/check_name_clearance.py bb54d108d1dff56291169624d26eac4f44db00ebf98e38bf7e6d56cd9b8d9647 skills/writwall-adopt/assets/checks/check_work_order_dispatch.py 3cf88f936599e0e84bc2368bc0503a39b9f96e47b473e09b26569e5c3c9edbd9 skills/writwall-adopt/assets/scripts/collect_name_clearance.py b567ce0c0867464328e81774d888f6491fa66b68ac73be01f993e5c4c66d3ed8 skills/writwall-adopt/assets/templates/A-charter.md @@ -111,7 +111,7 @@ d355e46f978f17de8824af805e05124e0f20b1c072523b518422044f88c6f079 skills/writwal 5c90584642f405534b2071f27396ff293ab01632e4dbb8ccb6b8ec043dca4cc9 skills/writwall-adopt/references/migration-guides/0.1-to-0.6.md ba4eff258ca5b9a45f3f9f1cbf646ba5bc5521fae812adacac65cd2e78698c9d skills/writwall-adopt/references/migration-guides/0.6-to-0.7.md 7be9ff49c33830f929584e9e6756f06be8634b1c79ff7210bf5184b51bfc0769 skills/writwall-adopt/references/migration-guides/0.7-to-0.8.md -4d54cd53db8c165b40af7eb11b97a7f4faf5f23479c0efc8238e5d463b159008 skills/writwall-adopt/references/name-clearance.md +b664a305cea2ea7de364df3aa05b9644c334c6ded7e4e0771ae11a512205d4d8 skills/writwall-adopt/references/name-clearance.md b567ce0c0867464328e81774d888f6491fa66b68ac73be01f993e5c4c66d3ed8 templates/A-charter.md 0a0c3c8c2317733b7ca951799de74a045c35d9a944a6448985f023feb49dcb4e templates/B-work-order.md 5bfaa890ffddd423644428606753bc2e6562e3e5d67c1dc522172710708a4bf0 templates/C-owner-brief.md @@ -121,15 +121,15 @@ d355e46f978f17de8824af805e05124e0f20b1c072523b518422044f88c6f079 templates/D-ad 9924816cbbeade6f88f79d3e06fe04d143d801783888210ac2325925d69e4bdb tests/test_check_distribution.py b046f2eea794070194294a33f2914e627eed384e63fccffc2ac46693db2a968c tests/test_check_licenses.py 9a106ff5182b4a15713575de42e90b0dc5cdeebcb17ba08d97522a4c9aa6b2fa tests/test_check_work_order_dispatch.py -6fc58f3e82282c51b0604c9bd24808f6f53c9d543fb4d4ccf18ec1fc5a638676 tests/test_coordinator_release.py -6483f84819c08284b989252f4a40a42e8eca64fec5fda303cec62350c19328d0 tests/test_distribution.py +a1f3472131beec3cd44fdd389d836a6a86b4269b94226b159e223b602bb8b32f tests/test_coordinator_release.py +13478c88a9d9951f4a90ef15fa389fc0bf19894f917cbada62fc366cbe70a635 tests/test_distribution.py e150a2f988a4b0beac5f70644f55f5e185a8aa575e988a19642bafabc0f07775 tests/test_identity_migration.py 11cd8090dbc53e8aa6a2f14cb181c8a11696335da8f40700eae5116798e49ba5 tests/test_init_sh.py -96c255d84e37b8884cde769897e763776b81027080c2308c33dc5e4b8df4a4bf tests/test_name_clearance.py +a2df93f79791a884d9c6c2db308591a3b18e95ee34702ad5330ccc7a4b683fa4 tests/test_name_clearance.py 677d5b532450ace267be9c834269c081368697cd83defa5531ce673f6d0ca252 tests/test_privacy_screen.py -ee771c239c0fc072675f88617dec7e330c1e19be90a8b22691d7049dbb5a4544 tests/test_public_projection.py -2bc96f1dfd6febc1d281494720125602f01293838614ffe9850b3d2a9fb78f6b tests/test_start_writwall.py +0d1d5cc19779e3539d46caba978fe18de7af751c8c98fe435dbf2e924860ca81 tests/test_public_projection.py +ef2ee15da47fa329f00961517d306fc8bfcba1b7514810fe5b006fdbb028e5fe tests/test_start_writwall.py 0684c04067eb95eadc9f72ab126d8662b4a5e2005c80b2dea174075a6140eebc tests/test_wo_capability_wall.py e8caf7f4421dc7f78b0d766741ec2ef4c2ac6dab6117fab6e4175b27d31e4d49 writwall_cli/__init__.py -9e0fbb2625b7a99ed80a3a708f8a0742d91bbf7e3cd2862d21a75515031083e5 writwall_cli/__main__.py -3d90b5f1e6ec3e0f46a47c04060c84936425861adf66c39a9317cb26689805dc writwall_cli/coordinator.py +aee1a6d4437f468c6c21cef95e3e45181ed40ca402a11092fed5db587865c21f writwall_cli/__main__.py +76def9691b26cf48f692ae7a921ae3a52f8ccfa302724ad12debec2bf8bf36ac writwall_cli/coordinator.py diff --git a/PROJECTION-PROVENANCE.md b/PROJECTION-PROVENANCE.md index 9400ca1..080af18 100644 --- a/PROJECTION-PROVENANCE.md +++ b/PROJECTION-PROVENANCE.md @@ -5,14 +5,15 @@ Legacy commit identifiers in projected records refer to that private source and are intentionally not resolvable from fresh public history. No private remote URL is recorded here. -- Source commit: `be8cfa99b53996d140433610b12886cf62c444f0` -- Source commit time: `2026-09-03T17:19:47-05:00` +- Source commit: `c8ad85737b7cd60360cee7561f28d666a9b73080` +- Source commit time: `2026-09-04T19:05:51-05:00` - Projection allowlist SHA-256: `fafcbf659c40d7260dddaa8a64b6b59c3b7de484f77d879a90323bb4b3e1a4a7` ## Legacy identifier inventory - `0000000000000000000000000000000000000000` — `checks/check_coordinator_release.py` - `11d5960a326750d5838078e36cf38b85af677262` — `tests/test_distribution.py` +- `2d7b23d33bf945b3e01460a1aaba773b17cf27e2` — `governance/STATE.md` - `3d3c42e5aac5ba805825da76410c181273ba90b1` — `.github/workflows/ci.yml`, `tests/test_distribution.py` - `5fda3b95a4ea91299a34e894583c3862153e4b97` — `.github/workflows/ci.yml`, `tests/test_distribution.py` - `6e165e585f907baf83a787ba5cc71270a5a4652e` — `checks/check_distribution.py`, `governance/decisions/DR-001.md`, `tests/test_distribution.py` @@ -23,5 +24,5 @@ No private remote URL is recorded here. - `a905c87987f31094121c11a3b8163f97ef1abcf4` — `SELF-HOSTING.md`, `governance/STATE.md`, `governance/decisions/DR-001.md` - `ba3c0754e5019f1fa93779d110843562cfa07307` — `governance/STATE.md` - `d790a2b8d500a1c3a5e10af9f0a78d1c3c3f4e3a` — `governance/STATE.md` -- `e0cef360843dff38d6a02dd48be8f61b2d2d300e` — `governance/PLAN.md`, `governance/STATE.md` +- `e0cef360843dff38d6a02dd48be8f61b2d2d300e` — `governance/PLAN.md` - `e270fd3235d170a28a21fd198b88857740b74acd` — `governance/STATE.md` diff --git a/PUBLICATION.md b/PUBLICATION.md index 2142d92..ec00b76 100644 --- a/PUBLICATION.md +++ b/PUBLICATION.md @@ -13,17 +13,33 @@ point. Before creating a release tag, run this gate against the final external candidate on native Windows and native Ubuntu, naming the exact intended tag: ```text -python checks/check_coordinator_release.py --expected-tag v0.10.0 +python checks/check_coordinator_release.py --expected-tag v0.11.0 ``` +For a future GitHub release that is required to be immutable, save the +platform's release JSON separately and supply it as bounded offline evidence: + +```text +python checks/check_coordinator_release.py --expected-tag vX.Y.Z --published-release-json +``` + +The checker performs no network request. When metadata is supplied, it rejects +malformed or duplicate fields, a mismatched `tag_name`, and anything other +than the JSON literal `immutable: true`. Repository release immutability was +enabled prospectively after v0.10.0. That release is pinned to its published +commit but GitHub reports it as `immutable: false`; do not describe it as +platform-immutable. The setting applies to future releases, whose actual +published metadata must still pass this gate. + The command fails before building unless the canonical intended tag matches the candidate's package version. It is network-free. It copies the candidate into temporary build space, builds a wheel using the already-provisioned backend declared in `pyproject.toml`, creates a fresh virtual environment without assuming the host can bootstrap `pip` inside it, installs through the already-provisioned host `pip`, checks -the installed version and help interface, runs the coordinator against a -disposable external project, verifies the complete create-only handoff, and +the installed version and help interface, runs `start` and the zero-write +`inspect` route against disposable external projects, verifies the complete +create-only handoff, and then verifies that the input candidate tree did not change. A pass is release readiness evidence; it does not create a tag, GitHub release, or publication. @@ -111,6 +127,17 @@ independently built candidates byte-for-byte. It is derived as follows: 7. Take the SHA-256 of those encoded bytes; that digest is the complete-tree ledger. +Compute it without changing the candidate: + +```sh +python -B scripts/build_public_projection.py --complete-tree-ledger +``` + +Run this against each bare candidate before adding Git metadata or build +outputs. Every regular file, including the manifest itself, participates; +linked entries and newline-bearing paths are rejected. Full-line ordering +differs from ordering by path. Do not substitute a path-sorted tree hash. + The complete-tree ledger digests two independently built candidates for equality; it is not shipped inside the candidate and is distinct from `PROJECTION-MANIFEST.sha256`, which enumerates only the allowlisted diff --git a/README.md b/README.md index 235c3ab..0cac266 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@

Start here · Five-minute start · + Use at work · Adopt · How it differs · Pilot evidence · @@ -93,73 +94,102 @@ actually blocks the current session before real work begins. ## Try it in five minutes -Run the lifecycle-aware coordinator from this clean source distribution. It -inspects the target before intake, assigns the correct fresh role, and changes -target bytes only for clean/new bootstrap. It never installs the wall or claims -adoption. This routing happens without installing the wall or claiming adoption: - -Release `v0.10.0` packages the conversation-first Architect handoff, -canonical-root enforcement, and corrected lifecycle classification described -below. +Release `v0.11.0` has one canonical lifecycle and two ordinary entry commands: ```text -python -m pip install "https://github.com/HLLMR/writwall/archive/refs/tags/v0.10.0.zip" +python -m pip install "https://github.com/HLLMR/writwall/archive/refs/tags/v0.11.0.zip" -# Installed command +# New idea or clean project: create a temporary local handoff writwall start --project-root /path/to/your-project -# Source-tree fallback on Windows -# Windows -py -3 scripts/start_writwall.py --project-root C:\path\to\your-project - -# Source-tree fallback on macOS or Linux -python3 scripts/start_writwall.py --project-root /path/to/your-project +# Existing project or no-write first look: print an Architect handoff only +writwall inspect --project-root /path/to/your-project --role architect ``` +From an unpacked source distribution, the fallback is +`py -3 scripts/start_writwall.py --project-root C:\path\to\your-project` on Windows +or `python3 scripts/start_writwall.py --project-root /path/to/your-project` on +macOS/Linux. + +`start` observes the target and, only when it is clean/new, creates a +create-only `.writwall-bootstrap/` containing the local adoption bundle and +role handoffs. `inspect` is the safer first command for an employer repository +or any existing project: it prints a bounded, read-only handoff and changes no +project, temporary, profile, privacy-screen, cache, or bytecode state. + +Neither command launches an AI provider. Open the employer-approved agent and +interface you want to use, then paste the exact prompt the command emits. A +fresh Architect begins read-only, summarizes bounded local evidence instead of +making you re-explain visible project history, listens to the pitch, challenges +assumptions, and returns a project sketch. Nothing is adopted or implemented +until the human Owner explicitly promotes that sketch. + | Observed state | Fresh role | Prior session stops | Target bytes | |---|---|---|---| -| Clean/new (ordinary invocation) | Architect (conversation-first) | Launcher returns; the Architect stops before adoption mechanics until the Owner makes an explicit promotion decision | Create-only bootstrap may be added | -| Clean/new (`--structured-intake`) | Adoption coordinator | Launcher returns; onboarding later stops at closeout | Create-only bootstrap may be added | -| Partial/recovery | Recovery coordinator | Incomplete or locked session | Unchanged | -| Adopted/retired lockout | Fresh General (the continuity role previously labeled Project-Architect) | Onboarding or prior work session | Unchanged | -| Active work order | Bounded Operator/Implementer | Prior coordinator/Implementer context | Unchanged | -| Malformed/contradictory | No role; fail-closed diagnostic | Invoking session | Unchanged | - -For clean/new state, the ordinary command above is conversation-first: it -asks nothing on the command line and never blocks on a questionnaire. It -creates `.writwall-bootstrap/` and initializes a durable project-specific -privacy screen in your operating system's local user state outside the -repository, then hands off to a fresh Architect. For an existing project, -the handoff carries a bounded, local, non-secret inventory (Git branch, -cleanliness, a few recent commit subjects, and top-level project-relative -names), and the Architect begins read-only, summarizes what it found from -that evidence, and asks whether to explore that work or start elsewhere. For -a genuinely empty target, it opens with exactly: "Tell me what you are -thinking." Read its `HANDOFF.md`, open the named agent in the named -location, and paste the supplied prompt. Later states print their fresh-role -handoff directly without intake, privacy initialization, or target writes. -Do not enter passwords, API tokens, private keys, mailbox contents, DNS -values, or other secrets; intake is stored as local plain text. - -The former full structured questionnaire (Owner-time capture, secret -confirmation, project name, purpose, agent, location, environment, external -operators, privacy identifiers, one question at a time) remains available -verbatim behind the explicit `--structured-intake` flag. Deterministic, -non-interactive automation keeps using `--non-interactive` with its existing -required flags, exactly as before. See the -[coordinator reference](docs/day-zero-coordinator.md), or use -[START-HERE.md](START-HERE.md) for the manual and recovery routes. -The coordinator does not register, activate, or birth-test the wall. -For an unnamed idea and the full qualification/role-packet contract, see the -[architect interview](docs/architect-interview.md). -The privacy screen automatically carries machine-path sentinels and may also -hold private names, codenames, client identifiers, or domains. It never belongs -in the repository and must never contain credentials. See the -[privacy-screen guide](docs/privacy-screen.md). - -The lower-level `init.sh` scaffolder remains available after you understand -the adoption sequence. It creates directories and template copies but does not -inventory authority, register or birth-test a hook, ratify adoption, or commit. +| Clean/new | Fresh Architect | Launcher; Architect stops before promotion | Create-only bootstrap may be added by `start`; never by `inspect` | +| Partial bootstrap | Recovery coordinator | Incomplete or locked session | Unchanged | +| Adopted or retired lockout | Fresh General (formerly Project-Architect) | Prior onboarding or work session | Unchanged | +| Active work order | Bounded Operator/Implementer | Prior coordinator or Implementer | Unchanged | +| Malformed or contradictory | No role; fail-closed diagnostic | Invoking session | Unchanged | + +After promotion, the local adoption bundle guides materialization, wall +installation where supported, and the adoption record. Adoption ends by +handing the repository to a fresh General. The General maintains continuity, +prepares bounded work orders or external-Operator packets, and routes new +design questions back to a fresh Architect. Operators execute; a fresh +Reviewer checks; the Owner accepts or rejects the evidence. + +For a source-tree fallback, structured automation, lifecycle-state table, and +recovery prompts, see [START-HERE.md](START-HERE.md). The complete mechanics +are in [ADOPTING.md](ADOPTING.md) and the +[coordinator reference](docs/day-zero-coordinator.md). + +### Using Writwall at work + +Use only employer-approved AI accounts, interfaces, source-code locations, and +data-handling practices. Writwall records authority inside a project; it does +not override company security, confidentiality, retention, procurement, or +acceptable-use policy. + +For an existing workplace repository, start with the no-write command: + +```text +writwall inspect --project-root /path/to/work-repository --role architect +``` + +Then open a fresh approved agent session with access to that repository and +paste the emitted handoff. If you must route it manually, use: + +```text +Act as the Writwall Architect for this repository. Start read-only. Use the +handoff below, summarize what the repository already shows, listen to my +project pitch, challenge assumptions, and do not materialize adoption or +dispatch work until I explicitly approve the project sketch. +``` + +After adoption closes, open a new session and use the coordinator's emitted +General handoff. Its short form is: + +```text +Act as a fresh Writwall General for this adopted repository. Begin read-only, +verify the governed lifecycle from repository bytes, recommend the next +bounded decision or work order, and do not activate or implement it until I +approve it. +``` + +Keep production, infrastructure, DNS, mail, and other account-bearing work in +separately bounded Operator packets and, where practical, separate project +roots and sessions. Never place credentials or secret values in Writwall +intake, handoffs, work orders, prompts, or privacy-screen entries. + +The document workflow is provider-neutral. The shipped mechanical capability +wall is currently for Claude Code only and applies only after project-local +installation and a successful birth test in the actual executing session. +Codex, Cursor, Copilot, and other agents remain instruction-bounded unless an +equivalent adapter is installed and birth-tested. + +The coordinator does not register, activate, or birth-test the wall. It performs +this routing without installing the wall or claiming adoption. If the project does not yet have a settled public identity, stop before naming packages, repositories, domains, or launch assets. Run the evidence-producing @@ -214,31 +244,18 @@ The public candidate intentionally ships the canonical adapter but no active host-specific hook registration. See the adapter README and [ADOPTING.md](ADOPTING.md) for installation, preflight, and birth-test requirements. -## On-ramps - -Three ways in, from least to most tooling. All of them end at the same place: -an adoption record (DR-001) and a birth-tested wall. See `ADOPTING.md` for the -full sequence and `START-HERE.md` for the human operating model and starter -prompts. - -**1. Chat companion, no tooling.** Open any capable chat model and paste the -adoption prompt from `ADOPTING.md` section 2 together with `DOCTRINE.md`. The -model interviews you and drafts the adoption mapping and DR-001; you place the -artifacts in your repository. +## One lifecycle, several execution methods -**2. Coding agent with the skill.** Copy `skills/writwall-adopt/` into your -coding agent's skills directory and invoke it in the target repository. Its -bootstrap mode inventories existing authority, scaffolds `governance/`, -installs the Claude Code adapter and validator where applicable, runs the birth -test, and returns proposals for the Owner. Its separately authorized recorder -mode records already-ratified decisions and may make one local adoption commit. -Neither mode ratifies intent, pushes, publishes, tags, or changes visibility. -Keep the temporary skill bundle through the final recorder action that needs -it; delete it only before the adoption commit. +Writwall has one canonical lifecycle: Owner → fresh Architect → explicit +promotion → adoption materialization → fresh General → bounded Operator → +fresh Reviewer → Owner disposition. The coordinator is the normal front door. -**3. Scaffolder.** The command above creates directories and copies templates -and the pre-dispatch validator. It skips existing files and refuses to merge a -charter or hook registration. It is deliberately not a complete adoption flow. +Prompt-only Architect conversation, the bundled `writwall-adopt` skill, +`--structured-intake`, and the low-level `init.sh` scaffolder are fallback or +specialized execution methods inside that lifecycle. They are not separate +governance routes and do not change who may ratify, activate, implement, +review, or accept. See [ADOPTING.md](ADOPTING.md) for the exact adoption +sequence and [START-HERE.md](START-HERE.md) for copy/paste prompts. The pre-dispatch validator is deterministic, read-only, and standard-library only. `--lockout` checks the between-order state, `--work-order ` checks a @@ -256,7 +273,7 @@ Three things carry three different names here: - **`DOCTRINE.md` is the methodology.** It is clause-numbered, change-controlled, and written for humans. - **The public distribution and reference implementation** ships the doctrine, - templates, adapters, adoption routes, checks, and selected public-safe + templates, adapters, adoption tooling, checks, and selected public-safe evidence. It does not govern its own public checkout. - **A project-local instantiation is a governance system.** It belongs to the adopting project from the moment it is created. @@ -320,7 +337,7 @@ writwall/ │ adapter makes physical and what it does not. ├── migration-guides/ One companion per revision transition, e.g. │ 0.1-to-0.6.md. Followed only when a project moves. -├── skills/ Entry points for chat and coding agents (see On-ramps). +├── skills/ Agent-assisted mechanics within the canonical lifecycle. │ └── writwall-adopt/ Self-contained bootstrap bundle: carries its own │ doctrine, guides, adapter, and templates. ├── checks/ Deterministic distribution checks. Nonzero exit on @@ -349,7 +366,7 @@ build output. Neither is part of a positive-allowlist public candidate. Unpacking it on top of a repository would drop Writwall's own charter, doctrine, work history, and eventually its governance directory into your project root, where your agents would read another project's records as if they were yours. That is precisely the stale-but-discoverable failure the doctrine exists to eliminate (Doctrine 5.3.4). -What you actually do is instantiate **only the applicable project-side artifacts**, through one of the three documented adoption routes in `ADOPTING.md`. Those artifacts are: +What you actually do is instantiate **only the applicable project-side artifacts** through the one canonical lifecycle in `ADOPTING.md`. Those artifacts are: ``` / @@ -361,7 +378,7 @@ What you actually do is instantiate **only the applicable project-side artifacts Everything else in the archive is either the methodology itself, which you read, or Writwall's own working records, which you may read as an example and never copy. -**Never copied into an adopting project:** Writwall's root `CLAUDE.md`, its `governance/` directory, its decisions, plan, state, routing map, work orders, reports, briefs, history, and its authority. Doctrine 5.1.5 states this as a rule; the adoption routes enforce it by only ever copying templates, the adapter, and the pre-dispatch validator, and `checks/check_distribution.py` fails if a Writwall governance record ever appears inside the adoption skill's bundle. +**Never copied into an adopting project:** Writwall's root `CLAUDE.md`, its `governance/` directory, its decisions, plan, state, routing map, work orders, reports, briefs, history, and its authority. Doctrine 5.1.5 states this as a rule; the adoption tooling enforces it by only ever copying templates, the adapter, and the pre-dispatch validator, and `checks/check_distribution.py` fails if a Writwall governance record ever appears inside the adoption skill's bundle. ## What is instantiated into a project diff --git a/START-HERE.md b/START-HERE.md index c8ddce3..256b0b2 100644 --- a/START-HERE.md +++ b/START-HERE.md @@ -10,6 +10,86 @@ self-contained `writwall-adopt` bundle local before the wall is registered. The wall may intentionally deny network access once the project enters lockout; an agent cannot fetch instructions it does not already have. +## The one canonical lifecycle + +Every project follows one canonical lifecycle: + +1. **Owner → fresh Architect.** The human brings an idea or an existing + repository. The Architect begins read-only, listens, inspects bounded local + evidence, challenges the pitch, and returns a project sketch. The + conversation has no fixed length and may end without adoption. +2. **Owner promotion → adoption materialization.** Only after the Owner accepts + the sketch do the local adoption bundle and recorder mechanics create the + ratified charter, Plan, Routing, State, adoption record, and tested provider + boundary. +3. **Fresh General → bounded Operators.** Adoption ends the onboarding context. + A new General maintains continuity and prepares work orders or external + Operator packets. Operators execute only active bounded work. +4. **Fresh Reviewer → Owner disposition.** A separate Reviewer checks the + result and record. The Owner accepts, rejects, or ratifies a deviation. + +Prompt-only use, the bundled skill, `--structured-intake`, and `init.sh` are +fallback or specialist execution methods inside this lifecycle. They are not +different governance routes. + +## Which command do I run? + +Point `--project-root` at your project, not the downloaded Writwall distribution. +When you inspect the distribution itself, the coordinator reports +`public_distribution` and asks you to select a target project. Retained +self-hosting records describe the source and do not adopt your checkout. +To discuss contributing to Writwall itself, use `inspect --role architect` +with that checkout as the project root and follow `CONTRIBUTING.md`. + +```text +# New idea or clean project; may create .writwall-bootstrap/ +writwall start --project-root /path/to/your-project + +# Existing or workplace project; no project or local-state writes +writwall inspect --project-root /path/to/your-project --role architect + +# Already adopted project; derive the next safe role from repository bytes +writwall inspect --project-root /path/to/your-project --role auto +``` + +The command does not launch an agent. It tells you which fresh role to open and +prints the exact prompt to paste. Use `inspect --role architect` when you want +to discuss an existing repository before allowing Writwall to create anything. +Use `start` for a genuinely new project or after you have decided that a +create-only bootstrap is acceptable. + +### Using Writwall at work + +Use an employer-approved Python environment, AI account, agent interface, +repository location, and data policy. For an existing work repository, run the +no-write `inspect --role architect` command above, then open a fresh approved +agent session with access to the repository and paste the emitted handoff. + +If you must begin manually, paste: + +```text +Act as the Writwall Architect for this repository. Start read-only. Use the +handoff below, summarize what the repository already shows, listen to my +project pitch, challenge assumptions, and do not materialize adoption or +dispatch work until I explicitly approve the project sketch. +``` + +After adoption, use the emitted handoff or this short form in a new session: + +```text +Act as a fresh Writwall General for this adopted repository. Begin read-only, +verify the governed lifecycle from repository bytes, recommend the next +bounded decision or work order, and do not activate or implement it until I +approve it. +``` + +Writwall does not override company policy. Do not put secrets, customer data, +private keys, production record values, or credentials into prompts or Writwall +records. Keep infrastructure and account-bearing actions in separately bounded +Operator packets. Only the supplied Claude Code adapter currently provides a +shipped mechanical wall; other agents are instruction-bounded unless an +equivalent adapter is installed and birth-tested. + ## Who does what | Function | Who or what performs it | May share an agent? | @@ -59,12 +139,13 @@ names, repository slugs, domains, logos, or launch copy. The coordinator may collect evidence, but the Owner chooses the identity; unavailable sources are not clear results. -1. Release `v0.10.0` packages the conversation-first Architect handoff, - canonical-root enforcement, and corrected lifecycle classification. +1. Release `v0.11.0` packages the conversation-first Architect handoff, + canonical-root enforcement, corrected lifecycle classification, and the + repository-nonmutating `writwall inspect` entry. Install it without unpacking it over your project: ```text - python -m pip install "https://github.com/HLLMR/writwall/archive/refs/tags/v0.10.0.zip" + python -m pip install "https://github.com/HLLMR/writwall/archive/refs/tags/v0.11.0.zip" ``` Release `v0.9.0` first introduced the coordinator. Release `v0.9.1` corrected @@ -73,6 +154,8 @@ not clear results. routing and the terminal Architect handoff. Release `v0.10.0` adds conversation-first inception, corrected adoption-state classification, and canonical project-root enforcement. + Release `v0.11.0` adds the installed, read-only `writwall inspect` entry and + prospective immutable-release verification. If you are testing an unpublished release candidate, use its checked external candidate tree and the release gate in `PUBLICATION.md`. 2. Run one command: @@ -88,17 +171,46 @@ not clear results. python3 scripts/start_writwall.py --project-root /path/to/your-project ``` + If you only want to understand or re-enter a project without creating a + bootstrap, use the read-only interface instead: + + ```text + # Installed command + writwall inspect --project-root /path/to/your-project --role auto + + # Source-tree fallback on Windows + py -3 -B -m writwall_cli inspect --project-root C:\path\to\your-project --role architect + + # Source-tree fallback on macOS or Linux + python3 -B -m writwall_cli inspect --project-root /path/to/your-project --role architect + ``` + + `--role auto` derives the safe next role from repository lifecycle bytes. + An explicit `architect`, `general`, or `recovery` selection works only in + lifecycle states compatible with that role and grants no mutation or + lifecycle authority. `inspect` writes no bootstrap, project, temporary, + profile, privacy-screen, cache, or bytecode state. The source-tree fallback + requires running from an unpacked Writwall source tree; no executable can + run when neither Writwall nor its source is locally available. Use the + prompt-only fallback below in that case. + The same command is the entry point throughout the project lifecycle: | Observed state | Fresh role receiving the output | Session that stops | May target bytes change? | |---|---|---|---| | Clean/new (ordinary invocation) | Architect (conversation-first) | The human's current launcher returns after creating the bootstrap; the Architect stops before adoption mechanics until you make an explicit promotion decision | Yes: create-only `.writwall-bootstrap/` | - | Clean/new (`--structured-intake`) | Adoption coordinator | The human's current launcher returns after creating the bootstrap; the adoption coordinator later stops at closeout | Yes: create-only `.writwall-bootstrap/` | + | Clean/new (`--structured-intake`) | Fresh Architect (prepared intake) | The human's current launcher returns after creating the bootstrap; the Architect stops before adoption mechanics until you explicitly promote the sketch | Yes: create-only `.writwall-bootstrap/` | | Partial bootstrap or recovery | Recovery coordinator | The incomplete adoption or locked session | No | | Adopted or retired lockout | Fresh General | The onboarding coordinator or prior work session | No | | Active work order | Bounded Operator/Implementer | Any prior coordinator or Implementer context | No | | Malformed or contradictory | No role; precise stop diagnostic | The invoking session | No | + `inspect` follows the same table without the clean/new bootstrap write. + Explicit Architect inspection is supported for clean/new, partial, + adopted, and retired states; explicit General only for adopted/retired + lockout; explicit recovery only for a partial bootstrap. An active work + order remains routed only to its bounded Operator under `--role auto`. + 3. For a clean/new target, the ordinary command above is conversation-first: it asks nothing on the command line and never blocks on a questionnaire. It observes actual repository lifecycle state, creates @@ -143,14 +255,26 @@ adoption commit. Paste this first: +```text +Act as the Writwall Architect for this repository. Start read-only. Use the +local writwall-adopt bundle for reference, summarize what the repository +already shows, listen to my pitch, challenge assumptions, and return a concise +project sketch. Do not begin adoption mechanics until I explicitly promote the +sketch. If I promote it, switch only to the bundle's bootstrap procedure; I +decide and ratify, and an authorized recorder may perform the clerical steps. +Do not install or register the wall until the complete bundle and recovery +instructions are locally available. Do not begin product work until adoption +is complete and a fresh General has taken over. +``` + +Only after you explicitly promote the Architect's sketch, the manual +continuation may begin: + ```text Act as my Writwall adoption coordinator, not as an Implementer. Use the local writwall-adopt bundle and follow its bootstrap mode. I decide and ratify; you -perform every clerical step an authorized recorder may perform. Ask me one question at a time -in plain language, with your recommendation first. -Do not install or register the wall until you have confirmed that the complete -bundle and recovery instructions are locally available. Do not begin product -work or ask about WO-001 until adoption is complete. +may perform only the separately authorized recorder mechanics. Do not begin +product work or continue as the General. ``` ## If the archive was already unpacked into your project diff --git a/checks/check_coordinator_release.py b/checks/check_coordinator_release.py index 0fd9668..82f7193 100644 --- a/checks/check_coordinator_release.py +++ b/checks/check_coordinator_release.py @@ -10,6 +10,7 @@ import os import re import shutil +import stat import subprocess import sys import tempfile @@ -17,6 +18,11 @@ from pathlib import Path +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + + REQUIRED_CANDIDATE_PATHS = ( "pyproject.toml", "writwall_cli/__init__.py", @@ -43,6 +49,7 @@ "writwall-adopt/assets/checks/check_name_clearance.py", "writwall-adopt/references/name-clearance.md", ) +MAX_RELEASE_METADATA_BYTES = 1024 * 1024 class ReleaseCheckError(RuntimeError): @@ -144,12 +151,8 @@ class ReleaseCheckError(RuntimeError): def tree_digest(root: Path) -> str: - lines: list[str] = [] - for path in sorted(item for item in root.rglob("*") if item.is_file()): - relative = path.relative_to(root).as_posix() - digest = hashlib.sha256(path.read_bytes()).hexdigest() - lines.append(f"{digest} {relative}") - return hashlib.sha256(("\n".join(lines) + "\n").encode("utf-8")).hexdigest() + from scripts.build_public_projection import complete_tree_ledger + return complete_tree_ledger(root) def run(command: list[str], *, cwd: Path, environment: dict[str, str], @@ -201,6 +204,74 @@ def verify_expected_tag(candidate_version: str, expected_tag: str) -> None: ) +def verify_published_release_json(metadata_path: Path, expected_tag: str) -> None: + """Verify saved GitHub release metadata without network access.""" + def unique_object(pairs: list[tuple[str, object]]) -> dict[str, object]: + result: dict[str, object] = {} + for key, value in pairs: + if key in result: + raise ReleaseCheckError( + f"published-release metadata contains duplicate field {key!r}" + ) + result[key] = value + return result + + try: + path_info = metadata_path.lstat() + isjunction = getattr(os.path, "isjunction", None) + if ( + metadata_path.is_symlink() + or (isjunction is not None and isjunction(metadata_path)) + or not stat.S_ISREG(path_info.st_mode) + ): + raise ReleaseCheckError( + "published-release metadata must be a non-link regular file" + ) + flags = ( + os.O_RDONLY + | getattr(os, "O_BINARY", 0) + | getattr(os, "O_NOFOLLOW", 0) + | getattr(os, "O_NONBLOCK", 0) + ) + descriptor = os.open(metadata_path, flags) + with os.fdopen(descriptor, "rb") as handle: + opened_info = os.fstat(handle.fileno()) + if not stat.S_ISREG(opened_info.st_mode): + raise ReleaseCheckError( + "published-release metadata must be a non-link regular file" + ) + raw_bytes = handle.read(MAX_RELEASE_METADATA_BYTES + 1) + if len(raw_bytes) > MAX_RELEASE_METADATA_BYTES: + raise ReleaseCheckError( + "published-release metadata exceeds the 1 MiB size limit" + ) + raw = raw_bytes.decode("utf-8") + payload = json.loads(raw, object_pairs_hook=unique_object) + except ReleaseCheckError: + raise + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise ReleaseCheckError( + f"published-release metadata is malformed JSON: {exc}" + ) from exc + if not isinstance(payload, dict): + raise ReleaseCheckError("published-release metadata must be a JSON object") + tag_name = payload.get("tag_name") + if tag_name is None: + raise ReleaseCheckError("published-release metadata lacks tag_name") + if tag_name != expected_tag: + raise ReleaseCheckError( + f"published-release tag_name {tag_name!r} does not match " + f"expected tag {expected_tag!r}" + ) + if payload.get("immutable") is not True: + value = payload.get("immutable") + if value is False: + raise ReleaseCheckError("published release is not immutable") + raise ReleaseCheckError( + "published-release immutable field must be the JSON literal true" + ) + + def verify_candidate_unchanged(candidate: Path, before: str) -> None: if tree_digest(candidate) != before: raise ReleaseCheckError("candidate changed during the release check") @@ -302,6 +373,27 @@ def check_candidate(candidate: Path, expected_tag: str) -> None: ) if "Start with an idea" not in help_result.stdout: raise ReleaseCheckError("installed help omitted the coordinator promise") + root_help = run( + [str(command), "--help"], + cwd=workspace, + environment=environment, + label="installed root help", + ) + if "inspect" not in root_help.stdout: + raise ReleaseCheckError("installed help omitted the inspect command") + + if (candidate / "PROJECTION-PROVENANCE.md").is_file(): + for verb in ("inspect", "start"): + distribution = run( + [str(command), verb, "--project-root", str(candidate)], + cwd=workspace, environment=environment, + label=f"installed distribution {verb}", closed_stdin=True, + ) + if ("Observed lifecycle state: public_distribution" not in distribution.stdout + or "target project" not in distribution.stdout + or "Fresh General" in distribution.stdout): + raise ReleaseCheckError("installed distribution routing inferred adoption") + verify_candidate_unchanged(candidate, before) conversation_project = workspace / "conversation-first-project" conversation_project.mkdir() @@ -441,6 +533,34 @@ def check_candidate(candidate: Path, expected_tag: str) -> None: raise ReleaseCheckError( "installed adopted-lockout route changed target bytes" ) + inspect_result = run( + [ + str(command), "inspect", "--project-root", str(adopted), + "--role", "architect", + ], + cwd=workspace, + environment=environment, + label="installed inspect route", + ) + inspect_output = " ".join(inspect_result.stdout.split()) + required_inspect_text = ( + "Observed lifecycle state: adopted_lockout", + "Selected role: Fresh Architect", + "Begin read-only", + "grants no mutation or lifecycle authority", + ) + missing_inspect_text = [ + text for text in required_inspect_text if text not in inspect_output + ] + if missing_inspect_text: + raise ReleaseCheckError( + "installed inspect route omitted: " + + ", ".join(missing_inspect_text) + ) + if tree_digest(adopted) != adopted_before: + raise ReleaseCheckError( + "installed inspect route changed target bytes" + ) retired = workspace / "retired-project" retired_governance = retired / "governance" @@ -590,6 +710,7 @@ def check_candidate(candidate: Path, expected_tag: str) -> None: print(" complete handoff : all required packets present; no bytecode residue") print(" canonical root : installed coordinator recorded the resolved project root") print(" adopted lockout : fresh General route; zero target-byte change") + print(" read-only inspect : installed Architect re-entry; zero target-byte change") print(" retired lockout : ratified adoption plus closed history; zero target-byte change") print(" draft regression : draft adoption record never reports adopted/retired lockout") print(" unrelated regression: signed unrelated document at the exact adoption-record") @@ -611,12 +732,24 @@ def parser() -> argparse.ArgumentParser: required=True, help="canonical release tag that must match candidate package metadata", ) + value.add_argument( + "--published-release-json", + type=Path, + help=( + "optional saved GitHub release JSON; verifies matching tag_name and " + "literal immutable:true without network access" + ), + ) return value def main(argv: list[str] | None = None) -> int: arguments = parser().parse_args(argv) try: + if arguments.published_release_json is not None: + verify_published_release_json( + arguments.published_release_json, arguments.expected_tag + ) check_candidate(arguments.candidate, arguments.expected_tag) except (OSError, ReleaseCheckError, subprocess.SubprocessError) as exc: print(f"FAIL: coordinator release candidate: {exc}") diff --git a/checks/check_distribution.py b/checks/check_distribution.py index afaacd6..f6cb540 100644 --- a/checks/check_distribution.py +++ b/checks/check_distribution.py @@ -1207,7 +1207,7 @@ def check_onboarding_contract(failures: Failures) -> None: def check_name_clearance_ledgers(failures: Failures) -> None: - """Release candidates retain complete, current identity evidence.""" + """Preserve four historical decisions; all other evidence must be current.""" checker = REPO_ROOT / "checks" / "check_name_clearance.py" ledger_dir = REPO_ROOT / "examples" / "name-clearance-ledgers" if not checker.is_file() or not ledger_dir.is_dir(): @@ -1227,7 +1227,8 @@ def check_name_clearance_ledgers(failures: Failures) -> None: "writwall-candidate.json": ("Writwall", "accept"), } for ledger in sorted(ledger_dir.glob("*.json")): - for problem in module.check_ledger(ledger): + for problem in module.check_ledger( + ledger, historical=ledger.name in expected_dispositions): failures.add( "name-clearance", f"{ledger.relative_to(REPO_ROOT).as_posix()}: {problem}", diff --git a/checks/check_name_clearance.py b/checks/check_name_clearance.py index a7208a1..4ed0c26 100644 --- a/checks/check_name_clearance.py +++ b/checks/check_name_clearance.py @@ -157,9 +157,15 @@ def canonical_digest(value: object) -> str: return hashlib.sha256(encoded).hexdigest() -def check_ledger(path: Path) -> list[str]: +def check_ledger(path: Path, *, historical: bool = False) -> list[str]: + """Validate live evidence by default; history uses its recorded decision time. + + Historical validation proves only the recorded evidence and disposition, + never current clearance. The public CLI intentionally retains live mode. + """ failures: list[str] = [] - future_limit = datetime.now(timezone.utc) + timedelta(minutes=5) + observed_at = datetime.now(timezone.utc) + future_limit = observed_at + timedelta(minutes=5) try: ledger = json.loads(path.read_text(encoding="utf-8")) except (OSError, UnicodeError, json.JSONDecodeError) as exc: @@ -167,6 +173,15 @@ def check_ledger(path: Path) -> list[str]: if not isinstance(ledger, dict): return ["[ledger] top level must be an object"] + evaluation_time = observed_at + if historical: + disposition = ledger.get("disposition") + try: + if not isinstance(disposition, dict): + raise ValueError("missing disposition") + evaluation_time = parse_utc_timestamp(disposition.get("decided_at")) + except (TypeError, ValueError): + return ["[historical] a valid recorded disposition time is required"] if type(ledger.get("schema")) is not int or ledger.get("schema") != 1: failures.append("[ledger] schema must be 1") candidate = ledger.get("candidate") @@ -242,7 +257,7 @@ def check_ledger(path: Path) -> list[str]: expires_at: datetime | None = None try: expires_at = parse_utc_timestamp(expires_raw) - if expires_at <= datetime.now(timezone.utc): + if expires_at <= evaluation_time: failures.append(f"[freshness] evidence expired at {expires_raw}") except (TypeError, ValueError): failures.append("[freshness] expires_at must be an ISO-8601 UTC timestamp") @@ -282,6 +297,8 @@ def check_ledger(path: Path) -> list[str]: failures.append( "[disposition] decided_at cannot be in the future" ) + if expires_at is not None and decided_at >= expires_at: + failures.append("[disposition] decided_at must precede evidence expiry") except (TypeError, ValueError): failures.append( "[disposition] decided_at must be an ISO-8601 UTC timestamp" diff --git a/docs/architect-interview.md b/docs/architect-interview.md index 481d3e3..6a1f874 100644 --- a/docs/architect-interview.md +++ b/docs/architect-interview.md @@ -1,6 +1,8 @@ # Architect interview -`writwall start` is the idea-first front door. By default — with only +The Architect is the conversational first stage of Writwall's one canonical lifecycle; +it is not a second adoption route. For a new or clean project, +`writwall start --project-root ` is the idea-first front door. By default — with only `--project-root` — it is conversation-first: it asks nothing on the command line and never blocks on stdin. For an existing project it hands a fresh Architect a bounded, local, non-secret inventory (Git branch, cleanliness, a @@ -13,6 +15,18 @@ thinking," and imposes no fixed question list. A supplied project or command name is always a `working_candidate`; generated files never make it canonical, available, cleared, or accepted. +For an existing project where the human wants conversation before any +bootstrap write, use `writwall inspect --project-root --role +architect`. It prints a fresh Architect handoff from bounded lifecycle +evidence and changes no project or local state. It also provides the explicit +Architect re-entry path after adoption. This role selection is conversational, +not authority: it does not adopt, activate, implement, or alter lifecycle. + +If the Owner promotes the resulting project sketch, adoption materialization +ends by handing the ratified repository to a fresh General. The Architect does +not continue as that General, and either role routes bounded work to Operators +rather than implementing by conversational momentum. + Add `--structured-intake` (or `--non-interactive` with the matching flags) to run the former full structured questionnaire instead. It may begin with an unnamed idea or a supplied brief. In that mode, the deterministic diff --git a/docs/day-zero-coordinator.md b/docs/day-zero-coordinator.md index 9d990cb..6564c65 100644 --- a/docs/day-zero-coordinator.md +++ b/docs/day-zero-coordinator.md @@ -1,7 +1,9 @@ # Day-zero coordinator The day-zero coordinator is Writwall's single human entry point across the -project lifecycle. It is a standard-library Python command that runs from a clean Writwall +project lifecycle. It routes one canonical lifecycle: Owner → fresh Architect +→ explicit promotion and adoption → fresh General → bounded Operators and a +fresh Reviewer. It is a standard-library Python command that runs from a clean Writwall source distribution. It prepares a temporary handoff inside a clean/new target or prints a zero-write fresh-role handoff for every later valid state. @@ -27,6 +29,31 @@ py -3 scripts/start_writwall.py --project-root C:\path\to\your-project python3 scripts/start_writwall.py --project-root /path/to/your-project ``` +For observation and role selection without bootstrap creation: + +```text +# Installed command +writwall inspect --project-root /path/to/your-project --role auto + +# Source-tree fallback on Windows +py -3 -B -m writwall_cli inspect --project-root C:\path\to\your-project --role architect + +# Source-tree fallback on macOS or Linux +python3 -B -m writwall_cli inspect --project-root /path/to/your-project --role architect +``` + +`inspect` reads the same bounded lifecycle evidence and prints a copyable +handoff, but creates no project, bootstrap, temporary, profile, +privacy-screen, cache, or bytecode state. `auto` preserves the lifecycle route. +Explicit Architect selection is allowed for clean/new, partial, adopted, and +retired states; General only for adopted/retired lockout; recovery only for a +partial bootstrap. Explicit overrides are rejected during an active work +order, whose only safe route remains the bounded Operator under `auto`. + +Both commands require an installed package or an unpacked Writwall source +tree. A strict read-only session with neither cannot invoke them; use the +prompt-only route in `START-HERE.md` from an external capable session. + By default — with only `--project-root` — the command is conversation-first: it asks nothing on the command line and never blocks on stdin. It observes the target, fills every required field from local evidence and plain @@ -79,11 +106,13 @@ The coordinator classifies before intake or privacy initialization: | Human command | Observed state | Fresh role receiving output | Prior session stops | Target bytes | |---|---|---|---|---| | `writwall start --project-root ` | No Writwall markers | Architect (conversation-first) | Launcher returns; the Architect stops before adoption mechanics until the Owner promotes | Create-only bootstrap may be added | -| `... --structured-intake` | No Writwall markers | Adoption coordinator | Launcher returns; coordinator later stops at closeout | Create-only bootstrap may be added | +| `... --structured-intake` | No Writwall markers | Fresh Architect (prepared intake) | Launcher returns; Architect stops before adoption mechanics until explicit promotion | Create-only bootstrap may be added | | Same command | Partial `.writwall-bootstrap/` or Writwall-shaped material | Recovery coordinator | Incomplete adoption or locked session | Unchanged | | Same command | Adopted or retired lockout | Fresh General (the continuity role previously labeled Project-Architect) | Onboarding coordinator or prior work session | Unchanged | | Same command | Exact pointer to the only `status: ACTIVE` order | Bounded Operator/Implementer | Prior coordinator or Implementer context | Unchanged | | Same command | Malformed, missing, retired, or contradictory active state | No role; precise diagnostic | Invoking session | Unchanged | +| `writwall inspect ... --role auto` | Any valid state | Same lifecycle-derived role | Invoking session | Unchanged | +| `writwall inspect ... --role architect` | Clean/new, partial, adopted, or retired | Fresh Architect | Invoking session | Unchanged | Repository bytes are authoritative for this observation. A prior chat message or remembered work-order name is not lifecycle state. diff --git a/docs/name-clearance.md b/docs/name-clearance.md index 56bf432..d77c774 100644 --- a/docs/name-clearance.md +++ b/docs/name-clearance.md @@ -1,8 +1,8 @@ # Name clearance before identity work Run this gate before a project adopts a public name, package name, repository -slug, domain, logo, or launch campaign. Run it again within seven days of the -public-release decision. It is cheaper to reject a name here than to migrate an +slug, domain, logo, or launch campaign. Run it again within seven days of a +public-release decision that selects or changes the identity. It is cheaper to reject a name here than to migrate an entire release later. This process produces evidence of what was searched, when, and with what @@ -10,6 +10,19 @@ result. It **does not prove legal clearance**, exclusivity, or noninfringement. The Owner makes the product-risk decision; qualified counsel makes any legal determination the Owner requires. +## Preserved examples versus new decisions + +The four named ledgers shipped under `examples/name-clearance-ledgers/` record +completed accept/reject decisions. The distribution check validates their +evidence at each recorded disposition time, including the requirement that the +decision preceded expiry and followed the source reviews. It does not renew +those searches or claim current clearance. Evidence bytes and dates stay intact. + +All other ledger files and the ordinary `check_name_clearance.py` command use +current-time freshness. Historical validation is an explicit library context, +not a CLI shortcut for approving a new name with expired evidence. A subsequent +decision to select or change identity requires fresh collection and human review. + ## Roles - A coordinator generates candidates and runs the public collector. diff --git a/governance/LOG.md b/governance/LOG.md index 6e08251..9053cf9 100644 --- a/governance/LOG.md +++ b/governance/LOG.md @@ -1075,6 +1075,23 @@ whitespace gates passed on the same final record bytes. ## Column definitions +### Post-pilot WO-WW-026 completed record + +Owner ACCEPT on 2026-09-04, including Amendment 1 and disclosed diagnostics; +active minutes NOT REPORTED. This order does not alter the ten-order pilot. +Public-distribution classification and complete-ledger derivation were corrected; +four historical naming examples now use recorded-decision freshness without +changing their evidence, while new naming checks remain current-time. +Windows: 810 tests OK, two skips. Ubuntu public candidate: 810 OK, four skips. +Both 136-file candidates passed checker and installed gates on both platforms +and matched byte-for-byte. Fresh implementation and record reviews: ACCEPT. +Two initial Reviewer edge cases required RED/GREEN correction; the subsequent +expiry blocker was resolved by Owner Amendment 1. Earlier failures and wrapper, +environment, identity-refresh and Windows timing limitations remain in the +private report. Eight declared surfaces were instruction-bound, zero claimed +mechanically enforced; no new live-wall proof is claimed. Records are retired +to private governed history. Publication proceeds separately after closeout. + **9.2.1 Denials.** One mutation attempt rejected by the enforcement provider and attributable to the active work order. Count raw denial events; identify surface and mutation channel. Birth-test and regression-test probes are excluded from pilot totals and recorded separately in `LOG-denials-probes.md`. Every **successful** out-of-grant mutation is recorded as a deviation in Notes. Predicted count for successes: zero. **9.2.2 RFIs.** Each distinct numbered RFI first opened during the work order. Replies and updates to an existing RFI do not add counts. diff --git a/governance/PLAN.md b/governance/PLAN.md index 276223c..79e62fa 100644 --- a/governance/PLAN.md +++ b/governance/PLAN.md @@ -776,3 +776,71 @@ corrected fresh re-review returned **ACCEPT — HIGH confidence** after removal, full rerun, and two-platform candidate evidence. Public issue #29 records the release gap. The authorized post-closeout PR/CI/tag/release lifecycle remains in progress; External Pilot B resumes only from the verified immutable release. + +## 29. External Pilot B read-only entry and release-integrity corrections — 2026-09-03 + +The first v0.10.0 passes over `hllmr-site` and `hllmr-infra` independently +exposed the same coordinator boundary: lifecycle-derived routing provides no +explicit, repository-nonmutating fresh-Architect entry for an incomplete or +already-adopted project. Both pilots also observed that GitHub reports v0.10.0 +as non-immutable. Public issues #31 and #32 preserve the sanitized findings. +The separate PyYAML dependency belongs to `hllmr-site`, not Writwall, and is +routed in that repository's issue #1. + +**WO-WW-024 — COMPLETE, accepted 2026-09-03: read-only role entry and release integrity.** Added a +backward-compatible `writwall inspect` interface with explicit lifecycle-safe +Architect, General, recovery, and auto routing; prove it creates no repository, +temporary, profile, privacy, cache, or bytecode state; and add offline +published-release metadata verification requiring a matching immutable +release. Document v0.10.0's actual non-immutable status and the Owner-enabled +prospective GitHub control without rewriting history. Fresh review rejected +three first-pass overclaims; corrected final bytes execute no subprocess from +`inspect`, bound release metadata reads to a non-link regular file, and state +that v0.10.0 predates the new command. Corrected review returned ACCEPT/HIGH; +104 focused and 796 full tests passed. No version bump, public projection, +release, or external-project mutation occurred. + +## 30. Read-only role-entry release — 2026-09-04 + +**WO-WW-025 — COMPLETE, accepted 2026-09-04: publish v0.11.0 with one +canonical onboarding lifecycle.** The Owner accepted the initial +release qualification, then identified before retirement that README, +START-HERE, and ADOPTING still mixed the accepted conversation-first lifecycle +with a superseded three-route model. Amendment 2 preserves the qualified +`writwall inspect` and immutable-release work while synchronizing the public +entry surfaces around Owner → fresh Architect → explicit promotion and +adoption → fresh General → bounded Operators and fresh review. It must give +workplace and existing-project users exact commands, prompts, provider truth, +and external-Operator boundaries. Amendment 3 corrects the discovered +executable structured/non-interactive route so every clean/new mode reaches +the fresh Architect and promotion gate before adoption mechanics. Prove the +contract across both native platforms and two public candidates, and receive +fresh review and renewed Owner acceptance before closeout or publication. Both +amendments passed those gates and the Owner accepted the corrected result. +Truthful v0.10.0 history remains. The authorized v0.11.0 public lifecycle +follows closeout. + +## 31. Public-distribution inspection and ledger correction — 2026-09-04 + +**WO-WW-026 — COMPLETE, accepted 2026-09-04.** The final publication review found +that projected source-adoption records incorrectly selected a General for the +public distribution itself, and a test helper used a different digest order +from the publication specification. Correct these bounded defects, preserve +existing lifecycle safety checks, add Windows/Ubuntu regressions, and qualify +two public candidates with fresh review. Then resume the authorized v0.11.0 +publication and pilot handoffs. Public issue #33 records the defect. + +Owner Amendment 1 extends this order to distinguish four preserved historical +naming decisions from current naming evidence. Preserve every evidence byte +and timestamp, validate historical decisions within their recorded windows, +and retain current-time freshness for all new naming decisions. Repeat release +verification and fresh review before the pending publication lifecycle. + +The Owner accepted the corrected implementation and Amendment 1 with the +disclosed diagnostics. Windows ran 810 tests with two skips; native Ubuntu's +public candidate ran 810 with four skips, both OK. Two 136-file candidates +passed distribution and installed gates on both platforms and matched exactly. +Independent implementation and record reviews returned ACCEPT. Owner active +minutes: NOT REPORTED. Resume the authorized post-closeout projections, fresh +publication review, protected-CI PR/merge, immutable v0.11.0 release, and pilot +handoffs; external project mutation is not part of this closeout. diff --git a/governance/STATE.md b/governance/STATE.md index 102577d..0951865 100644 --- a/governance/STATE.md +++ b/governance/STATE.md @@ -26,12 +26,16 @@ WO-WW-016 release identity 0.9.3, accepted WO-WW-017 post-release truth and CI reliability, accepted WO-WW-018 test-race repair, accepted WO-WW-019 adoption lifecycle classification, accepted WO-WW-020 canonical-root enforcement, and accepted WO-WW-021 conversation-first inception and existing-project -continuity, and accepted WO-WW-022 v0.10.0 release qualification. No work order -is active; the authorized public PR/CI/tag/release lifecycle is in progress and -no external project is being mutated by it. -Release `v0.9.3` is public at commit -`e0cef360843dff38d6a02dd48be8f61b2d2d300e`; its complete CI run passed, -public issue #24 is closed, and no public pull request is open. External Pilot A successfully adopted Writwall, +continuity, accepted WO-WW-022 v0.10.0 release qualification, and accepted +WO-WW-023 post-release State truth reconciliation, and accepted WO-WW-024 +read-only role entry and prospective release-integrity verification, and +accepted WO-WW-025 v0.11.0 release qualification, including Amendment 2 +onboarding-lifecycle canonicalization and Amendment 3 executable structured- +intake role correction. The prior v0.10.0 public +PR/CI/tag/release lifecycle is complete, and no external +project is being mutated by this repository. Release `v0.10.0` is public at +commit `2d7b23d33bf945b3e01460a1aaba773b17cf27e2`; public PR #30 passed all +15 CI jobs and merged at that commit, and public issue #29 is closed. External Pilot A successfully adopted Writwall, completed its channel-local Windows birth test, and closed two genuine work orders with no successful forbidden mutation. Public issue #24 records the product lesson that onboarding did not terminate in an explicit fresh Project- @@ -46,12 +50,12 @@ combined disposition/action gate. Public issues #18 and #19 remain open pending the authorized WO-WW-017 public projection and PR. The obsolete `dist/plumbline-0.6.zip` (private governed-source reference, not present in this candidate) was retired at Owner closeout. No external media content, website, DNS, or mail mutation is active. -**Derived:** 2026-09-02 from the ten accepted pilot records, the Doctrine 9.3.1 +**Derived:** 2026-09-03 from the ten accepted pilot records, the Doctrine 9.3.1 fresh-agent evaluation, ratified DR-002 and project-migration DR-003, and accepted WO-PL-017 through WO-PL-023 and WO-PL-025 through WO-PL-033 records, the WO-PL-024 sequencing recovery, the verified public-release events, the -accepted WO-WW-001 through WO-WW-021 closeout records, plus the observed -v0.9.3 publication. +accepted WO-WW-001 through WO-WW-024 closeout records, plus the observed +v0.10.0 publication. **Boundary:** post-adoption, all 10 counted pilot work orders and their evaluation complete; WO-PL-017 remediation complete; DR-003 ratified; WO-PL-018 through WO-PL-023 complete; WO-PL-024 void before implementation; @@ -63,11 +67,16 @@ the current identity with the two-line wall glyph; WO-PL-039 complete and accepted, with public PR #5 merged after the required CI passed and issue #4 closed; WO-PL-040 complete and accepted; public PR #8 merged and issue #1 closed; the historical `WO-PL` series ends at 040; **WO-WW-001 through -WO-WW-022 are COMPLETE and accepted**; **no work order is active**; public PR #9, -#12, #13, #17, and #21 merged with protected CI green; public issue #14 records -the coordinator release gate; release `v0.9.3` is published; External Pilot A +WO-WW-026 are COMPLETE and accepted**; WO-WW-026 corrected the +public-distribution inspection and ledger defects reported in public issue +#33; Owner Amendment 1 also authorizes historical naming-evidence validation +without changing recorded dates; v0.11.0 publication resumes after +accepted corrected gates and review; public PR #9, +#12, #13, #17, #21, +#30 merged with protected CI green; public issue #14 records +the coordinator release gate; release `v0.10.0` is published; External Pilot A adopted successfully and closed two genuine work orders; public issue #24 is -closed; public issues #18 and #19 remain open. No external media content, +closed; public issues #18 and #19 remain open; public issue #29 is closed. No external media content, website, DNS, or mail mutation is active from this repository. The hash of the commit containing this file is intentionally recorded only externally. @@ -144,7 +153,11 @@ externally. | WO-WW-019 | **COMPLETE**, accepted 2026-09-03; lifecycle lockout now requires complete affirmative Appendix D adoption evidence. Draft, unrelated, malformed, and contradictory records cannot establish adoption; genuine established and alternate-path records remain compatible. Windows passed 761 tests with two skips on a clean prospective tree, 51 coordinator tests, 18 release tests, and all repository gates. Fresh review returned **ACCEPT — HIGH confidence** after retracting one disproved control-flow finding. Owner active minutes **NOT REPORTED**. External Pilot B remains frozen pending later authorization | | WO-WW-020 | **COMPLETE**, accepted 2026-09-03; generated repository and external-Operator packets now carry one canonical resolved project root and explicit no-shadow-repository storage rules. Nested Git-worktree paths stop before bootstrap mutation; non-Git, linked-worktree-root, and existing lifecycle routes remain compatible. Windows and native Ubuntu installed-wheel sets each passed 80 tests; the final closeout suite passed 772 tests with two expected skips. Fresh Sonnet review returned **ACCEPT — medium-high confidence** with no substantive blocker. Owner active minutes **NOT REPORTED**. External Pilot B remained frozen and untouched | | WO-WW-021 | **COMPLETE**, accepted 2026-09-03; the ordinary installed coordinator now begins with a nonblocking Architect conversation. Existing Git repositories receive bounded local observations and empty targets receive one open invitation; structured/non-interactive and lifecycle routes remain compatible. Owner / Architect / General / Operator / Reviewer topology is explicit, and the installed-wheel gate executes the bare conversation-first command with closed stdin. Final Windows and native Ubuntu affected/installed-wheel sets each passed 90 tests; the complete closeout suite passed 782 tests with two expected skips; final fresh Sonnet review returned **ACCEPT** with no remaining defect. Owner active minutes **NOT REPORTED**. Temporary environments and mounts were deleted and verified absent | -| Post-pilot sequence | **WO-PL-017 through WO-PL-023 COMPLETE**; WO-PL-024 **VOID BEFORE IMPLEMENTATION**; WO-PL-025 through WO-PL-040 **COMPLETE**; historical `WO-PL` identifiers end at 040; **WO-WW-001 through WO-WW-022 COMPLETE and accepted**; no work order is active; public PR #9, #12, #13, #17, #21, and #26 merged with protected CI green; public issues #1, #4, #10, #11, #16, #18, #19, #20, #24, and #27 closed; public issue #14 records the accepted release gate; `v0.9.3` is published; public issue #29 and the authorized `v0.10.0` PR/CI/release lifecycle remain in progress; External Pilot A adopted and closed two genuine work orders; public issue #28 tracks the corrected lifecycle defect; canonical-root enforcement and conversation-first onboarding are complete; website and infrastructure pilots proceed under their own repositories and authority | +| WO-WW-022 | **COMPLETE**, accepted 2026-09-03; current package identity and guidance agree on `0.10.0` / `v0.10.0`. Final governed source passed 782 tests with two expected skips; two independent 136-file public candidates passed Windows and native Ubuntu installed-release gates, projection checks, and byte reproducibility; corrected fresh review returned **ACCEPT — HIGH confidence**. Public PR #30 passed all 15 CI jobs and merged at `2d7b23d33bf945b3e01460a1aaba773b17cf27e2`; release `v0.10.0` is public and issue #29 is closed | +| WO-WW-023 | **COMPLETE**, accepted 2026-09-03; post-v0.10.0 State truth reconciliation recorded public PR #30's 15 passing CI jobs and merge at `2d7b23d33bf945b3e01460a1aaba773b17cf27e2`, public release `v0.10.0`, and closed issue #29 without product or Plan change. A final 136-file synthetic-pattern projection passed, the focused final-byte suite passed 83 tests, and identity, licensing, dispatch, whitespace, and byte-hygiene gates passed. The Owner accepted the disclosed verification and scope-narrowing deviations; active minutes **NOT REPORTED** | +| WO-WW-024 | **COMPLETE**, accepted 2026-09-03; public issue #31's `writwall inspect` interface provides repository-nonmutating lifecycle-safe Architect, General, recovery, and auto entry, while public issue #32's offline metadata gate requires a matching platform-immutable future release. Fresh review rejected subprocess, input-bound, and command-availability overclaims; all three were corrected and re-review returned **ACCEPT — HIGH confidence**. Final focused suite: 104 tests, OK. Final full suite: 796 tests, OK, two expected skips. Owner active minutes **NOT REPORTED**. Published v0.10.0 remains honestly recorded as commit-pinned and `immutable: false`; no public projection or release occurred | +| WO-WW-025 | **COMPLETE**, accepted 2026-09-04 including Amendments 2 and 3; package identity is `0.11.0` / `v0.11.0`. Public entry documents and clean/new ordinary, structured, and deterministic executable routes agree on one Owner-to-Architect-to-General lifecycle with exact workplace guidance. Final Windows and native Ubuntu suites each passed 798 tests; both installed-release gates and two independent 136-file public candidates passed; final fresh re-review returned **ACCEPT — HIGH confidence**. Owner active minutes **NOT REPORTED** | +| Post-pilot sequence | **WO-PL-017 through WO-PL-023 COMPLETE**; WO-PL-024 **VOID BEFORE IMPLEMENTATION**; WO-PL-025 through WO-PL-040 **COMPLETE**; historical `WO-PL` identifiers end at 040; **WO-WW-001 through WO-WW-026 COMPLETE and accepted**; WO-WW-026 accepted with Amendment 1 for public-distribution inspection, canonical ledger, and historical naming-evidence corrections; public PR #9, #12, #13, #17, #21, #26, and #30 merged with protected CI green; public issues #1, #4, #10, #11, #16, #18, #19, #20, #24, #27, and #29 closed; public issue #14 records the accepted release gate; `v0.10.0` is published; issues #31, #32, and #33 remain open pending the authorized v0.11.0 lifecycle; External Pilot A adopted and closed two genuine work orders; public issue #28 tracks the corrected lifecycle defect; canonical-root enforcement and conversation-first onboarding are complete; website and infrastructure pilots proceed under their own repositories and authority | | Bootstrap history | Eleven completed work orders retained as uncounted pre-adoption evidence under `archive/pre-adoption-bootstrap/` | ### Verification accepted at WO-PL-016 closeout @@ -986,14 +999,14 @@ it on 2026-08-28 and reported active minutes **NOT REPORTED**. | RFI-28 | **RESOLVED** at WO-PL-014 closeout under the Owner-approved parser-free design. Frontmatter is sole grant authority; B.4 is generated and unmatched B.3/B.4 paths block dispatch | | RFI-25 / 27 / 28 routing | The records remain separate but were implemented and disposed together by WO-PL-014; their bounded residuals remain visible in each record | | RFI-03 / licensing | **RESOLVED by DR-003, 2026-08-20.** License map selected; mechanization and publication remain separate gates | -| Active work order | None; intentional between-work-order lockout after accepted WO-WW-021 | -| Queued work | None under the current Writwall Plan; private pilots retain their own authority | +| Active work order | None; between-work-order lockout | +| Queued work | Complete the authorized v0.11.0 public lifecycle, then resume hllmr-site and hllmr-infra from the verified immutable release | | WO-PL-010 follow-up | **Complete.** The three adoption-recorder consistency findings from WO-PL-009 are closed | | Second-project boundary | Each adopting project maintains its own adoption and product state. No second-project source, private benchmark material, proprietary design detail, or trade-secret material is imported into Plumbline by this closeout | | Published / tagged / licensed | **Yes**: clean-history public repository `HLLMR/plumbline`, release tag `v0.8`; DR-003 path-based license map remains operative | | Push | WO-PL-034 private and public updates authorized; remote commit identities are recorded after the push completes | | Repository visibility | Private governed source remains private; clean-history `HLLMR/plumbline` is public | -| Stage | **Writwall v0.9.3 is public; WO-WW-022 is accepted and closed; v0.10.0 publication is in progress; no Writwall work order is active** | +| Stage | **Writwall v0.10.0 remains public at `2d7b23d33bf945b3e01460a1aaba773b17cf27e2`; WO-WW-025 is accepted and closed; WO-WW-026 is accepted and closed following corrected publication review; the authorized v0.11.0 public lifecycle remains pending; issues #31, #32, and #33 remain open** | --- diff --git a/identity/legacy-references.json b/identity/legacy-references.json index 531647f..4736ead 100644 --- a/identity/legacy-references.json +++ b/identity/legacy-references.json @@ -16,7 +16,7 @@ { "path": "README.md", "context": "migration_provenance", - "sha256": "d76060c70c361a64b52e9df88e400789c24fbc4fecd2b731dfe8a709a34e5e5e" + "sha256": "bb9083c43def4a803c3f01e296ccbdb0402068ec39145e3ddd892ac3e922eea3" }, { "path": "SELF-HOSTING.md", @@ -31,7 +31,7 @@ { "path": "checks/check_distribution.py", "context": "historical_evidence_path", - "sha256": "b872c46f8a8ea2fc317b4c4fd0af00c9145012b11256c923987d878d79c726c2" + "sha256": "20df8e937b6efeb7930894b7d4eba42761283b6d0166e78bcabaa2ab6dc75a7c" }, { "path": "checks/check_identity.py", @@ -94,19 +94,19 @@ { "path": "governance/LOG.md", "context": "historical_pilot_summary", - "sha256": "0720409460b68d93ae72010abab3162d1cc09cd275ddae6fa309ef7fe7b6eef3", + "sha256": "ffa9029e76fbe660f9c4eced42b11960bfd96ef0451f7d18e874c349de264aba", "projection_transform": "private_evidence_redaction" }, { "path": "governance/PLAN.md", "context": "ratified_historical_intent", - "sha256": "020bfe95a6b70da6a14dcc63087f1d90f1625fd6c08454ff8951044e04a2b0be" + "sha256": "ed0bdab770d734e83debbecc4d740f95018b39a290946d1941e2bbec09f57ee0" }, { "path": "governance/STATE.md", "context": "mixed_current_state_and_history", - "sha256": "d598988b8fa33ab64da1c0f8883948de9f619f8bb1816f2428d1b860b0241299", - "projection_sha256": "acea5eaf4e6dbee69d8e425c2848fd7f324fd67ea3a57f29a953efab46e28ce6" + "sha256": "bff2aba6ff42a6943f0571aabe8f60d8e4b59d681c069cafcc3dd7d7fd296e45", + "projection_sha256": "646887c40607a97b418ec8f09be1c94c51f7eb053bca08908441007a397d31c7" }, { "path": "governance/decisions/DR-001.md", @@ -127,7 +127,7 @@ { "path": "tests/test_distribution.py", "context": "historical_evidence_fixture", - "sha256": "6483f84819c08284b989252f4a40a42e8eca64fec5fda303cec62350c19328d0" + "sha256": "13478c88a9d9951f4a90ef15fa389fc0bf19894f917cbada62fc366cbe70a635" }, { "path": "tests/test_identity_migration.py", diff --git a/pyproject.toml b/pyproject.toml index 449bf2b..0238755 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "writwall" -version = "0.10.0" +version = "0.11.0" description = "Start governed project work from an idea." requires-python = ">=3.10" license = "Apache-2.0" diff --git a/scripts/build_public_projection.py b/scripts/build_public_projection.py index 75b1f0d..c485b8f 100644 --- a/scripts/build_public_projection.py +++ b/scripts/build_public_projection.py @@ -93,6 +93,27 @@ def sha256_bytes(data: bytes) -> str: return hashlib.sha256(data).hexdigest() +def complete_tree_ledger(root: Path) -> str: + """Hash all regular candidate files using PUBLICATION's full-line order. + + Use a bare candidate tree, not a Git checkout or installed/build directory. + Includes the shipped manifest; no files are implicitly excluded. + """ + root = root.resolve(strict=True) + if not root.is_dir(): + raise ValueError("complete-tree ledger root must be a directory") + lines = [] + for path in root.rglob("*"): + if path.is_symlink() or getattr(path, "is_junction", lambda: False)(): + raise ValueError("complete-tree ledger rejects linked entries") + if path.is_file(): + relative = path.relative_to(root).as_posix() + if "\n" in relative or "\r" in relative: + raise ValueError("complete-tree ledger rejects newline-bearing paths") + lines.append(f"{sha256_bytes(path.read_bytes())} {relative}".encode("utf-8")) + return sha256_bytes(b"\n".join(sorted(lines)) + b"\n") + + def read_private_input(path: Path) -> bytes: try: raw = path.read_bytes() @@ -296,11 +317,17 @@ def build(source_root: Path, output: Path, def main() -> None: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--output", required=True, type=Path) + action = parser.add_mutually_exclusive_group(required=True) + action.add_argument("--output", type=Path) + action.add_argument("--complete-tree-ledger", type=Path, + help="print the canonical ledger of an existing bare candidate; no writes") parser.add_argument("--private-pattern-file", type=Path) parser.add_argument("--source-root", type=Path, default=REPO_ROOT) args = parser.parse_args() - build(args.source_root, args.output, args.private_pattern_file) + if args.complete_tree_ledger is not None: + print(complete_tree_ledger(args.complete_tree_ledger)) + else: + build(args.source_root, args.output, args.private_pattern_file) if __name__ == "__main__": diff --git a/scripts/start_writwall.py b/scripts/start_writwall.py index 1714c51..7522fba 100644 --- a/scripts/start_writwall.py +++ b/scripts/start_writwall.py @@ -76,6 +76,27 @@ uncertainties and risks, and the one explicit Owner promotion decision before any adoption mechanics begin.""" +ARCHITECT_PREPARED_INTAKE_PROMPT = f"""Act as a fresh Writwall Architect. Begin read-only; do +not implement, install, register a wall, adopt the project, or activate work. Read +`.writwall-bootstrap/intake.json`, `discovery.json`, and `ARCHITECT.md`. Treat every collected +answer as unratified discovery evidence, not authority. Summarize the Owner's apparent pitch in +plain language, challenge material assumptions and alternatives, and continue the conversation +one question at a time for as long as it is useful. Produce a project sketch, recommended +Owner/Architect/General/Operator topology, provisional backlog, uncertainties, risks, and stop +conditions. Nothing advances until the human Owner explicitly promotes that sketch. + +Only after explicit promotion, use the local `writwall-adopt` bundle to prepare the proposed +adoption and recorder actions. The Owner separately ratifies material intent and authorizes +mechanics. Before wall registration or any Level 1 call, copy +`.writwall-bootstrap/writwall-adopt/assets/bootstrap-charter-addendum.md` verbatim into the +engine-visible pre-adoption charter. Ordinary no-pointer work remains forbidden; the addendum +permits only exact expected-denial probes named by a durably Owner-ratified lifecycle. It confers no mutation authority. Denial is the only valid outcome, and any success stops adoption. Remove +the addendum before the adoption commit. Do not begin product work or WO-001 before adoption. +After adoption closeout, present +the following exact fresh-role handoff and stop; the Architect does not continue as General: + +{GENERAL_PROMPT}""" + DNS_MAIL_SCENARIO = ( "DNS provider selection", "DNS inventory and cutover", @@ -446,6 +467,41 @@ def reject_bootstrap_conflict(lifecycle: str) -> None: f"pointer: {names}" ) + # Distribution evidence describes the source, not authority over this copy. + # Resolve active work first so these markers never suppress its checks. + distribution_names = ("PROJECTION-MANIFEST.sha256", "PROJECTION-PROVENANCE.md") + public_distribution = False + notice = "" + if _entry_exists(project / "CLAUDE.md"): + charter = _safe_project_path(project, project / "CLAUDE.md", "project charter") + if not charter.is_file(): + raise CoordinatorError("inconsistent state: project charter is not a file") + try: + notice = charter.read_text(encoding="utf-8-sig") + except (OSError, UnicodeError) as exc: + raise CoordinatorError("inconsistent state: project charter is unreadable") from exc + if (notice.startswith("# Public projection instructions\n") + or any(_entry_exists(project / name) for name in distribution_names)): + reject_bootstrap_conflict("public distribution") + for name in (*distribution_names, "CLAUDE.md"): + path = _safe_project_path(project, project / name, "public distribution marker") + if not path.is_file(): + raise CoordinatorError(f"inconsistent public distribution: missing {name}") + try: + notice = (project / "CLAUDE.md").read_text(encoding="utf-8-sig") + provenance = (project / "PROJECTION-PROVENANCE.md").read_text(encoding="utf-8-sig") + manifest = (project / "PROJECTION-MANIFEST.sha256").read_text(encoding="utf-8-sig") + except (OSError, UnicodeError) as exc: + raise CoordinatorError("inconsistent public distribution: unreadable marker") from exc + if (not notice.startswith("# Public projection instructions\n") + or "does not govern its own maintenance." not in notice + or not provenance.startswith("# Projection provenance\n") + or "This candidate is derived from a private governed source repository." not in provenance + or not re.search(r"(?m)^[0-9a-f]{64} CLAUDE\.md$", manifest) + or not re.search(r"(?m)^[0-9a-f]{64} PROJECTION-PROVENANCE\.md$", manifest)): + raise CoordinatorError("inconsistent public distribution: conflicting notice or marker") + public_distribution = True + governance = project / "governance" if _entry_exists(governance): resolved_governance = _safe_project_path( @@ -484,6 +540,13 @@ def reject_bootstrap_conflict(lifecycle: str) -> None: if path in adoption_paths: resolved_adoption_paths.append(resolved) + if public_distribution: + return ObservedState("public_distribution", ( + "public distribution notice and projection markers observed", + "retained source governance is evidence, not this checkout's adoption", + "classification is not a projection-integrity verification", + )) + # A filename alone is never adoption authority: read each candidate's own # Appendix D title, D.1-D.9 sections, baseline, revision, and Signature. adoption_evidence = [ @@ -818,10 +881,12 @@ def _observe_git_cleanliness(project: Path) -> str | None: git_executable = shutil.which("git") if not git_executable: return None + environment = os.environ.copy() + environment["GIT_OPTIONAL_LOCKS"] = "0" try: result = subprocess.run( [git_executable, "-C", str(project), "status", "--porcelain=v1"], - capture_output=True, text=True, timeout=10, + capture_output=True, text=True, timeout=10, env=environment, ) except (OSError, subprocess.SubprocessError): return None @@ -872,15 +937,24 @@ def evidence_lines(self) -> tuple[str, ...]: return tuple(lines) -def gather_local_inventory(project: Path) -> LocalInventory: - """Gather the bounded local inventory for a clean/new project root.""" +def gather_local_inventory( + project: Path, *, observe_cleanliness: bool = True +) -> LocalInventory: + """Gather bounded local inventory for a clean/new project root. + + ``inspect`` disables cleanliness observation so its read-only contract + never executes repository-configured Git helpers such as ``core.fsmonitor``. + """ git_dir = project / ".git" has_git = _entry_exists(git_dir) and not _is_linklike(git_dir) ordinary_git_dir = has_git and git_dir.is_dir() return LocalInventory( has_git=has_git, branch=_read_git_head_branch(project) if ordinary_git_dir else None, - cleanliness=_observe_git_cleanliness(project) if ordinary_git_dir else None, + cleanliness=( + _observe_git_cleanliness(project) + if ordinary_git_dir and observe_cleanliness else None + ), recent_commit_subjects=( _read_recent_commit_subjects(project) if ordinary_git_dir else () ), @@ -898,27 +972,20 @@ def conversation_first_opening(inventory: LocalInventory) -> tuple[str, str]: def next_prompt(state: ObservedState) -> tuple[str, str]: + if state.name == "public_distribution": + return ( + "Select target project", + "This checkout is the Writwall public distribution. Choose your target project " + "and run writwall inspect --project-root --role architect " + "for read-only discovery, or writwall start --project-root " + "for a new bootstrap. Retained governance records are source evidence, " + "not adoption authority for this checkout. To discuss contributing here, " + "use inspect with --role architect; follow CONTRIBUTING.md.", + ) if state.name == "clean_new": return ( - "Adoption coordinator before wall registration", - f"""Act as my Writwall adoption coordinator, not as an Implementer. Read -`.writwall-bootstrap/writwall-adopt/SKILL.md` and use bootstrap mode. Treat -`.writwall-bootstrap/intake.json` as unratified intake, not authority. I decide -and ratify; perform every clerical step an authorized recorder may perform. -Ask one question at a time in plain language, recommendation first. Do not -install or register the wall until the complete bundle and recovery instructions -are locally readable. Before registration or any Level 1 call, copy -`.writwall-bootstrap/writwall-adopt/assets/bootstrap-charter-addendum.md` -verbatim into the engine-visible pre-adoption charter. Ordinary no-pointer work -remains forbidden; the addendum permits only exact expected-denial probes named -by a durably Owner-ratified lifecycle and confers no mutation authority. Denial -is the only valid outcome, and any success stops adoption. Remove it before the -adoption commit. Do not begin product work or WO-001 before adoption. - -After adoption closeout, present the following exact fresh-role handoff and -stop. The onboarding coordinator stops before product work: - -{PROJECT_ARCHITECT_PROMPT}""", + "Fresh Architect (prepared intake)", + ARCHITECT_PREPARED_INTAKE_PROMPT, ) if state.name == "partial_bootstrap": return ( @@ -960,6 +1027,100 @@ def emit_lifecycle_handoff(state: ObservedState) -> None: print(prompt) +def _architect_inspection_prompt( + state: ObservedState, inventory: LocalInventory | None = None +) -> str: + if state.name == "public_distribution": + return """Act as a fresh Architect reviewing the Writwall public distribution. +Begin read-only, follow CONTRIBUTING.md, and ask what the Owner wants to explore. +The retained source governance records do not adopt or govern this checkout. +Do not initiate a General handoff or infer ratification from those records. +For a separate project, ask the Owner to select that target project instead.""" + if state.name == "clean_new" and inventory is not None and not inventory.is_existing: + return """Act as the Architect for a new, empty project. Begin read-only; do not +implement, install, adopt, activate a work order, or change lifecycle state. +Open with exactly: "Tell me what you are thinking." Let the Owner's words guide +the conversation without imposing a fixed questionnaire. Nothing said becomes +ratified intent until the human Owner ratifies it. This explicit role selection +grants no mutation or lifecycle authority.""" + return f"""Act as a fresh Architect for this existing project. Begin read-only; do not +implement, install, adopt, activate a work order, or change lifecycle state. +The observed lifecycle is {state.name}. Inspect the canonical project in place, +listen to the Owner's current intent, and distinguish preserved authority from +legacy or incomplete material. Explain what the project appears to be doing, +then ask what the Owner wants to explore. This explicit role selection grants +no mutation or lifecycle authority.""" + + +def inspect_main(argv: list[str] | None = None) -> int: + """Print a role handoff without creating repository or profile state.""" + parser = argparse.ArgumentParser( + description="Inspect one project and print a read-only Writwall handoff." + ) + parser.add_argument("--project-root", required=True) + parser.add_argument( + "--role", choices=("auto", "architect", "general", "recovery"), + default="auto", + ) + args = parser.parse_args(argv) + try: + project = resolve_project_root(args.project_root) + state = classify_project(project) + inventory = ( + gather_local_inventory(project, observe_cleanliness=False) + if state.name == "clean_new" and args.role in {"auto", "architect"} + else None + ) + inspection_evidence = ( + (*state.evidence, *inventory.evidence_lines()) + if inventory is not None else state.evidence + ) + allowed = { + "architect": { + "clean_new", "partial_bootstrap", "adopted_lockout", + "retired_lockout", "public_distribution", + }, + "general": {"adopted_lockout", "retired_lockout"}, + "recovery": {"partial_bootstrap"}, + } + if args.role == "auto": + if state.name == "clean_new": + selected_role = "Fresh Architect (conversation-first)" + prompt = _architect_inspection_prompt(state, inventory) + else: + selected_role, prompt = next_prompt(state) + elif state.name not in allowed[args.role]: + allowed_states = ", ".join(sorted(allowed[args.role])) + active_detail = ( + "; the bounded Operator remains the only routed execution role" + if state.name == "active_work_order" else "" + ) + raise CoordinatorError( + f"role {args.role!r} is not allowed for lifecycle " + f"{state.name!r}; allowed lifecycle states: {allowed_states}" + f"{active_detail}" + ) + elif args.role == "architect": + selected_role = "Fresh Architect" + prompt = _architect_inspection_prompt(state, inventory) + elif args.role == "general": + selected_role = "Fresh General" + prompt = GENERAL_PROMPT + else: + selected_role, prompt = next_prompt(state) + except CoordinatorError as exc: + print(f"STOP: {exc}", file=sys.stderr) + return 2 + print(f"Canonical project root: {project.as_posix()}") + print(f"Observed lifecycle state: {state.name}") + for item in inspection_evidence: + print(f" - {item}") + print(f"Selected role: {selected_role}") + print("\nCopy this prompt into a fresh session:\n") + print(prompt) + return 0 + + def render_time(owner_time: str) -> str: if owner_time == "yes": return """**Owner active-minute capture: ENABLED.** Start when the first intake diff --git a/skills/writwall-adopt/assets/checks/check_name_clearance.py b/skills/writwall-adopt/assets/checks/check_name_clearance.py index a7208a1..4ed0c26 100644 --- a/skills/writwall-adopt/assets/checks/check_name_clearance.py +++ b/skills/writwall-adopt/assets/checks/check_name_clearance.py @@ -157,9 +157,15 @@ def canonical_digest(value: object) -> str: return hashlib.sha256(encoded).hexdigest() -def check_ledger(path: Path) -> list[str]: +def check_ledger(path: Path, *, historical: bool = False) -> list[str]: + """Validate live evidence by default; history uses its recorded decision time. + + Historical validation proves only the recorded evidence and disposition, + never current clearance. The public CLI intentionally retains live mode. + """ failures: list[str] = [] - future_limit = datetime.now(timezone.utc) + timedelta(minutes=5) + observed_at = datetime.now(timezone.utc) + future_limit = observed_at + timedelta(minutes=5) try: ledger = json.loads(path.read_text(encoding="utf-8")) except (OSError, UnicodeError, json.JSONDecodeError) as exc: @@ -167,6 +173,15 @@ def check_ledger(path: Path) -> list[str]: if not isinstance(ledger, dict): return ["[ledger] top level must be an object"] + evaluation_time = observed_at + if historical: + disposition = ledger.get("disposition") + try: + if not isinstance(disposition, dict): + raise ValueError("missing disposition") + evaluation_time = parse_utc_timestamp(disposition.get("decided_at")) + except (TypeError, ValueError): + return ["[historical] a valid recorded disposition time is required"] if type(ledger.get("schema")) is not int or ledger.get("schema") != 1: failures.append("[ledger] schema must be 1") candidate = ledger.get("candidate") @@ -242,7 +257,7 @@ def check_ledger(path: Path) -> list[str]: expires_at: datetime | None = None try: expires_at = parse_utc_timestamp(expires_raw) - if expires_at <= datetime.now(timezone.utc): + if expires_at <= evaluation_time: failures.append(f"[freshness] evidence expired at {expires_raw}") except (TypeError, ValueError): failures.append("[freshness] expires_at must be an ISO-8601 UTC timestamp") @@ -282,6 +297,8 @@ def check_ledger(path: Path) -> list[str]: failures.append( "[disposition] decided_at cannot be in the future" ) + if expires_at is not None and decided_at >= expires_at: + failures.append("[disposition] decided_at must precede evidence expiry") except (TypeError, ValueError): failures.append( "[disposition] decided_at must be an ISO-8601 UTC timestamp" diff --git a/skills/writwall-adopt/references/name-clearance.md b/skills/writwall-adopt/references/name-clearance.md index 56bf432..d77c774 100644 --- a/skills/writwall-adopt/references/name-clearance.md +++ b/skills/writwall-adopt/references/name-clearance.md @@ -1,8 +1,8 @@ # Name clearance before identity work Run this gate before a project adopts a public name, package name, repository -slug, domain, logo, or launch campaign. Run it again within seven days of the -public-release decision. It is cheaper to reject a name here than to migrate an +slug, domain, logo, or launch campaign. Run it again within seven days of a +public-release decision that selects or changes the identity. It is cheaper to reject a name here than to migrate an entire release later. This process produces evidence of what was searched, when, and with what @@ -10,6 +10,19 @@ result. It **does not prove legal clearance**, exclusivity, or noninfringement. The Owner makes the product-risk decision; qualified counsel makes any legal determination the Owner requires. +## Preserved examples versus new decisions + +The four named ledgers shipped under `examples/name-clearance-ledgers/` record +completed accept/reject decisions. The distribution check validates their +evidence at each recorded disposition time, including the requirement that the +decision preceded expiry and followed the source reviews. It does not renew +those searches or claim current clearance. Evidence bytes and dates stay intact. + +All other ledger files and the ordinary `check_name_clearance.py` command use +current-time freshness. Historical validation is an explicit library context, +not a CLI shortcut for approving a new name with expired evidence. A subsequent +decision to select or change identity requires fresh collection and human review. + ## Roles - A coordinator generates candidates and runs the public collector. diff --git a/tests/test_coordinator_release.py b/tests/test_coordinator_release.py index 3f5bb25..6e029d1 100644 --- a/tests/test_coordinator_release.py +++ b/tests/test_coordinator_release.py @@ -6,6 +6,7 @@ import hashlib import importlib.util +import json import shutil import subprocess import sys @@ -38,11 +39,8 @@ def load_checker(): def tree_digest(root: Path) -> str: - lines: list[str] = [] - for path in sorted(item for item in root.rglob("*") if item.is_file()): - relative = path.relative_to(root).as_posix() - lines.append(f"{hashlib.sha256(path.read_bytes()).hexdigest()} {relative}") - return hashlib.sha256(("\n".join(lines) + "\n").encode()).hexdigest() + from scripts.build_public_projection import complete_tree_ledger + return complete_tree_ledger(root) class CoordinatorReleaseTests(unittest.TestCase): @@ -53,7 +51,7 @@ def setUp(self) -> None: def run_checker(self, candidate: Path, *extra: str): arguments = [str(candidate), *extra] if "--expected-tag" not in extra: - arguments.extend(("--expected-tag", "v0.10.0")) + arguments.extend(("--expected-tag", "v0.11.0")) return subprocess.run( [sys.executable, "-B", str(CHECKER), *arguments], cwd=REPO_ROOT, @@ -96,6 +94,57 @@ def test_release_check_requires_an_intended_tag(self): self.assertIn("--expected-tag", result.stdout + result.stderr) self.assertIn("required", result.stdout + result.stderr) + def test_published_release_metadata_accepts_matching_immutable_release(self): + checker = load_checker() + metadata = self.temp / "release.json" + metadata.write_text( + json.dumps({"tag_name": "v0.10.0", "immutable": True}), + encoding="utf-8", + ) + checker.verify_published_release_json(metadata, "v0.10.0") + + def test_published_release_metadata_rejects_nonimmutable_mismatch_and_malformed(self): + checker = load_checker() + cases = ( + ({"tag_name": "v0.10.0", "immutable": False}, "not immutable"), + ({"tag_name": "v0.9.3", "immutable": True}, "does not match"), + ({"tag_name": "v0.10.0", "immutable": "true"}, "literal true"), + ({"immutable": True}, "tag_name"), + ) + for index, (payload, diagnostic) in enumerate(cases): + with self.subTest(payload=payload): + metadata = self.temp / f"release-{index}.json" + metadata.write_text(json.dumps(payload), encoding="utf-8") + with self.assertRaisesRegex(checker.ReleaseCheckError, diagnostic): + checker.verify_published_release_json(metadata, "v0.10.0") + + malformed = self.temp / "malformed.json" + malformed.write_text('{"tag_name":', encoding="utf-8") + with self.assertRaisesRegex(checker.ReleaseCheckError, "malformed JSON"): + checker.verify_published_release_json(malformed, "v0.10.0") + + def test_published_release_metadata_rejects_duplicate_security_fields(self): + checker = load_checker() + metadata = self.temp / "duplicate.json" + metadata.write_text( + '{"tag_name":"v0.10.0","immutable":true,"immutable":false}', + encoding="utf-8", + ) + with self.assertRaisesRegex(checker.ReleaseCheckError, "duplicate field"): + checker.verify_published_release_json(metadata, "v0.10.0") + + def test_published_release_metadata_rejects_oversized_input(self): + checker = load_checker() + metadata = self.temp / "oversized.json" + metadata.write_bytes(b" " * (checker.MAX_RELEASE_METADATA_BYTES + 1)) + with self.assertRaisesRegex(checker.ReleaseCheckError, "size limit"): + checker.verify_published_release_json(metadata, "v0.10.0") + + def test_published_release_metadata_rejects_nonregular_input(self): + checker = load_checker() + with self.assertRaisesRegex(checker.ReleaseCheckError, "regular file"): + checker.verify_published_release_json(self.temp, "v0.10.0") + def test_complete_external_candidate_installs_and_emits_full_handoff(self): candidate = self.make_candidate() before = tree_digest(candidate) @@ -188,6 +237,23 @@ def test_installed_help_mismatch_fails_with_diagnostic(self): self.assertNotEqual(result.returncode, 0) self.assertIn("installed help omitted", result.stdout + result.stderr) + def test_installed_missing_inspect_route_fails_with_diagnostic(self): + candidate = self.make_candidate() + entry = candidate / "writwall_cli" / "__main__.py" + original = entry.read_text(encoding="utf-8") + self.assertIn('if arguments[0] == "inspect":', original) + entry.write_text( + original.replace( + 'if arguments[0] == "inspect":', + 'if arguments[0] == "inspection":', + ), + encoding="utf-8", + newline="\n", + ) + result = self.run_checker(candidate) + self.assertNotEqual(result.returncode, 0) + self.assertIn("installed inspect route", result.stdout + result.stderr) + def test_missing_promised_handoff_fails_with_diagnostic(self): candidate = self.make_candidate() start = candidate / "scripts" / "start_writwall.py" @@ -272,15 +338,15 @@ def test_intended_release_tag_must_match_candidate_metadata(self): pyproject = candidate / "pyproject.toml" pyproject.write_text( pyproject.read_text(encoding="utf-8").replace( - 'version = "0.10.0"', 'version = "0.9.0"' + 'version = "0.11.0"', 'version = "0.9.0"' ), encoding="utf-8", newline="\n", ) - result = self.run_checker(candidate, "--expected-tag", "v0.10.0") + result = self.run_checker(candidate, "--expected-tag", "v0.11.0") self.assertNotEqual(result.returncode, 0) self.assertIn( - "candidate version '0.9.0' does not match intended tag 'v0.10.0'", + "candidate version '0.9.0' does not match intended tag 'v0.11.0'", result.stdout + result.stderr, ) @@ -326,25 +392,27 @@ def test_release_check_exercises_nested_worktree_stop_on_the_installed_wheel(sel def test_release_identity_and_public_payload_are_coherent(self): with (REPO_ROOT / "pyproject.toml").open("rb") as handle: project = tomllib.load(handle)["project"] - self.assertEqual(project["version"], "0.10.0") + self.assertEqual(project["version"], "0.11.0") readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") adopting = (REPO_ROOT / "ADOPTING.md").read_text(encoding="utf-8") contributing = (REPO_ROOT / "CONTRIBUTING.md").read_text(encoding="utf-8") publication = (REPO_ROOT / "PUBLICATION.md").read_text(encoding="utf-8") start = (REPO_ROOT / "START-HERE.md").read_text(encoding="utf-8") - tagged_archive = "archive/refs/tags/v0.10.0.zip" + tagged_archive = "archive/refs/tags/v0.11.0.zip" self.assertIn(tagged_archive, readme) self.assertIn(tagged_archive, adopting) self.assertIn(tagged_archive, start) - self.assertIn("--expected-tag v0.10.0", publication) - self.assertIn("--expected-tag v0.10.0", contributing) + self.assertIn("--expected-tag v0.11.0", publication) + self.assertIn("--expected-tag v0.11.0", contributing) for document in (readme, adopting, start): self.assertNotIn("not yet published", document) self.assertIn( 'python -m pip install ' - '"https://github.com/HLLMR/writwall/archive/refs/tags/v0.10.0.zip"', + '"https://github.com/HLLMR/writwall/archive/refs/tags/v0.11.0.zip"', document, ) + self.assertIn("writwall inspect", document) + self.assertNotIn("does **not** contain `writwall inspect`", document) self.assertIn("Release `v0.9.0` first introduced", start) self.assertIn("Release `v0.9.1` corrected", start) self.assertIn("Release `v0.9.2` corrects", start) @@ -353,6 +421,49 @@ def test_release_identity_and_public_payload_are_coherent(self): self.assertIn("checks/check_coordinator_release.py", public_files) self.assertIn("tests/test_coordinator_release.py", public_files) + def test_public_entry_docs_share_one_canonical_onboarding_lifecycle(self): + documents = { + name: (REPO_ROOT / name).read_text(encoding="utf-8") + for name in ( + "README.md", + "START-HERE.md", + "ADOPTING.md", + "docs/day-zero-coordinator.md", + "docs/architect-interview.md", + ) + } + for name, document in documents.items(): + with self.subTest(document=name): + self.assertIn("one canonical lifecycle", document) + self.assertIn("writwall start --project-root", document) + self.assertIn("writwall inspect --project-root", document) + self.assertIn("fresh Architect", document) + self.assertIn("fresh General", document) + self.assertNotIn("## 2. Route A", document) + self.assertNotIn("## 3. Route B", document) + self.assertNotIn("## 4. Route C", document) + + workplace = documents["README.md"] + documents["START-HERE.md"] + self.assertIn("Using Writwall at work", workplace) + self.assertIn("employer-approved", workplace) + self.assertIn("does not launch", workplace) + self.assertIn("Act as the Writwall Architect", workplace) + self.assertIn("Act as a fresh Writwall General", workplace) + + for name in ( + "START-HERE.md", + "ADOPTING.md", + "docs/day-zero-coordinator.md", + ): + rows = [ + line for line in documents[name].splitlines() + if line.lstrip().startswith("|") and "--structured-intake" in line + ] + self.assertTrue(rows, name) + for row in rows: + self.assertIn("Architect", row, name) + self.assertNotIn("Adoption coordinator", row, name) + # -- WO-WW-021: the installed-wheel release gate confirms the new # Owner/Architect/General/Operator topology reaches the installed # coordinator, including the adopted-lockout route now naming a fresh diff --git a/tests/test_distribution.py b/tests/test_distribution.py index c900aa2..484ba47 100644 --- a/tests/test_distribution.py +++ b/tests/test_distribution.py @@ -407,6 +407,17 @@ def test_human_ramp_and_coordinator_reference_carry_lifecycle_table(self): class CheckerFailureCategories(DistributionTestCase): + def test_preserved_naming_examples_pass_as_history(self): + self.assertNotIn("[name-clearance]", self.check().stdout) + + def test_an_unlisted_stale_ledger_still_fails_current_validation(self): + folder = self.repo / "examples" / "name-clearance-ledgers" + payload = json.loads((folder / "writwall-candidate.json").read_text(encoding="utf-8")) + payload["expires_at"] = "2000-01-01T00:00:00Z" + (folder / "new-decision.json").write_text(json.dumps(payload), encoding="utf-8") + result = self.check() + self.assertIn("new-decision.json: [freshness] evidence expired", result.stdout) + def test_unclassified_former_identity_fails_release_gate(self): self.edit( "START-HERE.md", diff --git a/tests/test_name_clearance.py b/tests/test_name_clearance.py index 418756a..8a25388 100644 --- a/tests/test_name_clearance.py +++ b/tests/test_name_clearance.py @@ -33,6 +33,40 @@ class NameClearanceProcessTests(unittest.TestCase): + def test_historical_validation_preserves_evidence_after_expiry(self): + from checks import check_name_clearance as checker + ledger = self.valid_ledger() + future = datetime.now(timezone.utc) + timedelta(days=30) + class FutureClock(datetime): + @classmethod + def now(cls, tz=None): + return future + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "ledger.json" + path.write_text(json.dumps(ledger), encoding="utf-8") + before = path.read_bytes() + with patch.object(checker, "datetime", FutureClock): + self.assertTrue(any("expired" in e for e in checker.check_ledger(path))) + self.assertEqual(checker.check_ledger(path, historical=True), []) + self.assertEqual(path.read_bytes(), before) + + def test_historical_validation_rejects_late_decision_and_bad_evidence(self): + from checks import check_name_clearance as checker + for defect in ("late", "missing", "tampered", "missing_date"): + with self.subTest(defect=defect), tempfile.TemporaryDirectory() as tmp: + ledger = self.valid_ledger() + if defect == "late": + ledger["disposition"]["decided_at"] = ledger["expires_at"] + elif defect == "missing": + ledger["sources"].pop() + elif defect == "missing_date": + ledger["disposition"].pop("decided_at") + else: + ledger["sources"][0]["response_sha256"] = "0" * 64 + path = Path(tmp) / "ledger.json" + path.write_text(json.dumps(ledger), encoding="utf-8") + self.assertTrue(checker.check_ledger(path, historical=True)) + def valid_ledger(self) -> dict: now = datetime.now(timezone.utc).replace(microsecond=0) sources = [] diff --git a/tests/test_public_projection.py b/tests/test_public_projection.py index 031c47d..040f29d 100644 --- a/tests/test_public_projection.py +++ b/tests/test_public_projection.py @@ -28,6 +28,31 @@ def load_projection_checker(): class PublicProjectionProcessTests(unittest.TestCase): + def test_complete_ledger_matches_independent_full_line_vector(self): + from scripts.build_public_projection import complete_tree_ledger + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "a").write_bytes(b"z") + (root / "b").write_bytes(b"a") + (root / "PROJECTION-MANIFEST.sha256").write_bytes(b"manifest\n") + lines = [ + hashlib.sha256(b"z").hexdigest() + " a", + hashlib.sha256(b"a").hexdigest() + " b", + hashlib.sha256(b"manifest\n").hexdigest() + " PROJECTION-MANIFEST.sha256", + ] + expected = hashlib.sha256(("\n".join(sorted(lines)) + "\n").encode("utf-8")).hexdigest() + self.assertEqual(complete_tree_ledger(root), expected) + command = subprocess.run( + [sys.executable, "-B", str(BUILDER), "--complete-tree-ledger", str(root)], + cwd=root, capture_output=True, text=True, timeout=30) + self.assertEqual(command.returncode, 0, command.stdout + command.stderr) + self.assertEqual(command.stdout.strip(), expected) + (root / "b").rename(root / "renamed") + self.assertNotEqual(complete_tree_ledger(root), expected) + renamed = complete_tree_ledger(root) + (root / "PROJECTION-MANIFEST.sha256").write_bytes(b"changed\n") + self.assertNotEqual(complete_tree_ledger(root), renamed) + def setUp(self) -> None: self.tmp = Path(tempfile.mkdtemp()).resolve() self.addCleanup(__import__("shutil").rmtree, self.tmp, True) diff --git a/tests/test_start_writwall.py b/tests/test_start_writwall.py index d447a1e..b4de585 100644 --- a/tests/test_start_writwall.py +++ b/tests/test_start_writwall.py @@ -5,12 +5,14 @@ from __future__ import annotations import json +import io import os import shutil import subprocess import sys import tempfile import unittest +from contextlib import redirect_stdout from pathlib import Path from types import SimpleNamespace from unittest import mock @@ -231,6 +233,20 @@ def run_lifecycle_start(self, project: Path | None = None): timeout=60, ) + def run_inspect(self, role: str = "auto", project: Path | None = None): + return subprocess.run( + [ + sys.executable, "-B", "-m", "writwall_cli", "inspect", + "--project-root", str(project or self.project), + "--role", role, + ], + cwd=REPO_ROOT, + env=self.environment(), + capture_output=True, + text=True, + timeout=60, + ) + @staticmethod def tree_snapshot(root: Path) -> dict[str, bytes | None]: return { @@ -513,10 +529,20 @@ def test_clean_project_creates_bundle_and_exact_handoff(self): self.assertTrue((self.output / "writwall-adopt" / "SKILL.md").is_file()) handoff = self.handoff() flat = " ".join(handoff.split()) - self.assertIn("Act as my Writwall adoption coordinator", handoff) + self.assertIn("Act as a fresh Writwall Architect", handoff) + self.assertIn("explicitly promotes", handoff) self.assertIn("does not install or adopt Writwall", flat) self.assertIn("Do not enter passwords, API tokens", handoff) + def test_clean_structured_intake_still_routes_through_fresh_architect(self): + result = self.run_start("--structured-intake") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + handoff = self.handoff() + self.assertIn("Fresh Architect", handoff) + self.assertIn("unratified discovery evidence", handoff) + self.assertIn("explicitly promote", handoff) + self.assertNotIn("Next role: Adoption coordinator", handoff) + def test_existing_bootstrap_routes_to_recovery_without_overwrite(self): self.output.mkdir() sentinel = self.output / "keep.txt" @@ -807,6 +833,93 @@ def test_bootstrap_mixed_with_established_lifecycle_fails_closed(self): self.assertIn("inconsistent state", result.stderr) self.assertIn(".writwall-bootstrap", result.stderr) + def seed_public_distribution(self): + from scripts.build_public_projection import PUBLIC_CLAUDE_BYTES + import hashlib + governance = self.project / "governance" + (governance / "decisions").mkdir(parents=True) + for name in ("PLAN.md", "STATE.md", "ROUTING.md"): + (governance / name).write_text(f"# {name}\n", encoding="utf-8") + (governance / "decisions" / "DR-001.md").write_text( + ratified_adoption_record(), encoding="utf-8") + (self.project / "CLAUDE.md").write_bytes(PUBLIC_CLAUDE_BYTES) + (self.project / "PROJECTION-PROVENANCE.md").write_text( + "# Projection provenance\n\nThis candidate is derived from a private governed source repository.\n", + encoding="utf-8") + paths = ("CLAUDE.md", "PROJECTION-PROVENANCE.md") + (self.project / "PROJECTION-MANIFEST.sha256").write_text( + "".join(f"{hashlib.sha256((self.project / p).read_bytes()).hexdigest()} {p}\n" + for p in paths), encoding="utf-8") + + def test_public_distribution_inspect_and_start_do_not_inherit_adoption(self): + self.seed_public_distribution() + before = self.tree_snapshot(self.project) + for result in (self.run_inspect(), self.run_lifecycle_start()): + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("public_distribution", result.stdout) + self.assertIn("target project", result.stdout) + self.assertNotIn("Fresh General", result.stdout) + self.assertEqual(self.tree_snapshot(self.project), before) + + def test_public_distribution_rejects_explicit_execution_roles(self): + self.seed_public_distribution() + for role in ("general", "recovery"): + result = self.run_inspect(role) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + result = self.run_inspect("architect") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("public distribution", result.stdout) + self.assertIn("read-only", result.stdout) + + def test_public_notice_with_missing_manifest_stops(self): + self.seed_public_distribution() + (self.project / "PROJECTION-MANIFEST.sha256").unlink() + result = self.run_inspect() + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("distribution", result.stderr) + + def test_public_distribution_does_not_hide_active_or_bootstrap_state(self): + self.seed_public_distribution() + (self.project / ".writwall-bootstrap").mkdir() + result = self.run_inspect() + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + (self.project / ".writwall-bootstrap").rmdir() + orders = self.project / "governance" / "work-orders" + orders.mkdir() + (orders / "WO-001.md").write_text("---\nstatus: ACTIVE\n---\n", encoding="utf-8") + result = self.run_inspect() + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + pointer = self.project / ".claude" / "active-wo.txt" + pointer.parent.mkdir() + pointer.write_text("governance/work-orders/WO-001.md\n", encoding="utf-8") + result = self.run_inspect() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("active_work_order", result.stdout) + + def test_public_marker_cannot_override_a_changed_local_charter(self): + self.seed_public_distribution() + (self.project / "CLAUDE.md").write_text("# Local charter\n", encoding="utf-8") + result = self.run_inspect() + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("conflicting", result.stderr) + + def test_public_notice_without_both_markers_never_infers_adoption(self): + self.seed_public_distribution() + for name in ("PROJECTION-MANIFEST.sha256", "PROJECTION-PROVENANCE.md"): + (self.project / name).unlink() + result = self.run_inspect() + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("distribution", result.stderr) + + def test_public_distribution_preserves_governance_path_checks(self): + self.seed_public_distribution() + plan = self.project / "governance" / "PLAN.md" + plan.unlink() + plan.mkdir() + result = self.run_inspect() + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("governance control", result.stderr) + def test_adopted_lockout_routes_to_fresh_general(self): governance = self.project / "governance" governance.mkdir() @@ -832,6 +945,172 @@ def test_adopted_lockout_routes_to_fresh_general(self): self.assertIn("Do not ask for the same decision again", flat) self.assertIn("perform every mechanically available authorized step", flat) + def test_inspect_architect_reenters_adopted_lockout_without_writes(self): + governance = self.project / "governance" + governance.mkdir() + for name in ("PLAN.md", "STATE.md", "ROUTING.md"): + (governance / name).write_text(f"# {name}\n", encoding="utf-8") + decision = governance / "decisions" / "DR-001.md" + decision.parent.mkdir() + decision.write_text(ratified_adoption_record(), encoding="utf-8") + (self.project / "README.md").write_text("# Existing project\n", encoding="utf-8") + before = self.tree_snapshot(self.project) + + result = self.run_inspect("architect") + + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.tree_snapshot(self.project), before) + self.assertFalse(self.state.exists()) + self.assertIn("Observed lifecycle state: adopted_lockout", result.stdout) + self.assertIn("Selected role: Fresh Architect", result.stdout) + self.assertIn("Begin read-only", result.stdout) + self.assertIn( + "grants no mutation or lifecycle authority", + " ".join(result.stdout.split()), + ) + + def test_inspect_partial_bootstrap_routes_recovery_without_writes(self): + settings = self.project / ".claude" / "settings.json" + settings.parent.mkdir(parents=True) + settings.write_text("{}\n", encoding="utf-8") + before = self.tree_snapshot(self.project) + + result = self.run_inspect("recovery") + + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.tree_snapshot(self.project), before) + self.assertFalse(self.state.exists()) + self.assertIn("Observed lifecycle state: partial_bootstrap", result.stdout) + self.assertIn("Selected role: Fresh external recovery coordinator", result.stdout) + + architect = self.run_inspect("architect") + self.assertEqual(architect.returncode, 0, architect.stdout + architect.stderr) + self.assertEqual(self.tree_snapshot(self.project), before) + self.assertIn("Selected role: Fresh Architect", architect.stdout) + self.assertIn("observed lifecycle is partial_bootstrap", architect.stdout) + + def test_inspect_general_routes_adopted_and_retired_lockout(self): + for lifecycle in ("adopted_lockout", "retired_lockout"): + with self.subTest(lifecycle=lifecycle): + project = self.temp / lifecycle + governance = project / "governance" + governance.mkdir(parents=True) + for name in ("PLAN.md", "STATE.md", "ROUTING.md"): + (governance / name).write_text(f"# {name}\n", encoding="utf-8") + decision = governance / "decisions" / "DR-001.md" + decision.parent.mkdir() + decision.write_text(ratified_adoption_record(), encoding="utf-8") + if lifecycle == "retired_lockout": + closed = governance / "history" / "WO-001.md" + closed.parent.mkdir() + closed.write_text( + "---\nid: WO-001\nstatus: CLOSED\n---\n", encoding="utf-8" + ) + before = self.tree_snapshot(project) + + result = self.run_inspect("general", project) + + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.tree_snapshot(project), before) + self.assertIn(f"Observed lifecycle state: {lifecycle}", result.stdout) + self.assertIn("Selected role: Fresh General", result.stdout) + + def test_inspect_rejects_unsafe_explicit_role_lifecycle_combinations(self): + cases = (("general", "clean_new"), ("recovery", "clean_new")) + for role, lifecycle in cases: + with self.subTest(role=role, lifecycle=lifecycle): + before = self.tree_snapshot(self.project) + result = self.run_inspect(role) + self.assertEqual(result.returncode, 2) + self.assertEqual(self.tree_snapshot(self.project), before) + self.assertIn(f"role {role!r}", result.stderr) + self.assertIn(lifecycle, result.stderr) + self.assertIn("allowed", result.stderr) + + def test_inspect_auto_preserves_active_operator_routing_without_writes(self): + order = self.project / "governance" / "work-orders" / "WO-001.md" + order.parent.mkdir(parents=True) + order.write_text( + "---\nid: WO-001\nstatus: ACTIVE\n---\n# Work\n", encoding="utf-8" + ) + pointer = self.project / ".claude" / "active-wo.txt" + pointer.parent.mkdir(parents=True) + pointer.write_text("governance/work-orders/WO-001.md\n", encoding="utf-8") + before = self.tree_snapshot(self.project) + + result = self.run_inspect("auto") + + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.tree_snapshot(self.project), before) + self.assertIn("Observed lifecycle state: active_work_order", result.stdout) + self.assertIn("Selected role: Fresh walled repository Operator/Implementer", result.stdout) + self.assertIn("active work order only", result.stdout) + + rejected = self.run_inspect("architect") + self.assertEqual(rejected.returncode, 2) + self.assertIn("bounded Operator", rejected.stderr) + + def test_inspect_default_auto_is_read_only_and_creates_no_state_or_cache(self): + (self.project / "README.md").write_text( + "# Existing unadopted project\n", encoding="utf-8" + ) + before = self.tree_snapshot(self.project) + result = subprocess.run( + [ + sys.executable, "-B", "-m", "writwall_cli", "inspect", + "--project-root", str(self.project), + ], + cwd=REPO_ROOT, + env=self.environment(), + capture_output=True, + text=True, + timeout=60, + ) + + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.tree_snapshot(self.project), before) + self.assertFalse(self.output.exists()) + self.assertFalse(self.state.exists()) + self.assertEqual(list(self.temp.rglob("__pycache__")), []) + self.assertEqual(list(self.temp.rglob("*.pyc")), []) + self.assertIn("Observed lifecycle state: clean_new", result.stdout) + self.assertIn("Selected role: Fresh Architect (conversation-first)", result.stdout) + self.assertIn("Top-level project entries: README.md", result.stdout) + self.assertNotIn("discovery.json", result.stdout) + self.assertNotIn("ARCHITECT.md", result.stdout) + + def test_local_inventory_git_status_disables_optional_index_writes(self): + with mock.patch.object(starter_module.shutil, "which", return_value="git"), \ + mock.patch.object( + starter_module.subprocess, + "run", + return_value=SimpleNamespace(returncode=0, stdout="", stderr=""), + ) as run: + self.assertEqual(starter_module._observe_git_cleanliness(self.project), "clean") + + environment = run.call_args.kwargs["env"] + self.assertEqual(environment["GIT_OPTIONAL_LOCKS"], "0") + + def test_inspect_never_invokes_repository_configured_git_processes(self): + git_dir = self.project / ".git" + git_dir.mkdir() + (git_dir / "HEAD").write_text( + "ref: refs/heads/main\n", encoding="utf-8" + ) + output = io.StringIO() + with mock.patch.object( + starter_module.subprocess, + "run", + side_effect=AssertionError("inspect must not execute git"), + ), redirect_stdout(output): + result = starter_module.inspect_main( + ["--project-root", str(self.project), "--role", "architect"] + ) + + self.assertEqual(result, 0) + self.assertIn("Git repository observed", output.getvalue()) + self.assertNotIn("Git working tree:", output.getvalue()) + def test_draft_adoption_record_with_closed_history_never_reports_adopted_or_retired(self): governance = self.project / "governance" governance.mkdir() diff --git a/writwall_cli/__main__.py b/writwall_cli/__main__.py index 462b350..fcf0df8 100644 --- a/writwall_cli/__main__.py +++ b/writwall_cli/__main__.py @@ -17,6 +17,17 @@ def build_parser() -> argparse.ArgumentParser: description="Start with an idea and prepare a governed project handoff.", help="Start with an idea", ) + inspect = commands.add_parser( + "inspect", + description="Inspect a project and print a read-only role handoff.", + help="Inspect without changing project state", + ) + inspect.add_argument("--project-root", required=True) + inspect.add_argument( + "--role", + choices=("auto", "architect", "general", "recovery"), + default="auto", + ) privacy = commands.add_parser( "privacy", description="Manage the local project privacy screen.", @@ -43,6 +54,9 @@ def main(argv: Sequence[str] | None = None) -> int: if arguments[0] == "privacy": from scripts.privacy_screen import main as privacy_main return privacy_main(arguments[1:]) + if arguments[0] == "inspect": + from writwall_cli.coordinator import inspect + return inspect(arguments[1:]) if arguments[0] != "start": build_parser().error(f"unknown command: {arguments[0]}") from writwall_cli.coordinator import start diff --git a/writwall_cli/coordinator.py b/writwall_cli/coordinator.py index 8feea0d..9881147 100644 --- a/writwall_cli/coordinator.py +++ b/writwall_cli/coordinator.py @@ -12,3 +12,10 @@ def start(argv: Sequence[str] | None = None) -> int: from scripts.start_writwall import main return main(list(argv) if argv is not None else None) + + +def inspect(argv: Sequence[str] | None = None) -> int: + """Print a read-only coordinator handoff through the supported API.""" + from scripts.start_writwall import inspect_main + + return inspect_main(list(argv) if argv is not None else None)