diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 27b82c2..c891b72 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/ef0d18195ef87f96891ea35c96866c456c855b1d/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/9749f1b5397c73f6ea9077719a85f60ae7eee1d6/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 30434a2..a9bab53 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -12,7 +12,7 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "0f4d2d703febb566c2a03aa94684be12915ecf8ca022bed118321214c9efce58", + "CONTRIBUTING.md": "e1a9752e2aa9f716579096a2ed8f1d0025673ad7775a7eb3a106dc238285420a", "README.md": "83ea436685782c5c2d2d375ceae21cb95c72a5f6608eb8187a5efc817350e081", "assets/boatstack-journey.svg": "c1f7fe2741f5e9ca66bb3fe9b103e6364ba5acbca8b7a8054768ffd85cf325ea", "assets/boatstack-mark.svg": "ec96165583b15cfd446c27049d49217973f3e9b1defa5771cc08eec0c9542ce4", @@ -37,8 +37,8 @@ "boatstack/export.go": "faf905a8d45a73a03792fb000e56cba351f86caf3b3c261ff8d324e612e48377", "boatstack/export_test.go": "9961313036b8ca8681d804193921357f29d6acd30a9532aa522986d48a9894ef", "boatstack/go.mod": "57c377eccea51372d6664de4169e2ca45806b046f7e8a98a1e35a9eb454b4b8d", - "boatstack/hooks.go": "cf1959f5b6594853180f463dcddb0a6b1aee3e1408a7e44b063abe9ac22f1c56", - "boatstack/hooks_test.go": "a5298b7f46709bce617913085fe3b597a4bb4f730a5b5b51f459be85adbefcbb", + "boatstack/hooks.go": "718161de45165e450369577e5ef1e9fdc76a484d61fa56a36eb6a117b2e63249", + "boatstack/hooks_test.go": "c3f359416ea53f258d8747d0247381e8946efd4d4a5bcf072c4147f885475ad3", "boatstack/init.go": "ddee0cfd8ab9f3416fb895afd99d1682ca09550d2d9b2ad3cd11f5e06438c585", "boatstack/init_test.go": "b761ada1f5a04c0a27225a6f1eb99baf5477a424c5a9748a3267f07ba5a84605", "boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c", @@ -63,15 +63,15 @@ "boatstack/testdata/reviewer-pr-body.md": "4c64e3788e5d61a377aeb0f797f7fc8d2316ab6e49572d15636eea7ba9e34ac4", "boatstack/testdata/safety/safe_apply.py.txt": "c9ec7fb932cf21b6aa8df597c4d4c54d6ec65e796240e49118d699f583383975", "boatstack/testdata/safety/unsafe_apply.py.txt": "42db1751865cc15c4dd69a03146b5deca8f21f916d258e433b27bbef5f884ab1", - "boatstack/update.go": "b801318dce2268f9c02fabc71ab783a36aff9b9b110457204d3231bc18382370", + "boatstack/update.go": "4538ae8138cd8d9e25261a849e7b4a4a543c22ef18c43e21d502597ebfc250b5", "boatstack/update_test.go": "aa0c2ca97038aad661c46321600034e206e88639f89e0216b3c4cf597312cbae", "docs/account-recovery-walkthrough.md": "676034974594a7d1a559b24dbed31d7ccc429eb81404b203ca07bbdaa19ec3d3", "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", - "docs/evidence-engineered-coding.md": "03dc197a7fd3024911c23c93c34e1e2e417426493aa31ee603b86c504d71519e", + "docs/evidence-engineered-coding.md": "2368d5fba09ad6f24f8317fcc40f6d2208c01d1dc6d4a673d0f2477f5cca6757", "docs/generated-files.md": "7b2e8c10a35aa351fb87753492ed3cadb05011002d2fd6ffeb1951c356f6b286", "docs/getting-started.md": "6d98555b9d7a27091169a6a8c1efb64c84791c73a72f814e2a4dbdc149ac58a6", - "docs/public-claims.json": "192b0333ce701af1534ceb4cd3ee2f3e8ff09085a247dffe3240a9fabb100c49", + "docs/public-claims.json": "b9080efb0c1d995aef961345e7dc2343cc7c83715a822af5d0ed89bca979227a", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "d65c66e323037bda5d45aacef5d48afa6bf93da55901378891d235aca3a5684f", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -85,7 +85,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": "3860c98ddc84b7ab1a2e9038fa92a7dedac05b4fa823cc5db763fd4412cf3c7a", + "labs/diagram-json/plan.lock.json": "86398343fb6a7cc35a97099753ec3118ee03985b3869fd225a13b18c6b16d86c", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -99,6 +99,7 @@ "release-notes/2026-07-18-base-aware-release-preflight.md": "cdface46ccd959a5299de4c363c9e4057e7257820dab77dcf0e587daef5d3d99", "release-notes/2026-07-18-global-reply-shortcuts.md": "329d6fd104079bc5f66e7c3d477f4ff2a6bb264d429485c6f42f9c203d17fa29", "release-notes/2026-07-18-harbor-lab-namespace.md": "6419c049e5a3024c5a8604e4d9fb241c27ceb80bf1d4cc62f66d1a0eaf09ea21", + "release-notes/2026-07-18-host-hook-migrations.md": "1c9f81d9318854214f72802045e8e39c9ca45435af0f9d2c28fcf4ff4c1e0071", "release-notes/2026-07-18-intelligence-flow-labs.md": "b236dddcf22dab718698b05c5dcf162ffddf9f5b53ea98468fd49b342d75edb9", "release-notes/2026-07-18-safety-sql-boundaries.md": "32011ca3d02a371e8f3f2899ffb34df3af0843d18d25e7db95fbca32c2dcf18c", "release-notes/2026-07-18-stacked-bar-mark.md": "c4d5bd5fb89c280d7fba015384fd795fcb8c31ffe501078aa55a90cbcf66ba7b" @@ -106,7 +107,7 @@ "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "ef0d18195ef87f96891ea35c96866c456c855b1d", + "commit": "9749f1b5397c73f6ea9077719a85f60ae7eee1d6", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/hooks.go b/boatstack/hooks.go index c658add..2be19df 100644 --- a/boatstack/hooks.go +++ b/boatstack/hooks.go @@ -299,6 +299,42 @@ func InstallHostHooks(repo string, adapters []string) error { } func CheckHostHooks(repo string, adapters []string) error { + return checkHostHooks(repo, adapters, func(host, event string) (any, error) { + return desiredHostHookForEvent(host, event), nil + }) +} + +// CheckInstalledHostHooks validates merged host settings against the committed +// fragment from the installed release. Update preflight must use this boundary: +// comparing an old, healthy hook with the incoming release template would +// misclassify an intentional template migration as user drift. +func CheckInstalledHostHooks(repo string, adapters []string) error { + fragments := map[string]map[string]any{} + return checkHostHooks(repo, adapters, func(host, event string) (any, error) { + fragment := fragments[host] + if fragment == nil { + path := filepath.Join(repo, ".product-loop", "hooks", host+".fragment.json") + value, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("cannot read installed %s hook fragment: %w", host, err) + } + if err := json.Unmarshal(value, &fragment); err != nil { + return nil, fmt.Errorf("invalid installed %s hook fragment: %w", host, err) + } + if fragment["schema_version"] != float64(1) || fragment["host"] != host { + return nil, fmt.Errorf("invalid installed %s hook fragment identity", host) + } + fragments[host] = fragment + } + events, ok := fragment["events"].(map[string]any) + if !ok || events[event] == nil { + return nil, fmt.Errorf("installed %s hook fragment is missing %s", host, event) + } + return events[event], nil + }) +} + +func checkHostHooks(repo string, adapters []string, expectedForEvent func(host, event string) (any, error)) error { for _, host := range []string{"cursor", "claude", "codex"} { if !contains(adapters, host) { continue @@ -313,6 +349,10 @@ func CheckHostHooks(repo string, adapters []string) error { return fmt.Errorf("missing %s hooks in %s", host, path) } for _, event := range hookEvents(host) { + expectedEntry, err := expectedForEvent(host, event) + if err != nil { + return err + } entries, ok := hooks[event].([]any) if !ok { return fmt.Errorf("missing %s safety event %s in %s", host, event, path) @@ -322,7 +362,7 @@ func CheckHostHooks(repo string, adapters []string) error { if containsBoatstackHook(entry) { matches++ current, _ := json.Marshal(entry) - expected, _ := json.Marshal(desiredHostHookForEvent(host, event)) + expected, _ := json.Marshal(expectedEntry) if string(current) != string(expected) { return fmt.Errorf("drifted %s Boatstack safety hook", host) } diff --git a/boatstack/hooks_test.go b/boatstack/hooks_test.go index 782a500..39fc142 100644 --- a/boatstack/hooks_test.go +++ b/boatstack/hooks_test.go @@ -60,6 +60,52 @@ func TestHostHookMergeRejectsAmbiguousCollisionAndDrift(t *testing.T) { } } +func TestInstalledHookValidationAllowsTemplateMigrationButRejectsUserDrift(t *testing.T) { + repo := t.TempDir() + adapters := []string{"claude"} + if err := InstallHostHooks(repo, adapters); err != nil { + t.Fatal(err) + } + + fragment, err := hookFragmentJSON("claude") + if err != nil { + t.Fatal(err) + } + fragment = []byte(strings.ReplaceAll(string(fragment), "Checking Boatstack execution policy", "Checking irreversible-operation policy")) + fragmentPath := filepath.Join(repo, ".product-loop", "hooks", "claude.fragment.json") + if err := os.MkdirAll(filepath.Dir(fragmentPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(fragmentPath, fragment, 0o644); err != nil { + t.Fatal(err) + } + + hookPath := filepath.Join(repo, ".claude", "settings.json") + hookValue, err := os.ReadFile(hookPath) + if err != nil { + t.Fatal(err) + } + hookValue = []byte(strings.ReplaceAll(string(hookValue), "Checking Boatstack execution policy", "Checking irreversible-operation policy")) + if err := os.WriteFile(hookPath, hookValue, 0o644); err != nil { + t.Fatal(err) + } + + if err := CheckHostHooks(repo, adapters); err == nil || !strings.Contains(err.Error(), "drifted") { + t.Fatalf("incoming template unexpectedly accepted the installed hook: %v", err) + } + if err := CheckInstalledHostHooks(repo, adapters); err != nil { + t.Fatalf("healthy installed hook blocked template migration: %v", err) + } + + hookValue = []byte(strings.ReplaceAll(string(hookValue), `"timeout": 10`, `"timeout": 99`)) + if err := os.WriteFile(hookPath, hookValue, 0o644); err != nil { + t.Fatal(err) + } + if err := CheckInstalledHostHooks(repo, adapters); err == nil || !strings.Contains(err.Error(), "drifted") { + t.Fatalf("user drift was not rejected against the installed fragment: %v", err) + } +} + func TestMissingHelperLauncherFailsClosed(t *testing.T) { if _, err := exec.LookPath("bash"); err != nil { t.Skip("bash unavailable") diff --git a/boatstack/update.go b/boatstack/update.go index 8cced31..c599635 100644 --- a/boatstack/update.go +++ b/boatstack/update.go @@ -345,7 +345,7 @@ func ValidateUpdateWorkspace(repo string, config ProjectConfig) error { if err := CheckPreviousGeneratedState(repo); err != nil { return err } - if err := CheckHostHooks(repo, config.Adapters); err != nil { + if err := CheckInstalledHostHooks(repo, config.Adapters); err != nil { return fmt.Errorf("host-hook drift blocks update: %w", err) } return CheckExistingInstallProvenance(repo) diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 147b491..7b6cc9f 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -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 [`ef0d18195ef87f96891ea35c96866c456c855b1d`](https://github.com/operatorstack/intelligence-flow/tree/ef0d18195ef87f96891ea35c96866c456c855b1d/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 [`9749f1b5397c73f6ea9077719a85f60ae7eee1d6`](https://github.com/operatorstack/intelligence-flow/tree/9749f1b5397c73f6ea9077719a85f60ae7eee1d6/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 ee19d19..e301e90 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "ef0d18195ef87f96891ea35c96866c456c855b1d", + "source_commit": "9749f1b5397c73f6ea9077719a85f60ae7eee1d6", "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:ef0d18195ef87f96891ea35c96866c456c855b1d" + "last_verified_version": "source:9749f1b5397c73f6ea9077719a85f60ae7eee1d6" }, { "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:ef0d18195ef87f96891ea35c96866c456c855b1d" + "last_verified_version": "source:9749f1b5397c73f6ea9077719a85f60ae7eee1d6" }, { "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:ef0d18195ef87f96891ea35c96866c456c855b1d" + "last_verified_version": "source:9749f1b5397c73f6ea9077719a85f60ae7eee1d6" }, { "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:ef0d18195ef87f96891ea35c96866c456c855b1d" + "last_verified_version": "source:9749f1b5397c73f6ea9077719a85f60ae7eee1d6" }, { "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:ef0d18195ef87f96891ea35c96866c456c855b1d" + "last_verified_version": "source:9749f1b5397c73f6ea9077719a85f60ae7eee1d6" }, { "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:ef0d18195ef87f96891ea35c96866c456c855b1d" + "last_verified_version": "source:9749f1b5397c73f6ea9077719a85f60ae7eee1d6" }, { "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:ef0d18195ef87f96891ea35c96866c456c855b1d" + "last_verified_version": "source:9749f1b5397c73f6ea9077719a85f60ae7eee1d6" }, { "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:ef0d18195ef87f96891ea35c96866c456c855b1d" + "last_verified_version": "source:9749f1b5397c73f6ea9077719a85f60ae7eee1d6" }, { "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:ef0d18195ef87f96891ea35c96866c456c855b1d" + "last_verified_version": "source:9749f1b5397c73f6ea9077719a85f60ae7eee1d6" }, { "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:ef0d18195ef87f96891ea35c96866c456c855b1d" + "last_verified_version": "source:9749f1b5397c73f6ea9077719a85f60ae7eee1d6" }, { "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:ef0d18195ef87f96891ea35c96866c456c855b1d" + "last_verified_version": "source:9749f1b5397c73f6ea9077719a85f60ae7eee1d6" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index a0dc8f6..7066bd6 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": "ef0d18195ef87f96891ea35c96866c456c855b1d", + "source_commit": "9749f1b5397c73f6ea9077719a85f60ae7eee1d6", "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-07-18-host-hook-migrations.md b/release-notes/2026-07-18-host-hook-migrations.md new file mode 100644 index 0000000..86119f7 --- /dev/null +++ b/release-notes/2026-07-18-host-hook-migrations.md @@ -0,0 +1,3 @@ +### Updates migrate healthy host hooks safely + +Boatstack updates now validate Claude, Codex, and Cursor safety hooks against the committed fragment from the installed release before applying the incoming template. Intentional hook wording or structure changes can migrate normally, while genuine local hook edits still block replacement for review.