Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 56 additions & 183 deletions labs/12-product-engineering-loop/boatstack-distribution/CONFIGURATION.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,26 @@ Planning is Markdown-only. The adapter may use Boatstack's bounded planning writ

## `/build` says it is ready but cannot start

The plan is approved, but the host remains read-only. Enter the host's normal execution-capable mode and rerun `/build`. Boatstack deliberately creates no compiled state or lock before that transition.
The plan is authorized, but the host remains read-only. Enter the host's normal execution-capable mode and rerun `/build`. Boatstack deliberately creates no compiled state or lock before that transition.

## Approval is stale

The source plan, feature spec, or complete plan changed after approval. Return to `/auto-plan`, review the new plan at `/plan-gate`, and approve it again. Never edit approval metadata manually.

## Build does not create `approval.md`

Check `workflow.human_plan_approval`. When it is `false`, this is expected: activation writes a fingerprinted schema-v2 plan lock with `authorization_mode: policy` and does not claim human approval.

## A gate passes with gaps

The proven criteria passed while named non-critical gaps remain. Each gap needs an impact, owner, reason, affected criteria, and revisit trigger. A critical correctness, safety, or acceptance gap blocks instead.

If `workflow.allow_pass_with_gaps` is `false`, resolve the gaps and record `PASS`; changing evidence text alone cannot bypass the controller.

## High-risk review requires reviewer provenance

The current diff matches `project.high_risk_paths` and independent review is enabled. Rerun review with a real `--reviewer-identity` and `--review-method human_peer` or `separate_agent`. Boatstack retains these fields in the review receipt.

## An unrelated base-branch check fails

Reproduce the failure against the target branch. Keep its repair in a separate PR. Use a bypass only when repository policy permits it and a human explicitly authorizes it; do not hide unrelated edits in the approved feature.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Make public configuration controls trustworthy

Boatstack now enforces the three workflow switches that were previously serialized and documented without controlling delivery. Human approval can be replaced by an explicit fingerprinted policy-activation lock, `PASS_WITH_GAPS` is rejected unless allowed, and configured high-risk reviews retain typed reviewer provenance.

