diff --git a/labs/12-product-engineering-loop/boatstack-distribution/CONFIGURATION.md b/labs/12-product-engineering-loop/boatstack-distribution/CONFIGURATION.md new file mode 100644 index 000000000..413589ebb --- /dev/null +++ b/labs/12-product-engineering-loop/boatstack-distribution/CONFIGURATION.md @@ -0,0 +1,227 @@ +# Configure Boatstack + + + +Boatstack keeps delivery policy in `.boatstack-project.json` so the same project rules apply when the coding agent, model, session, or worktree changes. Start with the outcome you want, then set only the policies your repository needs. + +## Choose the outcome + +| If you want to… | Configure… | What changes | +|---|---|---| +| Run the right project checks | `project.commands` | Boatstack uses repository-owned commands instead of inventing validation. `test` is required. | +| Give planning durable project context | `project.context` | Planning can find the named documents and directories without scanning the whole repository. | +| Treat selected files as higher risk | `project.high_risk_paths` and `workflow.independent_review_for_high_risk` | Changes matching those globs require the configured independent review boundary. | +| Require a person to approve plans | `workflow.human_plan_approval` | Build waits for an explicit approval receipt. | +| Allow a gate to pass with recorded gaps | `workflow.allow_pass_with_gaps` | A gate may report a pass with visible, retained gaps instead of requiring a gap-free result. | +| Keep reader-facing release history | `workflow.maintain_changelog` | Every managed delivery slice and Boatstack-prepared ad-hoc PR must update `CHANGELOG.md`. | +| Look for a missing systemic boundary | `workflow.boundary_analysis` | Planning checks whether the request is a local symptom and asks before expanding it into boundary work. | +| Start features in fresh Git workspaces | `workspace` | Boatstack can create a branch or linked worktree and manage local cleanup under the selected policy. | +| Limit generated host adapters | `adapters` | Only the named Cursor, Claude Code, Codex, Gemini CLI, or GitHub surfaces are exported. | +| Add supported specialist workflows | `integrations` | The installer records whether gstack or Spec Kit was requested and its installed state. | + +Changing configuration is an infrastructure change. Regenerate the Boatstack export and review the resulting diff through the repository's normal change process. + +## Complete example + +JSON does not support comments, so the explanations follow the example. + +```json +{ + "schema_version": 1, + "project": { + "name": "example-product", + "default_branch": "main", + "context": [ + "README.md", + "AGENTS.md", + "docs/architecture/", + "docs/decisions/" + ], + "commands": { + "build": "npm run build", + "lint": "npm run lint", + "test": "npm test", + "typecheck": "npm run typecheck" + }, + "high_risk_paths": [ + "migrations/**", + "auth/**", + "billing/**" + ] + }, + "workflow": { + "human_plan_approval": true, + "independent_review_for_high_risk": true, + "allow_pass_with_gaps": true, + "maintain_changelog": false, + "boundary_analysis": false + }, + "workspace": { + "enabled": true, + "mode": "worktree", + "cleanup": "confirm", + "cleanup_after": "merge" + }, + "adapters": ["cursor", "claude", "codex", "gemini", "github"], + "integrations": { + "gstack": { + "requested": false, + "version": "" + }, + "spec-kit": { + "requested": false, + "version": "" + } + } +} +``` + +Use the versions written by the installer; the placeholders above describe ownership and are not literal version values to copy. + +## Field reference + +### Root fields + +| Field | Required | Values and default | Effect | +|---|---:|---|---| +| `schema_version` | Yes | Integer; currently `1` | Selects the configuration contract. A newer value requires a newer Boatstack; an older supported value is migrated during update. | +| `project` | Yes | Object | Names the project and supplies repository context and commands. | +| `workflow` | Yes | Object; booleans use `false` when omitted | Controls approval, review, gap, changelog, and boundary-analysis behavior. | +| `workspace` | No | Object; disabled when absent | Controls optional per-feature branch or worktree management. | +| `adapters` | No | Array of supported adapter names; empty or absent enables all supported adapters | Selects generated host surfaces. Duplicate and blank entries are removed during export. | +| `integrations` | No | Object keyed by supported integration name | Records requested specialist integrations and installer-maintained state. | + +### `project` + +| Field | Required | Values and default | Effect | +|---|---:|---|---| +| `name` | Yes | Non-empty string | Human-readable project name used in generated configuration. | +| `default_branch` | No | Branch name; PR operations fall back to `origin/HEAD`, then `main` | Sets the canonical base branch for freshness checks, PRs, updates, and managed workspace cuts. Boatstack updates require it to be explicit. | +| `context` | No | Array of repository-relative file or directory paths; empty by default | Identifies durable context that planning should consult when relevant. | +| `commands` | Yes | Object of command-name to shell-command strings | Declares repository-owned validation commands. | +| `commands.test` | Yes | Non-empty command string | Supplies the minimum test boundary; configuration validation fails if it is absent or blank. | +| Other `commands.*` entries | No | Command strings such as `build`, `lint`, or `typecheck` | Make additional project checks available under their chosen names. Only `test` has a required name. | +| `high_risk_paths` | No | Array of Git-style glob patterns; empty by default | Marks paths for safety scanning and, when enabled, independent high-risk review. | + +Context paths guide bounded discovery; they are not a request to load every listed file for every feature. Commands run from the repository and should be deterministic enough to act as evidence. + +### `workflow` + +The defaults below describe an omitted JSON field. A fresh installer-generated configuration writes its recommended policies explicitly, including human approval, independent high-risk review, and pass-with-gaps behavior, so review the actual file rather than assuming omission. + +| Field | Default | Effect | +|---|---:|---| +| `human_plan_approval` | `false` | When `true`, requires explicit human plan approval before Build can activate the plan. | +| `independent_review_for_high_risk` | `false` | When `true`, changes matching `project.high_risk_paths` require the independent review boundary before shipping. Configure both fields for this policy to have a target. | +| `allow_pass_with_gaps` | `false` | When `true`, verification may pass with explicitly recorded outstanding gaps. It does not hide or discard them. | +| `maintain_changelog` | `false` | When `true`, requires a reader-visible `CHANGELOG.md` entry for every managed delivery slice and Boatstack-prepared ad-hoc PR. | +| `boundary_analysis` | `false` | When `true`, planning checks whether a request indicates a missing systemic boundary. Scope expansion remains a material human decision; choosing programmatic enforcement produces a boundary slice followed by the feature slice. | + +### `workspace` + +Workspace management is off unless `workspace.enabled` is `true`. Empty policy fields receive defaults only after it is enabled. + +| Field | Values and default | Effect | +|---|---|---| +| `enabled` | Boolean; `false` | Master switch. When `false`, Boatstack does not create or remove branches or worktrees. | +| `mode` | `worktree` (default) or `branch` | Creates a linked worktree or switches to a fresh in-place feature branch. | +| `cleanup` | `confirm` (default), `auto`, or `off` | Asks before eligible cleanup, performs it automatically, or disables managed cleanup. | +| `cleanup_after` | `merge` (default) or `ship` | Makes cleanup eligible after the PR is confirmed merged or after the feature is published. Safety checks still prevent discarding uncommitted or unmerged local work without an explicit operator override. | + +Managed workspaces are cut from the current remote default branch. Boatstack does not rewrite history, reuse an existing branch, delete remote branches, merge pull requests, or silently discard local work. + +### `adapters` + +Supported values are `cursor`, `claude`, `codex`, `gemini`, and `github`. An empty or omitted array enables all five. Use a subset only when the repository intentionally does not support the other host surfaces. + +### `integrations` + +Supported keys are `gstack` and `spec-kit`. Installation normally owns this object; prefer selecting integrations through the installer instead of hand-editing its result. + +| Field | Ownership | Effect | +|---|---|---| +| `requested` | User choice recorded by installer | Whether the integration was requested. | +| `status` | Installer-maintained, optional | Current installation result, such as installed or partial. | +| `version` | Installer-maintained, optional | Pinned integration version or revision. | +| `detail` | Installer-maintained, optional | Human-readable installation or diagnostic detail. | + +## Common policies + +### Require a repository changelog + +```json +{ + "workflow": { + "maintain_changelog": true + } +} +``` + +Add a categorized entry under `CHANGELOG.md`'s current `Unreleased` heading. See [the format and first-entry example](getting-started.md#keep-a-repository-changelog). + +### Analyze systemic boundaries during planning + +```json +{ + "workflow": { + "boundary_analysis": true + } +} +``` + +This adds a product decision when repository evidence suggests that a local request is a symptom of a broader missing boundary. It does not silently turn every feature into a refactor. + +### Require independent review for high-risk paths + +```json +{ + "project": { + "high_risk_paths": ["migrations/**", "auth/**", "billing/**"] + }, + "workflow": { + "independent_review_for_high_risk": true + } +} +``` + +Choose paths where a distinct reviewer is meaningful. Broad patterns increase review cost and should reflect actual repository risk boundaries. + +### Manage a fresh worktree for each feature + +```json +{ + "workspace": { + "enabled": true, + "mode": "worktree", + "cleanup": "confirm", + "cleanup_after": "merge" + } +} +``` + +This is the conservative managed-workspace policy: start from a fresh remote base, use a linked worktree, and ask before reclaiming local state after merge. diff --git a/labs/12-product-engineering-loop/boatstack-distribution/README.md b/labs/12-product-engineering-loop/boatstack-distribution/README.md index 2984d0160..be9b8b9f4 100644 --- a/labs/12-product-engineering-loop/boatstack-distribution/README.md +++ b/labs/12-product-engineering-loop/boatstack-distribution/README.md @@ -133,19 +133,9 @@ Receipts remain as history; published corrections become linked deliveries. -### Optional changelog +## Configure repository policy -It is disabled by default. Enable it in `.boatstack-project.json`: - -```json -{ - "workflow": { - "maintain_changelog": true - } -} -``` - -Enabled repositories require a categorized `CHANGELOG.md` → `Unreleased` entry for every managed slice and Boatstack-prepared ad-hoc PR. The file stays user-owned; install and update never overwrite it. [See the format and first-entry example](docs/getting-started.md#keep-a-repository-changelog). +`.boatstack-project.json` controls the project commands and context Boatstack uses, which coding hosts it supports, and opt-in policies for changelogs, boundary analysis, high-risk review, and feature workspaces. [Choose the outcomes you want and see every configuration field](docs/configuration.md). ## How Boatstack fits into your AI stack diff --git a/labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-22-product-configuration-guide.md b/labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-22-product-configuration-guide.md new file mode 100644 index 000000000..205993357 --- /dev/null +++ b/labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-22-product-configuration-guide.md @@ -0,0 +1,5 @@ +### Make repository policy discoverable without lengthening the README + +Boatstack now links from its public README to a complete, value-first configuration guide. Maintainers can start from the delivery outcome they want, then see every supported `.boatstack-project.json` field, accepted value, default, interaction, and focused example without reverse-engineering the generated project file. The canonical internal schema now also covers managed workspaces, boundary analysis, supported adapters, integration metadata, and optional project commands. + +A supervisory contract now derives the public configuration surface from the implementation's JSON tags and compares it with both documentation slices. Adding, removing, or renaming a configuration field without updating the guide and canonical schema fails the Go test suite instead of silently creating documentation drift. diff --git a/labs/12-product-engineering-loop/product-engineering-loop/config_documentation_test.go b/labs/12-product-engineering-loop/product-engineering-loop/config_documentation_test.go new file mode 100644 index 000000000..6db1a77de --- /dev/null +++ b/labs/12-product-engineering-loop/product-engineering-loop/config_documentation_test.go @@ -0,0 +1,89 @@ +package boatstack + +import ( + "os" + "reflect" + "sort" + "strings" + "testing" +) + +const configFieldMarkerPrefix = "boatstack-config-field:" + +func configSurface(value reflect.Type, prefix string) []string { + if value.Kind() == reflect.Pointer { + value = value.Elem() + } + var fields []string + for index := 0; index < value.NumField(); index++ { + field := value.Field(index) + name := strings.Split(field.Tag.Get("json"), ",")[0] + if name == "" || name == "-" { + continue + } + path := name + if prefix != "" { + path = prefix + "." + name + } + fields = append(fields, path) + + nested := field.Type + if nested.Kind() == reflect.Pointer { + nested = nested.Elem() + } + switch nested.Kind() { + case reflect.Struct: + fields = append(fields, configSurface(nested, path)...) + case reflect.Map: + item := nested.Elem() + if item.Kind() == reflect.Struct { + fields = append(fields, configSurface(item, path+".*")...) + } + } + } + return fields +} + +func configFieldMarkers(content string) []string { + var fields []string + for _, line := range strings.Split(content, "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, configFieldMarkerPrefix) { + fields = append(fields, strings.TrimPrefix(line, configFieldMarkerPrefix)) + } + } + sort.Strings(fields) + return fields +} + +func documentedConfigSurface(t *testing.T, path string) []string { + t.Helper() + content, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read configuration documentation %s: %v", path, err) + } + return configFieldMarkers(string(content)) +} + +func TestConfigFieldMarkersAcceptWindowsLineEndings(t *testing.T) { + content := "\r\n" + want := []string{"project.name", "workflow"} + if got := configFieldMarkers(content); !reflect.DeepEqual(got, want) { + t.Fatalf("CRLF configuration markers were not parsed: got %v, want %v", got, want) + } +} + +func TestPublicConfigurationSurfaceIsDocumented(t *testing.T) { + want := configSurface(reflect.TypeOf(ProjectConfig{}), "") + sort.Strings(want) + + for _, document := range []string{ + "references/config-schema.md", + "../boatstack-distribution/CONFIGURATION.md", + } { + got := documentedConfigSurface(t, document) + if !reflect.DeepEqual(got, want) { + t.Errorf("configuration documentation drift in %s\nimplementation: %v\ndocumented: %v", document, want, got) + } + } +} diff --git a/labs/12-product-engineering-loop/product-engineering-loop/references/config-schema.md b/labs/12-product-engineering-loop/product-engineering-loop/references/config-schema.md index 5607693c2..bf392c8e8 100644 --- a/labs/12-product-engineering-loop/product-engineering-loop/references/config-schema.md +++ b/labs/12-product-engineering-loop/product-engineering-loop/references/config-schema.md @@ -1,5 +1,32 @@ # Boatstack Configuration Schema + + This reference document defines the schema and version history of `.boatstack-project.json`. ## Current Schema Version @@ -13,6 +40,7 @@ This reference document defines the schema and version history of `.boatstack-pr - `schema_version` (integer, required): Must be exactly `1`. - `project` (object, required): General project definition. - `workflow` (object, required): Flags controlling state machine transitions and safety gates. +- `workspace` (object, optional): Opt-in per-feature branch or worktree management. - `adapters` (array of strings, optional): Enabled host environment adapters. If empty, defaults to enabling all. - `integrations` (object, optional): Explicit configurations for individual third-party integrations. @@ -23,6 +51,7 @@ This reference document defines the schema and version history of `.boatstack-pr - `context` (array of strings, optional): Paths to persistent project directories or contextual documents. - `commands` (object, required): Custom development commands: - `test` (string, required): The exact command to execute project-local tests. + - Other command names (string, optional): Additional repository-owned commands such as `build`, `lint`, or `typecheck`. - `high_risk_paths` (array of strings, optional): Glob patterns of files requiring independent reviewer sign-off before shipping. ### workflow Fields @@ -30,10 +59,31 @@ This reference document defines the schema and version history of `.boatstack-pr - `human_plan_approval` (boolean, optional): Whether a parent plan requires explicit human approval before building. - `independent_review_for_high_risk` (boolean, optional): Whether modifications to high-risk files require a distinct peer review gate. - `allow_pass_with_gaps` (boolean, optional): Whether the delivery verification allows outstanding questions or gaps. -- `maintain_changelog` (boolean, optional): Whether a release-notes fragment is required for each delivery slice. +- `maintain_changelog` (boolean, optional): Whether a reader-visible `CHANGELOG.md` entry is required for each delivery slice. +- `boundary_analysis` (boolean, optional): Whether planning checks for a missing systemic boundary and presents local repair versus programmatic enforcement as a material product decision. + +### workspace Fields + +- `enabled` (boolean, optional): Enables managed per-feature workspaces. Defaults to `false`. +- `mode` (string, optional): `worktree` or `branch`. Defaults to `worktree` when workspace management is enabled. +- `cleanup` (string, optional): `confirm`, `auto`, or `off`. Defaults to `confirm`. +- `cleanup_after` (string, optional): `merge` or `ship`. Defaults to `merge`. + +### adapters Values + +Supported values are `cursor`, `claude`, `codex`, `gemini`, and `github`. An empty or omitted array enables all supported adapters. + +### integrations Fields + +Supported integration keys are `gstack` and `spec-kit`. Each integration state can contain: + +- `requested` (boolean, required when the integration is present): Whether installation was requested. +- `status` (string, optional): Installer-maintained installation status. +- `version` (string, optional): Installer-maintained pinned version or revision. +- `detail` (string, optional): Installer-maintained diagnostic detail. ## Version Changelog ### Version 1 -- Initial schema with `project`, `workflow`, `adapters`, and `integrations`. +- Initial schema with `project`, `workflow`, `workspace`, `adapters`, and `integrations`. diff --git a/labs/12-product-engineering-loop/scripts/build_boatstack.py b/labs/12-product-engineering-loop/scripts/build_boatstack.py index f085d0d08..4dbeea429 100644 --- a/labs/12-product-engineering-loop/scripts/build_boatstack.py +++ b/labs/12-product-engineering-loop/scripts/build_boatstack.py @@ -156,6 +156,7 @@ def build_files(source_commit: str) -> dict[Path, bytes]: "VALIDATION_AND_EVIDENCE.template.md", values ), Path("docs/getting-started.md"): text(DISTRIBUTION_ROOT / "GETTING_STARTED.md"), + Path("docs/configuration.md"): text(DISTRIBUTION_ROOT / "CONFIGURATION.md"), Path("docs/generated-files.md"): text(DISTRIBUTION_ROOT / "GENERATED_FILES.md"), Path("docs/troubleshooting.md"): text(DISTRIBUTION_ROOT / "TROUBLESHOOTING.md"), Path("docs/safety.md"): template("SAFETY.template.md", values), diff --git a/labs/12-product-engineering-loop/tests/test_product_loop.py b/labs/12-product-engineering-loop/tests/test_product_loop.py index 7a9b2f8bc..43eb0b282 100644 --- a/labs/12-product-engineering-loop/tests/test_product_loop.py +++ b/labs/12-product-engineering-loop/tests/test_product_loop.py @@ -750,9 +750,12 @@ def test_boatstack_is_a_reproducible_upstream_projection(self) -> None: for role in ("Coding agent", "Model", "Skill", "Boatstack"): self.assertIn(f"**{role}**", readme) self.assertIn("repository-local delivery harness", readme) + self.assertIn("## Configure repository policy", readme) + self.assertIn("docs/configuration.md", readme) self.assertTrue((repo / "docs/evidence-engineered-coding.md").is_file()) for relative in [ "docs/getting-started.md", + "docs/configuration.md", "docs/generated-files.md", "docs/troubleshooting.md", "docs/safety.md", @@ -768,6 +771,7 @@ def test_boatstack_is_a_reproducible_upstream_projection(self) -> None: self.assertFalse((repo / "assets/boatstack-model-choice.svg").exists()) for source, projected in ( ("GETTING_STARTED.md", "docs/getting-started.md"), + ("CONFIGURATION.md", "docs/configuration.md"), ("GENERATED_FILES.md", "docs/generated-files.md"), ("TROUBLESHOOTING.md", "docs/troubleshooting.md"), ("ACCOUNT_RECOVERY_WALKTHROUGH.md", "docs/account-recovery-walkthrough.md"), @@ -778,6 +782,18 @@ def test_boatstack_is_a_reproducible_upstream_projection(self) -> None: (DISTRIBUTION / source).read_text(), (repo / projected).read_text(), ) + configuration = (repo / "docs/configuration.md").read_text() + json_examples = re.findall( + r"```json\n(.*?)\n```", configuration, re.DOTALL + ) + self.assertGreaterEqual(len(json_examples), 5) + parsed_examples = [json.loads(example) for example in json_examples] + with tempfile.TemporaryDirectory() as config_temp: + config_path = Path(config_temp) / ".boatstack-project.json" + config_path.write_text(json.dumps(parsed_examples[0], indent=2) + "\n") + self.run_command( + self.helper, "export", "--repo", config_temp, "--config", config_path + ) getting_started = (repo / "docs/getting-started.md").read_text() for expected in ( "## Plan ready", @@ -896,6 +912,7 @@ def test_boatstack_is_a_reproducible_upstream_projection(self) -> None: docs_to_check = [ repo / "README.md", repo / "docs/getting-started.md", + repo / "docs/configuration.md", repo / "docs/generated-files.md", repo / "docs/troubleshooting.md", repo / "docs/safety.md",