From 7af153a14ef7aff2903f2479e13639a1d9065bf6 Mon Sep 17 00:00:00 2001 From: Dylan Trotter Date: Mon, 9 Nov 2026 00:22:32 +0000 Subject: [PATCH 1/3] stewards: attach a filed GitHub child as a native sub-issue (BB-250) --- plugins/stewards/config.test.ts | 8 +++++--- plugins/stewards/skills/stewards/SKILL.md | 18 ++++++++++++++++++ plugins/stewards/templates.ts | 20 +++++++++++++++++--- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/plugins/stewards/config.test.ts b/plugins/stewards/config.test.ts index bceaf3e..3065b42 100644 --- a/plugins/stewards/config.test.ts +++ b/plugins/stewards/config.test.ts @@ -516,9 +516,11 @@ describe("the GitHub issue playbook (BB-234)", () => { expect(parsed.value).toMatchObject({ kind: "ticket", source: "github" }); expect(Object.keys(parsed.value.phases)).toEqual(["open", "status:todo", "status:in-progress", "status:in-review"]); expect(parsed.value.terminal).toEqual(["closed", "closed (completed)", "closed (not planned)", "closed (duplicate)"]); - // No sub-issue handling: create-sub-issue is not in `may`, and there is no promote rule. - expect(parsed.value.may).not.toContain("create-sub-issue"); - expect(parsed.value.promote).toBeNull(); + // GitHub sub-issues (BB-250): create-sub-issue is in `may`, the promote rule is set, + // and the status:todo phase carries the attach commands. + expect(parsed.value.may).toContain("create-sub-issue"); + expect(parsed.value.promote).toBe("A sub-issue that needs more than one pull request, its own approval, or work in a repo the parent does not touch."); + expect(phaseInstructions(parsed.value, "status:todo")).toContain("sub_issues"); expect(phaseInstructions(parsed.value, "status:in-review")).toMatch(/gh issue\s+close/u); expect(phaseInstructions(parsed.value, "open")).toMatch(/Add `status:todo`/u); expect(phaseInstructions(parsed.value, "status:in-progress")).toContain("no `Fixes #N`"); diff --git a/plugins/stewards/skills/stewards/SKILL.md b/plugins/stewards/skills/stewards/SKILL.md index bcc4527..0d44952 100644 --- a/plugins/stewards/skills/stewards/SKILL.md +++ b/plugins/stewards/skills/stewards/SKILL.md @@ -318,6 +318,24 @@ Two tools are yours; everything else you do with the tools every thread has. reaches it. A report that needs nobody is marked read, so the sidebar's unread dot means "needs you"; a report that needs you is left unread. +### Filing a child on GitHub + +A GitHub child is attached as a native sub-issue of its parent in the same +action, so the parent's snapshot lists it. Create the child and read back its +numeric id, then attach it, keeping a `Part of #{parent}` line in the body +for a reader: + +``` +child=$(gh issue create --repo {owner}/{repo} --title "…" --body "Part of #{parent} + +…" | grep -oE '[0-9]+$' | xargs -I{} gh api repos/{owner}/{repo}/issues/{} --jq .id) +gh api -X POST repos/{owner}/{repo}/issues/{parent}/sub_issues -F sub_issue_id=$child +``` + +`-F` sends the numeric id as an integer. A child missing from the parent's +next wake under Sub-issues is re-attached with the second command, not +re-filed. + ## If you are an ops steward You watch one production target; the wake is the plugin's look at it (for diff --git a/plugins/stewards/templates.ts b/plugins/stewards/templates.ts index 48ef44b..dfe5fc4 100644 --- a/plugins/stewards/templates.ts +++ b/plugins/stewards/templates.ts @@ -178,7 +178,8 @@ ready: - public API or contract change - estimate >= 3 approval: project -may: [comment, edit-description, set-state, set-labels, open-pr, request-review, spawn-worker] +promote: "A sub-issue that needs more than one pull request, its own approval, or work in a repo the parent does not touch." +may: [comment, edit-description, set-state, set-labels, create-sub-issue, open-pr, request-review, spawn-worker] models: steward: claude/claude-sonnet-5-5/medium worker: claude/claude-opus-5-5/high @@ -244,8 +245,21 @@ move the issue to \`status:in-progress\` (remove \`status:todo\`, add \`status:in-progress\`). A new spec needs approval again, and is asked for the same way. -A GitHub issue has no sub-issues here: do the work in this binding, and open -one pull request per slice if it needs several. +A GitHub issue can have native sub-issues, so file a child as a real +sub-issue of its parent in the same action, keeping a \`Part of #{parent}\` +line in the body for a reader. Create the child and read back its numeric id, +then attach it: + +\`\`\` +child=$(gh issue create --repo {owner}/{repo} --title "…" --body "Part of #{parent} + +…" | grep -oE '[0-9]+$' | xargs -I{} gh api repos/{owner}/{repo}/issues/{} --jq .id) +gh api -X POST repos/{owner}/{repo}/issues/{parent}/sub_issues -F sub_issue_id=$child +\`\`\` + +\`-F\` sends the numeric id as an integer. A child missing from the parent's +next wake under Sub-issues is re-attached with the second command, not +re-filed. ## status:in-progress From 6e701e084659e414f80dceb3dfc2d033019ca569 Mon Sep 17 00:00:00 2001 From: Dylan Trotter Date: Mon, 9 Nov 2026 00:31:35 +0000 Subject: [PATCH 2/3] stewards: keep the GitHub child number and assign it for promotion (BB-250) --- plugins/stewards/config.test.ts | 5 ++++- plugins/stewards/skill.test.ts | 14 ++++++++++++++ plugins/stewards/skills/stewards/SKILL.md | 17 +++++++++-------- plugins/stewards/templates.ts | 15 ++++++++------- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/plugins/stewards/config.test.ts b/plugins/stewards/config.test.ts index 3065b42..2f7071a 100644 --- a/plugins/stewards/config.test.ts +++ b/plugins/stewards/config.test.ts @@ -520,7 +520,10 @@ describe("the GitHub issue playbook (BB-234)", () => { // and the status:todo phase carries the attach commands. expect(parsed.value.may).toContain("create-sub-issue"); expect(parsed.value.promote).toBe("A sub-issue that needs more than one pull request, its own approval, or work in a repo the parent does not touch."); - expect(phaseInstructions(parsed.value, "status:todo")).toContain("sub_issues"); + const todo = phaseInstructions(parsed.value, "status:todo")!; + expect(todo).toContain("sub_issues"); + // The child is assigned to this server's login, so a later `bb-managed` promotion can adopt it. + expect(todo).toContain("--assignee @me"); expect(phaseInstructions(parsed.value, "status:in-review")).toMatch(/gh issue\s+close/u); expect(phaseInstructions(parsed.value, "open")).toMatch(/Add `status:todo`/u); expect(phaseInstructions(parsed.value, "status:in-progress")).toContain("no `Fixes #N`"); diff --git a/plugins/stewards/skill.test.ts b/plugins/stewards/skill.test.ts index 339f8ac..57c62d2 100644 --- a/plugins/stewards/skill.test.ts +++ b/plugins/stewards/skill.test.ts @@ -46,3 +46,17 @@ describe("the shipped pr-reviewer skill's product ack (BB-207)", () => { expect(skill).toMatch(/product ack is of the pull request too/u); }); }); + +// BB-250: a GitHub ticket steward files a child as a native sub-issue, so the +// create command assigns it to this server's login (a GitHub issue is adopted +// only when the login is an assignee) and the attach uses the REST endpoint. +const stewardsSkill = readFileSync(fileURLToPath(new URL("./skills/stewards/SKILL.md", import.meta.url)), "utf8"); + +describe("the shipped stewards skill's GitHub child filing (BB-250)", () => { + it("carries the create-and-attach commands and assigns the child to this server's login", () => { + expect(stewardsSkill).toContain("### Filing a child on GitHub"); + expect(stewardsSkill).toContain("repos/{owner}/{repo}/issues/{parent}/sub_issues"); + expect(stewardsSkill).toContain("--assignee @me"); + expect(stewardsSkill).toContain("-F sub_issue_id="); + }); +}); diff --git a/plugins/stewards/skills/stewards/SKILL.md b/plugins/stewards/skills/stewards/SKILL.md index 0d44952..17215ba 100644 --- a/plugins/stewards/skills/stewards/SKILL.md +++ b/plugins/stewards/skills/stewards/SKILL.md @@ -321,20 +321,21 @@ Two tools are yours; everything else you do with the tools every thread has. ### Filing a child on GitHub A GitHub child is attached as a native sub-issue of its parent in the same -action, so the parent's snapshot lists it. Create the child and read back its -numeric id, then attach it, keeping a `Part of #{parent}` line in the body -for a reader: +action, so the parent's snapshot lists it. Create the child assigned to this +server's login (so a later `bb-managed` promotion can adopt it), keep its +number, and attach it, keeping a `Part of #{parent}` line in the body for a +reader: ``` -child=$(gh issue create --repo {owner}/{repo} --title "…" --body "Part of #{parent} +n=$(gh issue create --repo {owner}/{repo} --title "…" --body "Part of #{parent} -…" | grep -oE '[0-9]+$' | xargs -I{} gh api repos/{owner}/{repo}/issues/{} --jq .id) -gh api -X POST repos/{owner}/{repo}/issues/{parent}/sub_issues -F sub_issue_id=$child +…" --assignee @me | grep -oE '[0-9]+$') +gh api -X POST repos/{owner}/{repo}/issues/{parent}/sub_issues -F sub_issue_id=$(gh api repos/{owner}/{repo}/issues/$n --jq .id) ``` `-F` sends the numeric id as an integer. A child missing from the parent's -next wake under Sub-issues is re-attached with the second command, not -re-filed. +next wake under Sub-issues is re-attached with the second command, using the +child's number in place of `$n`, not re-filed. ## If you are an ops steward diff --git a/plugins/stewards/templates.ts b/plugins/stewards/templates.ts index dfe5fc4..402c530 100644 --- a/plugins/stewards/templates.ts +++ b/plugins/stewards/templates.ts @@ -247,19 +247,20 @@ same way. A GitHub issue can have native sub-issues, so file a child as a real sub-issue of its parent in the same action, keeping a \`Part of #{parent}\` -line in the body for a reader. Create the child and read back its numeric id, -then attach it: +line in the body for a reader. Create the child assigned to this server's +login (so a later \`bb-managed\` promotion can adopt it), keep its number, and +attach it: \`\`\` -child=$(gh issue create --repo {owner}/{repo} --title "…" --body "Part of #{parent} +n=$(gh issue create --repo {owner}/{repo} --title "…" --body "Part of #{parent} -…" | grep -oE '[0-9]+$' | xargs -I{} gh api repos/{owner}/{repo}/issues/{} --jq .id) -gh api -X POST repos/{owner}/{repo}/issues/{parent}/sub_issues -F sub_issue_id=$child +…" --assignee @me | grep -oE '[0-9]+$') +gh api -X POST repos/{owner}/{repo}/issues/{parent}/sub_issues -F sub_issue_id=$(gh api repos/{owner}/{repo}/issues/$n --jq .id) \`\`\` \`-F\` sends the numeric id as an integer. A child missing from the parent's -next wake under Sub-issues is re-attached with the second command, not -re-filed. +next wake under Sub-issues is re-attached with the second command, using the +child's number in place of \`$n\`, not re-filed. ## status:in-progress From 3504f64c8ae38aecaf2b3bba54b4223ecd48461c Mon Sep 17 00:00:00 2001 From: Dylan Trotter Date: Mon, 9 Nov 2026 00:37:13 +0000 Subject: [PATCH 3/3] stewards: print the filed GitHub child's number (BB-250) --- plugins/stewards/config.test.ts | 4 +++- plugins/stewards/skill.test.ts | 1 + plugins/stewards/skills/stewards/SKILL.md | 8 +++++--- plugins/stewards/templates.ts | 8 +++++--- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/plugins/stewards/config.test.ts b/plugins/stewards/config.test.ts index 2f7071a..d771947 100644 --- a/plugins/stewards/config.test.ts +++ b/plugins/stewards/config.test.ts @@ -522,8 +522,10 @@ describe("the GitHub issue playbook (BB-234)", () => { expect(parsed.value.promote).toBe("A sub-issue that needs more than one pull request, its own approval, or work in a repo the parent does not touch."); const todo = phaseInstructions(parsed.value, "status:todo")!; expect(todo).toContain("sub_issues"); - // The child is assigned to this server's login, so a later `bb-managed` promotion can adopt it. + // The child is assigned to this server's login, so a later `bb-managed` promotion can adopt it, + // and its number is printed so the steward can name it and re-attach it later. expect(todo).toContain("--assignee @me"); + expect(todo).toContain('echo "filed child #$n"'); expect(phaseInstructions(parsed.value, "status:in-review")).toMatch(/gh issue\s+close/u); expect(phaseInstructions(parsed.value, "open")).toMatch(/Add `status:todo`/u); expect(phaseInstructions(parsed.value, "status:in-progress")).toContain("no `Fixes #N`"); diff --git a/plugins/stewards/skill.test.ts b/plugins/stewards/skill.test.ts index 57c62d2..a2a0915 100644 --- a/plugins/stewards/skill.test.ts +++ b/plugins/stewards/skill.test.ts @@ -57,6 +57,7 @@ describe("the shipped stewards skill's GitHub child filing (BB-250)", () => { expect(stewardsSkill).toContain("### Filing a child on GitHub"); expect(stewardsSkill).toContain("repos/{owner}/{repo}/issues/{parent}/sub_issues"); expect(stewardsSkill).toContain("--assignee @me"); + expect(stewardsSkill).toContain('echo "filed child #$n"'); expect(stewardsSkill).toContain("-F sub_issue_id="); }); }); diff --git a/plugins/stewards/skills/stewards/SKILL.md b/plugins/stewards/skills/stewards/SKILL.md index 17215ba..dd940ab 100644 --- a/plugins/stewards/skills/stewards/SKILL.md +++ b/plugins/stewards/skills/stewards/SKILL.md @@ -330,12 +330,14 @@ reader: n=$(gh issue create --repo {owner}/{repo} --title "…" --body "Part of #{parent} …" --assignee @me | grep -oE '[0-9]+$') +echo "filed child #$n" gh api -X POST repos/{owner}/{repo}/issues/{parent}/sub_issues -F sub_issue_id=$(gh api repos/{owner}/{repo}/issues/$n --jq .id) ``` -`-F` sends the numeric id as an integer. A child missing from the parent's -next wake under Sub-issues is re-attached with the second command, using the -child's number in place of `$n`, not re-filed. +`echo` prints the child's number. `-F` sends the numeric id as an integer. A +child missing from the parent's next wake under Sub-issues is re-attached +with the attach command, using the child's number in place of `$n`, not +re-filed. ## If you are an ops steward diff --git a/plugins/stewards/templates.ts b/plugins/stewards/templates.ts index 402c530..cfdae58 100644 --- a/plugins/stewards/templates.ts +++ b/plugins/stewards/templates.ts @@ -255,12 +255,14 @@ attach it: n=$(gh issue create --repo {owner}/{repo} --title "…" --body "Part of #{parent} …" --assignee @me | grep -oE '[0-9]+$') +echo "filed child #$n" gh api -X POST repos/{owner}/{repo}/issues/{parent}/sub_issues -F sub_issue_id=$(gh api repos/{owner}/{repo}/issues/$n --jq .id) \`\`\` -\`-F\` sends the numeric id as an integer. A child missing from the parent's -next wake under Sub-issues is re-attached with the second command, using the -child's number in place of \`$n\`, not re-filed. +\`echo\` prints the child's number. \`-F\` sends the numeric id as an +integer. A child missing from the parent's next wake under Sub-issues is +re-attached with the attach command, using the child's number in place of +\`$n\`, not re-filed. ## status:in-progress