The public guide now presents only behavior-backed user controls. The exhaustive internal schema still documents compatibility and installer-owned fields without suggesting that integration status, versions, or project identity are hand-edited workflow policy.
Original file line number Diff line number Diff line change
Expand Up @@ -124,27 +124,28 @@ Treat repository-owned product context as canonical. Do not require it to be mig
```

2. Present the draft spec, plan, open decisions, accepted assumptions, gaps, risks, validation provenance, and `PLAN_FINGERPRINT` in a reviewable form.
3. Ask the developer to approve it or request changes. End the pending response with exactly this Markdown: Reply `a` to approve. Silence, continued conversation, tool permission, permission to build, `[a]`, and an `a` embedded in other text are not approval.
3. When `workflow.human_plan_approval` is true, ask the developer to approve it or request changes and end with: Reply `a` to approve. When false, state that Build will create a policy-activation lock and do not imply human approval.
4. On changes, return to `auto-plan`, preserve the feedback in the question ledger, and issue a new draft.
5. On explicit approval, invoke `boatstack-helper record-approval` with the plan, named human, RFC3339 timestamp, and exact fingerprint returned before approval. It verifies the current plan and creates only `approval.md`.
6. End in Plan mode and tell the developer the feature is approved and ready for the host's normal Build transition. Do not compile tasks, create a lock, request Agent mode merely to write a file, or edit product code.
5. When human approval is enabled, invoke `boatstack-helper record-approval` with the plan, named human, RFC3339 timestamp, and exact fingerprint. When disabled, create no `approval.md`.
6. End in Plan mode and tell the developer the feature is authorized for the host's normal Build transition. Do not compile tasks, create a lock, request Agent mode merely to write a file, or edit product code.

All files created or updated by `auto-plan` and `plan-gate` must be Markdown. gstack and Spec Kit may help produce those documents, but their implementation stages and non-Markdown executable state are deferred to `build`.

## Build without erasing evidence

- First confirm the host is in an execution-capable mode. If a requested transition is rejected or product-code writes remain unavailable, return `READY_FOR_BUILD` and stop without activating, compiling, or writing a lock.
- Before the first product-code edit, activate the exact approved Markdown plan:
- Before the first product-code edit, activate the exact authorized Markdown plan. Include `--approval` only when `workflow.human_plan_approval` is true:

```bash
.product-loop/bin/boatstack-helper activate-plan \
--plan .product-loop/features/<feature>/plan.md \
--approval .product-loop/features/<feature>/approval.md \
--out-dir .product-loop/features/<feature>/compiled \
--output .product-loop/features/<feature>/plan.lock.json
```

- Activation verifies the approval fingerprint, compiles `tasks.json`, `test-matrix.json`, and the evidence skeleton, writes the content-addressed lock last, and rechecks it. It adds no semantics. Missing approval, open blocking questions, or any change to the source plan, spec, or complete `plan.md` returns `BLOCKED`.
For human authorization, add `--approval .product-loop/features/<feature>/approval.md`.

- Activation verifies the plan fingerprint and any required approval, compiles `tasks.json`, `test-matrix.json`, and the evidence skeleton, then writes a schema-v2 lock with `authorization_mode: human` or `policy`. Missing required approval, open blocking questions, or any changed input returns `BLOCKED`.
- Activation also creates ignored delivery state bound to the plan lock. Read it with `delivery-status`; implement only the active slice's `task_ids`. A multi-slice plan advances only after the current slice publishes through `ship-gate`.
- Keep the source plan present and hash-current through completion of `build`.
- Choose any suitable model, tool, or implementation tactic inside the approved boundary. Boatstack controls transitions and claims, not local creativity.
Expand Down Expand Up @@ -180,14 +181,14 @@ A published delivery is immutable. Record the observation against it, then plan
- For relevant PR visual scenarios, use the repository runner first, then a host browser against the existing development server, one supplied launch instruction, or an explicitly approved machine-local runtime. Do not modify repository dependencies or configuration for capture. Review each exact PNG for secrets and private data, then import the temporary manifest with `record-pr-visual-evidence`; keep the images outside the repository.
- Treat model-authored tests and same-model self-review as evidence, not ground truth.
- Validate that tests load and exercise the intended interface. For high-risk code, add an independent oracle such as contract fixtures, mutation testing, differential checks, staging verification, or human acceptance.
- A failing check blocks the gate. A skipped check must include a reason and risk owner.
- A failing check blocks the gate. A skipped check must include a reason and risk owner. `PASS_WITH_GAPS` is accepted only when `workflow.allow_pass_with_gaps` is true.
- Commit the intentional active-slice product and evidence diff, then record the test result with `record-delivery-gate --feature <feature> --slice <slice> --gate test`. The receipt is bound to the base/head branches, commit, product diff, and evidence hash. Editing an evidence status is not a gate transition.

### Review gate

- Review the actual diff, not the intended plan alone.
- Check spec traceability, invariants, data/security/tenancy boundaries, failure behavior, backward compatibility, migrations, observability, tests, docs, and gaps.
- Use an independent reviewer for high-risk changes, repeated failures, or when the existing review evidence is circular.
- When configured high-risk paths changed, use a human peer or separate agent and record `--reviewer-identity` with `--review-method human_peer|separate_agent`.
- Convert actionable findings into tasks. Do not pass while critical findings are open.
- On pass, record `record-delivery-gate --feature <feature> --slice <slice> --gate review`. Review is rejected unless the same diff already has a test receipt; any later product change makes both receipts stale.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"schema_version": 1,
"status": "APPROVED",
"schema_version": 2,
"status": "LOCKED",
"authorization_mode": "human",
"activated_at": "<ISO-8601 timestamp>",
"approved_by": "<human identity>",
"approved_at": "<ISO-8601 timestamp>",
"source_commit": "<git commit>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ func activatePlanCommand(arguments []string) int {
if err := flags.Parse(arguments); err != nil {
return 2
}
if options.PlanPath == "" || options.ApprovalPath == "" || options.OutDir == "" || options.OutputPath == "" {
return fail(fmt.Errorf("activate-plan requires --plan, --approval, --out-dir, and --output"))
if options.PlanPath == "" || options.OutDir == "" || options.OutputPath == "" {
return fail(fmt.Errorf("activate-plan requires --plan, --out-dir, and --output; --approval is required when human_plan_approval is enabled"))
}
if err := boatstack.ActivatePlan(options); err != nil {
return fail(fmt.Errorf("plan activation failed: %w", err))
Expand Down Expand Up @@ -295,6 +295,8 @@ func recordDeliveryGateCommand(arguments []string) int {
flags.StringVar(&options.Status, "status", "", "PASS or PASS_WITH_GAPS")
flags.StringVar(&options.BaseBranch, "base", "", "delivery base branch; defaults from the active slice or project")
flags.StringVar(&options.EvidencePath, "evidence", "", "current evidence ledger")
flags.StringVar(&options.ReviewerIdentity, "reviewer-identity", "", "reviewer identity required for configured high-risk independent review")
flags.StringVar(&options.ReviewMethod, "review-method", "", "human_peer or separate_agent")
if err := flags.Parse(arguments); err != nil {
return 2
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import (
"testing"
)

const configFieldMarkerPrefix = "boatstack-config-field:"
const (
configFieldMarkerPrefix = "boatstack-config-field:"
userConfigFieldMarkerPrefix = "boatstack-user-config-field:"
)

func configSurface(value reflect.Type, prefix string) []string {
if value.Kind() == reflect.Pointer {
Expand Down Expand Up @@ -44,25 +47,25 @@ func configSurface(value reflect.Type, prefix string) []string {
return fields
}

func configFieldMarkers(content string) []string {
func configFieldMarkers(content, prefix 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))
if strings.HasPrefix(line, prefix) {
fields = append(fields, strings.TrimPrefix(line, prefix))
}
}
sort.Strings(fields)
return fields
}

func documentedConfigSurface(t *testing.T, path string) []string {
func documentedConfigSurface(t *testing.T, path, prefix 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))
return configFieldMarkers(string(content), prefix)
}

func publicConfigurationDocument(t *testing.T) string {
Expand Down Expand Up @@ -92,22 +95,48 @@ func publicConfigurationDocument(t *testing.T) string {
func TestConfigFieldMarkersAcceptWindowsLineEndings(t *testing.T) {
content := "<!--\r\nboatstack-config-field:project.name\r\nboatstack-config-field:workflow\r\n-->\r\n"
want := []string{"project.name", "workflow"}
if got := configFieldMarkers(content); !reflect.DeepEqual(got, want) {
if got := configFieldMarkers(content, configFieldMarkerPrefix); !reflect.DeepEqual(got, want) {
t.Fatalf("CRLF configuration markers were not parsed: got %v, want %v", got, want)
}
}

func TestPublicConfigurationSurfaceIsDocumented(t *testing.T) {
func TestSerializedConfigurationSurfaceIsDocumentedInternally(t *testing.T) {
want := configSurface(reflect.TypeOf(ProjectConfig{}), "")
sort.Strings(want)
document := "references/config-schema.md"
got := documentedConfigSurface(t, document, configFieldMarkerPrefix)
if !reflect.DeepEqual(got, want) {
t.Errorf("configuration documentation drift in %s\nimplementation: %v\ndocumented: %v", document, want, got)
}
}

for _, document := range []string{
"references/config-schema.md",
publicConfigurationDocument(t),
} {
got := documentedConfigSurface(t, document)
if !reflect.DeepEqual(got, want) {
t.Errorf("configuration documentation drift in %s\nimplementation: %v\ndocumented: %v", document, want, got)
func TestPublicConfigurationGuideContainsOnlySupportedUserControls(t *testing.T) {
want := []string{
"adapters",
"project.commands",
"project.context",
"project.default_branch",
"project.high_risk_paths",
"workflow.allow_pass_with_gaps",
"workflow.boundary_analysis",
"workflow.human_plan_approval",
"workflow.independent_review_for_high_risk",
"workflow.maintain_changelog",
"workflow.pr_visual_evidence",
"workspace.cleanup",
"workspace.cleanup_after",
"workspace.enabled",
"workspace.mode",
}
document := publicConfigurationDocument(t)
got := documentedConfigSurface(t, document, userConfigFieldMarkerPrefix)
if !reflect.DeepEqual(got, want) {
t.Errorf("public user-control documentation drift in %s\nsupported: %v\ndocumented: %v", document, want, got)
}
serialized := configSurface(reflect.TypeOf(ProjectConfig{}), "")
for _, field := range got {
if !contains(serialized, field) {
t.Errorf("public guide exposes unknown configuration field %s", field)
}
}
}
Loading