Skip to content

Commit b93127f

Browse files
Sync Boatstack from Intelligence Flow Labs @ c03b39132314 (#145)
Co-authored-by: operator-stack-publisher[bot] <operator-stack-publisher[bot]@users.noreply.github.com>
1 parent b63d6c7 commit b93127f

10 files changed

Lines changed: 519 additions & 30 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Contributing
44

5-
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).
5+
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).
66

77
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.
88

UPSTREAM.json

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"files": {
1414
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
15-
"CONTRIBUTING.md": "64703c794db57cec757149c25037a0f7ad28a922be86436261a5094a9bb26f30",
15+
"CONTRIBUTING.md": "fdfd3c320151827975d09023ad8746cdeb8efdad8fe791679a97b33e75fc26e8",
1616
"README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad",
1717
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
1818
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
@@ -129,7 +129,7 @@
129129
"boatstack/mutation_test.go": "68d5049c7f96c1ac558e4c781151f67e8deee2f8d6b9bf293b90d44e769ef7c6",
130130
"boatstack/mutation_undo.go": "697d11b600a276ddbcabe6a9f8040d4f7283e017a0e8fd689ef53a274638946c",
131131
"boatstack/mutation_undo_test.go": "39540e717e3f2136bf975594043a3db9072b28ebe61c6cb0b982cea5e8b1e14e",
132-
"boatstack/next.go": "c133dbf907dc86ca5aacec154f6e4a63e7aa9f5f0ebdd76e1803375b5700675a",
132+
"boatstack/next.go": "7bd3d143f74452399b875da61a5353e91cbf6218bea5516a39fc13126a5fa042",
133133
"boatstack/next_actor_conformance_test.go": "23c055bcc99d889344c3f86c7940eb9ef2ef6f4ddab34e91cf215c9d078f5d42",
134134
"boatstack/next_banner_test.go": "c431a6987ed1e479442fc9f5db4371632880b92aa790fa9dd0f5285293352c41",
135135
"boatstack/next_response.go": "11decf2e3b236cbaa183980946ec17ffbbbb1af9c08bd11a466a8487bf229d5f",
@@ -146,13 +146,15 @@
146146
"boatstack/planning_first_write_conformance_test.go": "873097aa9384b75bf01e74a475f3ec2ac7cca4a28f733e82f2c82959032c6a30",
147147
"boatstack/planning_test.go": "06ec7022222d926040c3ae28b84ab50c3d2f804ae6473e61b303804dd992d884",
148148
"boatstack/pr.go": "b6df3e000dd6d34ecb6575385e44b2f6ee84a8f35ad5696e299177a7cddd7629",
149+
"boatstack/pr_phase.go": "59f8cbb75b6b538a5345474acd6a725450979579bf8ecf9591956cbbe1cc4737",
150+
"boatstack/pr_phase_conformance_test.go": "bc9c834e9c4ed43b35d81abafd7b1bf2a264ea2a8c4a4ec9758ee18d1d438968",
149151
"boatstack/pr_test.go": "2e7709e2f163489a29ea3e7eb4bc932cfe6829b30d168f1aea9e942acbc3b4e7",
150152
"boatstack/provenance.go": "d44dcd5421306269326f1202ba1d52df8c252490550270ef9d022e8ec2b65210",
151153
"boatstack/provision.go": "eb7333a73331b011adc93f59a2d97415d850c2e588f0e2bbb5116984e2ef927d",
152154
"boatstack/provision_test.go": "214e9edb991a66d5bbb696a7c1b63876d2f799f2cab4e3f40785f4e8f1eac57b",
153155
"boatstack/publication_ignored_repro_test.go": "b6f3aeb8ba22949ff9af7ac5afe8fb828385d9708d5d5893ef41f33a3de873e1",
154156
"boatstack/published_slice_routing_test.go": "ea7e7351018bc13dcd31c4b96f50f8bc230e8a1dbf7806fba32a12ae58923e7e",
155-
"boatstack/recovery.go": "8e35cf7f0d73ec9708e00a5a9bfd5f30ec832537cb0bffc254581bb6b8ae33ea",
157+
"boatstack/recovery.go": "8c963dbaa30adcac929c52944171f76843f240194e53a2e2d4c1ff65463a93e4",
156158
"boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f",
157159
"boatstack/reexec.go": "fed55416479d7bd3e0c3637057ffe8eb58a032f93fc358f76df906ab7acc677b",
158160
"boatstack/reexec_unix.go": "ff86157a9aa20c82a56fcd859b70669b7eacf4e0a9f61a4546ef33808437939e",
@@ -204,10 +206,10 @@
204206
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
205207
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
206208
"docs/configuration.md": "060775c73431f28bd16066bdf9e0f89034d2855c7ca0f5544f660d24b91211d0",
207-
"docs/evidence-engineered-coding.md": "c83785a7651082402839257369b488aa906abb6395b9b99130cea42df19d7a33",
209+
"docs/evidence-engineered-coding.md": "359548672c7d786d81838f2bee6c74c0861e050a30b7e2a089775b1011aaf1c4",
208210
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
209211
"docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8",
210-
"docs/public-claims.json": "b5dd5544c932a4dbd549047fc90cecec8e9c14842daae12feb2500a0a566896f",
212+
"docs/public-claims.json": "df83f3ca329170435ede3c3d75d04f99b5266296a15bb44cc564abdf57ebb3a1",
211213
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
212214
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
213215
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
@@ -221,7 +223,7 @@
221223
"labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d",
222224
"labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71",
223225
"labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39",
224-
"labs/diagram-json/plan.lock.json": "e8b820e1c2f9bfc28e661842324f7c4455f57be29290d0909634a00b71e5919f",
226+
"labs/diagram-json/plan.lock.json": "fbc797694160cab69ae7287cf99d15d4391a5e2a0838664eb45f24832ce7f1c8",
225227
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
226228
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
227229
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
@@ -353,12 +355,13 @@
353355
"release-notes/2026-07-28-minimum-app-permissions.md": "9ef97e32e5591966ef34ba23f4e0a7aa14d061a4ac5f72f5f4dcecc9bfb62a89",
354356
"release-notes/2026-07-28-native-auto-merge-conformance.md": "67d0fab76fd4911b5836d19d06b319537cb1807650d35dbd534627a2c3622757",
355357
"release-notes/2026-07-28-operator-frontier-next-actor.md": "7e769625a2beb8a204d2d79158c18bdac350656de5c55998988a8a5aba2c319a",
358+
"release-notes/2026-07-28-pr-phase-observation.md": "8c5615013eb88ce9561d30897e47f6fa967e0157c4c245f0c35a1f31e4e132e2",
356359
"release-notes/2026-07-28-protected-native-auto-merge.md": "67dc76a6e7ce51034a0eadc541ba7a8946cfcabe5433cedc25db55321dfb8b62"
357360
},
358361
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
359362
"schema_version": 1,
360363
"source": {
361-
"commit": "ebc2162014928c17debb1ce5186f1d6e20608f36",
364+
"commit": "c03b391323146e8d05b6049e8ba6506c8fdfdf97",
362365
"path": "labs/12-product-engineering-loop",
363366
"repository": "operatorstack/intelligence-flow"
364367
}

