From 827932edfbfda753fb49937d2b04a46e376b17e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 00:40:13 +0000 Subject: [PATCH 1/2] initiative-planner: add initiative:replan label trigger for force_replan Re-planning an already-planned discussion previously required a manual workflow_dispatch with force_replan=true. Add a label-driven path so a maintainer can supersede an existing epic straight from the Ideas Discussion: - initiative-planner.yml: the discussion-[labeled] redispatch bridge now also fires on `initiative:replan` (in addition to `idea:approved`) and threads FORCE_REPLAN=1 for that label. - redispatch.sh: maps FORCE_REPLAN -> `-f force_replan=true/false` on the workflow_dispatch (mirrors the existing DRY_RUN mapping); default false so the `idea:approved` path is unchanged. - Tests: force_replan defaults to false; FORCE_REPLAN=1/true forwards true. - README: documents the new label trigger (label must exist in the repo, like idea:approved). shellcheck clean; redispatch bats 12/12. https://claude.ai/code/session_014djuNDBa3GASVDLQPXXg9L --- .github/workflows/initiative-planner.yml | 14 ++++++++++---- scripts/initiative-planner/README.md | 11 +++++++++-- scripts/initiative-planner/redispatch.sh | 19 +++++++++++++++++-- tests/test_initiative_planner_redispatch.bats | 18 ++++++++++++++++++ 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/.github/workflows/initiative-planner.yml b/.github/workflows/initiative-planner.yml index 01049c5d3..0b35027dd 100644 --- a/.github/workflows/initiative-planner.yml +++ b/.github/workflows/initiative-planner.yml @@ -67,12 +67,15 @@ concurrency: jobs: # ── discussion [labeled] bridge ─────────────────────────────────────────────── # claude-code-action aborts on `discussion` event contexts ("Unsupported event - # type: discussion"), so we never plan inline on a discussion event. When a - # maintainer adds `idea:approved`, this job resolves the discussion number and - # re-invokes THIS workflow via workflow_dispatch — under which the action runs. + # type: discussion"), so we never plan inline on a discussion event. Two labels + # fire this bridge job, which resolves the discussion number and re-invokes THIS + # workflow via workflow_dispatch — under which the action runs: + # - `idea:approved` → plan the idea (default; idempotent no-op if already planned) + # - `initiative:replan` → re-plan: force_replan=true so apply-plan SUPERSEDES + # the discussion's existing epic (close old, build fresh) # The label stays the human gate; the re-dispatch is an invisible bridge. redispatch: - if: github.event_name == 'discussion' && github.event.label.name == 'idea:approved' + if: github.event_name == 'discussion' && (github.event.label.name == 'idea:approved' || github.event.label.name == 'initiative:replan') runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -102,6 +105,9 @@ jobs: # self. Fleet callers (#820) live in their own repos and set their host. TARGET_REPO: ${{ github.repository }} DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + # The `initiative:replan` label supersedes an existing epic; `idea:approved` + # uses the default idempotent plan path. + FORCE_REPLAN: ${{ github.event.label.name == 'initiative:replan' && '1' || '0' }} run: bash scripts/initiative-planner/redispatch.sh plan: diff --git a/scripts/initiative-planner/README.md b/scripts/initiative-planner/README.md index 719175b28..6fe325dbb 100644 --- a/scripts/initiative-planner/README.md +++ b/scripts/initiative-planner/README.md @@ -13,7 +13,7 @@ create-story template, which `plan.schema.json` mirrors field-for-field. | File | Role | |------|------| -| `redispatch.sh` | Bridge the `discussion [labeled]` trigger to `workflow_dispatch` (claude-code-action rejects `discussion` event contexts). Fired with a PAT so the dispatch actually starts a run. | +| `redispatch.sh` | Bridge the `discussion [labeled]` trigger to `workflow_dispatch` (claude-code-action rejects `discussion` event contexts). Fired with a PAT so the dispatch actually starts a run. Forwards `force_replan=true` when the firing label is `initiative:replan` (vs the default `idea:approved` plan path). | | `gather-context.sh` | Fetch the approved Discussion + repo context → `$CONTEXT_PATH`; export `DISCUSSION_NODE_ID`. | | `plan.schema.json` | The plan contract Bob must emit (epic + stories + `blocked_by`). | | `validate-plan.py` | Schema + semantic checks: unique ids, acyclic DAG, an entry point, no dangling edges. | @@ -85,10 +85,17 @@ cat /tmp/plan.jsonl | jq . re-dispatching / re-toggling the label never silently mints a duplicate epic + DAG. **Default:** if an open `initiative` epic already exists for the discussion, it creates **nothing** and points the discussion back at the - existing epic. **`FORCE_REPLAN=1`** (the `force_replan` workflow input) instead + existing epic. **`FORCE_REPLAN=1`** (the `force_replan` workflow input, or the + **`initiative:replan`** discussion label) instead *supersedes*: it builds the fresh epic/DAG, then **closes (never deletes)** the old epic and its sub-issues with a "superseded by #NEW" note, keeping history and inbound references resolvable. The agent must always run `apply-plan.sh` and let the script make this decision — it must not pre-judge "already planned" and skip (the failure mode that left discussion #653's re-runs as silent no-ops). +- **Re-plan from the discussion (`initiative:replan`).** Adding the + `initiative:replan` label to an Ideas Discussion fires the same redispatch + bridge as `idea:approved`, but threads `force_replan=true` through to + `apply-plan.sh` — so a maintainer can supersede an already-planned epic straight + from the discussion, with no manual `workflow_dispatch`. The label must exist in + the repo's label set (one-time config, like `idea:approved`). diff --git a/scripts/initiative-planner/redispatch.sh b/scripts/initiative-planner/redispatch.sh index 4d0fb82e8..22f9a5ccf 100755 --- a/scripts/initiative-planner/redispatch.sh +++ b/scripts/initiative-planner/redispatch.sh @@ -29,6 +29,10 @@ # The dogfood/self path leaves this unset so target == self. # DISCUSSION_NUMBER Ideas Discussion number to plan (required) # WORKFLOW_FILE workflow to dispatch (default: initiative-planner.yml) +# DRY_RUN "1"/"true" => dispatch dry_run=true (default: false) +# FORCE_REPLAN "1"/"true" => dispatch force_replan=true (default: false); +# set by the `initiative:replan` label path to supersede an +# already-planned discussion's epic instead of no-opping # GH_TOKEN a PAT with workflow scope (required at the call site) set -euo pipefail @@ -51,6 +55,16 @@ else DISPATCH_DRY_RUN="false" fi +# Map FORCE_REPLAN to a boolean string for the workflow input. The +# `initiative:replan` label path sets this so the re-dispatch supersedes the +# discussion's existing epic instead of no-opping on apply-plan's idempotency guard. +_force_replan="${FORCE_REPLAN:-}" +if [[ "$_force_replan" == "1" || "${_force_replan,,}" == "true" ]]; then + DISPATCH_FORCE_REPLAN="true" +else + DISPATCH_FORCE_REPLAN="false" +fi + # Use GITHUB_REF if it is a branch or tag to support testing on feature branches REF_FLAGS=() if [[ -n "${GITHUB_REF:-}" && ( "$GITHUB_REF" == refs/heads/* || "$GITHUB_REF" == refs/tags/* ) ]]; then @@ -61,5 +75,6 @@ echo "Discussion event → re-dispatching ${WORKFLOW_FILE} for discussion #${DIS gh workflow run "$WORKFLOW_FILE" --repo "$REPO" "${REF_FLAGS[@]}" \ -f discussion="$DISCUSSION_NUMBER" \ -f target_repo="$TARGET_REPO" \ - -f dry_run="$DISPATCH_DRY_RUN" -echo "Dispatched ${WORKFLOW_FILE} (discussion=${DISCUSSION_NUMBER}, target_repo=${TARGET_REPO}, dry_run=${DISPATCH_DRY_RUN})." + -f dry_run="$DISPATCH_DRY_RUN" \ + -f force_replan="$DISPATCH_FORCE_REPLAN" +echo "Dispatched ${WORKFLOW_FILE} (discussion=${DISCUSSION_NUMBER}, target_repo=${TARGET_REPO}, dry_run=${DISPATCH_DRY_RUN}, force_replan=${DISPATCH_FORCE_REPLAN})." diff --git a/tests/test_initiative_planner_redispatch.bats b/tests/test_initiative_planner_redispatch.bats index 107eadff5..3a56aa922 100644 --- a/tests/test_initiative_planner_redispatch.bats +++ b/tests/test_initiative_planner_redispatch.bats @@ -105,6 +105,24 @@ teardown() { grep -qF -- "-f target_repo=acme/widgets" "$GH_LOG" } +@test "forwards force_replan=false by default" { + run bash "$SCRIPT" + [ "$status" -eq 0 ] + grep -qF -- "-f force_replan=false" "$GH_LOG" +} + +@test "honors FORCE_REPLAN=1 or FORCE_REPLAN=true (initiative:replan path)" { + export FORCE_REPLAN="1" + run bash "$SCRIPT" + [ "$status" -eq 0 ] + grep -qF -- "-f force_replan=true" "$GH_LOG" + + export FORCE_REPLAN="true" + run bash "$SCRIPT" + [ "$status" -eq 0 ] + grep -qF -- "-f force_replan=true" "$GH_LOG" +} + @test "passes GITHUB_REF if it is a branch or tag" { export GITHUB_REF="refs/heads/feature-branch" run bash "$SCRIPT" From da9310168dce3ffcb04fe2f56b3e7540b706bc96 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 00:42:27 +0000 Subject: [PATCH 2/2] test(redispatch): split FORCE_REPLAN case into isolated tests Per review (gemini-code-assist): running the script twice in one test against the append-only mock gh log could mask a second-run failure. Split into two independent tests so each assertion sees only its own dispatch. https://claude.ai/code/session_014djuNDBa3GASVDLQPXXg9L --- tests/test_initiative_planner_redispatch.bats | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_initiative_planner_redispatch.bats b/tests/test_initiative_planner_redispatch.bats index 3a56aa922..862596db0 100644 --- a/tests/test_initiative_planner_redispatch.bats +++ b/tests/test_initiative_planner_redispatch.bats @@ -111,12 +111,14 @@ teardown() { grep -qF -- "-f force_replan=false" "$GH_LOG" } -@test "honors FORCE_REPLAN=1 or FORCE_REPLAN=true (initiative:replan path)" { +@test "honors FORCE_REPLAN=1 (initiative:replan path)" { export FORCE_REPLAN="1" run bash "$SCRIPT" [ "$status" -eq 0 ] grep -qF -- "-f force_replan=true" "$GH_LOG" +} +@test "honors FORCE_REPLAN=true (initiative:replan path)" { export FORCE_REPLAN="true" run bash "$SCRIPT" [ "$status" -eq 0 ]