diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e431a9c..4d3cbbe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ # Contributing -Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/e43a4ed28726995d848266cb39bb20902bbb1574/labs/12-product-engineering-loop). +Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/2359acff52f0c7a7568fbd7daf1b62a79a86f7b5/labs/12-product-engineering-loop). The Boatstack repository receives product/runtime changes through a generated pull request. Review the PR's `UPSTREAM.json`, tests, adapter diff, and context-size change; do not hand-edit generated output on `main`. `.github/workflows` is the exception: it is Boatstack's executable control plane, excluded from scheduled projection and changed only through a separate manually reviewed Boatstack PR. diff --git a/UPSTREAM.json b/UPSTREAM.json index e25951e..bfe10f8 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -1,7 +1,7 @@ { "canonical_context": { - "characters": 92700, - "estimated_tokens": 23175, + "characters": 93864, + "estimated_tokens": 23466, "estimator": "ceil(total characters / 4); compactness signal, not provider billing", "files": [ "product-engineering-loop/references/workflow.md", @@ -12,7 +12,7 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "9bcd3e989153aaed4b3ec7f4808ca3bb4a99a4719dbd4e7104565f1ff9d4bd62", + "CONTRIBUTING.md": "4edef93971d1ab39ad70a63830dcb25ad8d79b0da769c90829a8ed91cf615c3c", "README.md": "534091974042589c31978080b0268761164f2279850f02c30e07f48945ef2321", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", @@ -35,7 +35,7 @@ "boatstack/assets/templates/test-plan.md": "6db8a9f27dd171fb80222a501cae50eb051e7278c04703fa43b5ff86dd4d2df4", "boatstack/atomic_unix.go": "89f2723361591de2bb8bd22ce7e34ec529d3278509f0df78fd5c4a7d4140fbe9", "boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f", - "boatstack/attach.go": "6a855440fac9acc63be857efef9d76210a4619774728684832dfbb08caf42a30", + "boatstack/attach.go": "8d23596da7c0dc77704d603112e3c5ce7de57f61279bfc27fee810d50aecb8df", "boatstack/autonomy.go": "45091e2451b9f8862c5620e13f2cec1e990161aec54cf9ce5073084f3de85c9a", "boatstack/autonomy_conformance_test.go": "eb0e880671e3304f5bc3f3d6ebda4b46fab824d2ef1289ffbbd7c486c7be3593", "boatstack/capability.go": "270288270ac9689551232e772d1656acbb4c3b2e730c441beb7064b53dd21836", @@ -47,7 +47,7 @@ "boatstack/cmd/boatstack-helper/coverage_conformance_test.go": "92352049f704f4068b8d73de24b6b37aa01cf626e27733b8f1585ca2e40fd358", "boatstack/cmd/boatstack-helper/flow.go": "0d41c7a86b49004e897f59551780220841d5941396202c81de97a4d52f593520", "boatstack/cmd/boatstack-helper/insight.go": "a6bb2afbf631eecd6005662e71b9956950fe2e56b1522dfd6ca93092d9a8d729", - "boatstack/cmd/boatstack-helper/main.go": "a2b3750e1a67cdbdff65c3520fcc83b8f437ac3dd9dc28f327d3009c7ff36db3", + "boatstack/cmd/boatstack-helper/main.go": "9cfd1dd1e6df50afb395b30cb9d88098b3c314eca5c2e61904100a20876513fb", "boatstack/cmd/boatstack-helper/main_test.go": "b36c52d6d5c9dd2428730de10ff18194b7e32a98722e41341c301c6f7a04cad5", "boatstack/cmd/boatstack-helper/retro.go": "68b83e33ade5b5fec126c70ec798fdcbed22fda755dc1cad2758143fead8e187", "boatstack/command.go": "4726ac515dedab4947be7eb48f88c6cb8b53d674124504b69f03e6396b080ee8", @@ -55,10 +55,10 @@ "boatstack/compiled_artifact_resolution_test.go": "0748d67643263e698211eb04d46464e1dd3db15d94537f5fd5092b5aa689745b", "boatstack/config_documentation_test.go": "4117687181e1a283cd47cb92a911c38a03b5e2a2b4290a4e3fd206cd1af5796c", "boatstack/content_effect_conformance_test.go": "ebf4f6d50af0a76722177c717c79d33921948b9c06bec2e9d062769dce32b8aa", - "boatstack/context.go": "02510af176d2d040c0080086f06d1235e76a1d47fef5176f96f740ad18d27660", + "boatstack/context.go": "fcab6ad475603b30bc6a8a59d82e257e74c587636adfe20a2016f48e24019d17", "boatstack/decision.go": "944fceed965396c66c8089edbf73dbe8d4c716da17289e407e45832c91211885", "boatstack/decision_test.go": "ac36687c5012f6d142d89472490e1c5f60c4427470f275afbe3afd4247db2281", - "boatstack/delivery.go": "734e0f787536bce6028dfebca18db68fcc88b213b9bdd605f84077d72aed3bc9", + "boatstack/delivery.go": "a362ed25024a91b1e98260fb49d1f9d9182fa713452e2e079d525243f3d85a3e", "boatstack/delivery_boundary_conformance_test.go": "53dde765046420b9119e82034d137742e600019938ed908c608f725d8a0c84c6", "boatstack/delivery_migrate.go": "4f31a1f2665200e86616e5b9b1cc1d1ba2e46edf0c5b0e9df2e98bab90c3763c", "boatstack/delivery_migrate_conformance_test.go": "b8ba53681e1d0361ac62b06586c62b7763d55a65b5427976b5289e1fb1503bdc", @@ -74,6 +74,8 @@ "boatstack/denial_solutions_conformance_test.go": "0b329dabffbc2666dffd2ff9e2b7d9d27816472d6fbee91a47a79c67527dce8d", "boatstack/denial_test.go": "9dc9f0f79328c4947073efaa785479b34e70eb214da57cd72348f39fd672e4fd", "boatstack/detached.go": "b0ea2a1f31bf2a2a83f6089a3065a9b47221194b64de48af9120046e6d70dee8", + "boatstack/detached_migration.go": "01a45e392307d14033fbf40aca379d91ce745ef0f386cc57a6273275386fc2f7", + "boatstack/detached_ownership_conformance_test.go": "9a6044d33d3a49c916241846e51a4411cba6e64ddef8f142cc5627af3caad6dd", "boatstack/detached_test.go": "2cd744335a80b9fbc2db8fa7955658dd31691c43150d67bf15029d06ce84277a", "boatstack/docs/control-law-scoping.md": "0ae984821248eabda8c0eeaf201b367991e6742984e7c718df20ecc24caee475", "boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df", @@ -81,7 +83,7 @@ "boatstack/export_test.go": "6ea4997a6a9f29b1d16fe4fdc3b9fa1057cb1a1a220c631e1ee7638480b0c82e", "boatstack/flow_coding.go": "9fa53a0204f98a25f97775c3acf37392a591c14ce850b44aa587b5806e770bb9", "boatstack/flow_coding_test.go": "dddcd7a85892d4fa10af42739d4c1ff265721b0313e27b6e7a1bbb019d5c3b51", - "boatstack/flow_control.go": "c9845ef3d87f41257a444f033b93eed72a0f4766b8e539825626ef01ff3b7685", + "boatstack/flow_control.go": "109df8b193da0587495d7a1322b189c2ed13ebba5f7dcc553262ccb2dbd52587", "boatstack/flow_control_test.go": "02d788c83be55ebd79ffc73875bfd019de45325151eb1f70f506980eb8e77f29", "boatstack/flow_drive.go": "90f57e178884aff017195a126954ac0aeb85f27707b9d42844323341dc1fefd9", "boatstack/flow_drive_conformance_test.go": "23edea926c271a1f5718fb9dae1da11e4bf03cceb1357290cd61cd8ffb73beda", @@ -94,7 +96,7 @@ "boatstack/flow_report.go": "a31d0764725eb38f717bab1e5509da82860d3bed23dd2b65ff388bb39573f418", "boatstack/flow_report_test.go": "ec989f2f14c7ae52840f61822a11dd0ac91055390f775d6a4b04da4993166b50", "boatstack/flow_solutions.go": "78d639ebd1012326f3e7e67cb5a4068de24898148a29db9176a1237af932f6aa", - "boatstack/flow_tasks.go": "690db05d345dabdb24965015c94198aa3f93d2d9691599ae8e6a2ac3aafb9d44", + "boatstack/flow_tasks.go": "a3a8699ac0fd6cda3a3420acf83cc5881c5a821b17a3f9cf0d20da460c6e5252", "boatstack/flow_tasks_conformance_test.go": "fbc4d672536051f8e20a2e6c07c5b10f78cd84fc04765eee11cbed7a459b8e4b", "boatstack/flow_trace.go": "5097d90f02f69ed49f25a04098fd0f2b9c62f4406fce8314c79ccc4a6dbf2763", "boatstack/flow_trace_test.go": "99f89a831e904f6a8ef710b6977ed3a808ce1c7ddfaba457b292d84f2ddca51b", @@ -108,11 +110,11 @@ "boatstack/hooks_hydrate_test.go": "7beeb26b2b1398741e8a28963a9686e974047016cc736f233024004add1afc32", "boatstack/hooks_test.go": "fb75e3aabf2204871b3e6d16de98d26fb33b0ec19e41aae761cf1f34397c31f4", "boatstack/hydrate_runtime_test.go": "dbd5eae2ba85701e4af0430ba3a0d70ea98e028b66992bd4fc05f3f582398627", - "boatstack/init.go": "1fc4392a76de4ab2bdc9a0493d5df8ccc36bf2b7fdbbf1ce3f484fc9ac90c8af", + "boatstack/init.go": "0cc4cf8d74a6be419759697d412f3748899315904eb30093bddb50cc0595e339", "boatstack/init_test.go": "5fdf687205e7a5984a98a87336b7127e4ae9b651d57e21ec2dc8ca7e653ee602", "boatstack/init_transaction.go": "112456c4e1c4db54c4137bcf4f7a9a9e63399a6f5971e9b3dc952d0c4b2aa4b6", "boatstack/insight.go": "7ec492b65043f7b10dae9dd55104d22bc56775759b8c3fb288545dede01b9f89", - "boatstack/insight_conformance_test.go": "6093b5496338e52995c231847d3938c4fa97b0f6b4ccbc49161b625d69e9c90e", + "boatstack/insight_conformance_test.go": "ef36d80a7a91bf20630b1e4683ee4fdc93c2e3f8ec8a9d0e8e4336aba859c25e", "boatstack/installation_repair.go": "f6889e3d21102d2aedfe1af900945d0a3ea2a772b3fddad787cef99803ef91aa", "boatstack/installation_repair_test.go": "fe831f15458c10057654a296ee4472177ec150ca937462d169787566525a3976", "boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c", @@ -143,17 +145,17 @@ "boatstack/internal/retromine/retromine_conformance_test.go": "33883893db1e455901d9c0e93767c39b475fae728aa9ee9dd925a2c21e5b0ec4", "boatstack/internal/retromine/testdata/session-alpha.jsonl": "45ed8fa691af9db3f957e87175dbbeffd8fdd2f001324e86a5fd68e29ffbe244", "boatstack/internal/retromine/testdata/session-beta.txt": "8e85e6a4442e3c892166df97ec879d998b3a47bdad5ef2bf785674b3149c36c2", - "boatstack/journey.go": "a006c7a8014680f8bf77fb66ac99dbc0b2502de5af602a078c35153e08ceb811", + "boatstack/journey.go": "a6d3d6f9873288a629a0fa59836c54a62046522e266b40962f7a404664f57509", "boatstack/journey_conformance_test.go": "8acc6b93279b41ba1f56be64a4f598e4c505bd19bc98a1082de949d2fbe97f23", "boatstack/migrate.go": "eaf589e2b266238068e42c6d78e01dc040266d28e342cb24f09e33e8541749b3", "boatstack/migrate_effect_grade.go": "bccb58e770001aa9554d8e7f151663d907f152508a61118f56fd90465ba6f32e", "boatstack/migrate_effect_grade_test.go": "fea1d1057bc6d8eaf015e377864a3adab29ef5731f597fe0a38b96fa80355d14", "boatstack/migrate_test.go": "9f4bda2fb158c5e54bcc0242dace1da3c1965f9846a213c573956a35b7d1724e", - "boatstack/mutation.go": "aaed87f376beef5b38be370d784aa11d3ba18a365689455bb4a2eac0c751503b", + "boatstack/mutation.go": "c4dce871f63f259a7bb5521c702982c0a135fc97b7928f7849cf30396cde80b8", "boatstack/mutation_test.go": "68d5049c7f96c1ac558e4c781151f67e8deee2f8d6b9bf293b90d44e769ef7c6", "boatstack/mutation_undo.go": "697d11b600a276ddbcabe6a9f8040d4f7283e017a0e8fd689ef53a274638946c", "boatstack/mutation_undo_test.go": "39540e717e3f2136bf975594043a3db9072b28ebe61c6cb0b982cea5e8b1e14e", - "boatstack/next.go": "7e16153e71b5130359163bf00e5be8f54df481d8bf89d7a0bc6543bb037c4e77", + "boatstack/next.go": "262f5cca59dd72f3c31dbf454fc448531322f132fa78335425c4029d78088aee", "boatstack/next_actor_conformance_test.go": "8759285ed6133c99a3282bc597ca3f774fc051f414fb55e6fc59bfa5a30e22c9", "boatstack/next_banner_test.go": "c431a6987ed1e479442fc9f5db4371632880b92aa790fa9dd0f5285293352c41", "boatstack/next_response.go": "11decf2e3b236cbaa183980946ec17ffbbbb1af9c08bd11a466a8487bf229d5f", @@ -161,16 +163,16 @@ "boatstack/next_test.go": "6b5ec46ecf1a197d7644846cecbb6d99873a06b7c4e5562772b5016fa0a4cb11", "boatstack/operation.go": "1eac601c216282983dceb6f2f7c0be58b06312fb9659447bba02df896d23929a", "boatstack/operation_test.go": "9580b71ed4fa70cf73f02975737c824484341fa6751e670e7d183e898e1ffdde", - "boatstack/paths.go": "84b56d031389dd872dfb88d08cfbdf178befe6df3ef2ac6d75369da8d827e8c6", - "boatstack/plan.go": "5874bee9c437ea55495c7f2c3678195bf5d4370f8c383cebc0fd4d9abd46f02b", + "boatstack/paths.go": "9341d9fcda8f02f769cd81ef814789de5e167d79ae6d53300b76d5e6971bd952", + "boatstack/plan.go": "e3b6d43fa4a728a09dd1708135cbca2e9d6b603693fc9f1d22e6587ab66d368f", "boatstack/plan_test.go": "1b01e7d9d7794eb11c998e19a2f3532d3b8509d984eca934cf5f1662a0a7e573", "boatstack/plan_validation.go": "f54420da065eeafdeaa47f54244e4612d751b3b20244da9f5236c22e4bfcd8ca", "boatstack/plan_validation_test.go": "406c672470e909cb8d54f42175952c581ea872fa21bdcf34675f23187688fe33", - "boatstack/planning.go": "63f2dcfcce85a3c7a39d183b5a557f7085f306ed57b130282e0678209b05c6c8", + "boatstack/planning.go": "3263587dea8d055bdfb8bf791d98a0a0a1abcb11433e739f2bfa8a8a882cf58b", "boatstack/planning_first_write_conformance_test.go": "873097aa9384b75bf01e74a475f3ec2ac7cca4a28f733e82f2c82959032c6a30", "boatstack/planning_test.go": "06ec7022222d926040c3ae28b84ab50c3d2f804ae6473e61b303804dd992d884", "boatstack/post_publish_prescribe_conformance_test.go": "3c20d359ff84648db7dedb227b4d64e6574d9f41d3cdca0adefec1c60bfbf4ae", - "boatstack/pr.go": "8a7f25257d0c1e8afd4a1a55de9d254e5b337b4773d38ab9ca7a8316717041a0", + "boatstack/pr.go": "ba12bf51297a50ffb9ec19847b0af70b5660f60f6af3aadb1047b147093e64ec", "boatstack/pr_phase.go": "59f8cbb75b6b538a5345474acd6a725450979579bf8ecf9591956cbbe1cc4737", "boatstack/pr_phase_conformance_test.go": "bc9c834e9c4ed43b35d81abafd7b1bf2a264ea2a8c4a4ec9758ee18d1d438968", "boatstack/pr_test.go": "5c0ff03eb21e383026a4e9fbc5671b2e316040e4e4c55ab6583b930e7e117dda", @@ -179,14 +181,14 @@ "boatstack/provision_test.go": "70199eac574cc8843ce12f2bad58b7fea0f86a4205a6d3be0a96594abd967b5d", "boatstack/publication_ignored_repro_test.go": "b6f3aeb8ba22949ff9af7ac5afe8fb828385d9708d5d5893ef41f33a3de873e1", "boatstack/published_slice_routing_test.go": "ea7e7351018bc13dcd31c4b96f50f8bc230e8a1dbf7806fba32a12ae58923e7e", - "boatstack/readiness.go": "30ddf87d650c92e66f4393c6d18b6728b0b38f0831f564159998be2dbe12b708", + "boatstack/readiness.go": "121c566ac7a0945137f05b0c55ff0dad3f8fdf450ec1a3891f6caa906c907266", "boatstack/readiness_conformance_test.go": "e3df7730e18f671873400e8061f178310c45eab6d7cdebedf8234590d3e7eab8", - "boatstack/recovery.go": "a0549f5bbebe113f36aa832131bbddc34185d6ad561b6cc2face200bb1cbf6e0", + "boatstack/recovery.go": "63fe04b789113a2362037fe5bfc552db422537b6ae562e1029586cb5177efe85", "boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f", "boatstack/reexec.go": "fed55416479d7bd3e0c3637057ffe8eb58a032f93fc358f76df906ab7acc677b", "boatstack/reexec_unix.go": "ff86157a9aa20c82a56fcd859b70669b7eacf4e0a9f61a4546ef33808437939e", "boatstack/reexec_windows.go": "f5335c8c28cb4e89048b058b1c4d12f78644f99acb4f6167ff60e622dfb9e742", - "boatstack/references/artifacts.md": "60c8f3a780aa10681fa24d1aa3173a91cd1a79afa7fbfbde6cf0d12ca7e9cd3b", + "boatstack/references/artifacts.md": "6a7499e446dbc9c6514d50475675980872ae3006222096ddd42d1c177ef617fc", "boatstack/references/config-schema.md": "1da1326cef1573d60e5df6fb3e0ae656f324fb4f7ad1df6f57c5c51664616b46", "boatstack/references/failure-moves.md": "2be252c8de61380712e351edaeb51625bf5c7eb23ebced04bc2bf0f5b4fb3be6", "boatstack/references/host-hook-contracts.md": "2a89d44d0e418a53f2e3b6300fed957cdf878f45ea97ce24b55b66065f0eaa1d", @@ -201,11 +203,11 @@ "boatstack/retro_conformance_test.go": "827250d2fc49717fb5e58a4cf79e1d5c489c37574d8a2cf9348fa1cd8c328713", "boatstack/run.go": "3127e8c054b80e41413011e423ba9463fe06a7e6c9e1e71750ab98587871d89d", "boatstack/run_test.go": "5b291510fa90cefdc26eb89e18a3443385456a6ebc73408325ac1945b7c084d6", - "boatstack/runtime.go": "cd77c34910de45ca382275df1a36ce39fe88497169618096009962d2bcb8f591", + "boatstack/runtime.go": "72bf0ba69730a51fe668cd9adec6815be6e4aca6d43da3d1e00feecdcaa17ff8", "boatstack/runtime_cache.go": "6f6b023170cce982bf155e7c2fc7752cca2f7acff771967b4a523c1c13ea876f", "boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85", "boatstack/runtime_provenance_test.go": "1d52f1e6b0691cf4667729cc9b9f3c55c128f0aa3321f3a2843a9aa6fd0e73dc", - "boatstack/safety.go": "ab3faa48cae73991d6755ac087879734ce997787b8936d68711d4903ce55c374", + "boatstack/safety.go": "c624d05cab2ff9f7023e3ba0c08a15496a053eaf2a9ea2e81f831797f66dc2ce", "boatstack/safety_corpus_test.go": "824051705dd893338ac2246e2d75231703e36574cc9c23902237f72ab2d35960", "boatstack/safety_test.go": "ddd7a72d4ff50c046aa46fd97b108d629cef27cf15816150b6d54baf5f1c4c36", "boatstack/safety_update_publisher_test.go": "ed3f8187036623694dfe7c395cdae00fdae14609bab6124d1fdfc6fe73fa2196", @@ -213,7 +215,7 @@ "boatstack/skill_frontmatter_test.go": "a3ec52e7df357a72265c95dd66db15d9c0effc7e5f90f14ce69c27792ce394eb", "boatstack/solution_closure_conformance_test.go": "f73e6748dac373e2a10bc9269c2f2e060bd220bb4113bd0d5ff66ca4c8e91a54", "boatstack/statemap.go": "0db3a980f2fd00538498f6599b488a6719a5ee0f0764172834165e7d9d8f8057", - "boatstack/statemap_conformance_test.go": "7bf80f015d49791da94b51463573126de60f26caa6157e60ed5abc081ed2c7ab", + "boatstack/statemap_conformance_test.go": "504debd406c1b5ad6cc9cb38715dea08954ef9c258cb5f91c59fb04d1398e53e", "boatstack/supervisory_control_test.go": "c7ea4bcd678e8ec211dac772c834981c4e21762914be2770a5e181bc24605e06", "boatstack/testdata/reviewer-pr-body.md": "4c64e3788e5d61a377aeb0f797f7fc8d2316ab6e49572d15636eea7ba9e34ac4", "boatstack/testdata/safety/safe_apply.py.txt": "c9ec7fb932cf21b6aa8df597c4d4c54d6ec65e796240e49118d699f583383975", @@ -237,10 +239,10 @@ "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", "docs/configuration.md": "ed62dfd360377f7a15826ef5a2d3c9798d25c272168c65480961f9e0d011ccf1", - "docs/evidence-engineered-coding.md": "d1f964276b904b70a8850923b6e19851fc23dc0608354e06d4475d69669c59f5", + "docs/evidence-engineered-coding.md": "b72edea24e381dcf807601012e35f4c4cfee50a1e066cf060e54ee33a6fa8ff9", "docs/generated-files.md": "8679b960bacbdf2ca7b898aa44eb3a486ebb325eca8ce9cc4e316191e7ef5087", "docs/getting-started.md": "41f3fd80dc71a60c10964fd22fe0c079954d239c8782e62f325918190f59979b", - "docs/public-claims.json": "33fca1014a08ed087d92a5779b8153dd39d6384b909c8849b883d1b2f132fe41", + "docs/public-claims.json": "63d1139e39eb20f72de4428608750e0a677e2d8617713c635d1235178dc18e04", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -254,7 +256,7 @@ "labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d", "labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71", "labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39", - "labs/diagram-json/plan.lock.json": "f7c40ca8fa1c7a9567bfbdcc885792e0d2ae158c7d6b1b002cb6591f1b82e69e", + "labs/diagram-json/plan.lock.json": "1c910d868ecddf40c9704fd1227cd6e827ddefd62be4d7137d4bafacdb7cd3a6", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -405,12 +407,13 @@ "release-notes/2026-08-01-independent-insight-captures.md": "5d18f26fced9589fc7cfd70eac1bfe28038f99b921a5ea7766c310d6595349c3", "release-notes/2026-08-01-readable-projection-history.md": "93c4db5c3935b4c4c251f2f8a5dd2925a7241f34f6dd7bb9602badca6ad367bc", "release-notes/2026-08-01-terminal-update-postcondition.md": "65a502bfc4ee8002614e555189b082c3859dbfc2d0611c3f598dc951353c41a1", + "release-notes/2026-08-02-detached-ownership-boundary.md": "a85bdb80e28ccb9369bda06a03f73f3377889e0c2cc9f0ce4d85e8056946600b", "release-notes/2026-08-02-goal-driven-autonomous-run.md": "a5b66250c98ca6c23c3761bc09c81cebd02a1243de12fc6110bd1119fd8faf4a" }, "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "e43a4ed28726995d848266cb39bb20902bbb1574", + "commit": "2359acff52f0c7a7568fbd7daf1b62a79a86f7b5", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/attach.go b/boatstack/attach.go index 7d47a05..fbd43c8 100644 --- a/boatstack/attach.go +++ b/boatstack/attach.go @@ -21,14 +21,15 @@ type AttachOptions struct { // AttachResult is the deterministic outcome of an attach request. type AttachResult struct { - SchemaVersion int `json:"schema_version"` - VerificationStatus string `json:"verification_status"` // VERIFIED | BLOCKED - Mode string `json:"mode,omitempty"` - RepoID string `json:"repo_id,omitempty"` - RepoRoot string `json:"repo_root,omitempty"` - ControlRoot string `json:"control_root,omitempty"` - WorktreeID string `json:"worktree_id,omitempty"` - Reason string `json:"reason"` + SchemaVersion int `json:"schema_version"` + VerificationStatus string `json:"verification_status"` // VERIFIED | BLOCKED + Mode string `json:"mode,omitempty"` + RepoID string `json:"repo_id,omitempty"` + RepoRoot string `json:"repo_root,omitempty"` + ControlRoot string `json:"control_root,omitempty"` + WorktreeID string `json:"worktree_id,omitempty"` + Reason string `json:"reason"` + FeatureMigrations []DetachedFeatureMigration `json:"feature_migrations,omitempty"` } func blockedAttach(reason string) AttachResult { @@ -62,12 +63,22 @@ func AttachDetached(opts AttachOptions) (AttachResult, error) { ctx := detachedContextFromIdentity(stateRoot, identity) - // Synthesize configuration from the repository (test command, default branch, - // context) exactly as embedded init does. - config := defaultConfig(root, detectTestCommand(root)) - rawConfig, err := MarshalJSON(config) + // Prefer the repository's declared source configuration during explicit + // reattachment. Falling back to discovery is valid only when no source exists. + configPath := filepath.Join(root, sourceConfigName) + config, rawConfig, err := LoadConfig(configPath) + if os.IsNotExist(err) { + config = defaultConfig(root, detectTestCommand(root)) + rawConfig, err = MarshalJSON(config) + } if err != nil { - return blockedAttach(err.Error()), nil + return blockedAttach("Boatstack could not load the repository source configuration: " + err.Error()), nil + } + imports, migrationResults, migrationErr := planDetachedFeatureImports(root, ctx) + if migrationErr != nil { + result := blockedAttach("Boatstack refused detached feature migration: " + migrationErr.Error()) + result.FeatureMigrations = migrationResults + return result, nil } // Generate the controller bundle and write it under the external control root. @@ -86,6 +97,10 @@ func AttachDetached(opts AttachOptions) (AttachResult, error) { if err := os.WriteFile(ctx.SourceConfigPath(), rawConfig, 0o644); err != nil { return blockedAttach(err.Error()), nil } + migrationResults, err = applyDetachedFeatureImports(imports, migrationResults) + if err != nil { + return blockedAttach("Boatstack could not import embedded feature state: " + err.Error()), nil + } // Write the binding and index it in the registry. binding := DetachedBinding{ @@ -132,6 +147,7 @@ func AttachDetached(opts AttachOptions) (AttachResult, error) { RepoRoot: root, ControlRoot: ctx.controlRoot, WorktreeID: identity.WorktreeID, + FeatureMigrations: migrationResults, Reason: "Attached Boatstack in detached mode. The repository was not modified; all controller state lives under the external control root.", }, nil } diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go index 3daf0b1..efb033d 100644 --- a/boatstack/cmd/boatstack-helper/main.go +++ b/boatstack/cmd/boatstack-helper/main.go @@ -459,7 +459,7 @@ func checkPlanCommand(arguments []string) int { readinessFingerprint := "" if version, _ := check.Plan["schema_version"].(float64); version >= 3 { readiness, readinessErr := boatstack.CheckPlanReadiness(*plan) - repo, _ := boatstack.ResolveRepository(filepath.Dir(*plan)) + repo, _ := boatstack.ResolveControllerRepository(filepath.Dir(*plan)) if readinessErr != nil { boatstack.RecordFlowAttribution(repo, "readiness", deliverycontrol.CostQuery, true, readinessErr.Error()) return fail(readinessErr) @@ -1102,7 +1102,16 @@ func doctorCommand(arguments []string) int { if err := boatstack.DoctorRepairHint(boatstack.Doctor(*repo)); err != nil { return fail(err) } + root, err := boatstack.ResolveRepository(*repo) + if err != nil { + return fail(err) + } + ctx, err := boatstack.ResolveWorkspaceContext(root) + if err != nil { + return fail(err) + } fmt.Printf("PASS: Boatstack %s installation and generated adapters are healthy\n", boatstack.Version) + fmt.Printf("SUPERVISION_MODE=%s\nCONTROLLER_ROOT=%s\nHEALTH=VERIFIED\n", ctx.Mode, ctx.ExportRoot()) hosts, err := boatstack.DoctorHookHosts(*repo) if err != nil { return fail(err) diff --git a/boatstack/context.go b/boatstack/context.go index 87522ef..5402e1c 100644 --- a/boatstack/context.go +++ b/boatstack/context.go @@ -37,7 +37,7 @@ func ProjectOperatorContext(repoPath, operation, host string) (OperatorContext, } out := OperatorContext{ SchemaVersion: detachedSchemaVersion, Mode: string(SupervisionEmbedded), - RepoRoot: root, Operation: operation, Host: host, + RepoRoot: root, ControlRoot: root, Operation: operation, Host: host, } if ctx, ok, verifyErr := detachedContextFor(root); verifyErr != nil { diff --git a/boatstack/delivery.go b/boatstack/delivery.go index 2fac766..ae2f1d3 100644 --- a/boatstack/delivery.go +++ b/boatstack/delivery.go @@ -560,7 +560,7 @@ func archiveDeliveryReceipt(repo, feature, sliceID, gate, observationID string) } func appendChangeObservation(repo string, observation ChangeObservation) error { - path := filepath.Join(repo, ".product-loop", "features", observation.Feature, "changes.md") + path := filepath.Join(WorkspaceFor(repo).FeatureDir(observation.Feature), "changes.md") existing, err := os.ReadFile(path) if err != nil && !os.IsNotExist(err) { return err @@ -577,7 +577,7 @@ func appendChangeObservation(repo string, observation ChangeObservation) error { } func nextChangeObservationID(repo, feature string, fallback int) string { - path := filepath.Join(repo, ".product-loop", "features", feature, "changes.md") + path := filepath.Join(WorkspaceFor(repo).FeatureDir(feature), "changes.md") value, err := os.ReadFile(path) if err != nil { return fmt.Sprintf("CHG-%03d", fallback) @@ -625,7 +625,7 @@ func RecordChangeObservation(options ChangeObservationOptions) (ChangeObservatio evidenceHash := SHA256Bytes([]byte(strings.TrimSpace(options.Evidence))) mechanismHash := SHA256Bytes([]byte(strings.TrimSpace(options.Mechanism))) if repairClass { - changePath := filepath.Join(repo, ".product-loop", "features", options.Feature, "changes.md") + changePath := filepath.Join(WorkspaceFor(repo).FeatureDir(options.Feature), "changes.md") if prior, readErr := os.ReadFile(changePath); readErr == nil { for _, block := range strings.Split(string(prior), "\n## ") { if strings.Contains(block, "- Classification: `"+classification+"`") && @@ -905,7 +905,7 @@ func resolveAddressableSliceByBranch(state DeliveryState, branch string) (int, D } func checkDeliveryPlanLock(repo, feature string, state DeliveryState) error { - lockPath := filepath.Join(repo, ".product-loop", "features", feature, "plan.lock.json") + lockPath := filepath.Join(WorkspaceFor(repo).FeatureDir(feature), "plan.lock.json") lockHash, err := SHA256File(lockPath) if err != nil { return fmt.Errorf("managed delivery requires its current plan lock: %w", err) @@ -1109,7 +1109,7 @@ func RecordDeliveryGate(options DeliveryGateOptions) (DeliveryGateReceipt, error } evidencePath := strings.TrimSpace(options.EvidencePath) if evidencePath == "" { - evidencePath = featureEvidencePath(filepath.Join(repo, ".product-loop", "features", options.Feature)) + evidencePath = featureEvidencePath(WorkspaceFor(repo).FeatureDir(options.Feature)) } else if !filepath.IsAbs(evidencePath) { evidencePath = filepath.Join(repo, evidencePath) } @@ -1129,7 +1129,7 @@ func RecordDeliveryGate(options DeliveryGateOptions) (DeliveryGateReceipt, error if recorded := deliveryEvidenceGateStatus(string(evidenceValue), gateLabel, slice.ID, explicit); recorded != status { return DeliveryGateReceipt{}, fmt.Errorf("evidence ledger must mark the %s gate for delivery slice %s as %s; found %q", gate, slice.ID, status, recorded) } - relEvidence, err := repositoryRelativePath(repo, evidencePath) + relEvidence, err := repositoryRelativePath(WorkspaceFor(repo).ExportRoot(), evidencePath) if err != nil { return DeliveryGateReceipt{}, err } @@ -1451,7 +1451,7 @@ type DiscardDeliveryResult struct { // orphan, so discard-delivery must clear it. It refuses a dir carrying a // plan.lock.json (a registered, live feature) so it never touches active work. func discardOrphanFeatureArtifacts(repo, feature string) (DiscardDeliveryResult, bool, error) { - dir := filepath.Join(repo, ".product-loop", "features", feature) + dir := WorkspaceFor(repo).FeatureDir(feature) info, statErr := os.Stat(dir) if os.IsNotExist(statErr) { return DiscardDeliveryResult{}, false, nil diff --git a/boatstack/detached_migration.go b/boatstack/detached_migration.go new file mode 100644 index 0000000..39a6795 --- /dev/null +++ b/boatstack/detached_migration.go @@ -0,0 +1,268 @@ +package boatstack + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" +) + +// DetachedFeatureMigration reports one embedded feature package considered by +// explicit attachment repair. Status is IMPORTED, UNCHANGED, CONFLICTING, or +// REJECTED; the vocabulary is stable for host adapters. +type DetachedFeatureMigration struct { + Feature string `json:"feature"` + Status string `json:"status"` + Reason string `json:"reason"` +} + +type detachedFeatureImport struct { + feature string + source string + target string +} + +var detachedImportBeforeRename func(source, temporary, target string) error + +func directoryFingerprint(root string) (string, error) { + info, err := os.Lstat(root) + if err != nil { + return "", err + } + if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { + return "", fmt.Errorf("feature package root is not a real directory: %s", root) + } + parts := []string{} + err = filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if path == root { + return nil + } + relative, err := filepath.Rel(root, path) + if err != nil { + return err + } + if entry.Type()&os.ModeSymlink != 0 { + return fmt.Errorf("feature package contains a symlink: %s", relative) + } + if entry.IsDir() { + parts = append(parts, filepath.ToSlash(relative)+"/") + return nil + } + if !entry.Type().IsRegular() { + return fmt.Errorf("feature package contains a non-regular file: %s", relative) + } + hash, err := SHA256File(path) + if err != nil { + return err + } + parts = append(parts, filepath.ToSlash(relative)+"\x00"+hash) + return nil + }) + if err != nil { + return "", err + } + sort.Strings(parts) + return SHA256Bytes([]byte(joinNUL(parts))), nil +} + +func joinNUL(values []string) string { + result := "" + for index, value := range values { + if index > 0 { + result += "\x00" + } + result += value + } + return result +} + +func validateEmbeddedFeaturePackage(repo, directory, feature string) error { + check, err := CheckPlan(filepath.Join(directory, "plan.md")) + if err != nil { + return err + } + if stringValue(check.Plan["feature_id"]) != feature { + return fmt.Errorf("plan feature_id does not match directory") + } + if path := filepath.Join(directory, "approval.md"); fileExists(path) { + receipt, loadErr := LoadApprovalReceipt(path) + if loadErr != nil || receipt.Fingerprint != check.Fingerprint { + return fmt.Errorf("approval receipt fingerprint is invalid or stale") + } + } + if path := filepath.Join(directory, "autonomy.md"); fileExists(path) { + value, loadErr := loadJSONObject(path, "autonomy receipt", autonomyMarkerStart, autonomyMarkerEnd, true) + if loadErr != nil { + return loadErr + } + data, marshalErr := MarshalJSON(value) + if marshalErr != nil { + return marshalErr + } + var receipt AutonomyReceipt + if decodeErr := DecodeJSON("autonomy receipt", path, data, &receipt); decodeErr != nil { + return decodeErr + } + fingerprint, fingerprintErr := autonomyFingerprint(receipt) + if fingerprintErr != nil || fingerprint != receipt.Fingerprint || receipt.Feature != feature || receipt.PlanFingerprint != check.Fingerprint { + return fmt.Errorf("autonomy receipt fingerprint is invalid or stale") + } + } + return nil +} + +func planDetachedFeatureImports(repo string, ctx WorkspaceContext) ([]detachedFeatureImport, []DetachedFeatureMigration, error) { + sourceRoot := filepath.Join(repo, productLoopDirName, "features") + entries, err := os.ReadDir(sourceRoot) + if os.IsNotExist(err) { + return nil, nil, nil + } + if err != nil { + return nil, nil, err + } + candidates := []string{} + for _, entry := range entries { + if entry.IsDir() && featureSlugPattern.MatchString(entry.Name()) && fileExists(filepath.Join(sourceRoot, entry.Name(), "plan.md")) { + candidates = append(candidates, entry.Name()) + } + } + selected := detachedOpenFeatureCandidates(repo, candidates) + imports := []detachedFeatureImport{} + results := []DetachedFeatureMigration{} + blocked := false + for _, entry := range entries { + feature := entry.Name() + if !selected[feature] { + continue + } + source := filepath.Join(sourceRoot, feature) + target := ctx.FeatureDir(feature) + if err := validateEmbeddedFeaturePackage(repo, source, feature); err != nil { + results = append(results, DetachedFeatureMigration{Feature: feature, Status: "REJECTED", Reason: err.Error()}) + blocked = true + continue + } + sourceHash, err := directoryFingerprint(source) + if err != nil { + return nil, results, err + } + if pathExists(target) { + targetHash, targetErr := directoryFingerprint(target) + if targetErr != nil { + return nil, results, targetErr + } + if sourceHash == targetHash { + results = append(results, DetachedFeatureMigration{Feature: feature, Status: "UNCHANGED", Reason: "Embedded and detached packages are byte-identical."}) + continue + } + results = append(results, DetachedFeatureMigration{Feature: feature, Status: "CONFLICTING", Reason: "Embedded and detached packages differ; Boatstack will not choose by recency."}) + blocked = true + continue + } + imports = append(imports, detachedFeatureImport{feature: feature, source: source, target: target}) + } + if blocked { + return nil, results, fmt.Errorf("embedded feature migration requires conflict or receipt repair") + } + return imports, results, nil +} + +// detachedOpenFeatureCandidates excludes historical packages. The current +// feature branch is authoritative when it names an embedded package; otherwise +// one active delivery or the sole package can be recovered without ambiguity. +func detachedOpenFeatureCandidates(repo string, candidates []string) map[string]bool { + selected := map[string]bool{} + branch := strings.TrimSpace(gitOutput(repo, "branch", "--show-current")) + for _, prefix := range []string{"feat/", "fix/", "chore/", "ci/"} { + feature := strings.TrimPrefix(branch, prefix) + if feature == branch { + continue + } + for _, candidate := range candidates { + if candidate == feature { + selected[candidate] = true + return selected + } + } + } + active, _, err := scanManagedDeliveries(repo) + if err == nil && len(active) == 1 { + for _, candidate := range candidates { + if candidate == active[0] { + selected[candidate] = true + return selected + } + } + } + if len(candidates) == 1 { + selected[candidates[0]] = true + } + return selected +} + +func pathExists(path string) bool { + _, err := os.Lstat(path) + return err == nil +} + +func copyDirectoryAtomic(source, target string) error { + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + temporary, err := os.MkdirTemp(filepath.Dir(target), ".boatstack-feature-import-*") + if err != nil { + return err + } + defer os.RemoveAll(temporary) + err = filepath.WalkDir(source, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + relative, err := filepath.Rel(source, path) + if err != nil || relative == "." { + return err + } + destination := filepath.Join(temporary, relative) + if entry.IsDir() { + return os.MkdirAll(destination, 0o755) + } + value, err := os.ReadFile(path) + if err != nil { + return err + } + return atomicWriteMode(destination, value, 0o644) + }) + if err != nil { + return err + } + before, err := directoryFingerprint(source) + if err != nil { + return err + } + after, err := directoryFingerprint(temporary) + if err != nil || before != after { + return fmt.Errorf("copied feature package failed fingerprint verification") + } + if detachedImportBeforeRename != nil { + if err := detachedImportBeforeRename(source, temporary, target); err != nil { + return err + } + } + return os.Rename(temporary, target) +} + +func applyDetachedFeatureImports(imports []detachedFeatureImport, results []DetachedFeatureMigration) ([]DetachedFeatureMigration, error) { + for _, planned := range imports { + if err := copyDirectoryAtomic(planned.source, planned.target); err != nil { + return results, err + } + results = append(results, DetachedFeatureMigration{Feature: planned.feature, Status: "IMPORTED", Reason: "Validated embedded package was atomically imported into detached controller state."}) + } + sort.Slice(results, func(i, j int) bool { return results[i].Feature < results[j].Feature }) + return results, nil +} diff --git a/boatstack/detached_ownership_conformance_test.go b/boatstack/detached_ownership_conformance_test.go new file mode 100644 index 0000000..102a52a --- /dev/null +++ b/boatstack/detached_ownership_conformance_test.go @@ -0,0 +1,172 @@ +package boatstack + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" +) + +func embeddedFeatureForDetach(t *testing.T, repo, feature string, approvalFingerprint string) string { + t.Helper() + config := testConfig() + config.Workflow.HumanPlanApproval = false + // Host activation files remain repository-owned. They are orthogonal to the + // detached generated-state invariant exercised by these fixtures. + config.Adapters = nil + raw, err := MarshalJSON(config) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(repo, sourceConfigName), raw, 0o644); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(repo, productLoopDirName), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(repo, productLoopDirName, "project.json"), raw, 0o644); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(repo, "plans"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(repo, "plans", "source.md"), []byte("# Durable source plan\n"), 0o644); err != nil { + t.Fatal(err) + } + directory := filepath.Join(repo, productLoopDirName, "features", feature) + if err := os.MkdirAll(directory, 0o755); err != nil { + t.Fatal(err) + } + plan := validPlan() + plan["feature_id"] = feature + plan["source_plan_path"] = "../../../plans/source.md" + writeMarkdownPlan(t, filepath.Join(directory, "plan.md"), plan, true) + if err := os.WriteFile(filepath.Join(directory, "spec.md"), []byte("# Accepted specification\n"), 0o644); err != nil { + t.Fatal(err) + } + if approvalFingerprint != "" { + writeApprovalReceipt(t, filepath.Join(directory, "approval.md"), approvalFingerprint) + } + return directory +} + +func TestDetachedOpenFeatureCandidatesIgnoreHistoricalPackagesOnMain(t *testing.T) { + repo := detachedTestRepo(t, "https://github.com/acme/history.git") + selected := detachedOpenFeatureCandidates(repo, []string{"old-one", "old-two"}) + if len(selected) != 0 { + t.Fatalf("historical packages were selected on main: %v", selected) + } +} + +// control-law: detached-generated-state-has-one-resolved-owner +func TestDetachedAttachImportsFeatureAndIgnoresEmbeddedDrift(t *testing.T) { + repo := detachedTestRepo(t, "https://github.com/acme/import.git") + source := embeddedFeatureForDetach(t, repo, "feature-one", "") + result, err := AttachDetached(AttachOptions{Repo: repo}) + if err != nil || result.VerificationStatus != "VERIFIED" { + t.Fatalf("attach: %+v %v", result, err) + } + if len(result.FeatureMigrations) != 1 || result.FeatureMigrations[0].Status != "IMPORTED" { + t.Fatalf("migration result: %+v", result.FeatureMigrations) + } + target := WorkspaceFor(repo).FeatureDir("feature-one") + if strings.HasPrefix(target, repo+string(filepath.Separator)) || !fileExists(filepath.Join(target, "plan.md")) { + t.Fatalf("feature was not imported outside the repository: %s", target) + } + if err := os.WriteFile(filepath.Join(source, "embedded-only.md"), []byte("ignored\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(repo, productLoopDirName, "project.json"), []byte("{}\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := Doctor(repo); err == nil || !strings.Contains(err.Error(), "runtime provenance") { + t.Fatalf("doctor did not pass detached generated-state verification before the fixture's intentionally absent runtime: %v", err) + } + status, err := ResolveNext(repo, "") + if err != nil || status.Feature != "feature-one" || status.ObservedStage != "POLICY_READY" { + t.Fatalf("next did not use detached feature package: %+v %v", status, err) + } +} + +// control-law: detached-import-never-chooses-by-recency +func TestDetachedReattachBlocksConflictingFeaturePackages(t *testing.T) { + repo := detachedTestRepo(t, "https://github.com/acme/conflict.git") + source := embeddedFeatureForDetach(t, repo, "feature-one", "") + first, _ := AttachDetached(AttachOptions{Repo: repo}) + if first.VerificationStatus != "VERIFIED" { + t.Fatalf("first attach: %+v", first) + } + identical, identicalErr := AttachDetached(AttachOptions{Repo: repo, Force: true}) + if identicalErr != nil || identical.VerificationStatus != "VERIFIED" || len(identical.FeatureMigrations) != 1 || identical.FeatureMigrations[0].Status != "UNCHANGED" { + t.Fatalf("identical packages were not preserved: %+v %v", identical, identicalErr) + } + if err := os.WriteFile(filepath.Join(source, "questions.md"), []byte("# changed later\n"), 0o644); err != nil { + t.Fatal(err) + } + second, err := AttachDetached(AttachOptions{Repo: repo, Force: true}) + if err != nil || second.VerificationStatus != "BLOCKED" || len(second.FeatureMigrations) != 1 || second.FeatureMigrations[0].Status != "CONFLICTING" { + t.Fatalf("conflict was not fail-closed: %+v %v", second, err) + } +} + +// control-law: detached-import-requires-current-receipt-fingerprints +func TestDetachedAttachRejectsStaleApprovalReceipt(t *testing.T) { + repo := detachedTestRepo(t, "https://github.com/acme/stale.git") + embeddedFeatureForDetach(t, repo, "feature-one", "wrong") + result, err := AttachDetached(AttachOptions{Repo: repo}) + if err != nil || result.VerificationStatus != "BLOCKED" || len(result.FeatureMigrations) != 1 || result.FeatureMigrations[0].Status != "REJECTED" { + t.Fatalf("stale receipt was imported: %+v %v", result, err) + } +} + +// control-law: detached-import-is-atomic-before-directory-promotion +func TestDetachedImportInterruptionLeavesNoPartialTarget(t *testing.T) { + repo := detachedTestRepo(t, "https://github.com/acme/interrupted.git") + embeddedFeatureForDetach(t, repo, "feature-one", "") + old := detachedImportBeforeRename + detachedImportBeforeRename = func(_, _, _ string) error { return fmt.Errorf("injected interruption") } + t.Cleanup(func() { detachedImportBeforeRename = old }) + result, err := AttachDetached(AttachOptions{Repo: repo}) + if err != nil || result.VerificationStatus != "BLOCKED" { + t.Fatalf("interrupted attach: %+v %v", result, err) + } + identity, _ := repoIdentity(repo) + ctx := detachedContextFromIdentity(filepath.Join(os.Getenv(stateRootEnv), "boatstack"), identity) + if fileExists(ctx.FeatureDir("feature-one")) { + t.Fatal("interrupted import exposed a partial feature directory") + } +} + +// control-law: detached-activation-writes-and-verifies-one-feature-root +func TestDetachedActivationUsesCanonicalFeatureDirectory(t *testing.T) { + repo := detachedTestRepo(t, "https://github.com/acme/activate.git") + embeddedFeatureForDetach(t, repo, "feature-one", "") + result, _ := AttachDetached(AttachOptions{Repo: repo}) + if result.VerificationStatus != "VERIFIED" { + t.Fatalf("attach: %+v", result) + } + directory := WorkspaceFor(repo).FeatureDir("feature-one") + if resolved, resolveErr := ResolveControllerRepository(directory); resolveErr != nil || canonicalizeExistingAncestor(resolved) != canonicalizeExistingAncestor(repo) { + t.Fatalf("detached feature owner mismatch: directory=%s resolved=%s repo=%s err=%v", directory, resolved, repo, resolveErr) + } + resolved, _ := ResolveControllerRepository(directory) + if ctx, ctxErr := ResolveWorkspaceContext(resolved); ctxErr != nil || ctx.Mode != SupervisionDetached { + t.Fatalf("resolved owner lost detached context: resolved=%s ctx=%+v err=%v", resolved, ctx, ctxErr) + } + err := ActivatePlan(ActivationOptions{ + PlanPath: filepath.Join(directory, "plan.md"), OutDir: filepath.Join(directory, "compiled"), + OutputPath: filepath.Join(directory, "plan.lock.json"), SourceCommit: "test", + }) + if err != nil { + t.Fatal(err) + } + for _, path := range []string{"compiled/tasks.json", "compiled/journey-oracles.json", "plan.lock.json"} { + if !fileExists(filepath.Join(directory, filepath.FromSlash(path))) { + t.Errorf("missing detached activation artifact %s", path) + } + if fileExists(filepath.Join(repo, productLoopDirName, "features", "feature-one", filepath.FromSlash(path))) { + t.Errorf("activation artifact leaked into embedded package: %s", path) + } + } +} diff --git a/boatstack/flow_control.go b/boatstack/flow_control.go index 0e90b21..154df84 100644 --- a/boatstack/flow_control.go +++ b/boatstack/flow_control.go @@ -375,7 +375,7 @@ func prescribeCommand(repo, feature string, status NextStatus, transition delive // planningFeatureDir is the single joined form of a feature's planning // directory used by the prescription layer and the solution-set enumerator. func planningFeatureDir(repo, feature string) string { - return filepath.Join(repo, ".product-loop", "features", feature) + return WorkspaceFor(repo).FeatureDir(feature) } func prescribePlanning(repo string, status NextStatus) (*PrescribedCommand, string) { diff --git a/boatstack/flow_tasks.go b/boatstack/flow_tasks.go index 9a72ffe..cfd35b8 100644 --- a/boatstack/flow_tasks.go +++ b/boatstack/flow_tasks.go @@ -74,7 +74,7 @@ func FlowTasksForActiveSlice(repo, feature string) (FlowTasks, error) { // the graph is absent or malformed, so the caller stays Unresolved rather than // ordering a guess. func readCompiledTasks(repo, feature string) ([]FlowTask, bool) { - directory := filepath.Join(repo, ".product-loop", "features", feature) + directory := WorkspaceFor(repo).FeatureDir(feature) tasksPath := featureArtifactPath(directory, filepath.Join("compiled", "tasks.json"), "tasks.json") raw, err := os.ReadFile(tasksPath) if err != nil { diff --git a/boatstack/init.go b/boatstack/init.go index d5f047c..3c86c9d 100644 --- a/boatstack/init.go +++ b/boatstack/init.go @@ -599,6 +599,18 @@ func RunInit(options InitOptions) (returnErr error) { if writeErr != nil { return writeErr } + // An attached repository still receives the reviewed embedded update package, + // but its active controller reads the detached projection. Refresh that same + // bundle under the resolved export root before any smoke check; feature state + // is outside the bundle key set and remains untouched. + if ctx := WorkspaceFor(repo); ctx.Mode == SupervisionDetached { + if err := writeExport(ctx.ExportRoot(), bundle.Files, nil); err != nil { + return fmt.Errorf("refresh detached controller bundle: %w", err) + } + if err := atomicWriteMode(ctx.SourceConfigPath(), rawConfig, 0o644); err != nil { + return fmt.Errorf("refresh detached source configuration: %w", err) + } + } if err := initCheckpoint("export-written"); err != nil { return fmt.Errorf("initialization checkpoint export-written: %w", err) } diff --git a/boatstack/insight_conformance_test.go b/boatstack/insight_conformance_test.go index 7f93575..c051c17 100644 --- a/boatstack/insight_conformance_test.go +++ b/boatstack/insight_conformance_test.go @@ -209,7 +209,7 @@ func TestInsightCaptureRejectsStaleInputAndSupportsEmbeddedMode(t *testing.T) { func installInsightDelivery(t *testing.T, repo string, ctx WorkspaceContext, feature string, terminal DeliveryTerminal) { t.Helper() - directory := filepath.Join(repo, ".product-loop", "features", feature) + directory := ctx.FeatureDir(feature) if err := os.MkdirAll(directory, 0o755); err != nil { t.Fatal(err) } diff --git a/boatstack/journey.go b/boatstack/journey.go index 512e26e..9a84eb8 100644 --- a/boatstack/journey.go +++ b/boatstack/journey.go @@ -66,7 +66,7 @@ func CompileJourneyManifest(plan map[string]any) ([]byte, error) { } func journeyManifestPath(repo, feature string) string { - directory := filepath.Join(WorkspaceFor(repo).GeneratedRoot(), "features", feature) + directory := WorkspaceFor(repo).FeatureDir(feature) return featureArtifactPath(directory, filepath.Join("compiled", "journey-oracles.json"), "journey-oracles.json") } diff --git a/boatstack/mutation.go b/boatstack/mutation.go index fe944b8..ade0656 100644 --- a/boatstack/mutation.go +++ b/boatstack/mutation.go @@ -114,6 +114,7 @@ type MutationReceipt struct { Changes []MutationFileChange `json:"changes"` Authority string `json:"authority_sha256,omitempty"` RecordedAt string `json:"recorded_at"` + Root string `json:"root,omitempty"` } func mutationDirectory(repo string) (string, error) { @@ -238,12 +239,15 @@ func currentImage(native string) (string, bool, error) { // an identical proposal replays and a different proposal is a distinct mutation. // It deliberately excludes the transient Authority.Observed value so a rejected // authority check does not fork the identity of the corrected retry. -func mutationIdentity(m MutationSet, ops []resolvedOperation) string { +func mutationIdentity(m MutationSet, ops []resolvedOperation, root string) string { parts := make([]string, 0, len(ops)) for _, op := range ops { parts = append(parts, op.rel+"\x1f"+op.candidateHash+"\x1f"+m.Base[op.rel]) } sort.Strings(parts) + if root != "" { + parts = append(parts, "root\x1f"+root) + } fingerprint := SHA256Bytes([]byte(strings.Join(parts, "\x1e"))) target := SHA256Bytes([]byte(strings.Join(sortedScope(m.Scope), "\x1e"))) return operationID("mutation\x00"+strings.TrimSpace(m.Kind), target, fingerprint) @@ -264,7 +268,7 @@ type resolvedOperation struct { absent bool } -func (m MutationSet) resolve(repo string) ([]resolvedOperation, error) { +func (m MutationSet) resolve(root string) ([]resolvedOperation, error) { if strings.TrimSpace(m.Protocol) != MutationProtocol { return nil, fmt.Errorf("mutation protocol must be %s", MutationProtocol) } @@ -292,11 +296,11 @@ func (m MutationSet) resolve(repo string) ([]resolvedOperation, error) { return nil, fmt.Errorf("mutation names %s more than once", rel) } seen[rel] = true - native, err := resolveRepositoryRelativePath(repo, rel) + native, err := resolveRepositoryRelativePath(root, rel) if err != nil { return nil, err } - if err := rejectSymlinkComponents(repo, native); err != nil { + if err := rejectSymlinkComponents(root, native); err != nil { return nil, err } if op.Absent { @@ -324,11 +328,35 @@ func ApplyMutation(repoPath string, m MutationSet) (MutationReceipt, error) { if err != nil { return MutationReceipt{}, err } - ops, err := m.resolve(repo) + return applyMutationAt(repo, repo, "", m) +} + +// ApplyControllerMutation promotes controller-owned artifacts beneath the +// active WorkspaceContext export root while retaining receipts in the owning +// repository's Git-common ledger. +func ApplyControllerMutation(repoPath string, m MutationSet) (MutationReceipt, error) { + repo, err := ResolveRepository(repoPath) if err != nil { return MutationReceipt{}, err } - id := mutationIdentity(m, ops) + ctx, err := ResolveWorkspaceContext(repo) + if err != nil { + return MutationReceipt{}, err + } + root := ctx.ExportRoot() + rootMarker := "" + if filepath.Clean(root) != filepath.Clean(repo) { + rootMarker = root + } + return applyMutationAt(repo, root, rootMarker, m) +} + +func applyMutationAt(repo, root, rootMarker string, m MutationSet) (MutationReceipt, error) { + ops, err := m.resolve(root) + if err != nil { + return MutationReceipt{}, err + } + id := mutationIdentity(m, ops, rootMarker) authorityHash := SHA256Bytes([]byte(m.Authority.Expected)) var result MutationReceipt @@ -349,6 +377,7 @@ func ApplyMutation(repoPath string, m MutationSet) (MutationReceipt, error) { SchemaVersion: mutationSchemaVersion, MutationID: id, Protocol: MutationProtocol, Kind: m.Kind, Status: "REJECTED", Reason: "outdated supervisor authority", Scope: sortedScope(m.Scope), RecordedAt: operationTimestamp(), + Root: rootMarker, } return ErrMutationOutdatedAuthority } @@ -367,6 +396,7 @@ func ApplyMutation(repoPath string, m MutationSet) (MutationReceipt, error) { SchemaVersion: mutationSchemaVersion, MutationID: id, Protocol: MutationProtocol, Kind: m.Kind, Status: "REJECTED", Reason: "stale base artifact: " + op.rel, Scope: sortedScope(m.Scope), RecordedAt: operationTimestamp(), + Root: rootMarker, } return ErrMutationStaleBase } @@ -383,6 +413,7 @@ func ApplyMutation(repoPath string, m MutationSet) (MutationReceipt, error) { SchemaVersion: mutationSchemaVersion, MutationID: id, Protocol: MutationProtocol, Kind: m.Kind, Status: "REJECTED", Reason: "invalid candidate: " + checkErr.Error(), Scope: sortedScope(m.Scope), RecordedAt: operationTimestamp(), + Root: rootMarker, } return fmt.Errorf("%w: %v", ErrMutationInvalidCandidate, checkErr) } @@ -447,6 +478,7 @@ func ApplyMutation(repoPath string, m MutationSet) (MutationReceipt, error) { SchemaVersion: mutationSchemaVersion, MutationID: id, Protocol: MutationProtocol, Kind: m.Kind, Status: "ROLLED_BACK", Reason: "post-write verification failed: " + checkErr.Error(), Scope: sortedScope(m.Scope), RecordedAt: operationTimestamp(), + Root: rootMarker, } return fmt.Errorf("%w: %v", ErrMutationVerificationFailed, checkErr) } @@ -456,6 +488,7 @@ func ApplyMutation(repoPath string, m MutationSet) (MutationReceipt, error) { SchemaVersion: mutationSchemaVersion, MutationID: id, Protocol: MutationProtocol, Kind: m.Kind, Status: "APPLIED", Scope: sortedScope(m.Scope), Changes: changes, Authority: authorityHash, RecordedAt: operationTimestamp(), + Root: rootMarker, } return saveMutationReceipt(repo, result) }) @@ -506,8 +539,16 @@ func rollbackMutation(promoted []promotedChange) { // on-disk truth, which is the precondition for treating a repeat call as a // no-op replay rather than a fresh mutation. func receiptStillApplied(repo string, receipt MutationReceipt) bool { + root := repo + if receipt.Root != "" { + ctx, err := ResolveWorkspaceContext(repo) + if err != nil || filepath.Clean(receipt.Root) != filepath.Clean(ctx.ExportRoot()) { + return false + } + root = receipt.Root + } for _, change := range receipt.Changes { - native, err := resolveRepositoryRelativePath(repo, change.Path) + native, err := resolveRepositoryRelativePath(root, change.Path) if err != nil { return false } @@ -582,7 +623,13 @@ func UndoMutation(repoPath, mutationID string) (MutationReceipt, error) { Base: base, Operations: ops, } - undone, applyErr := ApplyMutation(repo, inverse) + var undone MutationReceipt + var applyErr error + if receipt.Root != "" { + undone, applyErr = ApplyControllerMutation(repo, inverse) + } else { + undone, applyErr = ApplyMutation(repo, inverse) + } if errors.Is(applyErr, ErrMutationStaleBase) { return undone, fmt.Errorf("%w: %s diverged from its recorded post-image", ErrMutationConflict, receipt.MutationID) } diff --git a/boatstack/next.go b/boatstack/next.go index ffcf9bb..f601448 100644 --- a/boatstack/next.go +++ b/boatstack/next.go @@ -16,6 +16,8 @@ const nextStatusSchemaVersion = 2 // adapters may present them as context, but they are not workflow evidence. type NextStatus struct { SchemaVersion int `json:"schema_version"` + SupervisionMode string `json:"supervision_mode"` + ControllerRoot string `json:"controller_root"` VerificationStatus string `json:"verification_status"` Feature string `json:"feature,omitempty"` ActiveSlice string `json:"active_slice,omitempty"` @@ -46,7 +48,7 @@ func decorateAutonomyStatus(repo string, status NextStatus) NextStatus { if status.Feature == "" { return status } - path := filepath.Join(repo, ".product-loop", "features", status.Feature, "autonomy.md") + path := filepath.Join(WorkspaceFor(repo).FeatureDir(status.Feature), "autonomy.md") value, err := loadJSONObject(path, "autonomy receipt", autonomyMarkerStart, autonomyMarkerEnd, true) if err != nil { return status @@ -72,7 +74,7 @@ func blockedNextStatus(stage, operation, reason string, ambiguity ...string) Nex } func featurePlanCandidates(repo string) ([]string, error) { - root := filepath.Join(repo, ".product-loop", "features") + root := WorkspaceFor(repo).FeatureRoot() entries, err := os.ReadDir(root) if os.IsNotExist(err) { return nil, nil @@ -110,7 +112,7 @@ func featurePlanCandidates(repo string) ([]string, error) { } func orphanedFeatureArtifacts(repo string) ([]string, error) { - root := filepath.Join(repo, ".product-loop", "features") + root := WorkspaceFor(repo).FeatureRoot() entries, err := os.ReadDir(root) if os.IsNotExist(err) { return nil, nil @@ -154,7 +156,7 @@ func nextForDelivery(repo, feature string) (NextStatus, error) { status.NextOperation = "review-gate" status.Reason = "The active delivery slice has current test evidence and still requires review." case StatusReviewPassed: - previewPath := filepath.Join(repo, ".product-loop", "features", feature, "pr.md") + previewPath := filepath.Join(WorkspaceFor(repo).FeatureDir(feature), "pr.md") if preview, previewErr := ParsePRPreview(previewPath); previewErr == nil && preview.Feature == feature && preview.SliceID == slice.ID { status.ObservedStage = "PR_PREVIEW" status.Reason = "A reviewer-ready PR preview exists for the reviewed active slice and must be reconfirmed through the ship gate." @@ -307,11 +309,23 @@ func completedManagedStates(repo string) ([]DeliveryState, error) { // ResolveNext performs bounded, read-only state inspection. Published states // use the recorded PR identity when GitHub is available; conversation and // process history are never treated as evidence. -func ResolveNext(repoPath, explicitFeature string) (NextStatus, error) { +func ResolveNext(repoPath, explicitFeature string) (result NextStatus, resultErr error) { repo, err := ResolveRepository(repoPath) if err != nil { return NextStatus{}, err } + defer func() { + ctx, ok, verifyErr := detachedContextFor(repo) + if verifyErr != nil { + result.SupervisionMode = string(SupervisionDetached) + return + } + if !ok { + ctx = embeddedWorkspace(repo) + } + result.SupervisionMode = string(ctx.Mode) + result.ControllerRoot = ctx.ExportRoot() + }() base := NextStatus{SchemaVersion: nextStatusSchemaVersion} if !fileExists(WorkspaceFor(repo).ProjectConfigPath()) { base.VerificationStatus = "UNVERIFIED" @@ -411,7 +425,7 @@ func ResolveNext(repoPath, explicitFeature string) (NextStatus, error) { } if len(candidates) == 1 { feature := candidates[0] - directory := filepath.Join(repo, ".product-loop", "features", feature) + directory := WorkspaceFor(repo).FeatureDir(feature) base.VerificationStatus = "VERIFIED" base.Feature = feature policyReady := !config.Workflow.HumanPlanApproval @@ -521,6 +535,8 @@ func FormatNextStatus(status NextStatus) string { parts := []string{ "Boatstack stage: " + status.ObservedStage, "Verification: " + status.VerificationStatus, + "Supervision: " + status.SupervisionMode, + "Controller root: " + status.ControllerRoot, } if status.Feature != "" { parts = append(parts, "Feature: "+status.Feature) diff --git a/boatstack/paths.go b/boatstack/paths.go index 6f587a6..1551334 100644 --- a/boatstack/paths.go +++ b/boatstack/paths.go @@ -3,6 +3,7 @@ package boatstack import ( "fmt" "path/filepath" + "strings" "sync" ) @@ -161,6 +162,41 @@ func embeddedWorkspace(repo string) WorkspaceContext { return WorkspaceContext{Mode: SupervisionEmbedded, RepoRoot: repo, controlRoot: repo} } +func pathWithin(root, target string) bool { + root = canonicalizeExistingAncestor(root) + target = canonicalizeExistingAncestor(target) + relative, err := filepath.Rel(filepath.Clean(root), filepath.Clean(target)) + return err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) +} + +// ResolveControllerRepository maps either a product path or a detached +// controller path back to the repository whose identity owns it. This is the +// inverse boundary required by plan validation after FeatureDir moves outside +// the Git worktree. +func ResolveControllerRepository(path string) (string, error) { + stateRoot, err := detachedStateRoot() + if err != nil { + return "", err + } + registry, err := loadRegistry(stateRoot) + if err != nil { + return "", err + } + for repo := range registry.Repositories { + ctx, ok, verifyErr := detachedContextFor(repo) + if !ok || verifyErr != nil { + continue + } + if pathWithin(ctx.ExportRoot(), path) { + return repo, nil + } + } + if repo, err := ResolveRepository(path); err == nil { + return repo, nil + } + return "", fmt.Errorf("path is not owned by a repository or verified detached controller: %s", path) +} + var ( workspaceCacheMu sync.Mutex workspaceCache = map[string]WorkspaceContext{} @@ -191,6 +227,29 @@ func (w WorkspaceContext) GeneratedRoot() string { return filepath.Join(w.configBase(), productLoopDirName) } +// ExportRoot is the base beneath which generated bundle paths are materialized. +// Bundle keys include .product-loop and host-adapter directories, so callers +// must pass this root — never RepoRoot — to export write/check operations. +func (w WorkspaceContext) ExportRoot() string { + return w.configBase() +} + +// FeatureRoot owns generated planning and delivery artifacts. Source plans are +// product inputs and remain at their declared repository paths; everything +// compiled from them lives below this controller-owned root. +func (w WorkspaceContext) FeatureRoot() string { + return filepath.Join(w.GeneratedRoot(), "features") +} + +// FeatureDir returns one validated feature package directory. Invalid slugs +// return an empty path so no caller can accidentally escape the ownership root. +func (w WorkspaceContext) FeatureDir(feature string) string { + if !featureSlugPattern.MatchString(feature) { + return "" + } + return filepath.Join(w.FeatureRoot(), feature) +} + // ProjectConfigPath is the generated runtime configuration copy that runtime // operations read. Embedded: /.product-loop/project.json. func (w WorkspaceContext) ProjectConfigPath() string { diff --git a/boatstack/plan.go b/boatstack/plan.go index 176d3aa..6f597c8 100644 --- a/boatstack/plan.go +++ b/boatstack/plan.go @@ -295,7 +295,25 @@ func SourcePlanForStructuredPlan(planPath string) (string, error) { return "", fmt.Errorf("source_plan_path is required") } if !filepath.IsAbs(sourcePlan) { - sourcePlan = filepath.Join(filepath.Dir(planPath), sourcePlan) + planRelative := filepath.Clean(filepath.Join(filepath.Dir(planPath), sourcePlan)) + if fileExists(planRelative) { + return planRelative, nil + } + if repo, repoErr := ResolveControllerRepository(filepath.Dir(planPath)); repoErr == nil { + repoRelative := filepath.Clean(filepath.Join(repo, sourcePlan)) + if fileExists(repoRelative) { + return repoRelative, nil + } + // Packages imported from the embedded layout retain their original + // relative source-plan reference. Resolve it against the virtual + // embedded feature directory without rewriting fingerprinted bytes. + feature := stringValue(plan["feature_id"]) + legacyRelative := filepath.Clean(filepath.Join(repo, productLoopDirName, "features", feature, sourcePlan)) + if fileExists(legacyRelative) { + return legacyRelative, nil + } + } + sourcePlan = planRelative } return filepath.Clean(sourcePlan), nil } @@ -346,7 +364,7 @@ func CheckPlan(planPath string) (PlanCheck, error) { if err != nil { return PlanCheck{}, err } - repoRoot, _ := ResolveRepository(filepath.Dir(planPath)) + repoRoot, _ := ResolveControllerRepository(filepath.Dir(planPath)) opts := &ValidatePlanOptions{ PlanPath: planPath, RepoRoot: repoRoot, @@ -790,7 +808,7 @@ func canonicalizeExistingAncestor(path string) string { } func compilePlanFiles(planPath, outDir, structuredPlanStatus string) error { - repoRoot, err := ResolveRepository(filepath.Dir(planPath)) + repoRoot, err := ResolveControllerRepository(filepath.Dir(planPath)) if err != nil { return err } @@ -812,7 +830,7 @@ func compilePlanFiles(planPath, outDir, structuredPlanStatus string) error { Operations: artifacts.ops, PostCheck: artifacts.postCheck, } - if _, err := ApplyMutation(repoRoot, mutation); err != nil { + if _, err := ApplyControllerMutation(repoRoot, mutation); err != nil { return err } return nil @@ -875,19 +893,24 @@ func compileArtifacts(repoRoot, planPath, outDir, structuredPlanStatus string) ( // directory (and one or more of its parents) may not exist yet, so resolve the // deepest existing ancestor and rejoin the not-yet-created remainder. absOut = canonicalizeExistingAncestor(absOut) - relTasks, err := repositoryRelativePath(repoRoot, filepath.Join(absOut, "tasks.json")) + workspace, err := ResolveWorkspaceContext(repoRoot) if err != nil { return compiledArtifacts{}, err } - relMatrix, err := repositoryRelativePath(repoRoot, filepath.Join(absOut, "test-matrix.json")) + artifactRoot := workspace.ExportRoot() + relTasks, err := repositoryRelativePath(artifactRoot, filepath.Join(absOut, "tasks.json")) if err != nil { return compiledArtifacts{}, err } - relEvidence, err := repositoryRelativePath(repoRoot, filepath.Join(absOut, "evidence.md")) + relMatrix, err := repositoryRelativePath(artifactRoot, filepath.Join(absOut, "test-matrix.json")) if err != nil { return compiledArtifacts{}, err } - relJourney, err := repositoryRelativePath(repoRoot, filepath.Join(absOut, "journey-oracles.json")) + relEvidence, err := repositoryRelativePath(artifactRoot, filepath.Join(absOut, "evidence.md")) + if err != nil { + return compiledArtifacts{}, err + } + relJourney, err := repositoryRelativePath(artifactRoot, filepath.Join(absOut, "journey-oracles.json")) if err != nil { return compiledArtifacts{}, err } @@ -898,13 +921,13 @@ func compileArtifacts(repoRoot, planPath, outDir, structuredPlanStatus string) ( scope := []string{relTasks, relMatrix, relEvidence, relJourney} base := map[string]string{} for _, rel := range scope { - if hash, hashErr := SHA256File(filepath.Join(repoRoot, filepath.FromSlash(rel))); hashErr == nil { + if hash, hashErr := SHA256File(filepath.Join(artifactRoot, filepath.FromSlash(rel))); hashErr == nil { base[rel] = hash } } postCheck := func() error { for _, rel := range []string{relTasks, relMatrix, relJourney} { - value, readErr := os.ReadFile(filepath.Join(repoRoot, filepath.FromSlash(rel))) + value, readErr := os.ReadFile(filepath.Join(artifactRoot, filepath.FromSlash(rel))) if readErr != nil { return readErr } @@ -912,7 +935,7 @@ func compileArtifacts(repoRoot, planPath, outDir, structuredPlanStatus string) ( return validateErr } } - info, statErr := os.Stat(filepath.Join(repoRoot, filepath.FromSlash(relEvidence))) + info, statErr := os.Stat(filepath.Join(artifactRoot, filepath.FromSlash(relEvidence))) if statErr != nil || info.Size() == 0 { return fmt.Errorf("promoted evidence ledger is missing or empty") } @@ -1051,7 +1074,7 @@ func CheckApprovalReceipt(path string, planCheck PlanCheck) (ApprovalReceipt, er return ApprovalReceipt{}, fmt.Errorf("stale approval receipt: readiness fingerprint changed after approval") } } - repo, err := ResolveRepository(filepath.Dir(planCheck.PlanPath)) + repo, err := ResolveControllerRepository(filepath.Dir(planCheck.PlanPath)) if err != nil { return ApprovalReceipt{}, err } @@ -1083,7 +1106,7 @@ func ActivatePlan(options ActivationOptions) error { if err != nil { return err } - repo, err := ResolveRepository(filepath.Dir(options.PlanPath)) + repo, err := ResolveControllerRepository(filepath.Dir(options.PlanPath)) if err != nil { return err } @@ -1218,7 +1241,7 @@ func ActivatePlan(options ActivationOptions) error { return fmt.Errorf("pre-activation readiness drifted before the immutable plan lock could be created") } } - if _, err := ApplyMutation(repo, mutation); err != nil { + if _, err := ApplyControllerMutation(repo, mutation); err != nil { return err } return initializeDeliveryState(repo, stringValue(check.Plan["feature_id"]), options.PlanPath, options.OutputPath) @@ -1244,7 +1267,12 @@ func activationMutation(repoRoot string, options ActivationOptions, structuredPl return MutationSet{}, err } absLock = canonicalizeExistingAncestor(absLock) - relLock, err := repositoryRelativePath(repoRoot, absLock) + workspace, err := ResolveWorkspaceContext(repoRoot) + if err != nil { + return MutationSet{}, err + } + artifactRoot := workspace.ExportRoot() + relLock, err := repositoryRelativePath(artifactRoot, absLock) if err != nil { return MutationSet{}, err } @@ -1253,7 +1281,7 @@ func activationMutation(repoRoot string, options ActivationOptions, structuredPl for rel, hash := range artifacts.base { base[rel] = hash } - if hash, hashErr := SHA256File(filepath.Join(repoRoot, filepath.FromSlash(relLock))); hashErr == nil { + if hash, hashErr := SHA256File(filepath.Join(artifactRoot, filepath.FromSlash(relLock))); hashErr == nil { base[relLock] = hash } ops := append(append([]MutationOperation{}, artifacts.ops...), MutationOperation{Path: relLock, Candidate: lockBytes}) @@ -1454,7 +1482,7 @@ func CheckApprovalLock(options ApprovalOptions) error { if fingerprint, fingerprintErr := readinessFingerprint(storedReadiness); fingerprintErr != nil || fingerprint != storedReadiness.Fingerprint { mismatches = append(mismatches, "readiness_fingerprint") } - repo, repoErr := ResolveRepository(filepath.Dir(options.PlanPath)) + repo, repoErr := ResolveControllerRepository(filepath.Dir(options.PlanPath)) plan, planErr := LoadPlan(options.PlanPath) if repoErr != nil || planErr != nil { mismatches = append(mismatches, "journey_manifest") diff --git a/boatstack/planning.go b/boatstack/planning.go index 8138127..97cc3a0 100644 --- a/boatstack/planning.go +++ b/boatstack/planning.go @@ -163,7 +163,7 @@ func PlanningBaselineForPlan(planPath string) (PlanningBaseline, error) { if err != nil { return PlanningBaseline{}, err } - repo, err := ResolveRepository(filepath.Dir(planPath)) + repo, err := ResolveControllerRepository(filepath.Dir(planPath)) if err != nil { return PlanningBaseline{}, err } @@ -238,14 +238,18 @@ func WritePlanningArtifact(options PlanningWriteOptions) (string, error) { if err != nil { return "", err } - destination := filepath.Join(repo, ".product-loop", "features", options.Feature, options.Artifact) - if err := rejectSymlinkComponents(repo, destination); err != nil { + ctx, err := ResolveWorkspaceContext(repo) + if err != nil { + return "", err + } + destination := filepath.Join(ctx.FeatureDir(options.Feature), options.Artifact) + if err := rejectSymlinkComponents(ctx.ExportRoot(), destination); err != nil { return "", err } if err := atomicWrite(destination, options.Content); err != nil { return "", err } - relative, err := filepath.Rel(repo, destination) + relative, err := filepath.Rel(ctx.ExportRoot(), destination) if err != nil { return "", err } @@ -267,7 +271,7 @@ func RecordApproval(options ApprovalRecordOptions) error { if options.Fingerprint != check.Fingerprint { return fmt.Errorf("approval fingerprint does not match the current plan; the plan now fingerprints as %s — re-approve against that value (run check-plan to confirm)", check.Fingerprint) } - repo, err := ResolveRepository(filepath.Dir(options.PlanPath)) + repo, err := ResolveControllerRepository(filepath.Dir(options.PlanPath)) if err != nil { return err } @@ -360,7 +364,7 @@ func Doctor(repoPath string) error { if err != nil { return err } - if err := CheckExport(repo, bundle.Files); err != nil { + if err := CheckExport(WorkspaceFor(repo).ExportRoot(), bundle.Files); err != nil { return err } // Best-effort hygiene: drop the orphaned clone-shared operation ledger left by diff --git a/boatstack/pr.go b/boatstack/pr.go index b2c1484..b910f0b 100644 --- a/boatstack/pr.go +++ b/boatstack/pr.go @@ -36,44 +36,44 @@ type PRSource struct { } type PRContext struct { - SchemaVersion int `json:"schema_version"` - Mode string `json:"mode"` - Feature string `json:"feature,omitempty"` - SliceID string `json:"slice_id,omitempty"` - SliceIndex int `json:"slice_index,omitempty"` - TotalSlices int `json:"total_slices,omitempty"` - BaseBranch string `json:"base_branch"` - HeadBranch string `json:"head_branch"` - BaseCommit string `json:"base_commit"` - MergeBaseCommit string `json:"merge_base_commit"` - HeadCommit string `json:"head_commit"` - ProductDiffSHA256 string `json:"product_diff_sha256"` - ContextFingerprint string `json:"context_fingerprint"` - ChangedFiles []string `json:"changed_files"` - Commits []string `json:"commits"` - DiffStat string `json:"diff_stat"` - ContextPaths []string `json:"context_paths,omitempty"` - ProjectCommands map[string]string `json:"project_commands,omitempty"` - HighRiskFiles []string `json:"high_risk_files,omitempty"` - GateStatus map[string]string `json:"gate_status,omitempty"` - SafetyStatus string `json:"safety_status"` - SafetyFindings []SafetyFinding `json:"safety_findings,omitempty"` - PRVisualEvidencePolicy string `json:"pr_visual_evidence_policy"` - PRVisualEvidenceStatus string `json:"pr_visual_evidence_status"` - PRVisualEvidenceCount int `json:"pr_visual_evidence_count"` - PRVisualEvidenceFingerprint string `json:"pr_visual_evidence_fingerprint"` - PRVisualEvidenceRelevance string `json:"pr_visual_evidence_relevance"` - PRVisualEvidenceSource string `json:"pr_visual_evidence_source"` + SchemaVersion int `json:"schema_version"` + Mode string `json:"mode"` + Feature string `json:"feature,omitempty"` + SliceID string `json:"slice_id,omitempty"` + SliceIndex int `json:"slice_index,omitempty"` + TotalSlices int `json:"total_slices,omitempty"` + BaseBranch string `json:"base_branch"` + HeadBranch string `json:"head_branch"` + BaseCommit string `json:"base_commit"` + MergeBaseCommit string `json:"merge_base_commit"` + HeadCommit string `json:"head_commit"` + ProductDiffSHA256 string `json:"product_diff_sha256"` + ContextFingerprint string `json:"context_fingerprint"` + ChangedFiles []string `json:"changed_files"` + Commits []string `json:"commits"` + DiffStat string `json:"diff_stat"` + ContextPaths []string `json:"context_paths,omitempty"` + ProjectCommands map[string]string `json:"project_commands,omitempty"` + HighRiskFiles []string `json:"high_risk_files,omitempty"` + GateStatus map[string]string `json:"gate_status,omitempty"` + SafetyStatus string `json:"safety_status"` + SafetyFindings []SafetyFinding `json:"safety_findings,omitempty"` + PRVisualEvidencePolicy string `json:"pr_visual_evidence_policy"` + PRVisualEvidenceStatus string `json:"pr_visual_evidence_status"` + PRVisualEvidenceCount int `json:"pr_visual_evidence_count"` + PRVisualEvidenceFingerprint string `json:"pr_visual_evidence_fingerprint"` + PRVisualEvidenceRelevance string `json:"pr_visual_evidence_relevance"` + PRVisualEvidenceSource string `json:"pr_visual_evidence_source"` // PRVisualEvidencePolicySource is "configured", or "plan-escalated" when // a plan-approved visual decision lifts suggest to require semantics. - PRVisualEvidencePolicySource string `json:"pr_visual_evidence_policy_source,omitempty"` + PRVisualEvidencePolicySource string `json:"pr_visual_evidence_policy_source,omitempty"` // PRVisualEvidenceCaptureDetail explains why automatic capture could not // produce current evidence. Deliberately outside the context fingerprint: // a flaky harness message must not destabilize preview equality. - PRVisualEvidenceCaptureDetail string `json:"pr_visual_evidence_capture_detail,omitempty"` - PRVisualEvidence *PRVisualEvidenceManifest `json:"pr_visual_evidence,omitempty"` - Sources []PRSource `json:"sources,omitempty"` - PreviewPath string `json:"preview_path"` + PRVisualEvidenceCaptureDetail string `json:"pr_visual_evidence_capture_detail,omitempty"` + PRVisualEvidence *PRVisualEvidenceManifest `json:"pr_visual_evidence,omitempty"` + Sources []PRSource `json:"sources,omitempty"` + PreviewPath string `json:"preview_path"` } type PRPreview struct { @@ -95,7 +95,7 @@ type PRPreview struct { } func planVisualDecision(repo, feature string) (string, string, []PRVisualScenario, error) { - plan, err := LoadPlan(filepath.Join(repo, ".product-loop", "features", feature, "plan.md")) + plan, err := LoadPlan(filepath.Join(WorkspaceFor(repo).FeatureDir(feature), "plan.md")) if err != nil { return "unresolved", "managed-plan", nil, err } @@ -559,7 +559,7 @@ func featureEvidencePath(featureDir string) string { } func managedPRSources(repo, feature string) ([]PRSource, map[string]string, error) { - directory := filepath.Join(repo, ".product-loop", "features", feature) + directory := WorkspaceFor(repo).FeatureDir(feature) planPath := filepath.Join(directory, "plan.md") approvalPath := filepath.Join(directory, "approval.md") lockPath := filepath.Join(directory, "plan.lock.json") @@ -1199,7 +1199,7 @@ func PublishPR(options PRPublishOptions) (string, error) { if context.Feature == "" { return "", fmt.Errorf("autonomous publication requires a managed feature") } - planPath := filepath.Join(repo, ".product-loop", "features", context.Feature, "plan.md") + planPath := filepath.Join(WorkspaceFor(repo).FeatureDir(context.Feature), "plan.md") check, checkErr := CheckPlan(planPath) if checkErr != nil { return "", checkErr @@ -1356,7 +1356,7 @@ func PublishPR(options PRPublishOptions) (string, error) { } func extractSystemicBoundaries(repo, feature string) error { - lockPath := filepath.Join(repo, ".product-loop", "features", feature, "plan.lock.json") + lockPath := filepath.Join(WorkspaceFor(repo).FeatureDir(feature), "plan.lock.json") value, err := os.ReadFile(lockPath) if err != nil { return nil // if it doesn't exist, ignore @@ -1369,7 +1369,7 @@ func extractSystemicBoundaries(repo, feature string) error { if !ok || len(boundaries) == 0 { return nil } - outPath := filepath.Join(repo, ".product-loop", "verified-boundaries.md") + outPath := filepath.Join(WorkspaceFor(repo).GeneratedRoot(), "verified-boundaries.md") f, err := os.OpenFile(outPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return err diff --git a/boatstack/readiness.go b/boatstack/readiness.go index e1360fc..616a5b2 100644 --- a/boatstack/readiness.go +++ b/boatstack/readiness.go @@ -39,7 +39,7 @@ func CheckPlanReadiness(planPath string) (ReadinessReceipt, error) { if err != nil { return ReadinessReceipt{}, err } - repo, err := ResolveRepository(filepath.Dir(planPath)) + repo, err := ResolveControllerRepository(filepath.Dir(planPath)) if err != nil { return ReadinessReceipt{}, err } diff --git a/boatstack/recovery.go b/boatstack/recovery.go index f8bf399..95e5e19 100644 --- a/boatstack/recovery.go +++ b/boatstack/recovery.go @@ -552,7 +552,7 @@ func RepairState(repoPath, feature string) (RepairStateResult, error) { } eligible := []string{} for _, candidate := range candidates { - if _, checkErr := CheckPlan(filepath.Join(repo, ".product-loop", "features", candidate, "plan.md")); checkErr != nil { + if _, checkErr := CheckPlan(filepath.Join(WorkspaceFor(repo).FeatureDir(candidate), "plan.md")); checkErr != nil { eligible = append(eligible, candidate) } } @@ -578,7 +578,7 @@ func RepairState(repoPath, feature string) (RepairStateResult, error) { return RepairStateResult{}, fmt.Errorf("invalid feature slug: %q", feature) } - directory := filepath.Join(repo, ".product-loop", "features", feature) + directory := WorkspaceFor(repo).FeatureDir(feature) planPath := filepath.Join(directory, "plan.md") if !fileExists(planPath) { return refusedRepairState(feature, "no plan.md exists for this feature; nothing to repair"), nil diff --git a/boatstack/references/artifacts.md b/boatstack/references/artifacts.md index f62d1d8..cb42c8f 100644 --- a/boatstack/references/artifacts.md +++ b/boatstack/references/artifacts.md @@ -159,6 +159,27 @@ clone, `external` outside the repository (Detached Supervision). | detached-registry | detached | external | attach, detach | | detached-repositories | detached | external | attach, detach, activate | +In detached mode, `WorkspaceContext` remaps the controller bundle and every +feature package to the external repository control root. Source plans stay at +their declared repository paths. Installation, update, hydration, host-hook, +and managed-worktree paths remain repository-owned. + +Direct `.product-loop` literals are frozen by a conformance inventory. Each +production file is classified as one of: canonical owner, controller bundle or +syntax, embedded installation, product-diff syntax, policy syntax, repository +workspace, or user guidance. A new unclassified literal fails the test. Runtime +controller reads and writes must use `WorkspaceContext.GeneratedRoot`, +`FeatureRoot`, or `FeatureDir`. + +## Detached feature reattachment + +Run `boatstack-helper attach --repo . --force` to reattach an older embedded +open-feature package. Boatstack verifies the plan and approval or autonomy +fingerprints, copies the package atomically, and verifies the copied hash. The +machine result is `IMPORTED`, `UNCHANGED`, `CONFLICTING`, or `REJECTED`. +Conflicts and stale receipts fail closed. Boatstack never chooses by recency and +never deletes the embedded source package. + ## Templates Copy only the templates required for the current slice from `assets/templates/`. Do not create empty ceremony. The feature spec, question ledger, test plan, gap ledger, and evidence ledger are the usual minimum for material product work. diff --git a/boatstack/runtime.go b/boatstack/runtime.go index 7954ecb..75f4211 100644 --- a/boatstack/runtime.go +++ b/boatstack/runtime.go @@ -163,6 +163,8 @@ func SHA256File(path string) (string, error) { } func repositoryRelativePath(repo, target string) (string, error) { + repo = canonicalizeExistingAncestor(repo) + target = canonicalizeExistingAncestor(target) relative, err := filepath.Rel(repo, target) if err != nil { return "", fmt.Errorf("cannot make path repository-relative: %w", err) diff --git a/boatstack/safety.go b/boatstack/safety.go index f4f8cf9..7f5725f 100644 --- a/boatstack/safety.go +++ b/boatstack/safety.go @@ -487,7 +487,7 @@ func preActivationFinding(repo, attemptedPath string) (SafetyFinding, bool) { return SafetyFinding{}, false } if len(candidates) == 1 && status.ObservedStage != "AMBIGUOUS" { - planPath := filepath.Join(repo, ".product-loop", "features", candidates[0], "plan.md") + planPath := filepath.Join(WorkspaceFor(repo).FeatureDir(candidates[0]), "plan.md") check, checkErr := CheckPlan(planPath) if checkErr != nil { return SafetyFinding{ diff --git a/boatstack/statemap_conformance_test.go b/boatstack/statemap_conformance_test.go index 78074ee..e37dabf 100644 --- a/boatstack/statemap_conformance_test.go +++ b/boatstack/statemap_conformance_test.go @@ -152,18 +152,19 @@ func TestGuardClassifiersMatchDeclaredOwnership(t *testing.T) { // extending this allowlist — the frozen inventory of declaring files. Growth // pressure should flow toward WorkspaceContext/statemap, not new literals. func TestProductLoopLiteralsStayInDeclaredFiles(t *testing.T) { - allowed := map[string]bool{ - "activation.go": true, "delivery.go": true, "export.go": true, - "flow_control.go": true, "flow_tasks.go": true, "hooks.go": true, - "init.go": true, "installation_repair.go": true, "mutation_undo.go": true, - "next.go": true, "paths.go": true, "planning.go": true, "pr.go": true, - "recovery.go": true, "runtime_cache.go": true, "safety.go": true, - "update.go": true, "update_publication.go": true, - "workspace.go": true, + allowed := map[string]string{ + "activation.go": "controller-syntax", "delivery.go": "controller-syntax", "export.go": "controller-bundle", + "hooks.go": "embedded-installation", + "init.go": "embedded-installation", "installation_repair.go": "embedded-installation", "mutation_undo.go": "controller-syntax", + "paths.go": "canonical-owner", "planning.go": "product-diff-syntax", "pr.go": "product-diff-syntax", + "recovery.go": "product-diff-syntax", "runtime_cache.go": "embedded-installation", "safety.go": "policy-syntax", + "update.go": "embedded-installation", "update_publication.go": "embedded-installation", + "workspace.go": "repository-workspace", // denial.go names .product-loop/features/ only in user-facing denial // copy (the owned-channel guidance), never as a joined path. - "denial.go": true, + "denial.go": "user-guidance", } + validClass := map[string]bool{"canonical-owner": true, "controller-bundle": true, "controller-syntax": true, "embedded-installation": true, "policy-syntax": true, "product-diff-syntax": true, "repository-workspace": true, "user-guidance": true} fset := token.NewFileSet() entries, err := os.ReadDir(".") @@ -191,7 +192,7 @@ func TestProductLoopLiteralsStayInDeclaredFiles(t *testing.T) { if err != nil { return true } - if strings.Contains(value, ".product-loop") && !allowed[name] { + if strings.Contains(value, ".product-loop") && allowed[name] == "" { offenders[name] = true } return true @@ -210,7 +211,10 @@ func TestProductLoopLiteralsStayInDeclaredFiles(t *testing.T) { } // Reverse check: a file on the allowlist that no longer carries a literal is // stale — shrink the list so the freeze stays honest. - for name := range allowed { + for name, class := range allowed { + if !validClass[class] { + t.Errorf("allowlist entry %s has unknown ownership class %q", name, class) + } content, err := os.ReadFile(name) if err != nil { t.Fatalf("allowlisted file %s unreadable: %v", name, err) diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index d549bdc..70a959a 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -96,7 +96,7 @@ subject to acceptance criteria pass approval is current ``` -That is why context trimming is not automatically an optimization. If removing state increases rework or false acceptance, total cost rises. The canonical runtime references are approximately **23175 estimated tokens**, while host adapters point to one operation at a time. +That is why context trimming is not automatically an optimization. If removing state increases rework or false acceptance, total cost rises. The canonical runtime references are approximately **23466 estimated tokens**, while host adapters point to one operation at a time. ## Control appears at transitions @@ -146,6 +146,6 @@ Delivery and system improvement also remain separate. A failed task may suggest ## What is evidence-backed -The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`e43a4ed28726995d848266cb39bb20902bbb1574`](https://github.com/operatorstack/intelligence-flow/tree/e43a4ed28726995d848266cb39bb20902bbb1574/labs/12-product-engineering-loop). +The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`2359acff52f0c7a7568fbd7daf1b62a79a86f7b5`](https://github.com/operatorstack/intelligence-flow/tree/2359acff52f0c7a7568fbd7daf1b62a79a86f7b5/labs/12-product-engineering-loop). The evidence supports specific failure mechanisms and guardrails. It does not establish that Boatstack is optimal, that control-theory notation proves software quality, or that one workflow dominates every team. Those are evaluation questions, so the distribution preserves measurements, provenance, gaps, and negative results. diff --git a/docs/public-claims.json b/docs/public-claims.json index 1f9a262..dd22f91 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "e43a4ed28726995d848266cb39bb20902bbb1574", + "source_commit": "2359acff52f0c7a7568fbd7daf1b62a79a86f7b5", "statuses": ["verified", "observed", "still_being_evaluated"], "claims": [ { @@ -12,7 +12,7 @@ "readable_evidence": "why-these-steps.md#portable-workflow-and-state", "implementation": ["../boatstack/export.go", "../boatstack/references/artifacts.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go"], - "last_verified_version": "source:e43a4ed28726995d848266cb39bb20902bbb1574" + "last_verified_version": "source:2359acff52f0c7a7568fbd7daf1b62a79a86f7b5" }, { "id": "human-decisions", @@ -23,7 +23,7 @@ "readable_evidence": "why-these-steps.md#human-decisions", "implementation": ["../boatstack/references/workflow.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:e43a4ed28726995d848266cb39bb20902bbb1574" + "last_verified_version": "source:2359acff52f0c7a7568fbd7daf1b62a79a86f7b5" }, { "id": "validation-provenance", @@ -34,7 +34,7 @@ "readable_evidence": "why-these-steps.md#validation-provenance", "implementation": ["validation-and-evidence.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go"], - "last_verified_version": "source:e43a4ed28726995d848266cb39bb20902bbb1574" + "last_verified_version": "source:2359acff52f0c7a7568fbd7daf1b62a79a86f7b5" }, { "id": "irreversible-operations", @@ -46,7 +46,7 @@ "readable_evidence": "why-these-steps.md#irreversible-operations", "implementation": ["safety.md", "../boatstack/safety.go", "../boatstack/hooks.go"], "verification": ["../boatstack/safety_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:e43a4ed28726995d848266cb39bb20902bbb1574" + "last_verified_version": "source:2359acff52f0c7a7568fbd7daf1b62a79a86f7b5" }, { "id": "reviewer-ready-pr", @@ -57,7 +57,7 @@ "readable_evidence": "why-these-steps.md#reviewer-ready-pr", "implementation": ["../boatstack/pr.go", "getting-started.md"], "verification": ["../boatstack/pr_test.go"], - "last_verified_version": "source:e43a4ed28726995d848266cb39bb20902bbb1574" + "last_verified_version": "source:2359acff52f0c7a7568fbd7daf1b62a79a86f7b5" }, { "id": "phase-scoped-delivery", @@ -68,7 +68,7 @@ "readable_evidence": "why-these-steps.md#phase-scoped-delivery", "implementation": ["../boatstack/delivery.go", "../boatstack/safety.go", "../boatstack/hooks.go", "../boatstack/references/workflow.md"], "verification": ["../boatstack/delivery_test.go", "../boatstack/pr_test.go"], - "last_verified_version": "source:e43a4ed28726995d848266cb39bb20902bbb1574" + "last_verified_version": "source:2359acff52f0c7a7568fbd7daf1b62a79a86f7b5" }, { "id": "model-neutral-contract", @@ -79,7 +79,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:e43a4ed28726995d848266cb39bb20902bbb1574" + "last_verified_version": "source:2359acff52f0c7a7568fbd7daf1b62a79a86f7b5" }, { "id": "cross-model-failures", @@ -90,7 +90,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:e43a4ed28726995d848266cb39bb20902bbb1574" + "last_verified_version": "source:2359acff52f0c7a7568fbd7daf1b62a79a86f7b5" }, { "id": "lower-cost-outcomes", @@ -101,7 +101,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:e43a4ed28726995d848266cb39bb20902bbb1574" + "last_verified_version": "source:2359acff52f0c7a7568fbd7daf1b62a79a86f7b5" }, { "id": "git-worktree-activation", @@ -112,7 +112,7 @@ "readable_evidence": "why-these-steps.md#git-worktree-activation", "implementation": ["../boatstack/runtime_cache.go", "../boatstack/hooks.go"], "verification": ["../boatstack/runtime_cache_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:e43a4ed28726995d848266cb39bb20902bbb1574" + "last_verified_version": "source:2359acff52f0c7a7568fbd7daf1b62a79a86f7b5" }, { "id": "visible-updates", @@ -123,7 +123,7 @@ "readable_evidence": "why-these-steps.md#visible-updates", "implementation": ["../boatstack/update.go", "../boatstack/init.go"], "verification": ["../boatstack/update_test.go", "../boatstack/init_test.go", "../boatstack/export_test.go"], - "last_verified_version": "source:e43a4ed28726995d848266cb39bb20902bbb1574" + "last_verified_version": "source:2359acff52f0c7a7568fbd7daf1b62a79a86f7b5" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 0a9f618..83ad681 100644 --- a/labs/diagram-json/plan.lock.json +++ b/labs/diagram-json/plan.lock.json @@ -6,7 +6,7 @@ "plan_path": "labs/diagram-json/plan.md", "plan_sha256": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "schema_version": 1, - "source_commit": "e43a4ed28726995d848266cb39bb20902bbb1574", + "source_commit": "2359acff52f0c7a7568fbd7daf1b62a79a86f7b5", "source_plan_path": "labs/diagram-json/source-plan.md", "source_plan_sha256": "e10593ddaa7522ab80cc991d0a09399257139799e37f737794cd49d68a39985b", "spec_path": "labs/diagram-json/spec.md", diff --git a/release-notes/2026-08-02-detached-ownership-boundary.md b/release-notes/2026-08-02-detached-ownership-boundary.md new file mode 100644 index 0000000..8bccd6b --- /dev/null +++ b/release-notes/2026-08-02-detached-ownership-boundary.md @@ -0,0 +1,5 @@ +### Detached workflows use one controller root + +Detached Boatstack workflows now read, write, and verify generated feature state under the same external controller root. + +Use `boatstack-helper attach --repo . --force` once to import a valid older embedded open-feature package. Boatstack verifies fingerprints and copies it atomically. It stops on conflicting packages or stale receipts and does not delete the embedded source.