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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/ebc2162014928c17debb1ce5186f1d6e20608f36/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/c03b391323146e8d05b6049e8ba6506c8fdfdf97/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.

Expand Down
17 changes: 10 additions & 7 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "64703c794db57cec757149c25037a0f7ad28a922be86436261a5094a9bb26f30",
"CONTRIBUTING.md": "fdfd3c320151827975d09023ad8746cdeb8efdad8fe791679a97b33e75fc26e8",
"README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
Expand Down Expand Up @@ -129,7 +129,7 @@
"boatstack/mutation_test.go": "68d5049c7f96c1ac558e4c781151f67e8deee2f8d6b9bf293b90d44e769ef7c6",
"boatstack/mutation_undo.go": "697d11b600a276ddbcabe6a9f8040d4f7283e017a0e8fd689ef53a274638946c",
"boatstack/mutation_undo_test.go": "39540e717e3f2136bf975594043a3db9072b28ebe61c6cb0b982cea5e8b1e14e",
"boatstack/next.go": "c133dbf907dc86ca5aacec154f6e4a63e7aa9f5f0ebdd76e1803375b5700675a",
"boatstack/next.go": "7bd3d143f74452399b875da61a5353e91cbf6218bea5516a39fc13126a5fa042",
"boatstack/next_actor_conformance_test.go": "23c055bcc99d889344c3f86c7940eb9ef2ef6f4ddab34e91cf215c9d078f5d42",
"boatstack/next_banner_test.go": "c431a6987ed1e479442fc9f5db4371632880b92aa790fa9dd0f5285293352c41",
"boatstack/next_response.go": "11decf2e3b236cbaa183980946ec17ffbbbb1af9c08bd11a466a8487bf229d5f",
Expand All @@ -146,13 +146,15 @@
"boatstack/planning_first_write_conformance_test.go": "873097aa9384b75bf01e74a475f3ec2ac7cca4a28f733e82f2c82959032c6a30",
"boatstack/planning_test.go": "06ec7022222d926040c3ae28b84ab50c3d2f804ae6473e61b303804dd992d884",
"boatstack/pr.go": "b6df3e000dd6d34ecb6575385e44b2f6ee84a8f35ad5696e299177a7cddd7629",
"boatstack/pr_phase.go": "59f8cbb75b6b538a5345474acd6a725450979579bf8ecf9591956cbbe1cc4737",
"boatstack/pr_phase_conformance_test.go": "bc9c834e9c4ed43b35d81abafd7b1bf2a264ea2a8c4a4ec9758ee18d1d438968",
"boatstack/pr_test.go": "2e7709e2f163489a29ea3e7eb4bc932cfe6829b30d168f1aea9e942acbc3b4e7",
"boatstack/provenance.go": "d44dcd5421306269326f1202ba1d52df8c252490550270ef9d022e8ec2b65210",
"boatstack/provision.go": "eb7333a73331b011adc93f59a2d97415d850c2e588f0e2bbb5116984e2ef927d",
"boatstack/provision_test.go": "214e9edb991a66d5bbb696a7c1b63876d2f799f2cab4e3f40785f4e8f1eac57b",
"boatstack/publication_ignored_repro_test.go": "b6f3aeb8ba22949ff9af7ac5afe8fb828385d9708d5d5893ef41f33a3de873e1",
"boatstack/published_slice_routing_test.go": "ea7e7351018bc13dcd31c4b96f50f8bc230e8a1dbf7806fba32a12ae58923e7e",
"boatstack/recovery.go": "8e35cf7f0d73ec9708e00a5a9bfd5f30ec832537cb0bffc254581bb6b8ae33ea",
"boatstack/recovery.go": "8c963dbaa30adcac929c52944171f76843f240194e53a2e2d4c1ff65463a93e4",
"boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f",
"boatstack/reexec.go": "fed55416479d7bd3e0c3637057ffe8eb58a032f93fc358f76df906ab7acc677b",
"boatstack/reexec_unix.go": "ff86157a9aa20c82a56fcd859b70669b7eacf4e0a9f61a4546ef33808437939e",
Expand Down Expand Up @@ -204,10 +206,10 @@
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "060775c73431f28bd16066bdf9e0f89034d2855c7ca0f5544f660d24b91211d0",
"docs/evidence-engineered-coding.md": "c83785a7651082402839257369b488aa906abb6395b9b99130cea42df19d7a33",
"docs/evidence-engineered-coding.md": "359548672c7d786d81838f2bee6c74c0861e050a30b7e2a089775b1011aaf1c4",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8",
"docs/public-claims.json": "b5dd5544c932a4dbd549047fc90cecec8e9c14842daae12feb2500a0a566896f",
"docs/public-claims.json": "df83f3ca329170435ede3c3d75d04f99b5266296a15bb44cc564abdf57ebb3a1",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -221,7 +223,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": "e8b820e1c2f9bfc28e661842324f7c4455f57be29290d0909634a00b71e5919f",
"labs/diagram-json/plan.lock.json": "fbc797694160cab69ae7287cf99d15d4391a5e2a0838664eb45f24832ce7f1c8",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -353,12 +355,13 @@
"release-notes/2026-07-28-minimum-app-permissions.md": "9ef97e32e5591966ef34ba23f4e0a7aa14d061a4ac5f72f5f4dcecc9bfb62a89",
"release-notes/2026-07-28-native-auto-merge-conformance.md": "67d0fab76fd4911b5836d19d06b319537cb1807650d35dbd534627a2c3622757",
"release-notes/2026-07-28-operator-frontier-next-actor.md": "7e769625a2beb8a204d2d79158c18bdac350656de5c55998988a8a5aba2c319a",
"release-notes/2026-07-28-pr-phase-observation.md": "8c5615013eb88ce9561d30897e47f6fa967e0157c4c245f0c35a1f31e4e132e2",
"release-notes/2026-07-28-protected-native-auto-merge.md": "67dc76a6e7ce51034a0eadc541ba7a8946cfcabe5433cedc25db55321dfb8b62"
},
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "ebc2162014928c17debb1ce5186f1d6e20608f36",
"commit": "c03b391323146e8d05b6049e8ba6506c8fdfdf97",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
33 changes: 32 additions & 1 deletion boatstack/next.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ type NextStatus struct {
Reason string `json:"reason"`
BlockingAmbiguity []string `json:"blocking_ambiguity,omitempty"`
Lifecycle string `json:"lifecycle,omitempty"`
PRPhase string `json:"pr_phase,omitempty"`
PRReviewDecision string `json:"pr_review_decision,omitempty"`
PRMergeState string `json:"pr_merge_state,omitempty"`
PRFailingChecks []string `json:"pr_failing_checks,omitempty"`
PRURL string `json:"pr_url,omitempty"`
HeadBranch string `json:"head_branch,omitempty"`
ParentDelivery string `json:"parent_delivery,omitempty"`
Expand Down Expand Up @@ -147,13 +151,31 @@ func nextForPublished(repo string, state DeliveryState) NextStatus {
TotalSlices: len(state.Slices), ObservedStage: "PUBLISHED", NextOperation: "none",
Lifecycle: pr.Lifecycle, PRURL: pr.URL, HeadBranch: pr.Branch,
ParentDelivery: state.ParentDelivery,
PRPhase: string(pr.Phase), PRReviewDecision: pr.ReviewDecision,
PRMergeState: pr.MergeState, PRFailingChecks: pr.FailingChecks,
}
switch pr.Lifecycle {
case "PUBLISHED_MERGED":
status.ObservedStage = "FEATURE_COMPLETE"
status.Reason = fmt.Sprintf("The published PR for feature %q is merged.", state.Feature)
case "PUBLISHED_OPEN":
status.Reason = fmt.Sprintf("Feature %q is published in an open PR; review and required checks may still produce a corrective delivery.", state.Feature)
// The observed PR phase sharpens the reason when it is known; the
// pre-phase sentence remains the fallback so a degraded observation
// reads exactly as it always did.
switch pr.Phase {
case PRPhaseChecksPending:
status.Reason = fmt.Sprintf("Feature %q is published; checks on its PR are still running.", state.Feature)
case PRPhaseChecksFailing:
status.Reason = fmt.Sprintf("Feature %q is published; %d PR check(s) are failing (%s).", state.Feature, pr.ChecksFailed, strings.Join(pr.FailingChecks, ", "))
case PRPhaseChangesRequested:
status.Reason = fmt.Sprintf("Feature %q is published; its PR review requested changes.", state.Feature)
case PRPhaseReviewRequired:
status.Reason = fmt.Sprintf("Feature %q is published; its PR checks pass and a required review approval is still owed.", state.Feature)
case PRPhaseMergeEligible:
status.Reason = fmt.Sprintf("Feature %q is published; its PR has passing checks, satisfied reviews, and a clean merge state.", state.Feature)
default:
status.Reason = fmt.Sprintf("Feature %q is published in an open PR; review and required checks may still produce a corrective delivery.", state.Feature)
}
case "PUBLISHED_CLOSED":
status.Reason = fmt.Sprintf("The PR for feature %q is closed without a verified merge; a future correction requires a fresh PR.", state.Feature)
default:
Expand Down Expand Up @@ -411,6 +433,15 @@ func FormatNextStatus(status NextStatus) string {
if status.Lifecycle != "" {
parts = append(parts, "Lifecycle: "+status.Lifecycle)
}
// An Unknown phase adds nothing the lifecycle line does not already say,
// so only a positively derived phase earns a line.
if status.PRPhase != "" && status.PRPhase != string(PRPhaseUnknown) {
phase := "PR phase: " + status.PRPhase
if len(status.PRFailingChecks) > 0 {
phase += " (" + strings.Join(status.PRFailingChecks, ", ") + ")"
}
parts = append(parts, phase)
}
if status.PRURL != "" {
parts = append(parts, "PR: "+status.PRURL)
}
Expand Down
171 changes: 171 additions & 0 deletions boatstack/pr_phase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package boatstack

import "strings"

// PRPhase is the observed position of a published pull request between
// publication and merge. It is derived ONLY from a live GitHub observation
// (checks, review decision, merge state) at the moment of a read-only
// resolution; it is never persisted, recorded by an agent, or accepted from
// text. Anything the derivation cannot classify with certainty degrades to
// PRPhaseUnknown, which downstream classification treats as the operator's.
// control-law: pr-phase-derives-only-from-live-observation
type PRPhase string

const (
// PRPhaseUnknown: the observation is missing, partial, or names a
// combination this derivation does not understand. Fail-closed default.
PRPhaseUnknown PRPhase = "PR_UNKNOWN"
// PRPhaseChecksPending: the PR is open and at least one check has not finished.
PRPhaseChecksPending PRPhase = "PR_CHECKS_PENDING"
// PRPhaseChecksFailing: the PR is open and at least one check concluded badly.
PRPhaseChecksFailing PRPhase = "PR_CHECKS_FAILING"
// PRPhaseChangesRequested: a reviewer requested changes. This outranks check
// status: a human review verdict is a stronger signal than CI and hands the
// step to the operator regardless of what the checks are doing.
PRPhaseChangesRequested PRPhase = "PR_CHANGES_REQUESTED"
// PRPhaseReviewRequired: checks are green but a required review approval is
// still owed. Granting approval is never Boatstack's or the agent's to do.
PRPhaseReviewRequired PRPhase = "PR_REVIEW_REQUIRED"
// PRPhaseMergeEligible: checks green, review satisfied, and GitHub reports
// the branch cleanly mergeable.
PRPhaseMergeEligible PRPhase = "PR_MERGE_ELIGIBLE"
// PRPhaseMerged / PRPhaseClosed: terminal, mirrors the PR lifecycle.
PRPhaseMerged PRPhase = "PR_MERGED"
PRPhaseClosed PRPhase = "PR_CLOSED"
)

// prStatusCheck is one element of gh's statusCheckRollup array. GitHub emits
// two shapes — CheckRun (Actions/checks API: status+conclusion+name) and
// StatusContext (legacy commit status: state+context) — and this struct holds
// the union so one decode covers both.
type prStatusCheck struct {
TypeName string `json:"__typename"`
Name string `json:"name"`
Status string `json:"status"`
Conclusion string `json:"conclusion"`
Context string `json:"context"`
State string `json:"state"`
}

// prCheckSummary aggregates a statusCheckRollup. Unrecognized is sticky: one
// entry the tables below cannot classify poisons the whole summary, because a
// phase derived from a partially understood rollup would be a guess.
type prCheckSummary struct {
Total int
Passed int
Failed int
Pending int
Failing []string
Unrecognized bool
}

// prFailingChecksCap bounds the failing-check name list carried into status
// output so one enormous check matrix cannot flood a rendered response.
const prFailingChecksCap = 8

func summarizeCheckRollup(entries []prStatusCheck) prCheckSummary {
summary := prCheckSummary{Total: len(entries)}
for _, entry := range entries {
name := strings.TrimSpace(entry.Name)
if name == "" {
name = strings.TrimSpace(entry.Context)
}
switch classifyStatusCheck(entry) {
case "passed":
summary.Passed++
case "pending":
summary.Pending++
case "failed":
summary.Failed++
if name != "" && len(summary.Failing) < prFailingChecksCap {
summary.Failing = append(summary.Failing, name)
}
default:
summary.Unrecognized = true
}
}
return summary
}

// classifyStatusCheck maps one rollup entry to passed/pending/failed, or ""
// when the entry's vocabulary is not in the tables. The typename is trusted
// first; when absent, the populated field set identifies the shape.
func classifyStatusCheck(entry prStatusCheck) string {
shape := strings.TrimSpace(entry.TypeName)
if shape == "" {
switch {
case entry.State != "" || entry.Context != "":
shape = "StatusContext"
case entry.Status != "" || entry.Conclusion != "":
shape = "CheckRun"
}
}
switch shape {
case "CheckRun":
if !strings.EqualFold(strings.TrimSpace(entry.Status), "COMPLETED") {
return "pending"
}
switch strings.ToUpper(strings.TrimSpace(entry.Conclusion)) {
case "SUCCESS", "NEUTRAL", "SKIPPED":
return "passed"
case "FAILURE", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED", "STARTUP_FAILURE", "STALE":
return "failed"
}
case "StatusContext":
switch strings.ToUpper(strings.TrimSpace(entry.State)) {
case "SUCCESS":
return "passed"
case "PENDING", "EXPECTED":
return "pending"
case "FAILURE", "ERROR":
return "failed"
}
}
return ""
}

// derivePRPhase turns one live observation into a PRPhase. The derivation is
// pure and total: every input lands somewhere, and everything outside the
// explicitly understood combinations lands on PRPhaseUnknown. Notably absent
// on purpose: DIRTY/BEHIND/BLOCKED/DRAFT merge states (conflicts, stale base,
// branch protection this derivation cannot see, drafts) all stay Unknown so
// they reach the operator instead of being guessed at.
func derivePRPhase(prState string, checks prCheckSummary, reviewDecision, mergeState string) PRPhase {
switch strings.ToUpper(strings.TrimSpace(prState)) {
case "MERGED":
return PRPhaseMerged
case "CLOSED":
return PRPhaseClosed
case "OPEN":
default:
return PRPhaseUnknown
}
if checks.Unrecognized {
return PRPhaseUnknown
}
decision := strings.ToUpper(strings.TrimSpace(reviewDecision))
if decision == "CHANGES_REQUESTED" {
return PRPhaseChangesRequested
}
if checks.Failed > 0 {
return PRPhaseChecksFailing
}
if checks.Pending > 0 {
return PRPhaseChecksPending
}
switch decision {
case "REVIEW_REQUIRED":
return PRPhaseReviewRequired
case "", "APPROVED":
default:
return PRPhaseUnknown
}
// An empty rollup means no checks are configured; green-by-absence is
// acceptable only because merge eligibility still requires GitHub itself
// to report the branch cleanly mergeable below.
switch strings.ToUpper(strings.TrimSpace(mergeState)) {
case "CLEAN", "HAS_HOOKS":
return PRPhaseMergeEligible
}
return PRPhaseUnknown
}
Loading
Loading