From 50df09bf218de3e86e1e617cc40f505ad048e1b5 Mon Sep 17 00:00:00 2001 From: HLLMR Date: Thu, 3 Sep 2026 17:33:58 -0500 Subject: [PATCH] Publish conversation-first onboarding in v0.10.0 --- ADOPTING.md | 63 ++- CONTRIBUTING.md | 2 +- PROJECTION-MANIFEST.sha256 | 38 +- PROJECTION-PROVENANCE.md | 6 +- PUBLICATION.md | 2 +- README.md | 44 +- START-HERE.md | 119 ++-- checks/check_coordinator_release.py | 311 +++++++++- docs/architect-interview.md | 66 ++- docs/day-zero-coordinator.md | 169 ++++-- governance/LOG-denials.jsonl | 9 + governance/LOG.md | 101 ++++ governance/PLAN.md | 69 ++- governance/STATE.md | 27 +- identity/legacy-references.json | 10 +- pyproject.toml | 2 +- scripts/start_writwall.py | 849 ++++++++++++++++++++++++---- skills/writwall-adopt/SKILL.md | 41 +- tests/test_coordinator_release.py | 73 ++- tests/test_start_writwall.py | 626 +++++++++++++++++++- 20 files changed, 2276 insertions(+), 351 deletions(-) diff --git a/ADOPTING.md b/ADOPTING.md index 7b82809..631207b 100644 --- a/ADOPTING.md +++ b/ADOPTING.md @@ -42,11 +42,12 @@ locked session may deny the network request that would otherwise retrieve them. For a first adoption, the default is the day-zero coordinator: -Release `v0.9.3` is published and contains the terminal Architect handoff -described below. +Release `v0.10.0` packages the conversation-first Architect handoff, +canonical-root enforcement, and corrected lifecycle classification described +below. ```text -python -m pip install "https://github.com/HLLMR/writwall/archive/refs/tags/v0.9.3.zip" +python -m pip install "https://github.com/HLLMR/writwall/archive/refs/tags/v0.10.0.zip" # Installed command writwall start --project-root /path/to/your-project @@ -67,7 +68,14 @@ specific privacy screen in per-user local state outside the repository. The bootstrap handoff records only ready status and entry count, never its location or contents. Its clean/new branch remains create-only bootstrap tooling. See [`docs/privacy-screen.md`](docs/privacy-screen.md). -Contradictory active state stops before output. Full interface and external- +Contradictory active state stops before output. + +For a clean/new target, the ordinary invocation is conversation-first: it +asks nothing on the command line and hands off to a fresh Architect with a +bounded, local, non-secret inventory for an existing project, or one open +invitation for an empty one. The former full questionnaire remains available +verbatim behind `--structured-intake`; deterministic automation keeps using +`--non-interactive`. Full interface and external- Operator packet behavior are documented in [`docs/day-zero-coordinator.md`](docs/day-zero-coordinator.md). The idea-first qualification and identity gate are documented in @@ -86,10 +94,11 @@ handoff is temporary and must be removed before the adoption commit. | Human command | Observed state | Fresh role receiving output | Prior session stops | Target bytes | |---|---|---|---|---| -| `writwall start --project-root ` | Clean/new | Adoption coordinator | Launcher returns; coordinator stops at adoption closeout | Create-only bootstrap may be added | +| `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 | | Same command | Partial/recovery | Recovery coordinator | Incomplete or locked session | Unchanged | -| Same command | Adopted/retired lockout | Owner-Agent / Project-Architect | Onboarding or prior work session | Unchanged | -| Same command | Active work order | Bounded Implementer | Prior coordinator or Implementer context | 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 | --- @@ -293,35 +302,37 @@ What counts as proof is a **live-wall canary**: an Owner-authorized, genuinely m 5.9 Make the adoption commit containing `governance/`, the charter, the adapter, and DR-001. Its message names the baseline hash. This is one local commit; it is not a push, a tag, or a release. You may make it yourself or have an authorized recorder make it on your behalf after you have ratified exactly what it will contain — in either case the commit records a decision that was already yours. -5.10 End onboarding and hand the adopted project to a fresh Owner-Agent / -Project-Architect. The recorder presents this exact prompt and stops: +5.10 End onboarding and hand the adopted project to a fresh General. Earlier +releases called this continuity role `Owner-Agent / Project-Architect`; that +term remains compatibility vocabulary for older records, not the current route. The +recorder presents this exact prompt and stops: ```text -Act as a fresh Owner-Agent / Project-Architect. Begin read-only and verify -the lifecycle from repository bytes rather than prior chat. Read the charter, -Plan, State, Routing, ratified adoption record, and open transactional records. -State the project's next decision plainly. Draft, but do not activate or -implement, the smallest genuine work order or bounded external Operator packet. -Lead with a concise Recommendation and material tradeoff; keep the detailed -packet behind it as supporting evidence rather than the conversational front -door. When the next safe mechanical action is available, ask once for one -combined disposition and action. If that action uses a new user-owned task, -explicitly include creation and dispatch of the named task in that approval -request; never infer task-creation permission afterward. Once approved, perform -every mechanically available authorized step. Do not ask for the same decision again. -The human Owner alone ratifies intent and activates work; preserve a distinct -fresh review after implementation. The onboarding coordinator stops here and -does not continue into project work. +Act as a fresh General for this already-adopted project's continuity. Begin +read-only and verify the lifecycle from repository bytes rather than prior chat. Read the +charter, Plan, State, Routing, ratified adoption record, and open transactional records. State +the project's next decision plainly. Prepare, but do not activate, the smallest genuine work +order or bounded Operator packet; route it to a fresh Architect instead only when the next +decision requires new design or design-conformance judgment rather than routine continuity. Lead +with a concise Recommendation and material tradeoff; keep the detailed packet behind it as +supporting evidence rather than the conversational front door. When the next safe mechanical +action is available, ask once for one combined disposition and action. If that action uses a new +user-owned task, explicitly include creation and dispatch of the named task in that approval +request; never infer task-creation permission afterward. Once approved, perform every +mechanically available authorized step. Do not ask for the same decision again. The human Owner +alone ratifies intent and activates work; preserve a distinct fresh Reviewer after +implementation. The onboarding coordinator stops here and does not continue into project work. ``` -The Architect drafts whatever genuine work the project needs next (6.1.4), +The General prepares whatever genuine work the project needs next (6.1.4), including a bounded external Operator packet when that is smaller than a repository work order. It leads with its recommendation and material tradeoff; the complete packet is supporting evidence. When a safe next mechanical action is available, its single approval request combines the disposition with that action. A new user-owned task is created or dispatched only when that same -request explicitly asks for it. After approval, the Architect performs all +request explicitly asks for it. After approval, the General performs all mechanically available authorized steps without asking for the decision again. +New design or design-conformance judgment is routed to a fresh Architect. For a repository work order, activation remains a distinct Owner decision. After the candidate passes `--work-order`, the Owner-authorized lifecycle diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ab1fb2..1e59440 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.9.3 +python checks/check_coordinator_release.py --expected-tag v0.10.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 e2da980..b228a71 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 -4e610d391b2c3ba95a269ab1112464feca5263c648e78fc2aefc6b713d3fc817 ADOPTING.md +32800f8e4b24c8bc96ad6ddb80ddb2c523d20719945a3042d81e09a74d3ce301 ADOPTING.md 1179c999034f4ec1c1d44c1946bd2955c4625905e80767abe760c8c3ab01c493 CLAUDE.md -86f3193d5174d8bc5a594a2c5a065de97255e9f223da374632c8eb3f592ca6ab CONTRIBUTING.md +881dc7401aaaa3aa148be143bd3e191fc7f883466027cbc77e47ed4cc8f79b6a CONTRIBUTING.md 664196054cd98585105be457afa09c788a482416ccb48a87bb269b2156e49ae6 DOCTRINE.md 9ba9550ad48438d0836ddab3da480b3b69ffa0aac7b7878b5a0039e7ab429411 LICENSE a38775f2d68b40577253ee48061ba67af3c75b7620dc506b34b40ce2a3b660ee LICENSE-MAP.md @@ -16,17 +16,17 @@ c274f80372d90c012937370f0e1f15087d22e308ef98b27cea5dc0d2d088366c LICENSES/Apach a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499 LICENSES/CC0-1.0.txt 59746d6285ffa44bfc7ecada352aa5d6a20dc8eab418a60ce091cc739012c135 LICENSES/MIT-0.txt 35e6d37b7c5fa0c1fc872315cbd362cd24bfa41e1b7dc3019fbcd31e99350f51 NAMING.md -bd0e416ac56565c27fa3ab00d3248a04ef825f4cd3a6c1a815bb919f35816202 PROJECTION-PROVENANCE.md -9716fb18ddca4626791e17474cdba2fd343f47886080c6ff8b1e7c41f63830ec PUBLICATION.md -d05596604edc06e5ac5c2502ecd61b945e0caa41fe63a39de66a3d58fa2e1e2b README.md +a4322cd5b31594139c3f5bde2f7442aad89812ba56cdaa32818f976217149544 PROJECTION-PROVENANCE.md +925d0d79412b04c9a4f80d4adef2f6dd21d76b4492d37f3d1fcf40b20a70c235 PUBLICATION.md +d76060c70c361a64b52e9df88e400789c24fbc4fecd2b731dfe8a709a34e5e5e README.md 284a0862f3be77e8d867aa4d3ef92ed1a64ad4315d6d9074f6bb64771b6d1dd0 REUSE.toml ab75b39490b4db4e203f5b23b480a1c998d87cf07d760cb787cb260778b21d0a SECURITY.md 6a51c1211cc675599634d144ca24a705ec1696f84640a6464b14efb1d6c3a629 SELF-HOSTING.md -b6a970033c7fb8d6976471216d68f8c961e2417657b655801f191319b9d3e702 START-HERE.md +d9eb0445ef4da08bae1e3a7e81a03e92f2488eee1c1e68254f5c5fc640a99ff8 START-HERE.md 75c7ae0f569148f489570d63df916a70b6ccf24b77db2076cdee29663f747428 adapters/claude-code/README.md aeb7f81d139e7ffa6de9a1782444b99eb6d549ac1c3a8b9c0f8bcb9c6addfa6d adapters/claude-code/SECURITY.md dd29af2a39d25e0270ad9acc23ee912f81e39c674e1179759f4a3010c6a0c1a0 adapters/claude-code/wo_capability_wall.py -cb1647439902b2ee7546ed7c7c84375f01164b662047e99f42002f5e5a8335bf checks/check_coordinator_release.py +ecc969827cb19e9d8cba3e7b369ff2b81d6407544f86a12a139b5c482624bbb7 checks/check_coordinator_release.py b872c46f8a8ea2fc317b4c4fd0af00c9145012b11256c923987d878d79c726c2 checks/check_distribution.py 60fe377dac32b8d1697f859371ef40d26ed4e695fdceeda6d29ec0318d539504 checks/check_identity.py 30986c40ff7c9b29e2fba39ec04c18af3c1c351490410bd532a8391bcb92ed11 checks/check_licenses.py @@ -42,13 +42,13 @@ d9f01820bd45d8dad46e7fd307e0b6986a41d6cd2071da353530421aeea46f70 decisions/LICE fa88788242d920999b6e6737ea60b03dfe3f9ba2e0d90386b6bbcfeb6acd0509 docs/agents/domain.md 98305d69cb8ff9ebdd44d815248c4619aecc09295891c043e1423d1189888a49 docs/agents/issue-tracker.md 2177e1dbec58d14cb20e3b15fcb2cfb3ba6671025ba86a72e603c59d5cc09a06 docs/agents/triage-labels.md -12348d1eaa4e769fc66a50de210fbfab19133002b5dea9a432ed37e0cd860411 docs/architect-interview.md +3c7c89cbe276c65fddf565a743042a5c1a58f7dfe0208b3957260177eb327a27 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 -d5c6554675bd34015949381bce96b7f46309bc27db12a5e583f3f4b2f9c2e2ca docs/day-zero-coordinator.md +fdab8ed3cbb4f0ac24f9d39565baddea6b83984f698c2c48166b05590aff21ef docs/day-zero-coordinator.md e1214e3e6018642809339249bb091a6fd754847b4f77c4bc7a39c5c87e6769cc docs/identity-migration.md 4d54cd53db8c165b40af7eb11b97a7f4faf5f23479c0efc8238e5d463b159008 docs/name-clearance.md 1eef400dd2e12b109ceb9b30c107dbd7f25d3c64182346dd0f370bf9ccaa7088 docs/privacy-screen.md @@ -61,11 +61,11 @@ d2c5a8ca21edf842dfd17a83862024afa0a92349abf693a60e55ce454c8d78fa examples/name- 0d62666ddc07a4283309bcbc8f9501add4ecec052d26369d30ce232e17c17702 examples/plumbline-self-hosting-pilot.md 30cdb11fbeb2fd9bbf4048255331ccbbdd6e516fae5adfa5317af00ce53607c9 governance/ADOPTION-MAPPING.md 08b235351ab7799715b1e2df4fa3dd9fa88bb85084c8aade4d01039bf255b489 governance/LOG-denials-probes.md -72581985a0c16bcd2c2abe5e871a23918be3acc320e0529c94c4a0aa07207033 governance/LOG-denials.jsonl -8a27c2cda1dbbb3d38ca03f1745352abb0ab07290d691dd4f063d3e274da425f governance/LOG.md -0a1cfae5dcd8d475a599fff8bb8c07c745f4baae63adb8def5ddbcad6cfb13ea governance/PLAN.md +5dba70f7d7263ecd10ecb2c18867b6c2c5870a3e001c25c7797d3d61feae680a governance/LOG-denials.jsonl +0720409460b68d93ae72010abab3162d1cc09cd275ddae6fa309ef7fe7b6eef3 governance/LOG.md +020bfe95a6b70da6a14dcc63087f1d90f1625fd6c08454ff8951044e04a2b0be governance/PLAN.md dd445eb2994e0d9615bc61fe2ae157a6b14ba7b190c65b705d380b31c49fdfe0 governance/ROUTING.md -4ad60876ac2ff78f8eb212e0c4b54a42dd5c1131780a2b1add58f9367bcd0ee7 governance/STATE.md +acea5eaf4e6dbee69d8e425c2848fd7f324fd67ea3a57f29a953efab46e28ce6 governance/STATE.md e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 governance/archive/.gitkeep e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 governance/briefs/.gitkeep e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 governance/decisions/.gitkeep @@ -82,20 +82,20 @@ b567ce0c0867464328e81774d888f6491fa66b68ac73be01f993e5c4c66d3ed8 governance/tem d355e46f978f17de8824af805e05124e0f20b1c072523b518422044f88c6f079 governance/templates/D-adoption-record.md 2b586efadab716a59fcafb74312a45a05401a4787fee6ae18cb5c9dd14ef3a09 governance/templates/E-adoption-mapping.md e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 governance/work-orders/.gitkeep -2bccff453a30dcc8556221b3552a2b3b58616c16b023e008478b912f10029fb0 identity/legacy-references.json +51963137bf9d19220e4164072e7b3dee7d1312b714494b8728466e22ad8e63c9 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 -cb875755a70e151f9ea75b11d88da8dff8f5e9732ff923c5d01156ecc392a445 pyproject.toml +1d9014a4540d81ecfd3b2bbe24577c5bc98999b53e3ca8158dd8600744772449 pyproject.toml 455ca1ab3c9e7e78afbb9946e13b94497ba24003ab6f411ea96cce26d4ecc39e scripts/build_distribution.py bdbe47e8ea246ceafb874229d5582a9cc18db7c66250c9873d50a787bd5d9fe2 scripts/build_public_projection.py 3cf88f936599e0e84bc2368bc0503a39b9f96e47b473e09b26569e5c3c9edbd9 scripts/collect_name_clearance.py c372f7f1736eb77bedaca43696ea0b060333733f912053479b363442022c4b24 scripts/privacy_screen.py -fb793b5d08981e9a2f44f62c26d79d3652aa56f5d6db600fca4ce5546ba2c141 scripts/start_writwall.py +c367c7f4397c926701dda0a4118a2e1286787157d7de1d8f51fa207ec50021c4 scripts/start_writwall.py 374f4e8a80b7b9e162b9360a3907b6ffe12ce94ba0ed827058c7b3c9c0658b2c skills/writwall-adopt/LICENSE-MAP.md -1c15f2a97cddf727f3173b8971e6ea3e05ab93f57664f8c3031eb0e634a87933 skills/writwall-adopt/SKILL.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 @@ -121,14 +121,14 @@ d355e46f978f17de8824af805e05124e0f20b1c072523b518422044f88c6f079 templates/D-ad 9924816cbbeade6f88f79d3e06fe04d143d801783888210ac2325925d69e4bdb tests/test_check_distribution.py b046f2eea794070194294a33f2914e627eed384e63fccffc2ac46693db2a968c tests/test_check_licenses.py 9a106ff5182b4a15713575de42e90b0dc5cdeebcb17ba08d97522a4c9aa6b2fa tests/test_check_work_order_dispatch.py -304066215be7c840888f9dc5afdd3e9b0470907ce18bb1f1157370c320743f16 tests/test_coordinator_release.py +6fc58f3e82282c51b0604c9bd24808f6f53c9d543fb4d4ccf18ec1fc5a638676 tests/test_coordinator_release.py 6483f84819c08284b989252f4a40a42e8eca64fec5fda303cec62350c19328d0 tests/test_distribution.py e150a2f988a4b0beac5f70644f55f5e185a8aa575e988a19642bafabc0f07775 tests/test_identity_migration.py 11cd8090dbc53e8aa6a2f14cb181c8a11696335da8f40700eae5116798e49ba5 tests/test_init_sh.py 96c255d84e37b8884cde769897e763776b81027080c2308c33dc5e4b8df4a4bf tests/test_name_clearance.py 677d5b532450ace267be9c834269c081368697cd83defa5531ce673f6d0ca252 tests/test_privacy_screen.py ee771c239c0fc072675f88617dec7e330c1e19be90a8b22691d7049dbb5a4544 tests/test_public_projection.py -478a470078cfb0ac10a9e93c5d4c656830bf089f7b7a4999c7ce975b7dc0b08e tests/test_start_writwall.py +2bc96f1dfd6febc1d281494720125602f01293838614ffe9850b3d2a9fb78f6b tests/test_start_writwall.py 0684c04067eb95eadc9f72ab126d8662b4a5e2005c80b2dea174075a6140eebc tests/test_wo_capability_wall.py e8caf7f4421dc7f78b0d766741ec2ef4c2ac6dab6117fab6e4175b27d31e4d49 writwall_cli/__init__.py 9e0fbb2625b7a99ed80a3a708f8a0742d91bbf7e3cd2862d21a75515031083e5 writwall_cli/__main__.py diff --git a/PROJECTION-PROVENANCE.md b/PROJECTION-PROVENANCE.md index 8b53ba8..9400ca1 100644 --- a/PROJECTION-PROVENANCE.md +++ b/PROJECTION-PROVENANCE.md @@ -5,17 +5,19 @@ 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: `ae6ca4e29493b7c33738756190dc1bbf1617388d` -- Source commit time: `2026-09-02T20:30:55-05:00` +- Source commit: `be8cfa99b53996d140433610b12886cf62c444f0` +- Source commit time: `2026-09-03T17:19:47-05:00` - Projection allowlist SHA-256: `fafcbf659c40d7260dddaa8a64b6b59c3b7de484f77d879a90323bb4b3e1a4a7` ## Legacy identifier inventory +- `0000000000000000000000000000000000000000` — `checks/check_coordinator_release.py` - `11d5960a326750d5838078e36cf38b85af677262` — `tests/test_distribution.py` - `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` - `8d5b2b3668ef626525e57028ac09661e17d44edc` — `governance/LOG.md`, `governance/PLAN.md`, `governance/STATE.md`, `governance/decisions/DR-001.md` +- `92846c03f60303efd9f0e2fc14a99bbca2e1daea` — `governance/PLAN.md` - `a26af69be951a213d495a4c3e4e4022e16d87065` — `tests/test_distribution.py` - `a6a71fee0b567e8c70a7ea518398af48b4d0175d` — `governance/decisions/DR-001.md` - `a905c87987f31094121c11a3b8163f97ef1abcf4` — `SELF-HOSTING.md`, `governance/STATE.md`, `governance/decisions/DR-001.md` diff --git a/PUBLICATION.md b/PUBLICATION.md index 085c6b8..2142d92 100644 --- a/PUBLICATION.md +++ b/PUBLICATION.md @@ -13,7 +13,7 @@ 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.9.3 +python checks/check_coordinator_release.py --expected-tag v0.10.0 ``` The command fails before building unless the canonical intended tag matches diff --git a/README.md b/README.md index 610c3c6..235c3ab 100644 --- a/README.md +++ b/README.md @@ -98,11 +98,12 @@ 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.9.3` is published and contains the terminal Architect handoff -described below. +Release `v0.10.0` packages the conversation-first Architect handoff, +canonical-root enforcement, and corrected lifecycle classification described +below. ```text -python -m pip install "https://github.com/HLLMR/writwall/archive/refs/tags/v0.9.3.zip" +python -m pip install "https://github.com/HLLMR/writwall/archive/refs/tags/v0.10.0.zip" # Installed command writwall start --project-root /path/to/your-project @@ -117,20 +118,35 @@ python3 scripts/start_writwall.py --project-root /path/to/your-project | Observed state | Fresh role | Prior session stops | Target bytes | |---|---|---|---| -| Clean/new | Adoption coordinator | Launcher returns; onboarding later stops at closeout | Create-only bootstrap may be added | +| 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 | Owner-Agent / Project-Architect | Onboarding or prior work session | Unchanged | -| Active work order | Bounded Implementer | Prior coordinator/Implementer context | 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 command asks one question at a time, creates -`.writwall-bootstrap/`, and initializes a durable project-specific privacy -screen in your operating system's local user state outside the repository. -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. See the +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. diff --git a/START-HERE.md b/START-HERE.md index 86a8878..c8ddce3 100644 --- a/START-HERE.md +++ b/START-HERE.md @@ -4,20 +4,20 @@ You do not need to understand the Doctrine before beginning. You need to know which role you are talking to, where that agent is running, and what decision belongs to you. -The safest default is: **prepare adoption with a coordinator before opening a -walled Implementer session.** Make the 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 safest default is: **run the coordinator, talk to the Architect it names, +and promote the project into adoption only when the idea is ready.** Make the +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. ## Who does what | Function | Who or what performs it | May share an agent? | |---|---|---| | **Owner** | You. You decide intent, ratify records, authorize lifecycle actions, and accept results. | Never delegated. | -| **Adoption coordinator / recorder** | A chat or coding agent that can inspect the project and prepare or record your decisions. During recovery it runs outside the walled Implementer grant. | May also act as Dispatcher on a small project. | -| **Dispatcher** | Turns ratified Plan intent into one bounded work order. | May be the coordinator in a fresh turn or session. | -| **Implementer** | The coding agent operating inside the project with one active work order. | Does not review or authorize its own work. | +| **Architect** | The fresh agent that listens to the pitch, inventories an existing project, challenges the idea, and returns a project sketch for your promotion decision. It returns later for new design or design-conformance judgment. | May be the same model used later, but not the same running context. | +| **General** | The fresh post-adoption continuity agent that maintains the Plan, prepares bounded dispatch, routes work, and records only decisions you already made. | May perform Dispatcher and recorder mechanics in a fresh turn or session. | +| **Operator / Implementer** | The coding, infrastructure, or other execution agent working under one bounded packet or active work order. | Does not review or authorize its own work. | | **Reviewer** | A fresh read-only agent given the work order, report, and changed result. | Use a fresh session with no implementation role; a different provider is optional, not required. | These are functions, not permanent job titles. One model can perform several @@ -28,26 +28,26 @@ between them and does not review its own implementation in the same context. ### Small project -Use one capable coordinator for interviewing, dispatch drafting, and recorder -mechanics; use your IDE coding agent as the walled Implementer; open a fresh -session for Reviewer work. This is the lightest credible arrangement. +Use one capable frontier model sequentially in separate sessions as Architect +and General; use your IDE coding agent as the walled Operator/Implementer; open +a fresh session for Reviewer work. This is the lightest credible arrangement. -Talk first to the coordinator outside the walled IDE, or to the IDE agent +Talk first to the Architect outside the walled IDE, or to the IDE agent **before** any project hook is registered. Give it the public Writwall source or the complete local adoption bundle. ### Split-role project -Use an external adoption coordinator / recorder (for example a general-purpose -coding task or chat with repository access), a walled Implementer inside the -IDE, and a fresh Reviewer. This is the recommended path when the wall is +Use an external Architect and General (for example general-purpose coding tasks +or chats with repository access), a walled Operator inside the IDE, and a fresh +Reviewer. This is the recommended path when the wall is already installed, the repository has substantial existing intent, or the IDE session cannot perform protected lifecycle mechanics. ### Provider-neutral -Use any capable model for coordinator, Dispatcher, Implementer, and Reviewer -functions. Without a birth-tested provider adapter, the grants are +Use any capable model for Architect, General, Operator, and Reviewer functions. +Without a birth-tested provider adapter, the grants are instruction-bounded rather than mechanically enforced. The records and review flow still work; describe the enforcement boundary honestly. @@ -59,17 +59,20 @@ 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.9.3` is published and contains the terminal Architect handoff. +1. Release `v0.10.0` packages the conversation-first Architect handoff, + canonical-root enforcement, and corrected lifecycle classification. Install it without unpacking it over your project: ```text - python -m pip install "https://github.com/HLLMR/writwall/archive/refs/tags/v0.9.3.zip" + python -m pip install "https://github.com/HLLMR/writwall/archive/refs/tags/v0.10.0.zip" ``` Release `v0.9.0` first introduced the coordinator. Release `v0.9.1` corrected first-use bytecode residue. Release `v0.9.2` corrects the bootstrap expected-denial contract. Release `v0.9.3` adds lifecycle-aware - routing and the terminal Architect handoff. + routing and the terminal Architect handoff. Release `v0.10.0` adds + conversation-first inception, corrected adoption-state classification, + and canonical project-root enforcement. 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: @@ -89,20 +92,29 @@ not clear results. | Observed state | Fresh role receiving the output | Session that stops | May target bytes change? | |---|---|---|---| - | Clean/new | 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 (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/` | | Partial bootstrap or recovery | Recovery coordinator | The incomplete adoption or locked session | No | - | Adopted or retired lockout | Owner-Agent / Project-Architect | The onboarding coordinator or prior work session | No | - | Active work order | Bounded Implementer | Any prior coordinator or Implementer context | 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 | -3. If you choose to track Owner active minutes, start the timer when the first - question tells you to; do not reconstruct time later. Answer one question at - a time without entering secrets. You may point it at an existing brief. The - command observes actual repository lifecycle state. Only a clean/new target - enters intake, creates `/.writwall-bootstrap/`, and initializes a - durable local privacy screen outside the repository. Later valid states emit - a fresh-role prompt without changing target bytes. Add only private names, codenames, client - identifiers, or domains; never add credentials or secret values. See +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 + `/.writwall-bootstrap/`, and initializes a durable local privacy + screen outside the repository, then hands off to a fresh Architect. If + the target already holds work, the Architect's opening carries a bounded, + local, non-secret inventory (Git branch, cleanliness, a few recent commit + subjects, and top-level project-relative names) and asks whether to + explore that work or start elsewhere; if the target is empty, it opens + with exactly: "Tell me what you are thinking." Later valid states emit a + fresh-role prompt without changing target bytes. To use the former full + questionnaire instead — Owner-time timer, one question at a time, no + local inventory — add `--structured-intake`; for deterministic, + non-interactive automation, use `--non-interactive` with its existing + required flags. Add only private names, codenames, client identifiers, or + domains; never add credentials or secret values. See [`docs/privacy-screen.md`](docs/privacy-screen.md). 4. Open its `HANDOFF.md`. Start the agent and location it names and paste the exact prompt. The complete local `writwall-adopt` bundle is already beside @@ -113,7 +125,7 @@ not clear results. The command does not install Writwall, interpret intake as ratified intent, create an activation pointer, contact an external system, or replace the -Owner-Agent. It stops on contradictory state instead of guessing from prior +human Owner, Architect, or General. It stops on contradictory state instead of guessing from prior chat. See [`docs/day-zero-coordinator.md`](docs/day-zero-coordinator.md) for the complete contract. It may start with an unnamed idea; see @@ -247,36 +259,39 @@ temporary bootstrap bundle before the adoption commit, return the project to lockout, and do not dispatch WO-001. ``` -## Terminal Project-Architect handoff +## Terminal General handoff + +Earlier releases labeled this continuity handoff `Owner-Agent / Project-Architect`; +that term is retained only as compatibility vocabulary for older records. Adoption closeout ends the onboarding session. After the adoption commit, open -a fresh Owner-Agent / Project-Architect and paste exactly: +a fresh General and paste exactly: ```text -Act as a fresh Owner-Agent / Project-Architect. Begin read-only and verify -the lifecycle from repository bytes rather than prior chat. Read the charter, -Plan, State, Routing, ratified adoption record, and open transactional records. -State the project's next decision plainly. Draft, but do not activate or -implement, the smallest genuine work order or bounded external Operator packet. -Lead with a concise Recommendation and material tradeoff; keep the detailed -packet behind it as supporting evidence rather than the conversational front -door. When the next safe mechanical action is available, ask once for one -combined disposition and action. If that action uses a new user-owned task, -explicitly include creation and dispatch of the named task in that approval -request; never infer task-creation permission afterward. Once approved, perform -every mechanically available authorized step. Do not ask for the same decision again. -The human Owner alone ratifies intent and activates work; preserve a distinct -fresh review after implementation. The onboarding coordinator stops here and -does not continue into project work. +Act as a fresh General for this already-adopted project's continuity. Begin +read-only and verify the lifecycle from repository bytes rather than prior chat. Read the +charter, Plan, State, Routing, ratified adoption record, and open transactional records. State +the project's next decision plainly. Prepare, but do not activate, the smallest genuine work +order or bounded Operator packet; route it to a fresh Architect instead only when the next +decision requires new design or design-conformance judgment rather than routine continuity. Lead +with a concise Recommendation and material tradeoff; keep the detailed packet behind it as +supporting evidence rather than the conversational front door. When the next safe mechanical +action is available, ask once for one combined disposition and action. If that action uses a new +user-owned task, explicitly include creation and dispatch of the named task in that approval +request; never infer task-creation permission afterward. Once approved, perform every +mechanically available authorized step. Do not ask for the same decision again. The human Owner +alone ratifies intent and activates work; preserve a distinct fresh Reviewer after +implementation. The onboarding coordinator stops here and does not continue into project work. ``` -The Architect leads with a concise recommendation and material tradeoff; its +The General leads with a concise recommendation and material tradeoff; its detailed packet remains supporting evidence. If the next safe step can be done, its one approval request includes both the disposition and that action. Creating a new user-owned task must be explicitly included in that request. Once you -approve it, the Architect performs every authorized mechanical step available +approve it, the General performs every authorized mechanical step available without asking the same question again. It still does not infer ratification, -activate a work order it was told only to draft, or implement product work. +activate a work order it was told only to prepare, or implement product work. +It routes new design and design-conformance decisions back to a fresh Architect. After you separately approve and activate a work order, start a fresh Implementer: diff --git a/checks/check_coordinator_release.py b/checks/check_coordinator_release.py index 7078cf7..0fd9668 100644 --- a/checks/check_coordinator_release.py +++ b/checks/check_coordinator_release.py @@ -6,6 +6,7 @@ import argparse import hashlib +import json import os import re import shutil @@ -28,6 +29,9 @@ REQUIRED_HANDOFF_PATHS = ( "HANDOFF.md", "intake.json", + "ARCHITECT.md", + "GENERAL.md", + "OPERATOR.md", "OWNER-AGENT.md", "REPOSITORY-OPERATOR.md", "REVIEWER.md", @@ -45,6 +49,100 @@ class ReleaseCheckError(RuntimeError): """A bounded, user-facing release-candidate failure.""" +RATIFIED_ADOPTION_RECORD = """# Adoption record + +## D.1 Date and Owner + +2026-01-01 · Owner: Example Owner + +## D.2 Pre-adoption baseline commit + +`0000000000000000000000000000000000000000` — baseline commit. Adoption +became effective at this commit. + +## D.3 Doctrine revision bound + +Revision **0.8**, ratified **2026-01-01** by `decisions/DR-EXAMPLE.md`. + +## D.4 Enforcement at adoption + +Observed enforcement surfaces at adoption. + +## D.5 Conformance gate during the pilot + +Reviewer-only controlled inference. + +## D.6 Recognized controlling sources at adoption + +Disposed by the Owner. + +## D.7 Pilot period + +10 counted work orders. + +## D.8 Reasoning: why adopt, and why now + +Recorded by the Owner. + +## D.9 Rejected alternatives + +1. Alternative rejected. + +## Signature + +Example Owner — Owner — 2026-01-01 +""" + +DRAFT_ADOPTION_RECORD = """# Adoption record + +## D.1 Date and Owner + +DRAFT — Owner: TBD + +## D.2 Pre-adoption baseline commit + +Proposed baseline; not yet selected. + +## D.3 Doctrine revision bound + +Revision **0.8**, PROPOSED. + +## D.4 Enforcement at adoption + +Draft enforcement notes. + +## D.5 Conformance gate during the pilot + +Draft conformance notes. + +## D.6 Recognized controlling sources at adoption + +Draft mapping. + +## D.7 Pilot period + +Draft pilot period. + +## D.8 Reasoning: why adopt, and why now + +Draft reasoning. + +## D.9 Rejected alternatives + +Draft rejected alternatives. +""" + +UNRELATED_RATIFIED_DECISION = """# DR-001: Naming decision + +Ratified by the Owner on 2026-01-01. This record ratifies a project naming +choice; it is not an adoption record and contains no Appendix D sections. + +## Signature + +Example Owner — Owner — 2026-01-01 +""" + + def tree_digest(root: Path) -> str: lines: list[str] = [] for path in sorted(item for item in root.rglob("*") if item.is_file()): @@ -55,11 +153,13 @@ def tree_digest(root: Path) -> str: def run(command: list[str], *, cwd: Path, environment: dict[str, str], - label: str, timeout: int = 180) -> subprocess.CompletedProcess[str]: + label: str, timeout: int = 180, + closed_stdin: bool = False) -> subprocess.CompletedProcess[str]: result = subprocess.run( command, cwd=cwd, env=environment, + stdin=subprocess.DEVNULL if closed_stdin else None, capture_output=True, text=True, timeout=timeout, @@ -203,6 +303,47 @@ 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") + conversation_project = workspace / "conversation-first-project" + conversation_project.mkdir() + run( + [ + str(command), "start", + "--project-root", str(conversation_project), + ], + cwd=workspace, + environment=environment, + label="installed conversation-first run", + closed_stdin=True, + ) + conversation_output = conversation_project / ".writwall-bootstrap" + conversation_handoff = conversation_output / "HANDOFF.md" + conversation_architect = conversation_output / "ARCHITECT.md" + if not conversation_handoff.is_file() or not conversation_architect.is_file(): + raise ReleaseCheckError( + "complete handoff failed: installed conversation-first run omitted " + "HANDOFF.md or ARCHITECT.md" + ) + conversation_text = conversation_handoff.read_text(encoding="utf-8") + required_conversation_text = ( + "Fresh Architect (conversation-first)", + 'Open with exactly: "Tell me what you are thinking."', + ) + missing_conversation_text = [ + text for text in required_conversation_text + if text not in conversation_text + ] + if missing_conversation_text: + raise ReleaseCheckError( + "installed conversation-first handoff omitted: " + + ", ".join(missing_conversation_text) + ) + conversation_residue = python_bytecode_residue(conversation_output) + if conversation_residue: + raise ReleaseCheckError( + "conversation-first handoff contains Python bytecode residue: " + + ", ".join(conversation_residue) + ) + run( [ str(command), "start", "--non-interactive", @@ -242,6 +383,22 @@ def check_candidate(candidate: Path, expected_tag: str) -> None: + ", ".join(residue) ) + intake_payload = json.loads( + (output / "intake.json").read_text(encoding="utf-8") + ) + recorded_root = intake_payload.get("project_root") + expected_root = project.resolve().as_posix() + if recorded_root in (None, ".", ""): + raise ReleaseCheckError( + "installed coordinator recorded no canonical root evidence: " + f"project_root={recorded_root!r}" + ) + if recorded_root != expected_root: + raise ReleaseCheckError( + f"installed coordinator recorded canonical root {recorded_root!r}, " + f"expected {expected_root!r}" + ) + adopted = workspace / "adopted-project" governance = adopted / "governance" decisions = governance / "decisions" @@ -251,7 +408,7 @@ def check_candidate(candidate: Path, expected_tag: str) -> None: f"# {name}\n", encoding="utf-8", newline="\n" ) (decisions / "DR-001.md").write_text( - "# Adoption record\n", encoding="utf-8", newline="\n" + RATIFIED_ADOPTION_RECORD, encoding="utf-8", newline="\n" ) adopted_before = tree_digest(adopted) adopted_result = run( @@ -262,7 +419,7 @@ def check_candidate(candidate: Path, expected_tag: str) -> None: ) required_route_text = ( "Observed lifecycle state: adopted_lockout", - "Act as a fresh Owner-Agent / Project-Architect", + "Act as a fresh General", "one combined disposition and action", "Do not ask for the same decision again", ) @@ -285,13 +442,159 @@ def check_candidate(candidate: Path, expected_tag: str) -> None: "installed adopted-lockout route changed target bytes" ) + retired = workspace / "retired-project" + retired_governance = retired / "governance" + retired_decisions = retired_governance / "decisions" + retired_decisions.mkdir(parents=True) + for name in ("PLAN.md", "STATE.md", "ROUTING.md"): + (retired_governance / name).write_text( + f"# {name}\n", encoding="utf-8", newline="\n" + ) + (retired_decisions / "DR-001.md").write_text( + RATIFIED_ADOPTION_RECORD, encoding="utf-8", newline="\n" + ) + (retired_governance / "history").mkdir() + (retired_governance / "history" / "WO-001.md").write_text( + "---\nid: WO-001\nstatus: CLOSED\n---\n", encoding="utf-8", newline="\n" + ) + retired_before = tree_digest(retired) + retired_result = run( + [str(command), "start", "--project-root", str(retired)], + cwd=workspace, + environment=environment, + label="installed retired-lockout route", + ) + if "Observed lifecycle state: retired_lockout" not in retired_result.stdout: + raise ReleaseCheckError( + "installed retired-lockout route omitted the expected lifecycle state" + ) + if tree_digest(retired) != retired_before: + raise ReleaseCheckError( + "installed retired-lockout route changed target bytes" + ) + + draft = workspace / "draft-unratified-project" + draft_governance = draft / "governance" + draft_decisions = draft_governance / "decisions" + draft_decisions.mkdir(parents=True) + for name in ("PLAN.md", "STATE.md", "ROUTING.md"): + (draft_governance / name).write_text( + f"# {name}\n", encoding="utf-8", newline="\n" + ) + (draft_decisions / "DR-001.md").write_text( + DRAFT_ADOPTION_RECORD, encoding="utf-8", newline="\n" + ) + (draft_decisions / "DR-999-unrelated.md").write_text( + UNRELATED_RATIFIED_DECISION, encoding="utf-8", newline="\n" + ) + (draft_governance / "history").mkdir() + (draft_governance / "history" / "WO-001.md").write_text( + "---\nid: WO-001\nstatus: CLOSED\n---\n", encoding="utf-8", newline="\n" + ) + draft_before = tree_digest(draft) + draft_result = run( + [str(command), "start", "--project-root", str(draft)], + cwd=workspace, + environment=environment, + label="installed draft-adoption-record regression route", + ) + if "Observed lifecycle state: adopted_lockout" in draft_result.stdout: + raise ReleaseCheckError( + "installed draft-adoption-record regression reported adopted_lockout" + ) + if "Observed lifecycle state: retired_lockout" in draft_result.stdout: + raise ReleaseCheckError( + "installed draft-adoption-record regression reported retired_lockout" + ) + if tree_digest(draft) != draft_before: + raise ReleaseCheckError( + "installed draft-adoption-record regression changed target bytes" + ) + + unrelated = workspace / "unrelated-signed-decision-project" + unrelated_governance = unrelated / "governance" + unrelated_decisions = unrelated_governance / "decisions" + unrelated_decisions.mkdir(parents=True) + for name in ("PLAN.md", "STATE.md", "ROUTING.md"): + (unrelated_governance / name).write_text( + f"# {name}\n", encoding="utf-8", newline="\n" + ) + (unrelated_decisions / "DR-001.md").write_text( + UNRELATED_RATIFIED_DECISION, encoding="utf-8", newline="\n" + ) + unrelated_before = tree_digest(unrelated) + unrelated_result = subprocess.run( + [str(command), "start", "--project-root", str(unrelated)], + cwd=workspace, + env=environment, + capture_output=True, + text=True, + timeout=180, + ) + if unrelated_result.returncode == 0: + raise ReleaseCheckError( + "installed coordinator did not fail closed for a signed but " + "unrelated document at the exact adoption-record path" + ) + unrelated_output = unrelated_result.stdout + unrelated_result.stderr + if "adopted_lockout" in unrelated_output or "retired_lockout" in unrelated_output: + raise ReleaseCheckError( + "installed coordinator reported a lockout state for a signed " + "but unrelated document at the exact adoption-record path" + ) + if tree_digest(unrelated) != unrelated_before: + raise ReleaseCheckError( + "installed unrelated-signed-decision regression changed target bytes" + ) + + worktree_root = workspace / "worktree-project" + worktree_root.mkdir() + (worktree_root / ".git").mkdir() + nested = worktree_root / "nested" / "workspace" + nested.mkdir(parents=True) + worktree_before = tree_digest(worktree_root) + worktree_result = subprocess.run( + [str(command), "start", "--project-root", str(nested)], + cwd=workspace, + env=environment, + capture_output=True, + text=True, + timeout=180, + ) + if worktree_result.returncode == 0: + raise ReleaseCheckError( + "installed coordinator did not stop for a directory nested " + "inside a Git worktree" + ) + worktree_output = worktree_result.stdout + worktree_result.stderr + if "worktree" not in worktree_output.lower(): + raise ReleaseCheckError( + "installed coordinator's nested-worktree stop omitted a " + "worktree diagnostic" + ) + if (nested / ".writwall-bootstrap").exists(): + raise ReleaseCheckError( + "installed coordinator's nested-worktree stop published a bootstrap" + ) + if tree_digest(worktree_root) != worktree_before: + raise ReleaseCheckError( + "installed coordinator's nested-worktree stop changed target bytes" + ) + verify_candidate_unchanged(candidate, before) print("OK: coordinator release candidate passed") print(f" installed version : {expected_version}") print(" installed command : help and real start passed under normal bytecode behavior") + print(" conversation-first: bare installed start produced the Architect handoff") print(" complete handoff : all required packets present; no bytecode residue") - print(" adopted lockout : fresh Architect route; zero target-byte change") + print(" canonical root : installed coordinator recorded the resolved project root") + print(" adopted lockout : fresh General route; 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") + print(" path fails closed; zero target-byte change") + print(" nested worktree : installed coordinator stops with a worktree diagnostic") print(" candidate unchanged: complete-tree digest preserved") diff --git a/docs/architect-interview.md b/docs/architect-interview.md index 7742ebf..481d3e3 100644 --- a/docs/architect-interview.md +++ b/docs/architect-interview.md @@ -1,35 +1,67 @@ # Architect interview -`writwall start` is the idea-first front door. It may begin with an unnamed -idea or a supplied brief. A supplied project or command name is always a -`working_candidate`; generated files never make it canonical, available, -cleared, or accepted. - -The deterministic coordinator asks one question at a time and records the -problem or opportunity, intended user, why the outcome matters, current -evidence and assumptions, smallest useful outcome, success signal, constraints, +`writwall start` 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 +few recent commit subjects, and top-level project-relative names); the +Architect begins read-only, uses that evidence before asking the Owner to +restate anything already visible in repository bytes, summarizes what it +found, 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," 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. + +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 +coordinator asks one question at a time and records the problem or +opportunity, intended user, why the outcome matters, current evidence and +assumptions, smallest useful outcome, success signal, constraints, non-goals, material risks, stop or kill conditions, existing assets, -repository/runtime/deployment environment, preferred agent/interface, external -systems and Operators, and optional Owner-time capture. These answers remain -unratified until the Owner explicitly accepts, rejects, or revises them. +repository/runtime/deployment environment, preferred agent/interface, +external systems and Operators, and optional Owner-time capture. These +answers remain unratified until the Owner explicitly accepts, rejects, or +revises them. The output separates adaptive judgment from mechanics: -- `discovery.json` is the complete unratified qualification record; -- `OWNER-AGENT.md` is the exact prompt for continued adaptive architecture; -- `REPOSITORY-OPERATOR.md` is inert until a ratified plan and active work order; +- `discovery.json` is the complete unratified qualification record. In the + conversation-first default it also carries `local_observations` (bounded + local evidence, kept separate from Owner-supplied statements) and + `unresolved_questions`; +- `ARCHITECT.md` is the exact prompt for continued adaptive architecture: it + owns pre-adoption discovery and later design-conformance judgment. Before + requesting promotion into adoption mechanics, the Architect returns a + concise project sketch, a recommended Owner/Architect/General/Operator + topology, a provisional first backlog, key uncertainties and risks, and + one explicit Owner promotion decision. If the idea stays exploratory or is + rejected, no adoption, work order, or construction control is created; +- `GENERAL.md` is the exact prompt for post-adoption continuity: preparing + bounded dispatch, routing work, and performing only explicitly authorized + recorder mechanics; +- `OPERATOR.md` is inert until a ratified plan and active work order; +- `OWNER-AGENT.md` and `REPOSITORY-OPERATOR.md` are compatibility aliases + for `ARCHITECT.md` and `OPERATOR.md`, kept for existing consumers of those + two filenames; - `REVIEWER.md` keeps fresh review separate from implementation; - `NAME-CLEARANCE.md` routes the canonical seven-source evidence process; and - `OWNER-RATIFICATION.md` is the explicit stop before implementation. +Every one of these packets, plus `discovery.json` itself, carries the same +one resolved canonical project root; see +[`day-zero-coordinator.md`](day-zero-coordinator.md#canonical-project-root) +for the invariant and its Git-worktree handling. + Topology is deterministic advice. Local-only work receives the smallest local role set. External account boundaries add separately bounded Operator packets. Production, identity, DNS, or mail boundaries receive a high-impact topology whose operation packets retain independent preconditions, authority, verification, rollback, credential boundaries, and returned evidence. One -agent may act sequentially as coordinator and repository Operator on a small -project only in separate sessions after Owner ratification. The human Owner -and a fresh non-implementing Reviewer remain separate roles. +agent may act sequentially as Architect and Operator on a small project only +in separate sessions after Owner ratification. The human Owner and a fresh +non-implementing Reviewer remain separate roles. Command names and aliases are identity surfaces. Before adding an alias, record collision evidence through the same inception name-clearance route; do not diff --git a/docs/day-zero-coordinator.md b/docs/day-zero-coordinator.md index 1898c21..9d990cb 100644 --- a/docs/day-zero-coordinator.md +++ b/docs/day-zero-coordinator.md @@ -27,13 +27,20 @@ py -3 scripts/start_writwall.py --project-root C:\path\to\your-project python3 scripts/start_writwall.py --project-root /path/to/your-project ``` -The command asks one question at a time. Its first question offers Owner-time -capture and tells you to start the timer before answering. It then offers an -existing brief-file path before asking you to restate the project purpose and -recommends the smallest credible role split before external functions are -assigned. Do not enter passwords, API tokens, private keys, mailbox contents, -DNS record values, or other secrets. The answers are stored as ordinary local -text. +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 +defaults, and hands the result to a fresh Architect. Do not enter passwords, +API tokens, private keys, mailbox contents, DNS record values, or other +secrets in any explicit flag you do supply; answers are stored as ordinary +local text. + +Add `--structured-intake` to run the former full questionnaire instead: it +asks one question at a time. Its first question offers Owner-time capture +and tells you to start the timer before answering. It then offers an +existing brief-file path before asking you to restate the project purpose, +and recommends the smallest credible role split before external functions +are assigned. Before publishing the bootstrap handoff, the coordinator creates or refreshes the target's managed privacy screen outside the repository. It seeds exact @@ -71,15 +78,63 @@ 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 | Adoption coordinator | Launcher returns; coordinator later stops at closeout | Create-only bootstrap may be added | +| `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 | | Same command | Partial `.writwall-bootstrap/` or Writwall-shaped material | Recovery coordinator | Incomplete adoption or locked session | Unchanged | -| Same command | Adopted or retired lockout | Owner-Agent / Project-Architect | Onboarding coordinator or prior work session | Unchanged | -| Same command | Exact pointer to the only `status: ACTIVE` order | Bounded Implementer | Prior coordinator or Implementer context | 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 | Repository bytes are authoritative for this observation. A prior chat message or remembered work-order name is not lifecycle state. +### Conversation-first opening + +For a clean/new target, the ordinary (non-`--structured-intake`, +non-`--non-interactive`) invocation gathers a deterministic, local-only, +bounded inventory before handing off: whether a Git repository is present, +its current branch, a best-effort clean/dirty label (only when a `git` +executable is on PATH; left unobserved otherwise), a few recent commit +subjects read from the local reflog, and a one-level, project-relative +listing of top-level entries — never file contents, never a path outside +the resolved project root, and never a linked-worktree `gitdir:` pointer +followed outside that root. Common dependency/build/cache directories are +skipped. + +If that inventory finds existing work, the Architect opening begins +read-only, uses the recorded evidence before asking the Owner to restate +anything already visible in repository bytes, summarizes the apparent +project in plain language, and asks whether the Owner wants to explore that +work or start elsewhere. If the target is genuinely empty, the opening +imposes no fixed question list and reads exactly: "Tell me what you are +thinking." Neither observation is ratified intent; `discovery.json` records +it under `local_observations`, kept separate from Owner-supplied statements, +alongside a deterministic, explicitly `unratified_recommendation` topology +and a short `unresolved_questions` list. + +Adopted and retired lockout require affirmative, internally consistent +ratification evidence from `governance/decisions/DR-001.md` or the supported +alternate `governance/ADOPTION-RECORD.md` path, not merely one of those paths +existing, and not merely carrying a Signature, an Owner, and a date -- a +signed document that is not an adoption record at all does not qualify +either. The coordinator requires each candidate to carry an Appendix D +adoption-record title, all of sections D.1 through D.9, a concrete D.2 +baseline commit (a 7-40 character hex identifier) with adoption-effective +language, a D.3 Doctrine revision, and a Signature section with an Owner +attribution and a date, with no unresolved `- [ ]` outstanding-checklist +item and no draft/proposed/pending/unsigned/placeholder status signal in the +fields where ratification status actually lives (its headings, D.1, D.3, and +Signature). Closed work-order history alone never establishes adoption, and +an unrelated ratified decision -- whether at a different path or, this +being the specific defect this classifier closes, sitting at the exact +adoption-record path itself -- never satisfies it. An explicit draft/ +unsigned candidate routes to recovery; a candidate that is signed but not a +recognizable, complete adoption record fails closed with a diagnostic that +names only the path, never its contents, instead of reporting adopted or +retired lockout. When both adoption-record paths coexist, the coordinator +accepts them only if both are ratified and agree on baseline, Doctrine +revision, and Owner; otherwise it fails closed. + ## What it creates Only clean/new mode performs intake, initializes privacy, or creates target @@ -88,12 +143,21 @@ same filesystem and publishes it with one atomic directory rename; a caught pre-publication failure verifies that the stage is gone and leaves no target output. On success that temporary, create-only directory contains: -- `HANDOFF.md`: observed state, role routing, exact next prompt, authority - boundaries, and optional Owner-time instructions; +- `HANDOFF.md`: observed state, local evidence (conversation-first mode + only), role routing, exact next prompt, authority boundaries, and optional + Owner-time instructions; - `intake.json`: machine-readable, explicitly unratified answers; -- `discovery.json`: complete idea qualification and deterministic unratified topology; -- `OWNER-AGENT.md`, `REPOSITORY-OPERATOR.md`, and `REVIEWER.md`: exact, - separately bounded role prompts; +- `discovery.json`: local observations, Owner-supplied statements, + unresolved questions, and a deterministic unratified topology + recommendation, kept in separate, explicitly labeled sections; +- `ARCHITECT.md`, `GENERAL.md`, and `OPERATOR.md`: the primary, exact, + separately bounded role prompts for pre-adoption discovery and + design-conformance judgment, post-adoption continuity and dispatch, and + bounded work-order execution, respectively; +- `OWNER-AGENT.md` and `REPOSITORY-OPERATOR.md`: compatibility aliases for + `ARCHITECT.md` and `OPERATOR.md`, kept for existing consumers of those two + filenames; +- `REVIEWER.md`: a fresh, separate review function; - `NAME-CLEARANCE.md` and `OWNER-RATIFICATION.md`: identity and Owner gates; - `writwall-adopt/`: the complete local adoption skill bundle; - `operations/*.md`: inert packet scaffolds for separately named external @@ -108,21 +172,56 @@ is removed before the adoption commit. The command does not modify an existing project file, register a hook, create `.claude/active-wo.txt`, or claim the project has adopted Writwall. -## Roles and external systems +## Canonical project root + +The coordinator resolves the Owner-supplied target directory once and records +that one resolved, absolute path as the canonical project root in +`intake.json`, `discovery.json`, `HANDOFF.md`, every Architect/Operator/ +Reviewer/ratification/name-clearance packet, and every external-Operator +packet. A portable relative path or a bare `.` is never a substitute for this +identity: a receiving agent must be able to locate the one real project +directory from any single generated file, not from the working directory an +agent happens to be launched in. + +If the supplied directory sits inside a Git worktree, the coordinator +discovers the worktree top level (from the `.git` entry itself, without +requiring a `git` executable) and uses it only when the supplied path *is* +that top level. A directory nested inside a worktree, but not its top level, +stops before any privacy or bootstrap write with a diagnostic naming the +discovered root; rerun with `--project-root` set to that exact directory. A +non-Git directory remains fully supported and is recorded canonically as +supplied. + +Durable project artifacts — governance records, source, plans, reports, and +work orders — belong under this one canonical root. Three other kinds of +storage are explicitly not that: the OS-local privacy screen (per-user state +outside the repository, location never disclosed); this bootstrap directory +and any other temporary staging (bounded evidence or atomic publication +bytes, removed after use); and an external Operator's own evidence staging +(returned sanitized evidence is incorporated under the canonical root by the +General or a separately authorized repository Operator, never a shadow +repository the external Operator creates on its own). -The human remains Owner. The Owner-Agent coordinates, drafts, routes, records -ratified decisions, and performs exactly authorized lifecycle mechanics. A -repository Operator works under one active work order. An infrastructure, -DNS, mail, deployment, or other external Operator receives a bounded packet -and returns evidence; it remains outside the repository wall unless it edits -repository bytes. A fresh Reviewer checks the relevant order, result, report, -and returned evidence. +## Roles and external systems -The Owner-Agent keeps the proverbial keys—authority and routing—not literal -passwords or cryptographic material. External packets separate preconditions, -permitted and prohibited actions, verification, rollback, evidence, and -credential handling. Blank packet fields authorize nothing. -An operation-packet scaffold confers no authority by itself. +The human remains Owner. The **Architect** owns pre-adoption discovery and +later design-conformance judgment: it interviews, drafts, routes, and +performs exactly authorized lifecycle mechanics. After adoption, a fresh +**General** owns continuity — preparing bounded dispatch, routing work, and +performing only explicitly authorized recorder mechanics — without ratifying +intent or judging design conformance itself. An **Operator** works only +under one active, Owner-ratified work order. An infrastructure, DNS, mail, +deployment, or other external Operator receives a bounded packet and returns +evidence; it remains outside the repository wall unless it edits repository +bytes. A fresh **Reviewer** checks the relevant order, result, report, and +returned evidence. `OWNER-AGENT.md` and `REPOSITORY-OPERATOR.md` remain as +compatibility aliases for the Architect and Operator packets. + +The Architect and General keep the proverbial keys—authority and +routing—not literal passwords or cryptographic material. External packets +separate preconditions, permitted and prohibited actions, verification, +rollback, evidence, and credential handling. Blank packet fields authorize +nothing. An operation-packet scaffold confers no authority by itself. ## Owner active minutes @@ -132,13 +231,13 @@ candidate. Count human reading, deciding, responding, authentication, and unavoidable UI work. Exclude agent execution and waiting. If capture is declined, the handoff records `NOT REPORTED`; no later agent reconstructs it. -For adopted/retired lockout, the printed Project-Architect prompt begins -read-only, derives lifecycle from bytes, keeps detailed packets behind a concise -recommendation and material tradeoff, and requests one combined disposition and -safe next action. If that action requires a new user-owned task, the request -explicitly includes creation and dispatch. Approval triggers every mechanically -available authorized step without a redundant permission turn; it never erases -Owner ratification or fresh review. +For adopted/retired lockout, the printed General prompt begins read-only, +derives lifecycle from bytes, keeps detailed packets behind a concise +recommendation and material tradeoff, and requests one combined disposition +and safe next action. If that action requires a new user-owned task, the +request explicitly includes creation and dispatch. Approval triggers every +mechanically available authorized step without a redundant permission turn; +it never erases Owner ratification or fresh review. The manual routes remain in [`START-HERE.md`](../START-HERE.md) and [`ADOPTING.md`](../ADOPTING.md) for environments without a supported Python diff --git a/governance/LOG-denials.jsonl b/governance/LOG-denials.jsonl index 6bd992e..e7aa2d2 100644 --- a/governance/LOG-denials.jsonl +++ b/governance/LOG-denials.jsonl @@ -318,3 +318,12 @@ {"schema":1,"timestamp":"2026-09-03T00:33:46Z","session_id":"a00059e2-867d-473d-a3e0-4ba015b2ec43","tool":"Glob","surface":"filesystem.read","work_order":"governance/work-orders/WO-WW-018-python-314-contention-test-race.md","decision":"deny","reason_code":"read_traversal_denied","reason":"Read traversal could reach a grant.filesystem.read.deny subtree."} {"schema":1,"timestamp":"2026-09-03T00:33:46Z","session_id":"a00059e2-867d-473d-a3e0-4ba015b2ec43","tool":"Glob","surface":"filesystem.read","work_order":"governance/work-orders/WO-WW-018-python-314-contention-test-race.md","decision":"deny","reason_code":"read_traversal_denied","reason":"Read traversal could reach a grant.filesystem.read.deny subtree."} {"schema":1,"timestamp":"2026-09-03T00:33:56Z","session_id":"a00059e2-867d-473d-a3e0-4ba015b2ec43","tool":"Write","surface":"filesystem.write","work_order":"governance/work-orders/WO-WW-018-python-314-contention-test-race.md","decision":"deny","reason_code":"write_target_out_of_grant","reason":"Write target is outside grant.filesystem.write."} +{"schema":1,"timestamp":"2026-09-03T12:35:05Z","session_id":"e491849f-a5ba-41d8-bddc-31053b8d172c","tool":"Write","surface":"filesystem.write","work_order":"governance/work-orders/WO-WW-019-ratified-adoption-lifecycle-classification.md","decision":"deny","reason_code":"write_target_out_of_grant","reason":"Write target is outside grant.filesystem.write."} +{"schema":1,"timestamp":"2026-09-03T14:11:21Z","session_id":"4dcc4f4b-45a1-4b93-b1d5-ffccb7c9ad89","tool":"Bash","surface":"shell.execute","work_order":"governance/work-orders/WO-WW-019-ratified-adoption-lifecycle-classification.md","decision":"deny","reason_code":"control_plane_channel_uninspectable","reason":"Mutation-capable channel cannot prove protected control-plane targets remain unchanged."} +{"schema":1,"timestamp":"2026-09-03T15:59:08Z","session_id":"502e1ab5-630d-442c-a6b8-177de0d4e2ef","tool":"Write","surface":"filesystem.write","work_order":"governance/work-orders/WO-WW-020-canonical-project-root-enforcement.md","decision":"deny","reason_code":"write_target_out_of_grant","reason":"Write target is outside grant.filesystem.write."} +{"schema":1,"timestamp":"2026-09-03T15:59:11Z","session_id":"502e1ab5-630d-442c-a6b8-177de0d4e2ef","tool":"Bash","surface":"shell.execute","work_order":"governance/work-orders/WO-WW-020-canonical-project-root-enforcement.md","decision":"deny","reason_code":"control_plane_channel_uninspectable","reason":"Mutation-capable channel cannot prove protected control-plane targets remain unchanged."} +{"schema":1,"timestamp":"2026-09-03T16:01:14Z","session_id":"cf657be8-69d1-4835-b911-028f48187e16","tool":"Read","surface":"filesystem.read","work_order":"governance/work-orders/WO-WW-020-canonical-project-root-enforcement.md","decision":"deny","reason_code":"read_target_outside_repository","reason":"Read target could not be resolved inside the repository."} +{"schema":1,"timestamp":"2026-09-03T16:14:27Z","session_id":"cc17d072-1275-4f14-9edb-c9fc2c9866c1","tool":"Grep","surface":"filesystem.read","work_order":"governance/work-orders/WO-WW-020-canonical-project-root-enforcement.md","decision":"deny","reason_code":"read_traversal_denied","reason":"Read traversal could reach a grant.filesystem.read.deny subtree."} +{"schema":1,"timestamp":"2026-09-03T16:16:37Z","session_id":"cc17d072-1275-4f14-9edb-c9fc2c9866c1","tool":"Glob","surface":"filesystem.read","work_order":"governance/work-orders/WO-WW-020-canonical-project-root-enforcement.md","decision":"deny","reason_code":"read_traversal_denied","reason":"Read traversal could reach a grant.filesystem.read.deny subtree."} +{"schema":1,"timestamp":"2026-09-03T16:18:05Z","session_id":"cc17d072-1275-4f14-9edb-c9fc2c9866c1","tool":"Bash","surface":"shell.execute","work_order":"governance/work-orders/WO-WW-020-canonical-project-root-enforcement.md","decision":"deny","reason_code":"control_plane_channel_uninspectable","reason":"Mutation-capable channel cannot prove protected control-plane targets remain unchanged."} +{"schema":1,"timestamp":"2026-09-03T17:47:14Z","session_id":"a3f59b20-0c4d-47bf-b134-02896bb085d7","tool":"Write","surface":"filesystem.write","work_order":"governance/work-orders/WO-WW-021-conversation-first-inception-and-existing-project-continuity.md","decision":"deny","reason_code":"write_target_out_of_grant","reason":"Write target is outside grant.filesystem.write."} diff --git a/governance/LOG.md b/governance/LOG.md index 8e1efa3..6e08251 100644 --- a/governance/LOG.md +++ b/governance/LOG.md @@ -970,6 +970,107 @@ post-closeout projection, review, and protected-CI pull request. The separate External Pilot B lifecycle-classification defect is queued after that merge; hllmr-site remains untouched. +### Post-pilot WO-WW-019 completed record + +WO-WW-019 is post-pilot and does not add an eleventh metrics row or change the +accepted ten-order aggregate. The Owner accepted it on 2026-09-03 and reported +active minutes **NOT REPORTED**. + +The coordinator now distinguishes complete affirmative Appendix D adoption +evidence from filenames, closed history, draft records, and unrelated signed +decisions. Genuine established and alternate-path records remain compatible; +coexisting records must agree on baseline, Doctrine revision, and Owner. +Malformed evidence fails closed with a useful nondisclosing structural reason. +The installed-wheel gate exercises adopted, retired, draft, and unrelated +exact-path cases while preserving candidate and target bytes. + +The live canary added record 321 and no out-of-grant mutation succeeded. Seven +focused regressions, all 51 coordinator tests, and all 18 release tests passed. +A clean prospective Windows tree passed 761 tests with two skips plus identity, +distribution, licensing, dispatch, and whitespace gates. The repository's own +ratified record classified as `retired_lockout` with zero byte change. Fresh +Claude Sonnet review returned **ACCEPT — HIGH confidence** after an initial +control-flow finding was disproved by the exact passing mutation test and +explicitly retracted. + +Accepted deviations are preserved in the report: strict RED chronology was not +independently observed; the complete-suite and self-classification commands +traversed routed historical/archive fixtures without exposing their contents; +and Windows packaging tests used an existing disposable build environment with +no dependency download. Owner acceptance authorized ordinary closeout, one +private commit/push, and subsequent activation of WO-WW-020. No publication, +release, tag, deployment, visibility change, or External Pilot B mutation +occurred under WO-WW-019. + +### Post-pilot WO-WW-020 completed record + +WO-WW-020 is post-pilot and does not add an eleventh metrics row or change the +accepted ten-order aggregate. The Owner accepted it on 2026-09-03 and reported +active minutes **NOT REPORTED**. + +Generated repository, role, handoff, and external-Operator packets now carry +one resolved canonical project root and state that durable project records stay +below it. OS-local privacy state and temporary evidence staging remain distinct. +Supplying a nested path inside a Git worktree stops before privacy/bootstrap +writes and identifies the exact root for a rerun; non-Git and exact worktree-root +invocations remain compatible. + +Nine focused RED assertions failed on unchanged product bytes and passed after +implementation. Windows and native Ubuntu installed-wheel coordinator/release +sets each passed 80 tests. The Ubuntu run installed only Owner-authorized +setuptools 84.0.0 inside one disposable environment; the environment and +temporary mount were deleted and verified absent. Fresh Claude Sonnet review +returned **ACCEPT — medium-high confidence** with no substantive correctness, +security, public-path-leak, or dual-tree blocker. + +The first closeout suite caught one omitted mechanically dependent projected- +State digest refresh in three projection tests. After correcting that single +ledger field, the three tests passed and the complete Windows closeout suite +passed 772 tests with two expected skips; lockout, identity, distribution, +licensing, and whitespace gates passed on the same record state. + +Records 323-328 contain the valid canary and five additional denied agent calls; +none produced its requested mutation. Accepted deviations and two non-blocking +follow-ups are preserved in the report. External Pilot B was neither accessed +nor resumed. No publication, release, tag, deployment, visibility change, or +external-project mutation occurred under WO-WW-020. + +### Post-pilot WO-WW-021 completed record + +WO-WW-021 is post-pilot and does not add an eleventh metrics row or change the +accepted ten-order aggregate. The Owner accepted it on 2026-09-03 and reported +active minutes **NOT REPORTED**. + +The ordinary `writwall start --project-root` route is now conversation-first. +An existing Git project receives bounded branch, cleanliness, recent-subject, +and top-level-name observations; an empty project receives one open invitation. +No recursive content dump, network access, or outside-root inspection is part +of discovery. Structured intake and non-interactive automation remain available. +Generated packets now present the Owner / Architect / General / Operator / +Reviewer topology while retaining explicit compatibility aliases. + +Initial RED produced 9/9 failures. Reviewer-directed validation and topology +rework added two further RED slices, and a later review exposed that the +installed-wheel gate did not execute the bare conversation-first command. A +final public-interface RED/GREEN slice added that exact installed command with +closed stdin and preserved the established failure diagnostic. Final Windows +and native Ubuntu affected/installed-wheel sets each passed 90/90. Fresh final +Sonnet review returned **ACCEPT** with no substantive or record-only defect. + +Denial record 329 is the valid excluded-target canary; no forbidden mutation +succeeded. The report preserves four bounded native-test orchestration +diagnostics, the host-Python build-backend limitation, and the accepted +environment lifecycle. Every disposable environment and temporary mount was +deleted and verified absent. No publication, public projection, public pull +request, release, tag, deployment, visibility change, or external-project +mutation occurred under WO-WW-021. + +The first complete closeout suite caught one stale mechanically dependent +projected-State digest in three projection tests. After refreshing that one +ledger field, those tests passed 3/3 and the complete suite passed 782 tests +with two expected skips. Lockout, identity, distribution, licensing, and +whitespace gates passed on the same final record bytes. + --- ## Column definitions diff --git a/governance/PLAN.md b/governance/PLAN.md index a470d98..276223c 100644 --- a/governance/PLAN.md +++ b/governance/PLAN.md @@ -724,32 +724,55 @@ authorized correction rather than an unexplained rerun. or product assertions. Native Windows Python 3.14 focused tests and 20 stress iterations passed; a clean prospective tree passed 753 tests with two skips and every repository gate. Fresh independent execution returned **ACCEPT - WITH NON-BLOCKING POLISH / high confidence**. Resume PR #26 only after the - complete required matrix passes. -2. **WO-WW-019 — External Pilot B lifecycle classification.** A clean external - pilot with draft/unratified adoption records was classified as retired - lockout. Require evidence of ratification, preserve clean/new and recovery - compatibility, and fail closed on contradictory records before that pilot - resumes. -3. **WO-WW-020 — Canonical project-root enforcement.** Generated repository and - external-Operator packets must name the discovered canonical repository root, - prohibit durable project artifacts in user/temp/bootstrap locations, and - distinguish external evidence staging from project records. Correct every - route that can otherwise create dual-repository or synchronization drift. -4. **WO-WW-021 — Conversation-first inception and existing-project continuity.** - Preserve the current CLI and lifecycle routes while adding a low-friction - discovery stage. For an existing project, begin with a read-only high-level - inventory of current documents, code, history, and recorded reasoning; - summarize the apparent project and ask whether to explore that work or start - elsewhere. Do not make the Owner re-enter facts the repository already - supports. For a new or unnamed idea, begin with one open invitation and let - adaptive discovery continue as long as useful. Before governance mechanics, - return a concise project sketch, recommended topology, and provisional first - backlog. Nothing in discovery is ratified authority; adoption and the first - executable work order begin only after one explicit promotion decision. + WITH NON-BLOCKING POLISH / high confidence**. Two independent 136-file + projections were byte-identical, fresh publication review returned ACCEPT, + and all fifteen public CI matrix jobs passed, including Windows/Python 3.14. + Public PR #26 merged at `92846c03f60303efd9f0e2fc14a99bbca2e1daea`; + issues #18, #19, and #27 are closed. +2. **WO-WW-019 — COMPLETE: External Pilot B lifecycle classification.** A clean + external pilot with draft/unratified adoption records was classified as + retired lockout. The accepted correction now requires complete affirmative + Appendix D evidence, preserves established and alternate ratified records, + rejects unrelated/malformed/contradictory evidence, and exercises the rule + through the installed-wheel release gate. Public issue #28 remains the + sanitized defect record; External Pilot B stays frozen pending separate + authorization after the remaining coordinator corrections. +3. **WO-WW-020 — COMPLETE, accepted 2026-09-03: Canonical project-root + enforcement.** Generated repository and external-Operator packets name one + resolved canonical root and prohibit shadow repositories and durable temp + state. Nested Git-worktree paths stop before bootstrap mutation; non-Git, + linked-worktree-root, and lifecycle routes remain compatible. Windows and + native Ubuntu installed-wheel sets each passed 80/80; fresh review returned + ACCEPT with no substantive blocker; the final closeout suite passed 772 + tests with two expected skips. +4. **WO-WW-021 — COMPLETE, accepted 2026-09-03: Conversation-first inception + and existing-project continuity.** The ordinary installed command now opens + a nonblocking Architect conversation from one project root. Existing Git + repositories receive bounded high-level observations; empty targets open + with one unconstrained invitation. Structured and non-interactive routes + remain compatible. Owner / Architect / General / Operator / Reviewer roles, + lifecycle routing, canonical-root controls, and the installed-wheel release + gate are synchronized. Final Windows and native Ubuntu affected sets each + passed 90/90, the closeout suite passed 782 tests with two expected skips, + and fresh review returned ACCEPT with no remaining defect. The conversational path is not capped at three questions or any fixed turn count. Fast first value is the target; useful inquiry may continue. Existing `writwall start --project-root ...` automation and lifecycle-aware behavior remain backward-compatible. The public on-ramp must make the simple path obvious without removing the detailed manual and recovery routes. + +## 28. Conversation-first public release — 2026-09-03 + +**WO-WW-022 — COMPLETE, accepted 2026-09-03: publish v0.10.0.** Accepted +WO-WW-019 through +WO-WW-021 as the first public conversation-first release. Advance current +release identity coherently to `0.10.0` / `v0.10.0`, preserve historical +release truth, pass Windows and native Ubuntu installed-candidate gates, build +two byte-identical privacy-screened public candidates, obtain fresh review, +and publish only through protected public CI. Final source and candidate suites +passed; a first fresh review correctly returned generated cache residue, and a +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. diff --git a/governance/STATE.md b/governance/STATE.md index 7ec486b..102577d 100644 --- a/governance/STATE.md +++ b/governance/STATE.md @@ -22,9 +22,13 @@ identity correction, accepted WO-WW-012 installed bootstrap-bundle completeness correction, accepted WO-WW-013 post-closeout projection-reference truth, accepted WO-WW-014 public current-record host-path privacy, accepted WO-WW-015 terminal Architect handoff and lifecycle-aware start routing, and accepted -WO-WW-016 release identity 0.9.3, and accepted WO-WW-017 post-release truth -and CI reliability. No work order is active during the closeout publication -tail. +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, @@ -46,7 +50,7 @@ website, DNS, or mail mutation is active. 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-017 closeout records, plus the observed +accepted WO-WW-001 through WO-WW-021 closeout records, plus the observed v0.9.3 publication. **Boundary:** post-adoption, all 10 counted pilot work orders and their evaluation complete; WO-PL-017 remediation complete; DR-003 ratified; @@ -59,7 +63,7 @@ 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-017 are COMPLETE and accepted**; **no work order is active**; public PR #9, +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 adopted successfully and closed two genuine work orders; public issue #24 is @@ -136,8 +140,11 @@ externally. | WO-WW-015 | **COMPLETE**, accepted 2026-09-01; lifecycle-aware `writwall start` now preserves clean/new intake while routing recovery, adopted/retired lockout, and active-work-order states to the correct fresh role with zero target-byte change. Adoption closes at an explicit fresh Project-Architect handoff using progressive disclosure and one combined approval/action gate. Windows passed 750 tests with two skips; native Ubuntu affected and installed-wheel gates passed; two 136-file candidates were checker-clean and byte-identical; fresh final re-review returned **ACCEPT — HIGH confidence**. Owner active minutes **NOT REPORTED**. Records retained in `governance/history/`; public issue #24 remains open pending separately authorized public projection/PR work | | WO-WW-016 | **COMPLETE**, accepted 2026-09-02; current package metadata, conditional install guidance, and executable release checks agree on `0.9.3` / `v0.9.3` without claiming the tag is published. Historical release facts remain unchanged. Windows passed 750 tests with two skips; native Ubuntu passed 17 release tests and the installed gate; two 136-file candidates were projection/release-clean and byte-identical; fresh review returned **ACCEPT — HIGH confidence**. Owner active minutes **NOT REPORTED**. Records retained in `governance/history/`; issue #24 proceeds through the authorized post-closeout public PR while tag and release remain unauthorized | | WO-WW-017 | **COMPLETE**, accepted 2026-09-02; published-release truth, bounded Windows privacy-profile replacement retry, and non-duplicating CI triggers are implemented. Windows passed 752 tests with two skips; the native Ubuntu affected set passed with one platform skip; fresh review returned **ACCEPT — HIGH confidence** after two rework cycles. Owner active minutes **NOT REPORTED**. The obsolete `dist/plumbline-0.6.zip` (private governed-source reference, not present in this candidate) was retired at closeout. Public issues #18 and #19 remain open pending the authorized public PR | -| WO-WW-018 | **COMPLETE**, accepted 2026-09-02; Python 3.14 contention-test signal publication and helper cleanup repaired without product-code change. Native Windows Python 3.14 focused tests and 20 stress iterations passed; a clean prospective tree passed 753 tests with two skips and all gates; independent public-clone review returned **ACCEPT WITH NON-BLOCKING POLISH / high confidence**. Owner active minutes **NOT REPORTED**. Public issue #27 and PR #26 proceed through protected CI | -| 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-018 COMPLETE and accepted**; no active work order; public PR #9, #12, #13, #17, and #21 merged with protected CI green; public issues #1, #4, #10, #11, #16, #20, and #24 closed; public issue #14 records the accepted release gate; `v0.9.3` is published; External Pilot A adopted and closed two genuine work orders; public issues #18, #19, and #27 remain open behind PR #26; lifecycle classification, canonical-root enforcement, and conversation-first onboarding are queued as WO-WW-019 through WO-WW-021; website and infrastructure pilots proceed under their own repositories and authority | +| WO-WW-018 | **COMPLETE**, accepted 2026-09-02; Python 3.14 contention-test signal publication and helper cleanup repaired without product-code change. Native Windows Python 3.14 focused tests and 20 stress iterations passed; a clean prospective tree passed 753 tests with two skips and all gates; independent public-clone review returned **ACCEPT WITH NON-BLOCKING POLISH / high confidence**. Owner active minutes **NOT REPORTED**. Two final 136-file projections were checker-clean and byte-identical; fresh publication review returned ACCEPT; public PR #26 passed all fifteen matrix jobs and merged at `92846c0`, closing issues #18, #19, and #27 | +| 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 | | Bootstrap history | Eleven completed work orders retained as uncounted pre-adoption evidence under `archive/pre-adoption-bootstrap/` | ### Verification accepted at WO-PL-016 closeout @@ -979,14 +986,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 | -| Queued work | Complete protected CI and merge PR #26; then WO-WW-019 lifecycle classification, WO-WW-020 canonical-root enforcement, and WO-WW-021 conversation-first inception/existing-project continuity; private pilots retain their own authority | +| 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 | | 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-018 is accepted and closed; PR #26 awaits a complete green CI matrix; hllmr-site remains frozen pending WO-WW-019** | +| 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** | --- diff --git a/identity/legacy-references.json b/identity/legacy-references.json index 76859e6..531647f 100644 --- a/identity/legacy-references.json +++ b/identity/legacy-references.json @@ -16,7 +16,7 @@ { "path": "README.md", "context": "migration_provenance", - "sha256": "d05596604edc06e5ac5c2502ecd61b945e0caa41fe63a39de66a3d58fa2e1e2b" + "sha256": "d76060c70c361a64b52e9df88e400789c24fbc4fecd2b731dfe8a709a34e5e5e" }, { "path": "SELF-HOSTING.md", @@ -94,19 +94,19 @@ { "path": "governance/LOG.md", "context": "historical_pilot_summary", - "sha256": "8a27c2cda1dbbb3d38ca03f1745352abb0ab07290d691dd4f063d3e274da425f", + "sha256": "0720409460b68d93ae72010abab3162d1cc09cd275ddae6fa309ef7fe7b6eef3", "projection_transform": "private_evidence_redaction" }, { "path": "governance/PLAN.md", "context": "ratified_historical_intent", - "sha256": "0a1cfae5dcd8d475a599fff8bb8c07c745f4baae63adb8def5ddbcad6cfb13ea" + "sha256": "020bfe95a6b70da6a14dcc63087f1d90f1625fd6c08454ff8951044e04a2b0be" }, { "path": "governance/STATE.md", "context": "mixed_current_state_and_history", - "sha256": "87ed1be87ddf391439baddcb2a1aef588387e813a3e42dc05e297384da806f0c", - "projection_sha256": "4ad60876ac2ff78f8eb212e0c4b54a42dd5c1131780a2b1add58f9367bcd0ee7" + "sha256": "d598988b8fa33ab64da1c0f8883948de9f619f8bb1816f2428d1b860b0241299", + "projection_sha256": "acea5eaf4e6dbee69d8e425c2848fd7f324fd67ea3a57f29a953efab46e28ce6" }, { "path": "governance/decisions/DR-001.md", diff --git a/pyproject.toml b/pyproject.toml index 955eb6b..449bf2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "writwall" -version = "0.9.3" +version = "0.10.0" description = "Start governed project work from an idea." requires-python = ">=3.10" license = "Apache-2.0" diff --git a/scripts/start_writwall.py b/scripts/start_writwall.py index 03e42ee..1714c51 100644 --- a/scripts/start_writwall.py +++ b/scripts/start_writwall.py @@ -12,6 +12,7 @@ import re import shutil import stat +import subprocess import sys import uuid from dataclasses import dataclass @@ -30,21 +31,50 @@ "record values, or other secrets. This tool writes your answers as plain " "text inside the target project." ) -PROJECT_ARCHITECT_PROMPT = """Act as a fresh Owner-Agent / Project-Architect. Begin read-only and verify -the lifecycle from repository bytes rather than prior chat. Read the charter, -Plan, State, Routing, ratified adoption record, and open transactional records. -State the project's next decision plainly. Draft, but do not activate or -implement, the smallest genuine work order or bounded external Operator packet. -Lead with a concise Recommendation and material tradeoff; keep the detailed -packet behind it as supporting evidence rather than the conversational front -door. When the next safe mechanical action is available, ask once for one -combined disposition and action. If that action uses a new user-owned task, -explicitly include creation and dispatch of the named task in that approval -request; never infer task-creation permission afterward. Once approved, perform -every mechanically available authorized step. Do not ask for the same decision again. -The human Owner alone ratifies intent and activates work; preserve a distinct -fresh review after implementation. The onboarding coordinator stops here and -does not continue into project work.""" +GENERAL_PROMPT = """Act as a fresh General for this already-adopted project's continuity. Begin +read-only and verify the lifecycle from repository bytes rather than prior chat. Read the +charter, Plan, State, Routing, ratified adoption record, and open transactional records. State +the project's next decision plainly. Prepare, but do not activate, the smallest genuine work +order or bounded Operator packet; route it to a fresh Architect instead only when the next +decision requires new design or design-conformance judgment rather than routine continuity. Lead +with a concise Recommendation and material tradeoff; keep the detailed packet behind it as +supporting evidence rather than the conversational front door. When the next safe mechanical +action is available, ask once for one combined disposition and action. If that action uses a new +user-owned task, explicitly include creation and dispatch of the named task in that approval +request; never infer task-creation permission afterward. Once approved, perform every +mechanically available authorized step. Do not ask for the same decision again. The human Owner +alone ratifies intent and activates work; preserve a distinct fresh Reviewer after +implementation. The onboarding coordinator stops here and does not continue into project work.""" + +# Compatibility export for existing imports and synchronized static handoff +# tests. The post-adoption role formerly called Project-Architect is now the +# General; retaining this symbol does not retain the obsolete role semantics. +PROJECT_ARCHITECT_PROMPT = GENERAL_PROMPT + +ARCHITECT_EXISTING_PROJECT_PROMPT = """Act as the Architect. Begin read-only; do not implement, install, or adopt +anything yet. Before asking the Owner to restate anything already visible in repository bytes, +use the observed lifecycle state and local evidence recorded above, and inspect other +high-signal local material (README-like files, top-level structure, and recent history) the same +way. Summarize the apparent project in plain language from that evidence alone. Then ask the +Owner plainly whether they want to explore and develop this existing work, or start elsewhere +with a different idea. Treat every local observation as evidence only, never as ratified intent; +the human Owner alone decides and ratifies. Read discovery.json and ARCHITECT.md in this +directory for the complete procedure, including the required project sketch, recommended +Owner/Architect/General/Operator topology, provisional first backlog, key uncertainties and +risks, and the one explicit Owner promotion decision before any adoption mechanics begin.""" + +ARCHITECT_EMPTY_PROJECT_PROMPT = """Act as the Architect for a new, empty project; no existing +project material was found at this root. Begin read-only and do not implement, install, or adopt +anything yet. Do not impose a fixed list of qualification questions. + +Open with exactly: "Tell me what you are thinking." + +Let the Owner's own words guide every question that follows, one at a time. Nothing said is +ratified intent until the human Owner explicitly ratifies it. Read discovery.json and +ARCHITECT.md in this directory for the complete procedure, including the required project +sketch, recommended Owner/Architect/General/Operator topology, provisional first backlog, key +uncertainties and risks, and the one explicit Owner promotion decision before any adoption +mechanics begin.""" DNS_MAIL_SCENARIO = ( "DNS provider selection", @@ -180,6 +210,154 @@ def _safe_pointer_target(project: Path, value: str) -> Path: return resolved_target +_ADOPTION_TITLE = re.compile(r"(?im)^#[ \t]+.*\bAdoption record\b") +_ADOPTION_DRAFT_MARKER = re.compile( + r"\b(?:DRAFT|PROPOSED|UNRATIFIED|PENDING|PLACEHOLDER|TBD|TODO|UNSIGNED)\b", + re.IGNORECASE, +) +_UNRESOLVED_CHECKLIST_ITEM = re.compile(r"(?m)^[ \t]*-[ \t]*\[[ \t]\]") +_ADOPTION_PLACEHOLDER_TOKEN = re.compile( + r"(?i)\[[ \t]*(?:date|owner|hash|name|insert|fill[- ]?in|tbd|todo|xxx)\b[^\]]*\]" +) +_SIGNATURE_HEADING = re.compile(r"(?im)^#{1,6}[ \t]+(?:[A-Z]+\.\d+[ \t]+)?Signature\b.*$") +_HEADING_LINE = re.compile(r"(?m)^#{1,6}[ \t]") +_SIGNATURE_DATE = re.compile(r"\b(?:19|20)\d{2}-\d{2}-\d{2}\b") +_ADOPTION_REVISION = re.compile(r"(?im)Revision[ \t]+\*{0,2}([0-9]+\.[0-9]+)\*{0,2}") +_ADOPTION_OWNER_FIELD = re.compile(r"(?im)^.*\bOwner:[ \t]*([^\n\r·]+)$") +_ADOPTION_BASELINE_HASH = re.compile(r"`([0-9a-fA-F]{7,40})`") +_ADOPTION_REQUIRED_SECTION_NUMBERS = tuple(range(1, 10)) + + +def _adoption_section_block(text: str, number: str) -> str: + """Return one Appendix D `D.` section's own text. + + Stops at the next heading whose `D.` prefix differs, so a nested + `D..x` subsection (as used by this repository's own D.4) stays + inside the block instead of truncating it. + """ + pattern = re.compile( + rf"(?ms)^#{{1,6}}[ \t]*D\.{re.escape(number)}\b.*?" + rf"(?=^#{{1,6}}[ \t]*D\.(?!{re.escape(number)}\b)\d|\Z)" + ) + match = pattern.search(text) + return match.group(0) if match else "" + + +@dataclass(frozen=True) +class AdoptionEvidence: + """One candidate adoption-record path and its deterministic ratification status.""" + relative: str + status: str + reason: str + revision: str | None = None + owner: str | None = None + baseline: str | None = None + + @property + def ratified(self) -> bool: + return self.status == "ratified" + + +def _classify_adoption_record(project: Path, resolved_path: Path) -> AdoptionEvidence: + """Require complete, affirmative Appendix D ratification evidence. + + A filename, a signed-but-unrelated document, or a partial Appendix D + section set is never adoption authority. An explicit draft/proposed/ + pending/unsigned/placeholder signal routes to recovery (status + "draft"); anything else at this exact adoption-record path that fails + to carry recognizable, complete Appendix D content is "malformed" and + the caller fails closed, naming only the path, never the contents. + """ + relative = resolved_path.relative_to(project).as_posix() + try: + text = resolved_path.read_text(encoding="utf-8-sig") + except (OSError, UnicodeError) as exc: + raise CoordinatorError(f"cannot read adoption record {relative}: {exc}") from exc + if not text.strip(): + return AdoptionEvidence(relative, "malformed", f"{relative} is empty") + + date_owner_block = _adoption_section_block(text, "1") + revision_block = _adoption_section_block(text, "3") + baseline_block = _adoption_section_block(text, "2") + signature = _SIGNATURE_HEADING.search(text) + if signature: + tail = text[signature.end():] + next_heading = _HEADING_LINE.search(tail) + signature_block = tail[: next_heading.start()] if next_heading else tail + else: + signature_block = "" + heading_lines = "\n".join(re.findall(r"(?m)^#{1,6}[ \t].*$", text)) + # Scope the draft/placeholder scan to structurally meaningful fields + # (headings, D.1, D.3, Signature) rather than the whole document, so a + # ratified record's own prose about *other*, unrelated draft/proposed + # material (e.g. an archived, never-ratified prior revision) is never + # mistaken for this record's own status. + draft_scope = "\n".join( + part for part in + (heading_lines, date_owner_block, revision_block, signature_block) + if part + ) + if _ADOPTION_DRAFT_MARKER.search(draft_scope): + return AdoptionEvidence( + relative, "draft", + f"{relative} carries an explicit draft/proposed/pending/unsigned status signal", + ) + if _UNRESOLVED_CHECKLIST_ITEM.search(text): + return AdoptionEvidence( + relative, "draft", + f"{relative} has an unresolved outstanding checklist item", + ) + if _ADOPTION_PLACEHOLDER_TOKEN.search(text): + return AdoptionEvidence( + relative, "draft", + f"{relative} contains an unfilled template placeholder", + ) + + if not _ADOPTION_TITLE.search(text): + return AdoptionEvidence( + relative, "malformed", + f"{relative} does not carry an Appendix D adoption-record title", + ) + missing_sections = [ + str(number) for number in _ADOPTION_REQUIRED_SECTION_NUMBERS + if not re.search(rf"(?m)^#{{1,6}}[ \t]*D\.{number}\b", text) + ] + if missing_sections: + return AdoptionEvidence( + relative, "malformed", + f"{relative} is missing required Appendix D section(s) D." + + ", D.".join(missing_sections), + ) + baseline_match = _ADOPTION_BASELINE_HASH.search(baseline_block) + if not baseline_match or "effective" not in baseline_block.lower(): + return AdoptionEvidence( + relative, "malformed", + f"{relative} D.2 lacks a concrete baseline commit and an " + "adoption-effective statement", + ) + revision_match = _ADOPTION_REVISION.search(revision_block) + if not revision_match: + return AdoptionEvidence( + relative, "malformed", + f"{relative} D.3 lacks a stated Doctrine revision", + ) + if not signature: + return AdoptionEvidence(relative, "malformed", f"{relative} has no Signature section") + if "owner" not in signature_block.lower() or not _SIGNATURE_DATE.search(signature_block): + return AdoptionEvidence( + relative, "malformed", + f"{relative} Signature section lacks an Owner attribution and date", + ) + owner_match = _ADOPTION_OWNER_FIELD.search(text) + return AdoptionEvidence( + relative, "ratified", + f"{relative} carries complete, ratified Appendix D adoption evidence", + revision_match.group(1), + owner_match.group(1).strip() if owner_match else None, + baseline_match.group(1).lower(), + ) + + def classify_project(project: Path) -> ObservedState: """Classify lifecycle state from repository bytes, never chat context.""" bootstrap = project / OUTPUT_NAME @@ -276,7 +454,7 @@ def reject_bootstrap_conflict(lifecycle: str) -> None: if not resolved_governance.is_dir(): raise CoordinatorError("inconsistent state: governance is not a directory") core = tuple(governance / name for name in ("PLAN.md", "STATE.md", "ROUTING.md")) - adoption_records = ( + adoption_paths = ( governance / "decisions" / "DR-001.md", governance / "ADOPTION-RECORD.md", ) @@ -295,33 +473,76 @@ def reject_bootstrap_conflict(lifecycle: str) -> None: if frontmatter_status(safe_path) in {"CLOSED", "COMPLETE"}: closed_records.append(safe_path) - for path in (*core, *adoption_records): + resolved_adoption_paths = [] + for path in (*core, *adoption_paths): if _entry_exists(path): resolved = _safe_project_path(project, path, "governance control file") if not resolved.is_file(): raise CoordinatorError( "inconsistent state: governance control path is not a file" ) + if path in adoption_paths: + resolved_adoption_paths.append(resolved) + + # 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 = [ + _classify_adoption_record(project, path) for path in resolved_adoption_paths + ] + malformed_evidence = [ + evidence for evidence in adoption_evidence if evidence.status == "malformed" + ] + if malformed_evidence: + reasons = "; ".join( + evidence.reason + for evidence in sorted(malformed_evidence, key=lambda item: item.relative) + ) + raise CoordinatorError( + "inconsistent state: candidate adoption record does not carry " + f"recognizable, complete Appendix D adoption evidence: {reasons}" + ) + ratified_evidence = [evidence for evidence in adoption_evidence if evidence.ratified] + if len(adoption_evidence) > 1: + if len(ratified_evidence) != len(adoption_evidence): + reasons = "; ".join(evidence.reason for evidence in adoption_evidence) + raise CoordinatorError( + "inconsistent state: multiple candidate adoption records coexist with " + f"incompatible ratification evidence: {reasons}" + ) + revisions = {evidence.revision for evidence in ratified_evidence} + owners = {evidence.owner for evidence in ratified_evidence} + baselines = {evidence.baseline for evidence in ratified_evidence} + if ( + len(revisions) != 1 or None in revisions + or len(owners) != 1 or None in owners + or len(baselines) != 1 or None in baselines + ): + names = ", ".join(evidence.relative for evidence in ratified_evidence) + raise CoordinatorError( + "inconsistent state: multiple candidate adoption records coexist with " + f"contradictory baseline, revision, or Owner evidence: {names}" + ) + adopted = bool(ratified_evidence) - if all(path.is_file() for path in core) and closed_records: + if all(path.is_file() for path in core) and adopted and closed_records: reject_bootstrap_conflict("retired lockout") return ObservedState( "retired_lockout", ( "activation pointer is absent", "Plan, State, and Routing exist", + f"ratified adoption evidence observed: {ratified_evidence[0].relative}", f"{len(closed_records)} closed work-order record(s) observed in history", ), ) - if all(path.is_file() for path in core) and any( - path.is_file() for path in adoption_records - ): + if all(path.is_file() for path in core) and adopted: reject_bootstrap_conflict("adopted lockout") return ObservedState( "adopted_lockout", ( "activation pointer is absent", - "Plan, State, Routing, and an adoption record exist", + "Plan, State, Routing, and ratified adoption evidence exist: " + f"{ratified_evidence[0].relative}", ), ) @@ -362,13 +583,42 @@ def portable_slug(value: str) -> str: return slug -def operation_packet(function_name: str) -> str: +def _canonical_root_block(canonical: str) -> str: + return f"""## Canonical project root + +`{canonical}` + +Durable project artifacts, governance records, source, plans, reports, and +work orders belong under this one canonical project root. This bootstrap +directory and any other temporary staging may hold bounded evidence or +atomic publication bytes; they never become the authoritative project tree +and must be removed after use. No agent creates a shadow repository or a +second durable project record outside this canonical root; a portable +relative path or a bare `.` is never a substitute for it. +""" + + +def _external_operator_root_block(canonical: str) -> str: + return f"""## Canonical project root + +`{canonical}` + +This external Operator's own evidence staging is separate from durable +project records. Returned sanitized evidence is incorporated under this +canonical project root by the General or a separately authorized +repository Operator; this external Operator does not create a shadow +repository or any other durable project record elsewhere. +""" + + +def operation_packet(function_name: str, canonical: str) -> str: return f"""# External operations packet: {function_name} This scaffold is inert. It confers no authority to access or mutate any system. -The Owner-Agent fills it from ratified intent; the named Operator executes only -the completed packet and returns evidence. +The Architect or General prepares it from ratified intent; the named Operator +executes only the completed packet and returns evidence. +{_external_operator_root_block(canonical)} ## Preconditions - [ ] Identify the exact system, account boundary, and observed baseline. @@ -415,7 +665,7 @@ def role_split_recommendation(functions: tuple[str, ...]) -> str: " Add no external Operator unless the project later names an external system." ) return ( - "Use one human Owner, one Owner-Agent coordinator, one repository " + "Use one human Owner, one Architect, one General, one repository " "Operator when repository mutation begins, and one fresh Reviewer." + external ) @@ -458,12 +708,13 @@ def topology_recommendation( "authority": "unratified_recommendation", "reason": reason, "roles": [ - "human Owner", "Owner-Agent architect/coordinator", - "repository Operator", "fresh Reviewer", *functions, + "human Owner", "Architect", "General", "repository Operator", + "fresh Reviewer", *functions, ], "sequential_combination": ( - "On a small project, one agent may act as Owner-Agent and later as " - "repository Operator only in separate sessions after Owner ratification." + "On a small project, one agent may act sequentially as Architect, " + "General, and repository Operator only in separate sessions after " + "Owner ratification." ), "mandatory_separation": ( "The human Owner remains the source of ratification; the fresh Reviewer " @@ -473,6 +724,179 @@ def topology_recommendation( } +_IGNORED_TOP_LEVEL_NAMES = frozenset({ + "__pycache__", "node_modules", ".venv", "venv", "env", "dist", "build", + ".mypy_cache", ".pytest_cache", ".tox", ".idea", ".vscode", "target", + "vendor", ".DS_Store", ".ruff_cache", "site-packages", OUTPUT_NAME, +}) + +_REFLOG_COMMIT_MESSAGE = re.compile(r"^commit(?: \(initial\)| \(amend\))?:\s*(.+)$") + + +def _top_level_entries(project: Path) -> tuple[str, ...]: + """Bounded, one-level, project-relative names only. + + Never recurses, never reads file contents, never follows a link outside + the project, and skips `.git`, the bootstrap output itself, and common + dependency/build/cache directories. + """ + try: + entries = sorted(project.iterdir(), key=lambda item: item.name.casefold()) + except OSError: + return () + names: list[str] = [] + for entry in entries: + if entry.name == ".git" or entry.name in _IGNORED_TOP_LEVEL_NAMES: + continue + if _is_linklike(entry): + continue + names.append(entry.name) + return tuple(names) + + +def _read_git_head_branch(project: Path) -> str | None: + """Read the current branch from an ordinary local `.git` directory only. + + Never chases a linked-worktree `gitdir:` pointer, which typically + resolves outside the project root; that case simply yields no branch. + """ + git_dir = project / ".git" + if _is_linklike(git_dir) or not git_dir.is_dir(): + return None + head = git_dir / "HEAD" + if _is_linklike(head) or not head.is_file(): + return None + try: + content = head.read_text(encoding="utf-8", errors="replace").strip() + except OSError: + return None + if content.startswith("ref:"): + ref = content[len("ref:"):].strip() + return ref.rsplit("/", 1)[-1] if ref else None + if re.fullmatch(r"[0-9a-fA-F]{7,40}", content): + return f"detached HEAD at {content[:12]}" + return None + + +def _read_recent_commit_subjects(project: Path, limit: int = 3) -> tuple[str, ...]: + """Bounded recent commit subjects from the local HEAD reflog only. + + Reads only the commit subjects Git itself already wrote into + `.git/logs/HEAD`; never parses the object database, never dumps a diff + or full commit body, and returns nothing when the reflog is absent. + """ + git_dir = project / ".git" + if _is_linklike(git_dir) or not git_dir.is_dir(): + return () + reflog = git_dir / "logs" / "HEAD" + if _is_linklike(reflog) or not reflog.is_file(): + return () + try: + lines = reflog.read_text(encoding="utf-8", errors="replace").splitlines() + except OSError: + return () + subjects: list[str] = [] + for line in reversed(lines): + if "\t" not in line: + continue + message = line.split("\t", 1)[1].strip() + match = _REFLOG_COMMIT_MESSAGE.match(message) + if match: + subjects.append(match.group(1).strip()) + if len(subjects) >= limit: + break + return tuple(subjects) + + +def _observe_git_cleanliness(project: Path) -> str | None: + """Best-effort local-only cleanliness label via an available `git`. + + Never contacts a network (plain `status` is local-only) and never lists + changed file names; returns `None` rather than failing when a `git` + executable is unavailable on PATH. + """ + git_executable = shutil.which("git") + if not git_executable: + return None + try: + result = subprocess.run( + [git_executable, "-C", str(project), "status", "--porcelain=v1"], + capture_output=True, text=True, timeout=10, + ) + except (OSError, subprocess.SubprocessError): + return None + if result.returncode != 0: + return None + changed = [line for line in result.stdout.splitlines() if line.strip()] + return "clean" if not changed else f"{len(changed)} uncommitted change(s)" + + +@dataclass(frozen=True) +class LocalInventory: + """A deterministic, local-only, bounded observation of a clean/new target. + + Records project-relative names and non-secret Git metadata only: + repository presence, branch, cleanliness, and recent commit subjects. + Never reads file contents, never inspects outside the canonical root, + and never contacts a network. These are observations, never ratified + intent. + """ + has_git: bool + branch: str | None + cleanliness: str | None + recent_commit_subjects: tuple[str, ...] + top_level_entries: tuple[str, ...] + + @property + def is_existing(self) -> bool: + return self.has_git or bool(self.top_level_entries) + + def evidence_lines(self) -> tuple[str, ...]: + lines: list[str] = [] + if self.has_git: + lines.append("Git repository observed at the project root") + if self.branch: + lines.append(f"Git branch: {self.branch}") + if self.cleanliness: + lines.append(f"Git working tree: {self.cleanliness}") + for subject in self.recent_commit_subjects: + lines.append(f"Recent commit: {subject}") + else: + lines.append("No Git repository observed at the project root") + if self.top_level_entries: + lines.append( + "Top-level project entries: " + ", ".join(self.top_level_entries) + ) + else: + lines.append("No top-level project entries observed") + return tuple(lines) + + +def gather_local_inventory(project: Path) -> LocalInventory: + """Gather the bounded local inventory for a clean/new project root.""" + 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, + recent_commit_subjects=( + _read_recent_commit_subjects(project) if ordinary_git_dir else () + ), + top_level_entries=_top_level_entries(project), + ) + + +def conversation_first_opening(inventory: LocalInventory) -> tuple[str, str]: + """The ordinary, conversation-first Architect opening for a clean/new + target: an existing-project summary-first opening, or one open + invitation for a genuinely empty project.""" + if inventory.is_existing: + return ("Fresh Architect (conversation-first)", ARCHITECT_EXISTING_PROJECT_PROMPT) + return ("Fresh Architect (conversation-first)", ARCHITECT_EMPTY_PROJECT_PROMPT) + + def next_prompt(state: ObservedState) -> tuple[str, str]: if state.name == "clean_new": return ( @@ -509,12 +933,12 @@ def next_prompt(state: ObservedState) -> tuple[str, str]: ) if state.name in {"adopted_lockout", "retired_lockout"}: return ( - "Fresh Owner-Agent / Project-Architect", - PROJECT_ARCHITECT_PROMPT, + "Fresh General", + GENERAL_PROMPT, ) if state.name == "active_work_order": return ( - "Fresh walled repository Implementer", + "Fresh walled repository Operator/Implementer", """Act as a fresh Implementer for the active work order only. Re-read the activation pointer and pointed work order from repository bytes, confirm the active dispatch and required live-wall canary before mutation, execute only its grant, @@ -548,9 +972,16 @@ def render_time(owner_time: str) -> str: def render_handoff(args: argparse.Namespace, state: ObservedState, - functions: tuple[str, ...], privacy_count: int) -> str: - role, prompt = next_prompt(state) - evidence = "\n".join(f"- {item}" for item in state.evidence) + functions: tuple[str, ...], privacy_count: int, + canonical: str, inventory: LocalInventory | None = None) -> str: + if inventory is not None: + role, prompt = conversation_first_opening(inventory) + else: + role, prompt = next_prompt(state) + evidence_lines = list(state.evidence) + if inventory is not None: + evidence_lines.extend(inventory.evidence_lines()) + evidence = "\n".join(f"- {item}" for item in evidence_lines) operator_rows = "\n".join( f"- **{name}:** give `operations/{portable_slug(name)}.md` to the agent " "or administrator that can reach only that external function." @@ -574,6 +1005,7 @@ def render_handoff(args: argparse.Namespace, state: ObservedState, {SECRET_WARNING} +{_canonical_root_block(canonical)} ## Observed lifecycle state **{state.name}**{active} @@ -619,12 +1051,14 @@ def render_handoff(args: argparse.Namespace, state: ObservedState, - The human Owner decides intent, identity, risk acceptance, lifecycle actions, provider selection, production cutovers, and acceptance. -- The Owner-Agent may interview, draft, route, record exact ratified decisions, - and perform explicitly authorized clerical lifecycle mechanics. +- The Architect may interview, challenge, and develop the project before adoption, + and later judge design conformance when the General routes that question back. +- The General may draft, route, record exact ratified decisions, and perform + explicitly authorized clerical lifecycle mechanics after adoption. - A repository Operator works only under the active work order. - A fresh Reviewer evaluates the order, result, evidence, and report without implementing corrections in the same context. -- External Operators receive only bounded packets. The Owner-Agent retains the +- External Operators receive only bounded packets. The General retains the proverbial keys: routing and authority, not passwords or cryptographic keys. ## External Operator routing @@ -641,16 +1075,44 @@ def render_handoff(args: argparse.Namespace, state: ObservedState, """ -def discovery_record(args: argparse.Namespace, functions: tuple[str, ...]) -> dict: - candidate = args.project_name if args.project_name != "Unnamed idea" else None +def discovery_record( + args: argparse.Namespace, functions: tuple[str, ...], canonical: str, + inventory: LocalInventory | None = None, +) -> dict: + candidate = ( + args.project_name + if args.project_name not in {"Unnamed idea", "Unnamed existing project"} + else None + ) + unresolved_questions: list[str] = [] + if inventory is not None: + unresolved_questions.append( + "Does the Owner want to explore this observed existing work, or " + "start elsewhere with a different idea?" + if inventory.is_existing else + "What is the Owner thinking?" + ) return { - "schema": 1, + "schema": 2, "authority": "unratified_discovery_only", + "project_root": canonical, "identity": { "state": "working_candidate" if candidate else "unnamed", "working_candidate": candidate, "canonical_name": None, }, + "local_observations": ( + { + "authority": "observed_local_evidence_only", + "has_git": inventory.has_git, + "branch": inventory.branch, + "git_working_tree": inventory.cleanliness, + "recent_commit_subjects": list(inventory.recent_commit_subjects), + "top_level_entries": list(inventory.top_level_entries), + } + if inventory is not None else None + ), + "unresolved_questions": unresolved_questions, "topology": topology_recommendation(args, functions), "qualification": { "problem_or_opportunity": getattr(args, "problem", None) or args.purpose, @@ -672,19 +1134,82 @@ def discovery_record(args: argparse.Namespace, functions: tuple[str, ...]) -> di } -def architect_packets(args: argparse.Namespace, functions: tuple[str, ...]) -> dict[str, str]: +def architect_packets( + args: argparse.Namespace, functions: tuple[str, ...], canonical: str, +) -> dict[str, str]: common = ( "This packet is unratified discovery. It confers no authority to implement, " "install, publish, configure, or operate an external system.\n" ) + root_block = _canonical_root_block(canonical) return { - "OWNER-AGENT.md": f"""# Owner-Agent architect packet + "ARCHITECT.md": f"""# Architect packet + +{common} +{root_block} +The Architect owns pre-adoption discovery and later design-conformance +judgment. Before requesting promotion into adoption mechanics, the Architect +returns a concise project sketch, a recommended Owner/Architect/General/ +Operator topology, a provisional first backlog, key uncertainties and risks, +and one explicit Owner promotion decision. If the idea remains exploratory or +is rejected, no adoption, work order, or construction control is created. + +Paste exactly into the preferred frontier Architect session: + +```text +Act as the Architect. Read discovery.json and every packet in this +directory. Continue qualification one question at a time where evidence is +incomplete or contradictory, using any local observations already recorded +before asking the Owner to restate them. Recommend the smallest credible +project and Owner/Architect/General/Operator role topology, label every +recommendation unratified, prepare the exact Owner ratification choices, and +stop. Do not implement the project, canonicalize its identity, install +tooling, or operate any external system. +``` +""", + "GENERAL.md": f"""# General packet + +{common} +{root_block} +The General maintains project continuity after adoption: preparing bounded +dispatch, routing work between the Architect and Operators, and performing +only explicitly authorized recorder mechanics. The General does not ratify +intent, judge design conformance, or adopt a project on the Owner's behalf. + +Paste exactly into the preferred frontier session: + +```text +{GENERAL_PROMPT} +``` +""", + "OPERATOR.md": f"""# Operator packet + +{common} +{root_block} +## Preconditions +- An Owner-ratified plan and active bounded work order exist. +## Permitted actions +- Only paths and commands granted by that work order. +## Prohibited actions +- No external-system operation or inferred identity decision. +## Verification +- Return exact checks and observed results. +## Evidence to return +- Changed paths, reasons, failures, and remaining boundaries. +""", + "OWNER-AGENT.md": f"""# Owner-Agent packet (compatibility alias for Architect) {common} -Paste exactly into the preferred frontier Owner-Agent session: +{root_block} +This is a compatibility alias for the **Architect** role packet +(`ARCHITECT.md`), kept for existing `OWNER-AGENT.md` consumers. New +integrations should read `ARCHITECT.md` directly; both describe the same +Architect role. + +Paste exactly into the preferred frontier Architect session: ```text -Act as the Owner-Agent architect/coordinator. Read discovery.json and every +Act as the Architect. Read discovery.json and every packet in this directory. Continue qualification one question at a time where evidence is incomplete or contradictory. Recommend the smallest credible project and role topology, label every recommendation unratified, prepare the @@ -692,9 +1217,15 @@ def architect_packets(args: argparse.Namespace, functions: tuple[str, ...]) -> d canonicalize its identity, install tooling, or operate any external system. ``` """, - "REPOSITORY-OPERATOR.md": f"""# Repository Operator packet + "REPOSITORY-OPERATOR.md": f"""# Repository Operator packet (compatibility alias) {common} +{root_block} +This is a compatibility alias for the **Operator** role packet +(`OPERATOR.md`), kept for existing `REPOSITORY-OPERATOR.md` consumers. New +integrations should read `OPERATOR.md` directly; both describe the same +Operator role. + ## Preconditions - An Owner-ratified plan and active bounded work order exist. ## Permitted actions @@ -709,8 +1240,9 @@ def architect_packets(args: argparse.Namespace, functions: tuple[str, ...]) -> d "REVIEWER.md": f"""# Fresh Reviewer packet {common} +{root_block} ## Preconditions -- Review only after the Owner-Agent returns a ratifiable packet or an Operator returns evidence. +- Review only after the Architect returns a ratifiable packet or an Operator returns evidence. ## Review - Challenge intent traceability, boundary fit, name state, topology, failure safety, and evidence. ## Prohibited actions @@ -721,6 +1253,7 @@ def architect_packets(args: argparse.Namespace, functions: tuple[str, ...]) -> d "NAME-CLEARANCE.md": f"""# Name-clearance packet {common} +{root_block} No name is canonical, available, cleared, or accepted. A supplied name is only a `working_candidate`. Before any repository slug, package, domain, logo, or launch route hardens identity, use @@ -739,6 +1272,7 @@ def architect_packets(args: argparse.Namespace, functions: tuple[str, ...]) -> d "OWNER-RATIFICATION.md": f"""# Owner ratification gate {common} +{root_block} The Owner must explicitly accept, reject, or revise the qualified problem, smallest useful outcome, success signal, constraints, non-goals, risks, kill conditions, role topology, external boundaries, and identity disposition. @@ -791,7 +1325,9 @@ def _atomic_publish(stage: Path, output: Path) -> None: def write_bootstrap(project: Path, args: argparse.Namespace, state: ObservedState, - functions: tuple[str, ...], privacy_count: int) -> Path: + functions: tuple[str, ...], privacy_count: int, + inventory: LocalInventory | None = None) -> Path: + canonical = project.as_posix() output = project / OUTPUT_NAME if _entry_exists(output): raise CoordinatorError( @@ -854,7 +1390,7 @@ def write_bootstrap(project: Path, args: argparse.Namespace, state: ObservedStat "recommended_role_split": role_split_recommendation(functions), "external_operator_functions": list(functions), "owner_time_capture": args.owner_time == "yes", - "project_root": ".", + "project_root": canonical, "authority": "unratified_intake_only", "privacy_screen": { "ready": True, @@ -868,24 +1404,24 @@ def write_bootstrap(project: Path, args: argparse.Namespace, state: ObservedStat newline="\n", ) (stage / "HANDOFF.md").write_text( - render_handoff(args, state, functions, privacy_count), + render_handoff(args, state, functions, privacy_count, canonical, inventory), encoding="utf-8", newline="\n", ) (stage / "discovery.json").write_text( - json.dumps(discovery_record(args, functions), indent=2, + json.dumps(discovery_record(args, functions, canonical, inventory), indent=2, ensure_ascii=False) + "\n", encoding="utf-8", newline="\n", ) - for name, content in architect_packets(args, functions).items(): + for name, content in architect_packets(args, functions, canonical).items(): (stage / name).write_text(content, encoding="utf-8", newline="\n") if functions: operations = stage / "operations" operations.mkdir() for name, slug in zip(functions, slugs): (operations / f"{slug}.md").write_text( - operation_packet(name), encoding="utf-8", newline="\n" + operation_packet(name, canonical), encoding="utf-8", newline="\n" ) residue = sorted( path.relative_to(stage).as_posix() @@ -944,7 +1480,8 @@ def interactive_args(args: argparse.Namespace) -> argparse.Namespace: if confirmation not in {"y", "yes"}: raise CoordinatorError("stopped before reading project intake") args.confirm_no_secrets = True - args.project_root = args.project_root or ask("Target project directory", ".") + # `args.project_root` is always already resolved by the caller before + # `interactive_args` runs; there is no reachable case where it is blank. if not args.project_name: args.project_name = ask("Working candidate name, or blank for an unnamed idea", "") if not args.brief_file and not args.purpose and not args.problem: @@ -978,9 +1515,9 @@ def interactive_args(args: argparse.Namespace) -> argparse.Namespace: "local repository only", ) print( - "Recommended minimum: one Owner-Agent, one repository Operator when " - "mutation starts, one fresh Reviewer, and external Operators only for " - "distinct account or rollback boundaries." + "Recommended minimum: one human Owner, one Architect, one General, " + "one repository Operator when mutation starts, one fresh Reviewer, " + "and external Operators only for distinct account or rollback boundaries." ) if not args.external_operator and not args.scenario: external = ask( @@ -1035,9 +1572,35 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser.add_argument("--owner-time", choices=("yes", "no")) parser.add_argument("--confirm-no-secrets", action="store_true") parser.add_argument("--non-interactive", action="store_true") + parser.add_argument( + "--structured-intake", action="store_true", + help=( + "run the former full interactive questionnaire instead of the " + "ordinary, nonblocking conversation-first flow" + ), + ) return parser.parse_args(argv) +def _discover_git_worktree_root(project: Path) -> Path | None: + """Return the Git worktree top level containing ``project``, if any. + + Walks upward looking for a directory with a `.git` entry: a directory + for an ordinary repository or the primary worktree, a file for a linked + worktree. This mirrors how Git itself discovers a repository top level, + without requiring a `git` executable. Returns `None` for a non-Git + directory tree. + """ + current = project + while True: + if _entry_exists(current / ".git"): + return current + parent = current.parent + if parent == current: + return None + current = parent + + def resolve_project_root(value: str | None) -> Path: if not value: raise CoordinatorError("missing required intake: project root") @@ -1050,11 +1613,68 @@ def resolve_project_root(value: str | None) -> Path: raise CoordinatorError(f"target project directory is not readable: {exc}") from exc if not project.is_dir() or project.is_symlink(): raise CoordinatorError("target project must be an existing, non-symlink directory") + worktree_root = _discover_git_worktree_root(project) + if worktree_root is not None and worktree_root != project: + raise CoordinatorError( + "the supplied project directory is nested inside a Git worktree; " + f"the discovered worktree top level is {worktree_root.as_posix()}. " + "Rerun this coordinator with --project-root set to that exact " + "worktree top level. Writwall never silently selects a nested " + "directory or an ancestor repository the Owner did not name." + ) return project +def _validate_idea_qualification(args: argparse.Namespace) -> None: + """Preserve the explicit ``--problem`` contract in every intake mode.""" + qualification = { + "problem or opportunity": args.problem, + "intended user": args.intended_user, + "why the outcome matters": args.why_matters, + "evidence and assumptions": args.evidence, + "smallest useful outcome": args.smallest_outcome, + "success signal": args.success_signal, + "constraints": args.constraint, + "non-goals": args.non_goal, + "material risks": args.risk, + "stop or kill conditions": args.kill_condition, + "existing assets": args.asset, + } + + def complete_answer(value: object) -> bool: + if isinstance(value, str): + return bool(value.strip()) + if isinstance(value, list): + return bool(value) and all( + isinstance(item, str) and bool(item.strip()) for item in value + ) + return False + + missing_qualification = [ + name for name, value in qualification.items() + if not complete_answer(value) + ] + if missing_qualification: + raise CoordinatorError( + "missing idea qualification: " + ", ".join(missing_qualification) + ) + normalized_constraints = { + re.sub(r"\s+", " ", value).strip().casefold() + for value in args.constraint if value.strip() + } + normalized_non_goals = { + re.sub(r"\s+", " ", value).strip().casefold() + for value in args.non_goal if value.strip() + } + if normalized_constraints & normalized_non_goals: + raise CoordinatorError( + "contradictory idea qualification: the same statement cannot " + "be both a constraint and a non-goal" + ) + + def normalize_args(args: argparse.Namespace) -> tuple[argparse.Namespace, Path, tuple[str, ...]]: - if not args.non_interactive: + if args.structured_intake and not args.non_interactive: args = interactive_args(args) idea_mode = bool(args.problem) if idea_mode: @@ -1076,49 +1696,7 @@ def normalize_args(args: argparse.Namespace) -> tuple[argparse.Namespace, Path, if missing: raise CoordinatorError("missing required intake: " + ", ".join(missing)) if idea_mode: - qualification = { - "problem or opportunity": args.problem, - "intended user": args.intended_user, - "why the outcome matters": args.why_matters, - "evidence and assumptions": args.evidence, - "smallest useful outcome": args.smallest_outcome, - "success signal": args.success_signal, - "constraints": args.constraint, - "non-goals": args.non_goal, - "material risks": args.risk, - "stop or kill conditions": args.kill_condition, - "existing assets": args.asset, - } - def complete_answer(value: object) -> bool: - if isinstance(value, str): - return bool(value.strip()) - if isinstance(value, list): - return bool(value) and all( - isinstance(item, str) and bool(item.strip()) for item in value - ) - return False - - missing_qualification = [ - name for name, value in qualification.items() - if not complete_answer(value) - ] - if missing_qualification: - raise CoordinatorError( - "missing idea qualification: " + ", ".join(missing_qualification) - ) - normalized_constraints = { - re.sub(r"\s+", " ", value).strip().casefold() - for value in args.constraint if value.strip() - } - normalized_non_goals = { - re.sub(r"\s+", " ", value).strip().casefold() - for value in args.non_goal if value.strip() - } - if normalized_constraints & normalized_non_goals: - raise CoordinatorError( - "contradictory idea qualification: the same statement cannot " - "be both a constraint and a non-goal" - ) + _validate_idea_qualification(args) project = resolve_project_root(args.project_root) @@ -1141,6 +1719,55 @@ def complete_answer(value: object) -> bool: return args, project, normalized +def normalize_conversation_first_args( + args: argparse.Namespace, project: Path, inventory: LocalInventory, +) -> tuple[argparse.Namespace, Path, tuple[str, ...]]: + """Fill required intake fields from defaults and local evidence only. + + Never calls `ask()`/`input()`: the ordinary `--project-root`-only + invocation must not block on stdin. Any explicitly supplied flag is + preserved as given; qualification is deliberately deferred to the live + Architect conversation rather than required up front. + """ + args.confirm_no_secrets = True + args.owner_time = args.owner_time or "no" + args.agent = args.agent or "your preferred agent/interface" + args.location = args.location or "local project workspace" + args.environment = args.environment or ( + "existing local repository; environment not yet stated by the Owner" + if inventory.has_git else + "local repository only; environment not yet stated by the Owner" + ) + if not args.project_name: + args.project_name = ( + "Unnamed existing project" if inventory.is_existing else "Unnamed idea" + ) + if args.problem: + _validate_idea_qualification(args) + if args.brief_file: + brief = Path(args.brief_file).expanduser() + try: + purpose = brief.read_text(encoding="utf-8-sig").strip() + except (OSError, UnicodeError) as exc: + raise CoordinatorError(f"cannot read supplied brief: {exc}") from exc + if not purpose: + raise CoordinatorError("supplied brief is empty") + args.purpose = purpose + elif not args.purpose and not args.problem: + args.purpose = ( + "An existing local project was observed; its purpose has not yet " + "been stated by the Owner." + if inventory.is_existing else + "An idea has not yet been stated by the Owner." + ) + + functions = list(args.external_operator) + if args.scenario == "dns-mail-migration": + functions.extend(name for name in DNS_MAIL_SCENARIO if name not in functions) + normalized = tuple(name.strip() for name in functions if name.strip()) + return args, project, normalized + + def main(argv: list[str] | None = None) -> int: try: from scripts.privacy_screen import ( @@ -1153,7 +1780,21 @@ def main(argv: list[str] | None = None) -> int: if state.name != "clean_new": emit_lifecycle_handoff(state) return 0 - args, project, functions = normalize_args(args) + + inventory: LocalInventory | None = None + if args.non_interactive or args.structured_intake: + args, project, functions = normalize_args(args) + else: + # The ordinary, low-friction invocation: only --project-root (and + # optionally a few explicit flags), no `input()` call, no + # blocking questionnaire. Local evidence stands in for answers + # the Owner has not yet given, and qualification is deferred to + # the live Architect conversation. + inventory = gather_local_inventory(project) + args, project, functions = normalize_conversation_first_args( + args, project, inventory + ) + current_state = classify_project(project) if current_state.name != state.name: raise CoordinatorError( @@ -1170,7 +1811,7 @@ def main(argv: list[str] | None = None) -> int: privacy_count = add_identifier(project, identifier) except PrivacyScreenError as exc: raise CoordinatorError(str(exc)) from exc - output = write_bootstrap(project, args, state, functions, privacy_count) + output = write_bootstrap(project, args, state, functions, privacy_count, inventory) except CoordinatorError as exc: print(f"STOP: {exc}", file=sys.stderr) return 2 diff --git a/skills/writwall-adopt/SKILL.md b/skills/writwall-adopt/SKILL.md index b05c6ba..28b6fa1 100644 --- a/skills/writwall-adopt/SKILL.md +++ b/skills/writwall-adopt/SKILL.md @@ -21,6 +21,14 @@ External-operation packet scaffolds are inert: blank fields authorize nothing, credentials remain outside them, and infrastructure, DNS, and mail Operators remain outside the repository wall unless they edit repository bytes. +By default the day-zero command is conversation-first and hands the human a +fresh Architect before any adoption mechanics: `ARCHITECT.md`, `GENERAL.md`, +and `OPERATOR.md` are its primary role packets, with `OWNER-AGENT.md` and +`REPOSITORY-OPERATOR.md` kept as compatibility aliases. This bootstrap mode +is unchanged and is entered only once the Owner has decided, through that +conversation or through `--structured-intake`'s adoption-coordinator prompt, +to proceed with adoption. + Read `references/DOCTRINE.md` in this skill bundle before doing anything, and cite clauses in your report. Doctrine 1.2.3 permits a bootstrap agent to receive the doctrine as an implementation specification under direct Owner supervision. That permission is bounded by this task: it ends when bootstrap ends. ## Two modes @@ -245,28 +253,27 @@ Report to the Owner: each decision recorded and whose words it came from; every The Owner's ratification of the decision packet **was** the adoption decision. The commit and the mechanics above only record it. So when they have all succeeded, report completion plainly and stop — do not ask the Owner to confirm again what they have already ratified, and do not describe adoption as pending their further word. If any step did not succeed, say exactly which one and stop there instead; a closeout you cannot complete is reported, never assumed. After a successful closeout report, present the exact handoff below for a fresh -Owner-Agent / Project-Architect, then stop. Do not continue as Architect, create +General, then stop. Do not continue as General, create or dispatch a user-owned task, activate a work order, or begin product work in -the onboarding context. The fresh Architect may request task creation and +the onboarding context. The fresh General may request task creation and dispatch only by including them explicitly in its single combined approval request. ```text -Act as a fresh Owner-Agent / Project-Architect. Begin read-only and verify -the lifecycle from repository bytes rather than prior chat. Read the charter, -Plan, State, Routing, ratified adoption record, and open transactional records. -State the project's next decision plainly. Draft, but do not activate or -implement, the smallest genuine work order or bounded external Operator packet. -Lead with a concise Recommendation and material tradeoff; keep the detailed -packet behind it as supporting evidence rather than the conversational front -door. When the next safe mechanical action is available, ask once for one -combined disposition and action. If that action uses a new user-owned task, -explicitly include creation and dispatch of the named task in that approval -request; never infer task-creation permission afterward. Once approved, perform -every mechanically available authorized step. Do not ask for the same decision again. -The human Owner alone ratifies intent and activates work; preserve a distinct -fresh review after implementation. The onboarding coordinator stops here and -does not continue into project work. +Act as a fresh General for this already-adopted project's continuity. Begin +read-only and verify the lifecycle from repository bytes rather than prior chat. Read the +charter, Plan, State, Routing, ratified adoption record, and open transactional records. State +the project's next decision plainly. Prepare, but do not activate, the smallest genuine work +order or bounded Operator packet; route it to a fresh Architect instead only when the next +decision requires new design or design-conformance judgment rather than routine continuity. Lead +with a concise Recommendation and material tradeoff; keep the detailed packet behind it as +supporting evidence rather than the conversational front door. When the next safe mechanical +action is available, ask once for one combined disposition and action. If that action uses a new +user-owned task, explicitly include creation and dispatch of the named task in that approval +request; never infer task-creation permission afterward. Once approved, perform every +mechanically available authorized step. Do not ask for the same decision again. The human Owner +alone ratifies intent and activates work; preserve a distinct fresh Reviewer after +implementation. The onboarding coordinator stops here and does not continue into project work. ``` ## Migration mode diff --git a/tests/test_coordinator_release.py b/tests/test_coordinator_release.py index 6fde034..3f5bb25 100644 --- a/tests/test_coordinator_release.py +++ b/tests/test_coordinator_release.py @@ -53,7 +53,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.9.3")) + arguments.extend(("--expected-tag", "v0.10.0")) return subprocess.run( [sys.executable, "-B", str(CHECKER), *arguments], cwd=REPO_ROOT, @@ -105,6 +105,9 @@ def test_complete_external_candidate_installs_and_emits_full_handoff(self): self.assertIn("installed command", result.stdout) self.assertIn("complete handoff", result.stdout) self.assertIn("adopted lockout", result.stdout) + self.assertIn("retired lockout", result.stdout) + self.assertIn("draft regression", result.stdout) + self.assertIn("unrelated regression", result.stdout) self.assertIn("zero target-byte change", result.stdout) self.assertIn("candidate unchanged", result.stdout) self.assertEqual(tree_digest(candidate), before) @@ -143,6 +146,26 @@ def test_installed_adopted_lockout_target_write_fails_release_gate(self): result.stdout + result.stderr, ) + def test_installed_reintroduced_filename_only_adoption_defect_fails_release_gate(self): + candidate = self.make_candidate() + start = candidate / "scripts" / "start_writwall.py" + original = start.read_text(encoding="utf-8") + self.assertIn("adopted = bool(ratified_evidence)", original) + start.write_text( + original.replace( + "adopted = bool(ratified_evidence)", + "adopted = bool(adoption_evidence)", + ), + encoding="utf-8", + newline="\n", + ) + result = self.run_checker(candidate) + self.assertNotEqual(result.returncode, 0) + self.assertIn( + "installed draft-adoption-record regression route failed", + result.stdout + result.stderr, + ) + def test_installed_help_mismatch_fails_with_diagnostic(self): candidate = self.make_candidate() entry = candidate / "writwall_cli" / "__main__.py" @@ -249,15 +272,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.9.3"', 'version = "0.9.0"' + 'version = "0.10.0"', 'version = "0.9.0"' ), encoding="utf-8", newline="\n", ) - result = self.run_checker(candidate, "--expected-tag", "v0.9.3") + result = self.run_checker(candidate, "--expected-tag", "v0.10.0") self.assertNotEqual(result.returncode, 0) self.assertIn( - "candidate version '0.9.0' does not match intended tag 'v0.9.3'", + "candidate version '0.9.0' does not match intended tag 'v0.10.0'", result.stdout + result.stderr, ) @@ -288,35 +311,67 @@ def test_candidate_mutation_is_rejected(self): with self.assertRaisesRegex(checker.ReleaseCheckError, "candidate changed"): checker.verify_candidate_unchanged(candidate, before) + def test_release_check_reports_installed_canonical_root_evidence(self): + candidate = self.make_candidate() + result = self.run_checker(candidate) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("canonical root", result.stdout.lower()) + + def test_release_check_exercises_nested_worktree_stop_on_the_installed_wheel(self): + candidate = self.make_candidate() + result = self.run_checker(candidate) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("worktree", result.stdout.lower()) + 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.9.3") + self.assertEqual(project["version"], "0.10.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.9.3.zip" + tagged_archive = "archive/refs/tags/v0.10.0.zip" self.assertIn(tagged_archive, readme) self.assertIn(tagged_archive, adopting) self.assertIn(tagged_archive, start) - self.assertIn("--expected-tag v0.9.3", publication) - self.assertIn("--expected-tag v0.9.3", contributing) + self.assertIn("--expected-tag v0.10.0", publication) + self.assertIn("--expected-tag v0.10.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.9.3.zip"', + '"https://github.com/HLLMR/writwall/archive/refs/tags/v0.10.0.zip"', 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) + self.assertIn("Release `v0.9.3` adds", start) public_files = PUBLIC_FILES.read_text(encoding="utf-8").splitlines() self.assertIn("checks/check_coordinator_release.py", public_files) self.assertIn("tests/test_coordinator_release.py", public_files) + # -- 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 + # General rather than the old "Owner-Agent / Project-Architect" text. + # Added RED in this work order; now exercised against the GREEN + # implementation. + + def test_installed_release_gate_confirms_general_role_for_adopted_lockout(self): + candidate = self.make_candidate() + result = self.run_checker(candidate) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("General", result.stdout) + + def test_installed_release_gate_exercises_conversation_first_clean_project(self): + candidate = self.make_candidate() + result = self.run_checker(candidate) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("conversation-first", result.stdout) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_start_writwall.py b/tests/test_start_writwall.py index be1c926..d447a1e 100644 --- a/tests/test_start_writwall.py +++ b/tests/test_start_writwall.py @@ -22,6 +22,125 @@ STARTER = REPO_ROOT / "scripts" / "start_writwall.py" +def ratified_adoption_record( + *, owner: str = "Test Owner", date: str = "2026-01-01", + revision: str = "0.8", revision_date: str = "2026-08-21", + revision_record: str = "decisions/DR-005.md", + baseline: str = "0" * 40, + title: str = "# Adoption record", +) -> str: + """A complete, well-formed, signed Appendix D record. + + This is the deterministic ratified shape: an adoption title, all of + D.1-D.9, a concrete D.2 baseline commit with adoption-effective + language, a D.3 revision, and a dated Owner Signature. It matches the + established shape of this repository's own `governance/decisions/DR-001.md`. + """ + return f"""{title} + +## D.1 Date and Owner + +{date} · Owner: {owner} + +## D.2 Pre-adoption baseline commit + +`{baseline}` — baseline commit. Adoption became effective at this commit. + +## D.3 Doctrine revision bound + +Revision **{revision}**, ratified **{revision_date}** by `{revision_record}`. + +## D.4 Enforcement at adoption + +Observed enforcement surfaces at adoption. + +## D.5 Conformance gate during the pilot + +Reviewer-only controlled inference. + +## D.6 Recognized controlling sources at adoption + +Disposed by the Owner. + +## D.7 Pilot period + +10 counted work orders. + +## D.8 Reasoning: why adopt, and why now + +Recorded by the Owner. + +## D.9 Rejected alternatives + +1. Alternative rejected. + +## Signature + +{owner} — Owner — {date} +""" + + +def draft_adoption_record() -> str: + """Complete Appendix D section shape, but explicitly unsigned/proposed.""" + return """# Adoption record + +## D.1 Date and Owner + +DRAFT — Owner: TBD + +## D.2 Pre-adoption baseline commit + +Proposed baseline; not yet selected. + +## D.3 Doctrine revision bound + +Revision **0.8**, PROPOSED. + +## D.4 Enforcement at adoption + +Draft enforcement notes. + +## D.5 Conformance gate during the pilot + +Draft conformance notes. + +## D.6 Recognized controlling sources at adoption + +Draft mapping. + +## D.7 Pilot period + +Draft pilot period. + +## D.8 Reasoning: why adopt, and why now + +Draft reasoning. + +## D.9 Rejected alternatives + +Draft rejected alternatives. +""" + + +def unrelated_ratified_decision() -> str: + """A signed, ratified decision that is not an Appendix D adoption record. + + Used both at an unrelated path (to prove it cannot lend adoption + authority to a draft record elsewhere) and at the exact adoption-record + path itself (to prove a filename alone, even carrying a real Signature, + is never adoption authority). + """ + return """# DR-001: Naming decision + +Ratified by the Owner on 2026-01-01. This record ratifies a project naming +choice; it is not an adoption record and contains no Appendix D sections. + +## Signature + +Test Owner — Owner — 2026-01-01 +""" + + class StartWritwallTests(unittest.TestCase): def setUp(self) -> None: self.temp = Path(tempfile.mkdtemp()).resolve() @@ -131,6 +250,45 @@ def intake(self) -> dict: def handoff(self) -> str: return (self.output / "HANDOFF.md").read_text(encoding="utf-8") + def assert_contains_canonical_root(self, text: str, root: Path, label: str) -> None: + """Accept either native or portable rendering of one resolved root.""" + native = str(root) + posix = root.as_posix() + self.assertTrue( + native in text or posix in text, + f"{label} does not carry the canonical project root {root!s}", + ) + + def git(self, *args: str, cwd: Path) -> subprocess.CompletedProcess: + return subprocess.run( + ["git", *args], cwd=cwd, capture_output=True, text=True, timeout=30, + ) + + def make_git_worktree(self, repo_name: str, worktree_name: str, + branch: str) -> Path: + """Build a minimal seeded repository plus one linked worktree. + + Skips the calling test outright when a local git executable is + unavailable; otherwise returns the worktree top-level path. + """ + repo = self.temp / repo_name + repo.mkdir() + init = self.git("init", "--quiet", cwd=repo) + if init.returncode != 0: + self.skipTest(f"git unavailable: {init.stderr}") + self.git("config", "user.email", "test@example.invalid", cwd=repo) + self.git("config", "user.name", "Test", cwd=repo) + (repo / "README.md").write_text("seed\n", encoding="utf-8") + self.git("add", "README.md", cwd=repo) + commit = self.git("commit", "--quiet", "-m", "seed", cwd=repo) + self.assertEqual(commit.returncode, 0, commit.stdout + commit.stderr) + worktree = self.temp / worktree_name + add = self.git( + "worktree", "add", "--quiet", str(worktree), "-b", branch, cwd=repo, + ) + self.assertEqual(add.returncode, 0, add.stdout + add.stderr) + return worktree + def install_writwall(self) -> Path: build_source = self.temp / "build-source" shutil.copytree( @@ -285,6 +443,13 @@ def test_observed_boundaries_select_different_smallest_credible_topologies(self) (self.output / "discovery.json").read_text(encoding="utf-8") ) self.assertEqual(local_discovery["topology"]["tier"], "local_only") + self.assertIn("Architect", local_discovery["topology"]["roles"]) + self.assertIn("General", local_discovery["topology"]["roles"]) + self.assertIn("repository Operator", local_discovery["topology"]["roles"]) + self.assertNotIn( + "Owner-Agent architect/coordinator", + local_discovery["topology"]["roles"], + ) high_impact_project = self.temp / "high-impact-project" high_impact_project.mkdir() @@ -369,6 +534,7 @@ def test_lifecycle_change_during_interactive_intake_stops_before_any_write(self) process = subprocess.Popen( [ sys.executable, "-B", "-m", "writwall_cli", "start", + "--structured-intake", "--project-root", str(self.project), "--project-name", "Example project", "--purpose", "Build a small, governed project.", @@ -398,7 +564,7 @@ def test_lifecycle_change_during_interactive_intake_stops_before_any_write(self) for name in ("PLAN.md", "STATE.md", "ROUTING.md"): (governance / name).write_text(f"# {name}\n", encoding="utf-8") (decisions / "DR-001.md").write_text( - "# Adoption record\n", encoding="utf-8" + draft_adoption_record(), encoding="utf-8" ) changed_state = self.tree_snapshot(self.project) @@ -529,6 +695,9 @@ def test_missing_pointer_with_closed_history_never_emits_resume_prompt(self): closed.write_text("---\nid: WO-001\nstatus: CLOSED\n---\n", encoding="utf-8") for name in ("PLAN.md", "STATE.md", "ROUTING.md"): (self.project / "governance" / name).write_text(f"# {name}\n", encoding="utf-8") + decision = self.project / "governance" / "decisions" / "DR-001.md" + decision.parent.mkdir(parents=True) + decision.write_text(ratified_adoption_record(), encoding="utf-8") before = self.tree_snapshot(self.project) result = self.run_lifecycle_start() self.assertEqual(result.returncode, 0, result.stdout + result.stderr) @@ -536,7 +705,9 @@ def test_missing_pointer_with_closed_history_never_emits_resume_prompt(self): self.assertFalse(self.output.exists()) self.assertFalse(self.state.exists()) self.assertIn("Observed lifecycle state: retired_lockout", result.stdout) - self.assertIn("Act as a fresh Owner-Agent / Project-Architect", result.stdout) + self.assertIn("Act as a fresh General", result.stdout) + self.assertIn("General", result.stdout) + self.assertNotIn("Owner-Agent / Project-Architect", result.stdout) self.assertNotIn("resume", result.stdout.lower()) self.assertNotIn("Act as a fresh Implementer", result.stdout) @@ -614,8 +785,11 @@ def test_bootstrap_mixed_with_established_lifecycle_fails_closed(self): if lifecycle == "adopted": decision = governance / "decisions" / "DR-001.md" decision.parent.mkdir() - decision.write_text("# Adoption record\n", encoding="utf-8") + decision.write_text(ratified_adoption_record(), encoding="utf-8") else: + decision = governance / "decisions" / "DR-001.md" + decision.parent.mkdir() + decision.write_text(ratified_adoption_record(), encoding="utf-8") closed = governance / "history" / "WO-001.md" closed.parent.mkdir() closed.write_text( @@ -633,14 +807,14 @@ def test_bootstrap_mixed_with_established_lifecycle_fails_closed(self): self.assertIn("inconsistent state", result.stderr) self.assertIn(".writwall-bootstrap", result.stderr) - def test_adopted_lockout_routes_to_fresh_project_architect(self): + def test_adopted_lockout_routes_to_fresh_general(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("# Adoption record\n", encoding="utf-8") + decision.write_text(ratified_adoption_record(), encoding="utf-8") before = self.tree_snapshot(self.project) result = self.run_lifecycle_start() self.assertEqual(result.returncode, 0, result.stdout + result.stderr) @@ -648,7 +822,8 @@ def test_adopted_lockout_routes_to_fresh_project_architect(self): self.assertFalse(self.output.exists()) self.assertFalse(self.state.exists()) self.assertIn("Observed lifecycle state: adopted_lockout", result.stdout) - self.assertIn("Act as a fresh Owner-Agent / Project-Architect", result.stdout) + self.assertIn("Act as a fresh General", result.stdout) + self.assertNotIn("Owner-Agent / Project-Architect", result.stdout) flat = " ".join(result.stdout.split()) self.assertIn("Recommendation and material tradeoff", flat) self.assertIn("supporting evidence", flat) @@ -657,6 +832,151 @@ def test_adopted_lockout_routes_to_fresh_project_architect(self): self.assertIn("Do not ask for the same decision again", flat) self.assertIn("perform every mechanically available authorized step", flat) + def test_draft_adoption_record_with_closed_history_never_reports_adopted_or_retired(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") + decisions = governance / "decisions" + decisions.mkdir(parents=True) + (decisions / "DR-001.md").write_text(draft_adoption_record(), encoding="utf-8") + (decisions / "DR-999-unrelated.md").write_text( + unrelated_ratified_decision(), encoding="utf-8" + ) + closed = governance / "history" / "WO-001.md" + closed.parent.mkdir(parents=True) + closed.write_text("---\nid: WO-001\nstatus: CLOSED\n---\n", encoding="utf-8") + before = self.tree_snapshot(self.project) + result = self.run_lifecycle_start() + 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.assertNotIn("Observed lifecycle state: adopted_lockout", result.stdout) + self.assertNotIn("Observed lifecycle state: retired_lockout", result.stdout) + self.assertIn("Observed lifecycle state: partial_bootstrap", result.stdout) + + def test_signed_unrelated_decision_at_exact_adoption_path_fails_closed(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") + decisions = governance / "decisions" + decisions.mkdir(parents=True) + (decisions / "DR-001.md").write_text( + unrelated_ratified_decision(), encoding="utf-8" + ) + closed = governance / "history" / "WO-001.md" + closed.parent.mkdir(parents=True) + closed.write_text("---\nid: WO-001\nstatus: CLOSED\n---\n", encoding="utf-8") + before = self.tree_snapshot(self.project) + result = self.run_lifecycle_start() + self.assertNotEqual(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.assertNotIn("Observed lifecycle state: adopted_lockout", result.stdout) + self.assertNotIn("Observed lifecycle state: retired_lockout", result.stdout) + self.assertIn("inconsistent state", result.stderr) + self.assertIn("does not carry", result.stderr) + self.assertIn("adoption-record title", result.stderr) + # Nondisclosing: the diagnostic names the path, never the document body. + self.assertNotIn("Test Owner", result.stderr) + self.assertNotIn("Naming decision", result.stderr) + + def test_adopted_lockout_requires_ratified_evidence_not_mere_filename(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(parents=True) + decision.write_text("# Adoption record\n", encoding="utf-8") + before = self.tree_snapshot(self.project) + result = self.run_lifecycle_start() + self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.tree_snapshot(self.project), before) + self.assertFalse(self.output.exists()) + self.assertNotIn("Observed lifecycle state: adopted_lockout", result.stdout) + self.assertIn("inconsistent state", result.stderr) + self.assertIn("missing required Appendix D section", result.stderr) + + def test_retired_lockout_requires_ratified_adoption_evidence(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") + closed = governance / "history" / "WO-001.md" + closed.parent.mkdir(parents=True) + closed.write_text("---\nid: WO-001\nstatus: CLOSED\n---\n", encoding="utf-8") + before = self.tree_snapshot(self.project) + result = self.run_lifecycle_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.tree_snapshot(self.project), before) + self.assertFalse(self.output.exists()) + self.assertNotIn("Observed lifecycle state: retired_lockout", result.stdout) + self.assertIn("Observed lifecycle state: partial_bootstrap", result.stdout) + + def test_alternate_adoption_record_path_routes_to_adopted_lockout(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") + (governance / "ADOPTION-RECORD.md").write_text( + ratified_adoption_record(title="# Adoption record (alternate path)"), + encoding="utf-8", + ) + before = self.tree_snapshot(self.project) + result = self.run_lifecycle_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.tree_snapshot(self.project), before) + self.assertIn("Observed lifecycle state: adopted_lockout", result.stdout) + + def test_contradictory_coexisting_adoption_records_fail_closed(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") + decisions = governance / "decisions" + decisions.mkdir(parents=True) + (decisions / "DR-001.md").write_text(ratified_adoption_record(), encoding="utf-8") + (governance / "ADOPTION-RECORD.md").write_text( + ratified_adoption_record( + title="# Adoption record (alternate path)", revision="0.6", + ), + encoding="utf-8", + ) + before = self.tree_snapshot(self.project) + result = self.run_lifecycle_start() + self.assertNotEqual(result.returncode, 0) + self.assertEqual(self.tree_snapshot(self.project), before) + self.assertFalse(self.output.exists()) + self.assertIn("inconsistent state", result.stderr) + self.assertIn("contradictory", result.stderr) + + def test_contradictory_baseline_between_coexisting_adoption_records_fails_closed(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") + decisions = governance / "decisions" + decisions.mkdir(parents=True) + (decisions / "DR-001.md").write_text(ratified_adoption_record(), encoding="utf-8") + (governance / "ADOPTION-RECORD.md").write_text( + ratified_adoption_record( + title="# Adoption record (alternate path)", baseline="1" * 40, + ), + encoding="utf-8", + ) + before = self.tree_snapshot(self.project) + result = self.run_lifecycle_start() + self.assertNotEqual(result.returncode, 0) + self.assertEqual(self.tree_snapshot(self.project), before) + self.assertFalse(self.output.exists()) + self.assertIn("inconsistent state", result.stderr) + self.assertIn("contradictory", result.stderr) + self.assertIn("baseline", result.stderr) + def test_owner_time_yes_defines_capture_and_no_records_not_reported(self): yes = self.run_start("--owner-time", "yes") self.assertEqual(yes.returncode, 0, yes.stdout + yes.stderr) @@ -710,9 +1030,12 @@ def test_dns_mail_scenario_is_split_without_real_values(self): path.read_text(encoding="utf-8") for path in (self.output / "operations").glob("*.md") ) - self.assertNotIn("fastmail", combined.lower()) - self.assertNotIn("proton", combined.lower()) - self.assertNotIn("hllmr", combined.lower()) + scenario_text = combined.replace( + self.project.resolve().as_posix(), "" + ).lower() + self.assertNotIn("fastmail", scenario_text) + self.assertNotIn("proton", scenario_text) + self.assertNotIn("hllmr", scenario_text) self.assertIn("eight domains", combined.lower()) self.assertIn("dns authority cutover", combined.lower()) self.assertIn("historical mailbox data", combined.lower()) @@ -783,7 +1106,12 @@ def test_role_split_recommends_minimum_and_bounded_external_functions(self): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) handoff = self.handoff() self.assertIn("Recommended smallest credible role split", handoff) - self.assertIn("one human Owner, one Owner-Agent coordinator", handoff) + self.assertIn( + "one human Owner, one Architect, one General, one repository Operator", + handoff, + ) + self.assertIn("The Architect may interview", handoff) + self.assertIn("The General may draft, route", handoff) self.assertIn("2 separately bounded external function packet(s)", handoff) def test_windows_reserved_operator_name_is_made_portable(self): @@ -891,6 +1219,7 @@ def test_interactive_flow_offers_brief_and_time_capture_before_intake(self): result = subprocess.run( [ sys.executable, "-B", str(STARTER), + "--structured-intake", "--project-root", str(self.project), "--project-name", "Interactive project", ], @@ -935,6 +1264,7 @@ def test_interactive_unnamed_idea_is_qualified_one_question_at_a_time(self): ) result = subprocess.run( [sys.executable, "-B", "-m", "writwall_cli", "start", + "--structured-intake", "--project-root", str(self.project)], cwd=REPO_ROOT, env=self.environment(), @@ -992,6 +1322,135 @@ def test_paths_are_recorded_portably(self): self.assertNotIn("\\", text) self.assertIn(".writwall-bootstrap", self.handoff()) + def test_intake_records_one_resolved_canonical_project_root(self): + result = self.run_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + canonical = self.project.resolve() + self.assertEqual(self.intake()["project_root"], canonical.as_posix()) + self.assertNotEqual(self.intake()["project_root"], ".") + + def test_discovery_record_carries_the_same_canonical_project_root(self): + result = self.run_idea_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + canonical = self.project.resolve() + discovery = json.loads( + (self.output / "discovery.json").read_text(encoding="utf-8") + ) + self.assertEqual(discovery.get("project_root"), canonical.as_posix()) + + def test_every_generated_role_packet_carries_the_canonical_project_root(self): + result = self.run_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + canonical = self.project.resolve() + for relative in ( + "HANDOFF.md", "OWNER-AGENT.md", "REPOSITORY-OPERATOR.md", + "REVIEWER.md", "NAME-CLEARANCE.md", "OWNER-RATIFICATION.md", + ): + text = (self.output / relative).read_text(encoding="utf-8") + self.assert_contains_canonical_root(text, canonical, relative) + + def test_handoff_states_the_no_shadow_repository_no_durable_temp_rule(self): + result = self.run_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + handoff = self.handoff() + flat = " ".join(handoff.split()).lower() + self.assertIn("durable project artifacts", flat) + self.assertIn("never become the authoritative project tree", flat) + self.assertIn("removed after use", flat) + + def test_operation_packets_carry_root_and_no_shadow_repository_rule(self): + result = self.run_start("--external-operator", "DNS administration") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + canonical = self.project.resolve() + packet = (self.output / "operations" / "dns-administration.md").read_text( + encoding="utf-8" + ) + self.assert_contains_canonical_root(packet, canonical, "operation packet") + self.assertIn("shadow", packet.lower()) + self.assertIn("canonical", packet.lower()) + + def test_supplied_path_spelling_is_resolved_to_one_canonical_form(self): + spelled = str(self.project) + os.sep + "." + os.sep + result = subprocess.run( + [ + sys.executable, "-B", "-m", "writwall_cli", "start", + "--non-interactive", + "--project-root", spelled, + "--project-name", "Example project", + "--purpose", "Build a small, governed project.", + "--agent", "Codex", + "--location", "local workstation", + "--environment", "local repository only", + "--owner-time", "no", + "--confirm-no-secrets", + ], + cwd=REPO_ROOT, + env=self.environment(), + capture_output=True, + text=True, + timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + canonical = self.project.resolve() + self.assertEqual(self.intake()["project_root"], canonical.as_posix()) + + def test_non_git_project_directory_is_recorded_canonically(self): + result = self.run_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertFalse((self.project / ".git").exists()) + canonical = self.project.resolve() + self.assertEqual(self.intake()["project_root"], canonical.as_posix()) + + def test_git_worktree_top_level_is_recorded_as_its_own_canonical_root(self): + worktree = self.make_git_worktree( + "wt-top-repo", "wt-top-worktree", "wt-top-branch" + ) + result = self.run_start(project=worktree) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + intake = json.loads( + ((worktree / ".writwall-bootstrap") / "intake.json").read_text( + encoding="utf-8" + ) + ) + self.assertEqual(intake["project_root"], worktree.resolve().as_posix()) + + def test_nested_directory_inside_git_worktree_stops_with_rerun_diagnostic(self): + worktree = self.make_git_worktree( + "wt-nested-repo", "wt-nested-worktree", "wt-nested-branch" + ) + nested = worktree / "nested" / "project" + nested.mkdir(parents=True) + before = self.tree_snapshot(worktree) + result = subprocess.run( + [ + sys.executable, "-B", "-m", "writwall_cli", "start", + "--non-interactive", + "--project-root", str(nested), + "--project-name", "Example project", + "--purpose", "Build a small, governed project.", + "--agent", "Codex", + "--location", "local workstation", + "--environment", "local repository only", + "--owner-time", "no", + "--confirm-no-secrets", + ], + cwd=REPO_ROOT, + env=self.environment(), + capture_output=True, + text=True, + timeout=60, + ) + self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.tree_snapshot(worktree), before) + self.assertFalse((nested / ".writwall-bootstrap").exists()) + combined = (result.stdout + result.stderr).lower() + self.assertIn("worktree", combined) + self.assertIn("rerun", combined) + self.assert_contains_canonical_root( + result.stdout + result.stderr, worktree.resolve(), + "worktree rerun diagnostic", + ) + def test_environment_is_captured_without_becoming_authority(self): result = self.run_start() self.assertEqual(result.returncode, 0, result.stdout + result.stderr) @@ -1014,7 +1473,9 @@ def test_start_initializes_privacy_without_disclosing_its_location(self): self.assertGreater(intake["privacy_screen"]["entry_count"], 0) combined = result.stdout + result.stderr + self.handoff() self.assertNotIn(str(self.state), combined) - self.assertNotIn(str(self.project.resolve()), self.handoff()) + self.assert_contains_canonical_root( + self.handoff(), self.project.resolve(), "HANDOFF.md" + ) self.assertEqual(len(list(self.state.rglob("private-patterns.txt"))), 1) def test_start_preserves_local_private_identifiers_without_copying_them_to_bootstrap(self): @@ -1039,6 +1500,149 @@ def test_start_preserves_local_private_identifiers_without_copying_them_to_boots profile = next(self.state.rglob("private-patterns.txt")) self.assertIn(private_identifier, profile.read_text(encoding="utf-8")) + # -- WO-WW-021: conversation-first inception and existing-project + # continuity. Prove the ordinary bare invocation no longer demands the + # long structured questionnaire, existing repositories get a bounded + # local inventory and conversation-first Architect opening, empty + # projects get one open invitation, structured/non-interactive intake + # still works and now also emits the Owner/Architect/General/Operator + # topology, and active work still routes to a bounded Operator/ + # Implementer. Added RED in this work order; now exercised against the + # GREEN implementation. + + def run_conversation_start(self, *extra: str, project: Path | None = None): + """The ordinary, low-friction invocation: only --project-root, no + other intake flags, and no answers available on stdin. Today this + falls straight into the full interactive questionnaire and crashes + with EOFError on the first blocking `input()` call; the conversation + -first coordinator must instead succeed without it. + """ + return subprocess.run( + [ + sys.executable, "-B", "-m", "writwall_cli", "start", + "--project-root", str(project or self.project), + *extra, + ], + cwd=REPO_ROOT, + env=self.environment(), + input="", + capture_output=True, + text=True, + timeout=60, + ) + + def test_ordinary_project_root_only_invocation_skips_long_questionnaire(self): + result = self.run_conversation_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + combined = result.stdout + result.stderr + for legacy_prompt in ( + "Track Owner active minutes?", + "Continue without entering secrets?", + "Problem or opportunity", + "Working candidate name", + ): + self.assertNotIn(legacy_prompt, combined) + + def test_existing_git_repository_yields_bounded_local_observations_and_architect_opening(self): + init = self.git("init", "--quiet", cwd=self.project) + if init.returncode != 0: + self.skipTest(f"git unavailable: {init.stderr}") + self.git("config", "user.email", "test@example.invalid", cwd=self.project) + self.git("config", "user.name", "Test", cwd=self.project) + (self.project / "README.md").write_text("An existing project.\n", encoding="utf-8") + self.git("add", "README.md", cwd=self.project) + commit = self.git( + "commit", "--quiet", "-m", "Seed existing project inventory marker", + cwd=self.project, + ) + self.assertEqual(commit.returncode, 0, commit.stdout + commit.stderr) + branch = self.git("branch", "--show-current", cwd=self.project).stdout.strip() + self.assertTrue(branch) + + # A direct ordinary Git repository root (not a linked or nested + # worktree) used as the project root itself. + result = self.run_conversation_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + combined = result.stdout + result.stderr + self.handoff() + self.assertIn(branch, combined) + self.assertIn("Seed existing project inventory marker", combined) + self.assertIn("clean", combined.lower()) + self.assertIn("read-only", combined.lower()) + self.assertTrue( + "explore" in combined.lower() or "start elsewhere" in combined.lower(), + combined, + ) + + def test_empty_new_project_receives_open_conversational_invitation(self): + result = self.run_conversation_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + combined = result.stdout + result.stderr + self.handoff() + self.assertIn("Tell me what you are thinking", combined) + self.assertNotIn("Problem or opportunity", combined) + self.assertNotIn("Working candidate name", combined) + + def test_conversation_first_partial_idea_flags_keep_validation(self): + result = self.run_conversation_start( + "--problem", "A stated problem without the remaining qualification.", + ) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("missing idea qualification", result.stderr) + self.assertFalse(self.output.exists()) + + def test_conversation_first_contradictory_idea_flags_keep_validation(self): + flags = [ + "--problem", "A fully stated idea.", + "--intended-user", "The Owner.", + "--why-matters", "It avoids drift.", + "--evidence", "A concrete observed failure.", + "--smallest-outcome", "One bounded fix.", + "--success-signal", "The regression stays green.", + "--constraint", "Do not publish.", + "--non-goal", "Do not publish.", + "--risk", "The intake could overreach.", + "--kill-condition", "Stop on ambiguity.", + "--asset", "The existing repository.", + ] + result = self.run_conversation_start(*flags) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("contradictory idea qualification", result.stderr) + self.assertFalse(self.output.exists()) + + def test_structured_non_interactive_intake_still_emits_new_role_topology(self): + result = self.run_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertTrue((self.output / "ARCHITECT.md").is_file()) + self.assertTrue((self.output / "GENERAL.md").is_file()) + self.assertTrue((self.output / "OPERATOR.md").is_file()) + self.assertTrue((self.output / "REPOSITORY-OPERATOR.md").is_file()) + self.assertTrue((self.output / "REVIEWER.md").is_file()) + + def test_legacy_role_packet_names_remain_as_documented_compatibility_aliases(self): + result = self.run_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + owner_agent = (self.output / "OWNER-AGENT.md").read_text(encoding="utf-8") + repository_operator = (self.output / "REPOSITORY-OPERATOR.md").read_text( + encoding="utf-8" + ) + self.assertIn("compatibility alias", owner_agent.lower()) + self.assertIn("Architect", owner_agent) + self.assertIn("compatibility alias", repository_operator.lower()) + self.assertIn("Operator", repository_operator) + + def test_active_work_order_routes_to_bounded_operator_implementer(self): + work_order = self.project / "governance" / "work-orders" / "WO-001.md" + work_order.parent.mkdir(parents=True) + work_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") + result = self.run_lifecycle_start() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("Observed lifecycle state: active_work_order", result.stdout) + self.assertIn("Operator", result.stdout) + if __name__ == "__main__": unittest.main()