boatstack/next.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ type NextStatus struct {
2727
Reason string `json:"reason"`
2828
BlockingAmbiguity []string `json:"blocking_ambiguity,omitempty"`
2929
Lifecycle string `json:"lifecycle,omitempty"`
30+
PRPhase string `json:"pr_phase,omitempty"`
31+
PRReviewDecision string `json:"pr_review_decision,omitempty"`
32+
PRMergeState string `json:"pr_merge_state,omitempty"`
33+
PRFailingChecks []string `json:"pr_failing_checks,omitempty"`
3034
PRURL string `json:"pr_url,omitempty"`
3135
HeadBranch string `json:"head_branch,omitempty"`
3236
ParentDelivery string `json:"parent_delivery,omitempty"`
@@ -147,13 +151,31 @@ func nextForPublished(repo string, state DeliveryState) NextStatus {
147151
TotalSlices: len(state.Slices), ObservedStage: "PUBLISHED", NextOperation: "none",
148152
Lifecycle: pr.Lifecycle, PRURL: pr.URL, HeadBranch: pr.Branch,
149153
ParentDelivery: state.ParentDelivery,
154+
PRPhase: string(pr.Phase), PRReviewDecision: pr.ReviewDecision,
155+
PRMergeState: pr.MergeState, PRFailingChecks: pr.FailingChecks,
150156
}
151157
switch pr.Lifecycle {
152158
case "PUBLISHED_MERGED":
153159
status.ObservedStage = "FEATURE_COMPLETE"
154160
status.Reason = fmt.Sprintf("The published PR for feature %q is merged.", state.Feature)
155161
case "PUBLISHED_OPEN":
156-
status.Reason = fmt.Sprintf("Feature %q is published in an open PR; review and required checks may still produce a corrective delivery.", state.Feature)
162+
// The observed PR phase sharpens the reason when it is known; the
163+
// pre-phase sentence remains the fallback so a degraded observation
164+
// reads exactly as it always did.
165+
switch pr.Phase {
166+
case PRPhaseChecksPending:
167+
status.Reason = fmt.Sprintf("Feature %q is published; checks on its PR are still running.", state.Feature)
168+
case PRPhaseChecksFailing:
169+
status.Reason = fmt.Sprintf("Feature %q is published; %d PR check(s) are failing (%s).", state.Feature, pr.ChecksFailed, strings.Join(pr.FailingChecks, ", "))
170+
case PRPhaseChangesRequested:
171+
status.Reason = fmt.Sprintf("Feature %q is published; its PR review requested changes.", state.Feature)
172+
case PRPhaseReviewRequired:
173+
status.Reason = fmt.Sprintf("Feature %q is published; its PR checks pass and a required review approval is still owed.", state.Feature)
174+
case PRPhaseMergeEligible:
175+
status.Reason = fmt.Sprintf("Feature %q is published; its PR has passing checks, satisfied reviews, and a clean merge state.", state.Feature)
176+
default:
177+
status.Reason = fmt.Sprintf("Feature %q is published in an open PR; review and required checks may still produce a corrective delivery.", state.Feature)
178+
}
157179
case "PUBLISHED_CLOSED":
158180
status.Reason = fmt.Sprintf("The PR for feature %q is closed without a verified merge; a future correction requires a fresh PR.", state.Feature)
159181
default:
@@ -411,6 +433,15 @@ func FormatNextStatus(status NextStatus) string {
411433
if status.Lifecycle != "" {
412434
parts = append(parts, "Lifecycle: "+status.Lifecycle)
413435
}
436+
// An Unknown phase adds nothing the lifecycle line does not already say,
437+
// so only a positively derived phase earns a line.
438+
if status.PRPhase != "" && status.PRPhase != string(PRPhaseUnknown) {
439+
phase := "PR phase: " + status.PRPhase
440+
if len(status.PRFailingChecks) > 0 {
441+
phase += " (" + strings.Join(status.PRFailingChecks, ", ") + ")"
442+
}
443+
parts = append(parts, phase)
444+
}
414445
if status.PRURL != "" {
415446
parts = append(parts, "PR: "+status.PRURL)
416447
}

boatstack/pr_phase.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package boatstack
2+
3+
import "strings"
4+
5+
// PRPhase is the observed position of a published pull request between
6+
// publication and merge. It is derived ONLY from a live GitHub observation
7+
// (checks, review decision, merge state) at the moment of a read-only
8+
// resolution; it is never persisted, recorded by an agent, or accepted from
9+
// text. Anything the derivation cannot classify with certainty degrades to
10+
// PRPhaseUnknown, which downstream classification treats as the operator's.
11+
// control-law: pr-phase-derives-only-from-live-observation
12+
type PRPhase string
13+
14+
const (
15+
// PRPhaseUnknown: the observation is missing, partial, or names a
16+
// combination this derivation does not understand. Fail-closed default.
17+
PRPhaseUnknown PRPhase = "PR_UNKNOWN"
18+
// PRPhaseChecksPending: the PR is open and at least one check has not finished.
19+
PRPhaseChecksPending PRPhase = "PR_CHECKS_PENDING"
20+
// PRPhaseChecksFailing: the PR is open and at least one check concluded badly.
21+
PRPhaseChecksFailing PRPhase = "PR_CHECKS_FAILING"
22+
// PRPhaseChangesRequested: a reviewer requested changes. This outranks check
23+
// status: a human review verdict is a stronger signal than CI and hands the
24+
// step to the operator regardless of what the checks are doing.
25+
PRPhaseChangesRequested PRPhase = "PR_CHANGES_REQUESTED"
26+
// PRPhaseReviewRequired: checks are green but a required review approval is
27+
// still owed. Granting approval is never Boatstack's or the agent's to do.
28+
PRPhaseReviewRequired PRPhase = "PR_REVIEW_REQUIRED"
29+
// PRPhaseMergeEligible: checks green, review satisfied, and GitHub reports
30+
// the branch cleanly mergeable.
31+
PRPhaseMergeEligible PRPhase = "PR_MERGE_ELIGIBLE"
32+
// PRPhaseMerged / PRPhaseClosed: terminal, mirrors the PR lifecycle.
33+
PRPhaseMerged PRPhase = "PR_MERGED"
34+
PRPhaseClosed PRPhase = "PR_CLOSED"
35+
)
36+
37+
// prStatusCheck is one element of gh's statusCheckRollup array. GitHub emits
38+
// two shapes — CheckRun (Actions/checks API: status+conclusion+name) and
39+
// StatusContext (legacy commit status: state+context) — and this struct holds
40+
// the union so one decode covers both.
41+
type prStatusCheck struct {
42+
TypeName string `json:"__typename"`
43+
Name string `json:"name"`
44+
Status string `json:"status"`
45+
Conclusion string `json:"conclusion"`
46+
Context string `json:"context"`
47+
State string `json:"state"`
48+
}
49+
50+
// prCheckSummary aggregates a statusCheckRollup. Unrecognized is sticky: one
51+
// entry the tables below cannot classify poisons the whole summary, because a
52+
// phase derived from a partially understood rollup would be a guess.
53+
type prCheckSummary struct {
54+
Total int
55+
Passed int
56+
Failed int
57+
Pending int
58+
Failing []string
59+
Unrecognized bool
60+
}
61+
62+
// prFailingChecksCap bounds the failing-check name list carried into status
63+
// output so one enormous check matrix cannot flood a rendered response.
64+
const prFailingChecksCap = 8
65+
66+
func summarizeCheckRollup(entries []prStatusCheck) prCheckSummary {
67+
summary := prCheckSummary{Total: len(entries)}
68+
for _, entry := range entries {
69+
name := strings.TrimSpace(entry.Name)
70+
if name == "" {
71+
name = strings.TrimSpace(entry.Context)
72+
}
73+
switch classifyStatusCheck(entry) {
74+
case "passed":
75+
summary.Passed++
76+
case "pending":
77+
summary.Pending++
78+
case "failed":
79+
summary.Failed++
80+
if name != "" && len(summary.Failing) < prFailingChecksCap {
81+
summary.Failing = append(summary.Failing, name)
82+
}
83+
default:
84+
summary.Unrecognized = true
85+
}
86+
}
87+
return summary
88+
}
89+
90+
// classifyStatusCheck maps one rollup entry to passed/pending/failed, or ""
91+
// when the entry's vocabulary is not in the tables. The typename is trusted
92+
// first; when absent, the populated field set identifies the shape.
93+
func classifyStatusCheck(entry prStatusCheck) string {
94+
shape := strings.TrimSpace(entry.TypeName)
95+
if shape == "" {
96+
switch {
97+
case entry.State != "" || entry.Context != "":
98+
shape = "StatusContext"
99+
case entry.Status != "" || entry.Conclusion != "":
100+
shape = "CheckRun"
101+
}
102+
}
103+
switch shape {
104+
case "CheckRun":
105+
if !strings.EqualFold(strings.TrimSpace(entry.Status), "COMPLETED") {
106+
return "pending"
107+
}
108+
switch strings.ToUpper(strings.TrimSpace(entry.Conclusion)) {
109+
case "SUCCESS", "NEUTRAL", "SKIPPED":
110+
return "passed"
111+
case "FAILURE", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED", "STARTUP_FAILURE", "STALE":
112+
return "failed"
113+
}
114+
case "StatusContext":
115+
switch strings.ToUpper(strings.TrimSpace(entry.State)) {
116+
case "SUCCESS":
117+
return "passed"
118+
case "PENDING", "EXPECTED":
119+
return "pending"
120+
case "FAILURE", "ERROR":
121+
return "failed"
122+
}
123+
}
124+
return ""
125+
}
126+
127+
// derivePRPhase turns one live observation into a PRPhase. The derivation is
128+
// pure and total: every input lands somewhere, and everything outside the
129+
// explicitly understood combinations lands on PRPhaseUnknown. Notably absent
130+
// on purpose: DIRTY/BEHIND/BLOCKED/DRAFT merge states (conflicts, stale base,
131+
// branch protection this derivation cannot see, drafts) all stay Unknown so
132+
// they reach the operator instead of being guessed at.
133+
func derivePRPhase(prState string, checks prCheckSummary, reviewDecision, mergeState string) PRPhase {
134+
switch strings.ToUpper(strings.TrimSpace(prState)) {
135+
case "MERGED":
136+
return PRPhaseMerged
137+
case "CLOSED":
138+
return PRPhaseClosed
139+
case "OPEN":
140+
default:
141+
return PRPhaseUnknown
142+
}
143+
if checks.Unrecognized {
144+
return PRPhaseUnknown
145+
}
146+
decision := strings.ToUpper(strings.TrimSpace(reviewDecision))
147+
if decision == "CHANGES_REQUESTED" {
148+
return PRPhaseChangesRequested
149+
}
150+
if checks.Failed > 0 {
151+
return PRPhaseChecksFailing
152+
}
153+
if checks.Pending > 0 {
154+
return PRPhaseChecksPending
155+
}
156+
switch decision {
157+
case "REVIEW_REQUIRED":
158+
return PRPhaseReviewRequired
159+
case "", "APPROVED":
160+
default:
161+
return PRPhaseUnknown
162+
}
163+
// An empty rollup means no checks are configured; green-by-absence is
164+
// acceptable only because merge eligibility still requires GitHub itself
165+
// to report the branch cleanly mergeable below.
166+
switch strings.ToUpper(strings.TrimSpace(mergeState)) {
167+
case "CLEAN", "HAS_HOOKS":
168+
return PRPhaseMergeEligible
169+
}
170+
return PRPhaseUnknown
171+
}

0 commit comments

Comments
 (0)