Skip to content

Commit 2069cdb

Browse files
Sync Boatstack from Intelligence Flow Labs @ c288814c8913
1 parent 0899136 commit 2069cdb

9 files changed

Lines changed: 297 additions & 24 deletions

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/dc7d340e0028db5df232d75b4c69ba866aa133f9/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/c288814c89139fa36e7b914ce14d9917f12d32d0/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 & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
},
1313
"files": {
1414
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
15-
"CONTRIBUTING.md": "e73050efeefb479d4e2694977de316dd0d6068bcf71ab61e69aaa1514c376cb3",
15+
"CONTRIBUTING.md": "47f98fbd40e12646c9314b53ce5a1b297cfd154bae919b5751bf93db28557bd0",
1616
"README.md": "125b47671a68556df382f19756fb61fa18925606cbbaf54d6bc9df8872b36870",
1717
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
1818
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
1919
"assets/boatstack-portability.svg": "66dfdfa85db857b3bd18b32047a6975f1fbbfc4dc091158e8277193f9969a346",
20+
"boatstack/BUG-worktree-delivery-state.md": "02469cf51c3849dad5743783e248e5c04583e4240507fbef0e3f890cd6a95724",
2021
"boatstack/SKILL.md": "7c7b3568d836cb176c92762a8315fa834cd61a531a629c97a5cd98d6f7689be0",
2122
"boatstack/agents/gemini.yaml": "cbf43b387399e456fa6178f86d83e6e35567e6142ff800f8de6ffca306fa963e",
2223
"boatstack/agents/openai.yaml": "68a30a60859556c5a26e16d184594ca243a6043d99c8cf7d66b5dd6d50a93cd1",
@@ -58,8 +59,8 @@
5859
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
5960
"boatstack/migrate.go": "eaf589e2b266238068e42c6d78e01dc040266d28e342cb24f09e33e8541749b3",
6061
"boatstack/migrate_test.go": "9f4bda2fb158c5e54bcc0242dace1da3c1965f9846a213c573956a35b7d1724e",
61-
"boatstack/next.go": "86ec196ac966c13b92b6302ded846d99a4e0e37d9d584bd80490e4e1047249e5",
62-
"boatstack/next_test.go": "24d8c86da4b320af085117117b8032fa59d0f78896393cf1c8b0c2194a430090",
62+
"boatstack/next.go": "ce2660e1d7649e0356ee4708aff8ce9ee5961165cf3026df4b8974528c703f71",
63+
"boatstack/next_test.go": "d442d22023831ba39fcfbbf73f1a2da4170a83fc2aab5ce1b44f10cf9d88e173",
6364
"boatstack/operation.go": "62f97bf2091f33eb2ca91915bf08bee73d53387611b673e849355bfd516ca467",
6465
"boatstack/operation_test.go": "2d624eaba342b2c81b45cdf50918a65a9c002b5376a02041b24180658ee6a25a",
6566
"boatstack/plan.go": "6854d744a14e60eb285216ddc90c41085d6dde08ca5cae497e6863a703123b2f",
@@ -107,10 +108,10 @@
107108
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
108109
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
109110
"docs/configuration.md": "4d8f207b415a5a1e3b9b1698ee7bb1221aa0e5496a061bb8054294df2f347ad1",
110-
"docs/evidence-engineered-coding.md": "1104a171aa4dcfcf881bc59adcc0018a90fa94868f08f5a596129aa4f4b8e344",
111+
"docs/evidence-engineered-coding.md": "295e908297c7a1c63d9e4784e7f5a5299ab6f515f9e6364f825b2a2b8fa3688d",
111112
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
112113
"docs/getting-started.md": "f314270c5ed1a55bbef5f3ddbcb5596693dbee9374e5f0d3df8838cefbd68052",
113-
"docs/public-claims.json": "baa8dbebf79506bc4071bbdeb122148055a634085b56eea138886cba66c38cae",
114+
"docs/public-claims.json": "dff1654cb0901c026770f0358918b29e11a5836a6fbc8668f7449b10ee208e53",
114115
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
115116
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
116117
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
@@ -124,7 +125,7 @@
124125
"labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d",
125126
"labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71",
126127
"labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39",
127-
"labs/diagram-json/plan.lock.json": "f7d44717599db90f46da9556ca4d7195fa43c65db28fa532c9e7f5b6ba738589",
128+
"labs/diagram-json/plan.lock.json": "100ee5d8381c04b3f11b4ffe3462a214883b85ddb1df73beb88a61251952cd41",
128129
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
129130
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
130131
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
@@ -186,12 +187,13 @@
186187
"release-notes/2026-07-23-canonical-update-ownership.md": "7f34f890b252493797519389b23f1b56ec7ec16db7547aac8ea196f75f3b8c2c",
187188
"release-notes/2026-07-23-explicit-source-plan.md": "ec1f97434f9263f6db4bc3b83ae83213053cd2bc6b7465e4fb2d67cbea5cf731",
188189
"release-notes/2026-07-23-ignore-ambiguous-deliveries.md": "9b1b9fd48db340b91fcced1c282297de0ecad8744723f2b1fdd94033927a88c4",
189-
"release-notes/2026-07-23-recoverable-repository-sync.md": "3afc4f6220ae76df3bd6dcd15fc180135274c808729e9c512a2c53462ec690c2"
190+
"release-notes/2026-07-23-recoverable-repository-sync.md": "3afc4f6220ae76df3bd6dcd15fc180135274c808729e9c512a2c53462ec690c2",
191+
"release-notes/2026-07-23-shipped-feature-candidate-resolution.md": "bd8ee8e7f3f216b356b121a83ef10cb0c8131a90b9ab803edebf23b882d9cf89"
190192
},
191193
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
192194
"schema_version": 1,
193195
"source": {
194-
"commit": "dc7d340e0028db5df232d75b4c69ba866aa133f9",
196+
"commit": "c288814c89139fa36e7b914ce14d9917f12d32d0",
195197
"path": "labs/12-product-engineering-loop",
196198
"repository": "operatorstack/intelligence-flow"
197199
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Bug: shipped features re-register as ambiguous plan candidates from worktrees
2+
3+
**Component:** `boatstack-helper` (labs/12-product-engineering-loop/product-engineering-loop)
4+
**Observed in:** v0.7.45 (commit 91e33a95)
5+
**Severity:** blocks `next-status` (BLOCKED / AMBIGUOUS) for any repo that has shipped >1 feature and uses worktree delivery mode.
6+
**Goal of fix:** stop miscounting shipped features as unshipped, WITHOUT deleting the committed
7+
`plan.md` / `plan.lock.json` / `pr.md` context. (A prune workaround was applied in taxweave PR #304
8+
this fix should let that be reverted.)
9+
10+
## Symptom
11+
12+
From a fresh build worktree, `next-status --repo . --json` returns:
13+
14+
```
15+
BLOCKED / AMBIGUOUS "More than one saved feature plan is available"
16+
```
17+
18+
listing every historically shipped feature as a candidate — in taxweave, 22 shipped features + the
19+
1 genuinely-active feature = 23 candidates.
20+
21+
## Root cause
22+
23+
Two facts combine:
24+
25+
### 1. Delivery state is resolved against the worktree-local git dir
26+
27+
`deliveryStateDirectory` (delivery.go ~L250) uses `git rev-parse --git-dir`, which inside a linked
28+
worktree returns the **worktree-local** dir, not the shared repo dir:
29+
30+
```
31+
--git-dir → <repo>/.git/worktrees/<name> (per-worktree, ephemeral)
32+
--git-common-dir → <repo>/.git (shared, durable)
33+
```
34+
35+
So delivery state is written to and read from:
36+
37+
```
38+
<repo>/.git/worktrees/<name>/boatstack/deliveries/<slug>/state.json
39+
```
40+
41+
Both `saveDeliveryState` and `deliveryStatePath` go through `deliveryStateDirectory`, so **write and
42+
read both target the ephemeral per-worktree location.**
43+
44+
### 2. Shipping a feature deletes the worktree that holds its state.json
45+
46+
`workspace.cleanup_after = merge` removes the feature's worktree on ship. That deletes
47+
`<repo>/.git/worktrees/<name>/…/state.json` along with it — i.e. **shipping a feature destroys its
48+
own "I am shipped" record.** The committed artifacts (`plan.md`, `plan.lock.json`, `pr.md`) live in
49+
the repo tree and survive forever.
50+
51+
### 3. The candidate check reads "planned + no state.json" as "unshipped"
52+
53+
`featurePlanCandidates` (next.go ~L42):
54+
55+
```go
56+
if fileExists(<dir>/plan.md) && !fileExists(deliveryStatePath(repo, name)) {
57+
features = append(features, name) // counted as an open candidate
58+
}
59+
```
60+
61+
Every shipped feature now matches (`plan.md` present, `state.json` gone), so from any new worktree
62+
they all re-register as open candidates → ambiguity. `ignored_deliveries` (PR #301) filters the
63+
*active-delivery* ambiguity path, NOT this feature-plan-candidate path.
64+
65+
## Evidence
66+
67+
- `git -C <worktree> rev-parse --git-dir``<repo>/.git/worktrees/wt-firm-status`
68+
- `git -C <worktree> rev-parse --git-common-dir``<repo>/.git`
69+
- The 22 blocked dirs each have `plan.lock.json`; 15 also have `pr.md`. The 1 legitimately-open
70+
feature (`firm-status-finance-aesthetic`) has neither.
71+
- Precedent: `hooks.go` (L120, L175) already uses `--git-common-dir` for exactly this "shared,
72+
survives worktrees" reason. Delivery state is the outlier still on `--git-dir`.
73+
74+
## Proposed fix — Part B only (Part A REJECTED, see below)
75+
76+
### DECISION (2026-07-23): do Part B alone. Do NOT do Part A.
77+
78+
An earlier draft of this spec proposed Part A (move delivery state to `--git-common-dir`) **and**
79+
Part B. Investigation in the source rejected Part A:
80+
81+
- `ActiveManagedDeliveries` (delivery.go:854) feeds `ClassifyCommand` push-denial
82+
(safety.go:222/269/543), `next`, `run`, and `pr`.
83+
- Per-worktree isolation of delivery state is **intentional**:
84+
`TestManagedDeliveryStateDoesNotBlockUnrelatedWorktrees` (delivery_test.go:550) asserts an
85+
unrelated worktree neither sees another feature's active delivery nor has its pushes denied.
86+
Operations, by contrast, are deliberately shared/serialized (operation_test.go:267).
87+
- Part A would make every active delivery visible in **all** worktrees — breaking that test and
88+
changing cross-worktree push-denial semantics.
89+
90+
Part A is also **unnecessary**: its only purpose was durability of the "shipped" record. Part B
91+
derives shipped-ness from the **committed, durable** `plan.lock.json`/`pr.md` instead of the
92+
ephemeral `state.json`, so the shipped record is durable by construction and the disappearance of a
93+
shipped feature's `state.json` on cleanup becomes harmless. Do not move delivery state.
94+
95+
### Part B — treat locked/shipped dirs as not-candidates
96+
97+
Make `featurePlanCandidates` exclude any feature that is demonstrably past planning —
98+
i.e. carries a `plan.lock.json` (locked/built) and/or `pr.md` (shipped):
99+
100+
```go
101+
if !fileExists(plan.md) { continue }
102+
if fileExists(plan.lock.json) || fileExists(pr.md) { continue } // shipped/locked, not an open plan
103+
if !fileExists(statePath) { features = append(features, name) }
104+
```
105+
106+
This is consistent with the existing `orphanedFeatureArtifacts` helper (next.go), which already
107+
distinguishes `pr.md` + `plan.lock.json` presence. It also lets taxweave **revert PR #304** and keep
108+
all the rich shipped-feature context in-repo.
109+
110+
`firm-status-finance-aesthetic` has no `plan.lock.json`/`pr.md`, so it correctly stays the single
111+
open candidate.
112+
113+
## Status (2026-07-23): FIXED via Part B
114+
115+
`featurePlanCandidates` (next.go) now skips any feature dir carrying `plan.lock.json` or `pr.md`
116+
before the `state.json` check, so locked/shipped features can no longer re-register as open plan
117+
candidates. No change was made to `deliveryStateDirectory`, worktree isolation, or
118+
`ActiveManagedDeliveries` / push-denial. Part A was **not** implemented (see rejection above).
119+
120+
## Tests added (`next_test.go`)
121+
122+
- `TestFeaturePlanCandidatesExcludesLockedAndShippedFeatures` — unit: a `plan.md`+`plan.lock.json`
123+
dir and a `plan.md`+`plan.lock.json`+`pr.md` dir (both without `state.json`) are NOT returned; a
124+
`plan.md`-only dir IS returned.
125+
- `TestResolveNextIgnoresShippedFeatureCandidates` — reproduction: one open feature plus several
126+
shipped dirs resolves to the single open candidate (`DRAFT_PLAN` / `plan-gate`), not `AMBIGUOUS`.
127+
- `TestResolveNextIgnoresShippedFeaturesFromLinkedWorktree` — worktree conformance: same result from
128+
a fresh linked build worktree (the exact reported symptom).
129+
- `TestManagedDeliveryStateDoesNotBlockUnrelatedWorktrees` (delivery_test.go:550) — unchanged and
130+
still green, confirming delivery-state locality was not touched.
131+
132+
## Rollout
133+
134+
1. Land Part B + tests, cut a new helper version.
135+
2. In taxweave: bump the helper, revert PR #304 (restore the 22 shipped feature dirs).
136+
3. Confirm `next-status` resolves `firm-status-finance-aesthetic` as the single candidate.

boatstack/next.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,19 @@ func featurePlanCandidates(repo string) ([]string, error) {
5050
}
5151
features := []string{}
5252
for _, entry := range entries {
53-
if !entry.IsDir() || !featureSlugPattern.MatchString(entry.Name()) || !fileExists(filepath.Join(root, entry.Name(), "plan.md")) {
53+
if !entry.IsDir() || !featureSlugPattern.MatchString(entry.Name()) {
54+
continue
55+
}
56+
directory := filepath.Join(root, entry.Name())
57+
if !fileExists(filepath.Join(directory, "plan.md")) {
58+
continue
59+
}
60+
// A feature that has been locked (built) or shipped is past planning and
61+
// must never re-register as an open plan candidate, even when its
62+
// ephemeral per-worktree delivery state.json was destroyed by worktree
63+
// cleanup on ship. plan.lock.json / pr.md are the durable committed
64+
// signals, mirroring orphanedFeatureArtifacts.
65+
if fileExists(filepath.Join(directory, "plan.lock.json")) || fileExists(filepath.Join(directory, "pr.md")) {
5466
continue
5567
}
5668
statePath, stateErr := deliveryStatePath(repo, entry.Name())

boatstack/next_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,26 @@ func writeSavedFeaturePlan(t *testing.T, repo, feature string) {
6262
}
6363
}
6464

65+
// writeShippedFeatureArtifacts models a feature that was built and shipped, then
66+
// had its worktree (and the per-worktree delivery state.json) removed by cleanup:
67+
// only the committed plan.md, plan.lock.json, and pr.md survive.
68+
func writeShippedFeatureArtifacts(t *testing.T, repo, feature string) {
69+
t.Helper()
70+
directory := filepath.Join(repo, ".product-loop", "features", feature)
71+
if err := os.MkdirAll(directory, 0o755); err != nil {
72+
t.Fatal(err)
73+
}
74+
for name, body := range map[string]string{
75+
"plan.md": "# Plan\n",
76+
"plan.lock.json": "lock\n",
77+
"pr.md": "# PR\n",
78+
} {
79+
if err := os.WriteFile(filepath.Join(directory, name), []byte(body), 0o644); err != nil {
80+
t.Fatal(err)
81+
}
82+
}
83+
}
84+
6585
func TestResolveNextReportsNotStartedWhenNoFeatureExists(t *testing.T) {
6686
repo := nextTestRepo(t)
6787
status, err := ResolveNext(repo, "")
@@ -154,6 +174,104 @@ func TestResolveNextBlocksHistoricalPlansWithoutSourceIntent(t *testing.T) {
154174
}
155175
}
156176

177+
// TestFeaturePlanCandidatesExcludesLockedAndShippedFeatures is the unit-level
178+
// guard for the shipped-feature ambiguity bug: locked (plan.lock.json) and
179+
// shipped (pr.md) feature dirs must never re-register as open plan candidates
180+
// even after their ephemeral per-worktree state.json was destroyed on cleanup.
181+
func TestFeaturePlanCandidatesExcludesLockedAndShippedFeatures(t *testing.T) {
182+
repo := nextTestRepo(t)
183+
writeSavedFeaturePlan(t, repo, "open-feature")
184+
185+
locked := filepath.Join(repo, ".product-loop", "features", "locked-feature")
186+
if err := os.MkdirAll(locked, 0o755); err != nil {
187+
t.Fatal(err)
188+
}
189+
if err := os.WriteFile(filepath.Join(locked, "plan.md"), []byte("# Plan\n"), 0o644); err != nil {
190+
t.Fatal(err)
191+
}
192+
if err := os.WriteFile(filepath.Join(locked, "plan.lock.json"), []byte("lock\n"), 0o644); err != nil {
193+
t.Fatal(err)
194+
}
195+
196+
writeShippedFeatureArtifacts(t, repo, "shipped-feature")
197+
198+
candidates, err := featurePlanCandidates(repo)
199+
if err != nil {
200+
t.Fatal(err)
201+
}
202+
if !reflect.DeepEqual(candidates, []string{"open-feature"}) {
203+
t.Fatalf("locked/shipped features leaked into plan candidates: %v", candidates)
204+
}
205+
}
206+
207+
// TestResolveNextIgnoresShippedFeatureCandidates reproduces the taxweave scenario
208+
// through ResolveNext: one genuinely open feature plus several shipped dirs whose
209+
// state.json was destroyed by worktree cleanup must resolve to the single open
210+
// candidate, not AMBIGUOUS.
211+
func TestResolveNextIgnoresShippedFeatureCandidates(t *testing.T) {
212+
repo := nextTestRepo(t)
213+
writeSavedFeaturePlan(t, repo, "open-feature")
214+
writeShippedFeatureArtifacts(t, repo, "shipped-one")
215+
writeShippedFeatureArtifacts(t, repo, "shipped-two")
216+
writeShippedFeatureArtifacts(t, repo, "shipped-three")
217+
218+
status, err := ResolveNext(repo, "")
219+
if err != nil {
220+
t.Fatal(err)
221+
}
222+
if status.ObservedStage == "AMBIGUOUS" {
223+
t.Fatalf("shipped features re-registered as ambiguous candidates: %+v", status)
224+
}
225+
if status.VerificationStatus != "VERIFIED" || status.Feature != "open-feature" ||
226+
status.ObservedStage != "DRAFT_PLAN" || status.NextOperation != "plan-gate" {
227+
t.Fatalf("single open feature did not resolve cleanly: %+v", status)
228+
}
229+
}
230+
231+
// TestResolveNextIgnoresShippedFeaturesFromLinkedWorktree reproduces the exact
232+
// reported symptom: from a fresh linked build worktree, shipped feature dirs
233+
// (committed plan.md/plan.lock.json/pr.md, no worktree-local state.json) must not
234+
// re-register as open candidates. Guards the durable-committed-artifact contract
235+
// under real worktree conditions.
236+
func TestResolveNextIgnoresShippedFeaturesFromLinkedWorktree(t *testing.T) {
237+
repo := t.TempDir()
238+
runGit(t, repo, "init", "-b", "main")
239+
runGit(t, repo, "config", "user.name", "Boatstack Test")
240+
runGit(t, repo, "config", "user.email", "boatstack@example.invalid")
241+
242+
if err := os.MkdirAll(filepath.Join(repo, ".product-loop", "features"), 0o755); err != nil {
243+
t.Fatal(err)
244+
}
245+
value, err := MarshalJSON(testConfig())
246+
if err != nil {
247+
t.Fatal(err)
248+
}
249+
if err := os.WriteFile(filepath.Join(repo, ".product-loop", "project.json"), value, 0o644); err != nil {
250+
t.Fatal(err)
251+
}
252+
writeSavedFeaturePlan(t, repo, "open-feature")
253+
writeShippedFeatureArtifacts(t, repo, "shipped-one")
254+
writeShippedFeatureArtifacts(t, repo, "shipped-two")
255+
256+
runGit(t, repo, "add", "-A")
257+
runGit(t, repo, "commit", "-m", "seed shipped and open features")
258+
259+
linked := filepath.Join(t.TempDir(), "linked")
260+
runGit(t, repo, "worktree", "add", "-b", "build-work", linked)
261+
262+
status, err := ResolveNext(linked, "")
263+
if err != nil {
264+
t.Fatal(err)
265+
}
266+
if status.ObservedStage == "AMBIGUOUS" {
267+
t.Fatalf("shipped features re-registered as ambiguous from linked worktree: %+v", status)
268+
}
269+
if status.VerificationStatus != "VERIFIED" || status.Feature != "open-feature" ||
270+
status.ObservedStage != "DRAFT_PLAN" || status.NextOperation != "plan-gate" {
271+
t.Fatalf("linked worktree did not resolve to the single open feature: %+v", status)
272+
}
273+
}
274+
157275
func TestResolveNextPlanningStates(t *testing.T) {
158276
for _, test := range []struct {
159277
name, approval, stage, next string

0 commit comments

Comments
 (0)