@plainconceptsplatform/workflows 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/action-validation.test.d.ts +1 -0
  2. package/dist/action-validation.test.js +87 -0
  3. package/dist/catalog-installation.d.ts +30 -0
  4. package/dist/catalog-installation.js +422 -0
  5. package/dist/catalog-installation.test.d.ts +1 -0
  6. package/dist/catalog-installation.test.js +485 -0
  7. package/dist/catalog-listing.d.ts +13 -0
  8. package/dist/catalog-listing.js +70 -0
  9. package/dist/catalog-listing.test.d.ts +1 -0
  10. package/dist/catalog-listing.test.js +150 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +218 -0
  13. package/dist/index.test.d.ts +1 -0
  14. package/dist/index.test.js +273 -0
  15. package/dist/repository-inspection.d.ts +23 -0
  16. package/dist/repository-inspection.js +113 -0
  17. package/dist/repository-inspection.test.d.ts +1 -0
  18. package/dist/repository-inspection.test.js +77 -0
  19. package/dist/route-processing.d.ts +6 -0
  20. package/dist/route-processing.js +149 -0
  21. package/dist/route-processing.test.d.ts +1 -0
  22. package/dist/route-processing.test.js +283 -0
  23. package/dist/stack-defaults.d.ts +11 -0
  24. package/dist/stack-defaults.js +104 -0
  25. package/dist/stack-defaults.test.d.ts +1 -0
  26. package/dist/stack-defaults.test.js +266 -0
  27. package/dist/tui.d.ts +25 -0
  28. package/dist/tui.js +287 -0
  29. package/dist/tui.test.d.ts +1 -0
  30. package/dist/tui.test.js +249 -0
  31. package/dist/workflow-catalog.d.ts +25 -0
  32. package/dist/workflow-catalog.js +55 -0
  33. package/dist/workflow-catalog.test.d.ts +1 -0
  34. package/dist/workflow-catalog.test.js +29 -0
  35. package/loops/actions/add-issue-labels/action.yml +2 -2
  36. package/loops/actions/apply-agent-bundle/apply-bundle.sh +14 -1
  37. package/loops/actions/classify-route/action.yml +7 -0
  38. package/loops/actions/classify-route/classify-route.sh +20 -5
  39. package/loops/actions/cleanup-artifacts/action.yml +38 -12
  40. package/loops/actions/identify-gate-subject/action.yml +3 -1
  41. package/loops/actions/remove-issue-labels/action.yml +4 -2
  42. package/loops/actions/validate-refine-output/validate-refine-output.sh +18 -2
  43. package/loops/actions/verify-refine-output/verify-refine-output.sh +20 -0
  44. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +300 -16
  45. package/loops/scripts/compile-agent-workflows.mjs +96 -1
  46. package/loops/scripts/merge-changelog.mjs +76 -0
  47. package/loops/templates/agentics/actionlint.yaml +13 -0
  48. package/loops/templates/agentics/agentics-checks.yml +203 -86
  49. package/loops/templates/ci/app-ci-dotnet-next.yml +64 -2
  50. package/loops/templates/ci/app-ci-node-monorepo.yml +52 -0
  51. package/loops/templates/opencode/opencode.ci.json +2 -2
  52. package/loops/templates/opencode/opencode.ci.json.md +4 -1
  53. package/loops/workflows/agent-apply-review.md +5 -1
  54. package/loops/workflows/agent-implement.md +105 -3
  55. package/loops/workflows/agent-merge-gate.md +139 -25
  56. package/loops/workflows/agent-refine.md +26 -2
  57. package/loops/workflows/work-router.yml +227 -51
  58. package/package.json +1 -1
@@ -8,6 +8,9 @@ env:
8
8
  REVIEW_LABEL: review
9
9
  PR_PENDING_LABEL: pr-pending
10
10
  GATE_MARKER: "<!-- agent-merge-gate -->"
11
+ ATTEMPT_MARKER: "<!-- agent-merge-gate-attempt -->"
12
+ MAX_ATTEMPTS: "6"
13
+ PARK_AT_ATTEMPT: "5"
11
14
  INCOMPLETE_COMMENT: "Automated CI failure remediation ended without an outcome. The issue remains for a retry."
12
15
  ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/issue-context.json
13
16
  GH_AW_ALLOWED_BOTS: "platform-devbox[bot],github-actions[bot]"
@@ -51,6 +54,18 @@ on:
51
54
  description: CI workflow run ID for fetching failing logs.
52
55
  required: false
53
56
  type: string
57
+ attempts_so_far:
58
+ description: Failed gate attempts already made against this CI verdict. Parked when it reaches the cap.
59
+ required: false
60
+ type: string
61
+ default: '0'
62
+ # gh-aw folds the top-level `if:` below into the generated activation job but does not carry
63
+ # the jobs that `if:` reads into activation's `needs`: only prompt-referenced custom jobs with
64
+ # no `needs:` of their own are hoisted (subject). protected_changes needs subject, so without
65
+ # this entry activation read needs.protected_changes.outputs.requires_review before
66
+ # protected_changes had started; the value was '' and the clause was always true
67
+ # (Pliny-Bot run 34042143350: activation finished 18 s before protected_changes began).
68
+ needs: [protected_changes]
54
69
 
55
70
  # Rung 4. Router has classified the event; identify-gate-subject validates PR ownership,
56
71
  # resolves the closing issue, and confirms the CI verdict.
@@ -160,13 +175,17 @@ jobs:
160
175
  with:
161
176
  client-id: ${{ secrets.BOT_APP_ID }}
162
177
  private-key: ${{ secrets.BOT_PRIVATE_KEY }}
178
+ # Only the reservation comes off. pr-pending says a pull request for this issue is open
179
+ # and waiting, which is still true when the gate hands it to a human, so taking it off
180
+ # here left a board where three issues with three open pull requests looked like they
181
+ # had none. The merge path is the one place the label stops being true.
163
182
  - name: Release the issue
164
183
  if: needs.protected_changes.outputs.requires_review == 'true' && needs.subject.outputs.conclusion != 'failure'
165
184
  uses: ./.github/actions/remove-issue-labels
166
185
  with:
167
186
  token: ${{ steps.app-token.outputs.token }}
168
187
  issue-number: ${{ needs.subject.outputs.issue }}
169
- labels: ${{ env.WORKING_LABEL }},${{ env.PR_PENDING_LABEL }}
188
+ labels: ${{ env.WORKING_LABEL }}
170
189
  - name: Flag human review
171
190
  if: needs.protected_changes.outputs.requires_review == 'true' && needs.subject.outputs.conclusion != 'failure'
172
191
  uses: ./.github/actions/add-issue-labels
@@ -188,6 +207,8 @@ jobs:
188
207
  Protected files:
189
208
  ${{ needs.protected_changes.outputs.files }}
190
209
 
210
+ **Verdict:** review
211
+
191
212
  reserve:
192
213
  needs: subject
193
214
  if: needs.subject.outputs.found == 'true'
@@ -223,14 +244,15 @@ jobs:
223
244
  else
224
245
  echo "has_conflicts=false" >> "$GITHUB_OUTPUT"
225
246
  fi
247
+ # First attempt only. Retries are recorded by the incomplete job's attempt comment, and
248
+ # every App comment is a router event: one issue collected sixteen of these in a day.
226
249
  - name: Comment on issue - problems found, solving them
227
- if: needs.subject.outputs.conclusion == 'failure'
250
+ if: needs.subject.outputs.conclusion == 'failure' && (inputs.attempts_so_far || '0') == '0'
228
251
  uses: ./.github/actions/create-issue-comment
229
252
  with:
230
253
  token: ${{ steps.app-token.outputs.token }}
231
254
  issue-number: ${{ needs.subject.outputs.issue }}
232
255
  body: |
233
- ${{ env.GATE_MARKER }}
234
256
  Problems found in PR #${{ needs.subject.outputs.pr }}. ${{ steps.conflicts.outputs.has_conflicts == 'true' && 'Merge conflicts detected.' || 'CI failed.' }}
235
257
  Bot is working on fixing it.
236
258
  validate_output:
@@ -300,6 +322,20 @@ jobs:
300
322
  token: ${{ steps.app-token.outputs.token }}
301
323
  push-to-branch: 'true'
302
324
  apply-labels: 'false'
325
+ # GITHUB_TOKEN on purpose: an App-token comment on a pull request is an issue_comment
326
+ # event, and GITHUB_TOKEN raises none. The full assessment lives on the issue, where the
327
+ # lifecycle is; this is what a reviewer opening the pull request sees. Carrying the
328
+ # marker and the Verdict line makes the router's verdict detection independent of
329
+ # whether the model remembered the marker.
330
+ - name: Show the verdict on the pull request
331
+ uses: ./.github/actions/create-issue-comment
332
+ with:
333
+ token: ${{ github.token }}
334
+ issue-number: ${{ needs.subject.outputs.pr }}
335
+ body: |
336
+ ${{ env.GATE_MARKER }}
337
+ **Verdict:** ${{ needs.validate_output.outputs.outcome }} (CI concluded ${{ needs.subject.outputs.conclusion }}).
338
+ Full assessment on the linked issue: #${{ needs.subject.outputs.issue }}. [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
303
339
  - name: Merge approved pull request
304
340
  if: needs.validate_output.outputs.outcome == 'merge'
305
341
  env:
@@ -324,20 +360,26 @@ jobs:
324
360
  token: ${{ steps.app-token.outputs.token }}
325
361
  issue-number: ${{ needs.subject.outputs.issue }}
326
362
  labels: ${{ env.REVIEW_LABEL }}
363
+ # The reservation only. The pull request is still open and still waiting, so pr-pending
364
+ # stays until the merge path below retires it.
327
365
  - name: Release review outcome
328
366
  if: needs.validate_output.outputs.outcome == 'review'
329
367
  uses: ./.github/actions/remove-issue-labels
330
368
  with:
331
369
  token: ${{ steps.app-token.outputs.token }}
332
370
  issue-number: ${{ needs.subject.outputs.issue }}
333
- labels: ${{ env.WORKING_LABEL }},${{ env.PR_PENDING_LABEL }}
371
+ labels: ${{ env.WORKING_LABEL }}
334
372
  - name: Clear merged issue labels
335
373
  if: needs.validate_output.outputs.outcome == 'merge'
336
374
  uses: ./.github/actions/remove-issue-labels
337
375
  with:
338
376
  token: ${{ steps.app-token.outputs.token }}
339
377
  issue-number: ${{ needs.subject.outputs.issue }}
340
- labels: ${{ env.IMPLEMENT_LABEL }},${{ env.WORKING_LABEL }},${{ env.REVIEW_LABEL }},${{ env.PR_PENDING_LABEL }}
378
+ labels: |
379
+ ${{ env.IMPLEMENT_LABEL }}
380
+ ${{ env.WORKING_LABEL }}
381
+ ${{ env.REVIEW_LABEL }}
382
+ ${{ env.PR_PENDING_LABEL }}
341
383
  incomplete:
342
384
  needs: [subject, protected_changes, agent, safe_outputs, validate_output]
343
385
  if: >
@@ -364,28 +406,52 @@ jobs:
364
406
  with:
365
407
  client-id: ${{ secrets.BOT_APP_ID }}
366
408
  private-key: ${{ secrets.BOT_PRIVATE_KEY }}
367
- - name: Release the issue
368
- uses: ./.github/actions/remove-issue-labels
409
+ # The attempt record comes first. The belt bounds its retries by counting attempt comments
410
+ # newer than the CI verdict, not by labels; releasing the labels before the record existed
411
+ # meant a failure in either step below un-reserved the issue with nothing to count, and the
412
+ # belt re-dispatched the same crash every cycle. The steps stay sequential on purpose: an
413
+ # always() release after a failed park would strip bot-working from an issue that was
414
+ # meant to be parked with review.
415
+ # attempts_so_far is a workflow_call input and arrives as '' when the caller passes an
416
+ # empty expression, declared default or not; fromJson('') is a hard failure, so the empty
417
+ # case reads as 0.
418
+ - name: Report the failed attempt
419
+ if: fromJson(inputs.attempts_so_far || '0') < fromJson(env.PARK_AT_ATTEMPT)
420
+ uses: ./.github/actions/create-issue-comment
421
+ with:
422
+ token: ${{ steps.app-token.outputs.token }}
423
+ issue-number: ${{ needs.subject.outputs.issue }}
424
+ body: |
425
+ ${{ env.ATTEMPT_MARKER }}
426
+ Attempt ${{ inputs.attempts_so_far || '0' }} of ${{ env.MAX_ATTEMPTS }} on PR #${{ needs.subject.outputs.pr }} ended without an outcome.
427
+ The issue keeps `implement`; the merge belt will retry.
428
+ [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
429
+ - name: Report the exhausted attempt budget
430
+ if: fromJson(inputs.attempts_so_far || '0') >= fromJson(env.PARK_AT_ATTEMPT)
431
+ uses: ./.github/actions/create-issue-comment
369
432
  with:
370
433
  token: ${{ steps.app-token.outputs.token }}
371
434
  issue-number: ${{ needs.subject.outputs.issue }}
372
- labels: ${{ env.WORKING_LABEL }},${{ env.IMPLEMENT_LABEL }},${{ env.PR_PENDING_LABEL }}
373
- - name: Flag for human review
435
+ body: |
436
+ ${{ env.ATTEMPT_MARKER }}
437
+ Attempt ${{ inputs.attempts_so_far || '0' }} of ${{ env.MAX_ATTEMPTS }} on PR #${{ needs.subject.outputs.pr }} ended without an outcome.
438
+ The attempt budget for this CI verdict is exhausted. The review label is set: a human must take over.
439
+ [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
440
+ - name: Park the issue for a human
441
+ if: fromJson(inputs.attempts_so_far || '0') >= fromJson(env.PARK_AT_ATTEMPT)
374
442
  uses: ./.github/actions/add-issue-labels
375
443
  with:
376
444
  token: ${{ steps.app-token.outputs.token }}
377
445
  issue-number: ${{ needs.subject.outputs.issue }}
378
446
  labels: ${{ env.REVIEW_LABEL }}
379
- - name: Report missing remediation outcome
380
- uses: ./.github/actions/create-issue-comment
447
+ # The reservation only. A failed attempt does not close the pull request, so pr-pending
448
+ # is still true and the board should keep saying so.
449
+ - name: Release the issue
450
+ uses: ./.github/actions/remove-issue-labels
381
451
  with:
382
452
  token: ${{ steps.app-token.outputs.token }}
383
453
  issue-number: ${{ needs.subject.outputs.issue }}
384
- body: |
385
- ${{ env.GATE_MARKER }}
386
- Bot could not resolve PR #${{ needs.subject.outputs.pr }} automatically. The `review` label is set: a human must take over.
387
- ${{ env.INCOMPLETE_COMMENT }}
388
- [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
454
+ labels: ${{ env.WORKING_LABEL }}
389
455
 
390
456
  agent:
391
457
  # The top-level guard reads both outputs. GitHub Actions does not make a
@@ -424,6 +490,32 @@ checkout:
424
490
  # Rung 3. The diff is what the risk assessment reads, and the failing logs are what a fix
425
491
  # starts from. Both are known from the inputs, so neither costs the agent a turn.
426
492
  steps:
493
+ # gh-aw checks out the router's ref. Its own "Checkout PR branch" step runs only when the event
494
+ # carries a pull request, which a router dispatch does not, so the agent would start on main.
495
+ # apply-agent-output fast-forwards origin/<branch> to the bundle tip and refuses anything else,
496
+ # and gh-aw builds that bundle from what the agent committed on top of the checkout; both need
497
+ # the agent to start on the branch it pushes to.
498
+ - name: Check out the pull request branch
499
+ env:
500
+ GH_TOKEN: ${{ github.token }}
501
+ REPO: ${{ github.repository }}
502
+ PR: ${{ needs.subject.outputs.pr }}
503
+ run: |
504
+ set -euo pipefail
505
+ branch=$(gh pr view "$PR" --repo "$REPO" --json headRefName --jq '.headRefName')
506
+ git switch --track "origin/$branch" 2>/dev/null || git switch "$branch"
507
+ echo "On $(git branch --show-current) at $(git rev-parse --short HEAD)"
508
+ # Every merge into the default branch invalidates every other open pull request that added a
509
+ # changelog entry, because they all insert at the top of the same list. The conflict is real
510
+ # and never interesting: both entries belong, newest first. Without a driver it costs a model
511
+ # run per sibling pull request, and it recurs on every merge while more than one is in flight.
512
+ # A consumer opts in by naming its own changelog in .gitattributes with `merge=changelog`;
513
+ # repositories that do not are unaffected, because git only calls a driver a path asks for.
514
+ - name: Teach git how to merge a changelog
515
+ run: |
516
+ set -euo pipefail
517
+ git config merge.changelog.name "newest-first changelog entries"
518
+ git config merge.changelog.driver "node $GITHUB_WORKSPACE/scripts/merge-changelog.mjs %O %A %B"
427
519
  - name: Load the issue context
428
520
  uses: ./.github/actions/load-issue-context
429
521
  with:
@@ -456,6 +548,13 @@ steps:
456
548
  fi
457
549
 
458
550
  safe-outputs:
551
+ # Path B. Without this, gh-aw's own safe_outputs job writes as well as the conclude job, and
552
+ # it runs first: it pushed a flattened, single-parent commit with GITHUB_TOKEN while conclude
553
+ # was still waiting, so the agent's merge commit was lost, the pull request stayed
554
+ # conflicting, and GITHUB_TOKEN raises no events, so no CI ran on the new head and the belt
555
+ # stalled (Pliny-Bot run 34051821011). Staged runs everything and writes nothing; conclude
556
+ # applies the bundle and the comment with the App token, which does start CI.
557
+ staged: true
459
558
  # A failed run is already a red run. An issue per failure buries the real backlog
460
559
  # under noise nobody closes.
461
560
  report-failure-as-issue: false
@@ -483,6 +582,12 @@ timeout-minutes: 60
483
582
  an issue, and that the issue carries `implement`. Do not re-check any of that, and do not
484
583
  poll for checks: the conclusion above is the answer.
485
584
 
585
+ You are on the pull request branch. Never rebase, reset, amend or otherwise rewrite
586
+ history: the workflow applies your commits as a bundle with a fast-forward-only push and
587
+ discards anything that is not a descendant of the branch tip. The
588
+ `push_to_pull_request_branch` tool's own description recommends rebasing; in this
589
+ repository that advice is wrong. Merge, commit, and let the workflow push.
590
+
486
591
  2. Read `${{ env.ISSUE_CONTEXT_PATH }}`. It contains the issue body and its discussion. When
487
592
  running `/repo-verify`, the acceptance criteria there define what the implementation must
488
593
  satisfy.
@@ -521,13 +626,13 @@ timeout-minutes: 60
521
626
  4b. **Merge conflict when CI is green.** If the conclusion is `success` and
522
627
  `has_conflicts` is `true` (current value: `${{ needs.reserve.outputs.has_conflicts }}`),
523
628
  resolve the conflict before assessing risk. You are already on the PR branch.
524
- Rebase onto `origin/${{ github.event.repository.default_branch }}`
525
- (`git rebase origin/${{ github.event.repository.default_branch }}`), resolve every
526
- conflict deliberately, and run the verification commands below. Do not use `--ours`,
527
- `--theirs`, or a blanket conflict-marker deletion without reviewing the intended
528
- behavior from both sides.
629
+ Merge `origin/${{ github.event.repository.default_branch }}` into it
630
+ (`git merge origin/${{ github.event.repository.default_branch }}`), resolve every
631
+ conflict deliberately, commit the merge, and run the verification commands below. Do not
632
+ use `--ours`, `--theirs`, or a blanket conflict-marker deletion without reviewing the
633
+ intended behavior from both sides, and never rebase: the push is fast-forward only.
529
634
 
530
- Scope verification to the files the rebase touched: pass changed file paths to
635
+ Scope verification to the files the merge touched: pass changed file paths to
531
636
  lint/format tools instead of running them repository-wide (see step 6's scoped
532
637
  verification guidance).
533
638
 
@@ -535,13 +640,13 @@ timeout-minutes: 60
535
640
  ${{ env.VERIFY_COMMANDS }}
536
641
  ```
537
642
 
538
- Push the rebased branch using `push_to_pull_request_branch` (pr_number: ${{ needs.subject.outputs.pr }},
643
+ Push the merged branch using `push_to_pull_request_branch` (pr_number: ${{ needs.subject.outputs.pr }},
539
644
  branch: the current PR branch), then emit the `add_comment` with
540
645
  **Verdict:** remediated. CI will re-run on the updated branch and the merge gate
541
646
  will be triggered again — the next cycle will see a clean, conflict-free PR and can
542
647
  make a proper merge or review decision.
543
648
 
544
- If the rebase fails or the conflicts are genuinely ambiguous, select the `review`
649
+ If the merge cannot be completed or the conflicts are genuinely ambiguous, select the `review`
545
650
  verdict instead and explain which conflicts could not be resolved safely.
546
651
 
547
652
  If the conclusion is `success` and `has_conflicts` is `false`, skip this step and
@@ -595,6 +700,15 @@ timeout-minutes: 60
595
700
  fix the actual cause. Run these verification commands before a push. Do not weaken a test,
596
701
  disable a check, or push an unverified guess.
597
702
 
703
+ **Empty failure evidence is not a reason to ask for review.** If `failed-jobs.json` is `[]`
704
+ or the logs say they were unavailable, then no CI run judged this head. That is the normal
705
+ state of a conflicting pull request: GitHub cannot build `refs/pull/N/merge` while the
706
+ conflict lasts, so no `pull_request` CI can run on it and there is nothing to read. The
707
+ conflict is the failure to fix. Resolve it as described above, push, and select
708
+ `remediated`; CI runs on the result and the next cycle gets a real verdict. Select `review`
709
+ for missing evidence only when `has_conflicts` is `false`, because then there is genuinely
710
+ nothing to act on.
711
+
598
712
  **Scoped verification.** The commands below are the full suite. This runner has limited
599
713
  memory, and a whole-repo lint or build can be killed mid-run. Scope verification to the
600
714
  files you actually changed first, and only escalate to the full suite when the scoped run
@@ -699,7 +813,7 @@ flowchart TD
699
813
  gateConflict{"Merge conflicts?"}
700
814
  gateConflict -->|yes| gateRebase
701
815
  gateConflict -->|no| gateTrivial
702
- gateRebase("Rebase<br/>Resolve conflicts, /repo-verify") -->|pushed| gateWait
816
+ gateRebase("Merge main in<br/>Resolve conflicts, /repo-verify") -->|pushed| gateWait
703
817
  gateRebase -.->|cannot resolve| gateHuman
704
818
  gateTrivial{"Trivial marker?"}
705
819
  gateTrivial -->|yes| gateMerge
@@ -382,8 +382,16 @@ safe-outputs:
382
382
  # under noise nobody closes.
383
383
  report-failure-as-issue: false
384
384
  threat-detection: false
385
+ # The refined story replaces the issue body, which is one call carrying the whole
386
+ # thing. The allowance is three rather than one because a single malformed call the
387
+ # bridge accepts as spent would otherwise end the run with the body unwritten and a
388
+ # comment already claiming success: seen on a real run, "the first update_issue call
389
+ # was sent with an incorrect parameter shape (20 bytes, missing body) and was accepted
390
+ # with success by the bridge, consuming the 1-per-run quota". The prompt still says to
391
+ # send the body once.
385
392
  update-issue:
386
393
  target: "*"
394
+ max: 3
387
395
  add-comment:
388
396
  # Split children. An oversized story becomes several implementable ones rather than
389
397
  # one issue nobody can land; the cap stops a runaway decomposition.
@@ -391,7 +399,12 @@ safe-outputs:
391
399
  max: 6
392
400
 
393
401
 
394
- timeout-minutes: 40
402
+ # Sixty rather than forty. A real run wrote the whole refined story and was killed by the
403
+ # clock four short turns later, on its way to submitting it: 36 model turns, most of them
404
+ # under 110 output tokens and still over a minute each, so the budget buys about that many
405
+ # turns whatever the issue is. Not ninety, which is what implement gets for writing code
406
+ # and running a build; this reads a repository and rewrites one issue body.
407
+ timeout-minutes: 60
395
408
  ---
396
409
 
397
410
  1. You are refining the triggering issue **#${{ inputs.issue-number }}**. Do not choose
@@ -553,7 +566,14 @@ timeout-minutes: 40
553
566
  them is a domain expert, not an engineer.
554
567
 
555
568
  **The story is complete.** You answered every exploration question yourself and none remain
556
- for the author. Call `update_issue` with the replacement body and `add_comment`
569
+ for the author.
570
+
571
+ Call `update_issue` first, with the replacement body, and wait for it to come back. Send
572
+ the whole body in that one call: it is the only thing this step has to get right, and a
573
+ call sent to see what the tool accepts still counts against your allowance even when it
574
+ fails. Do not send a partial payload, and do not probe.
575
+
576
+ Only once that call has succeeded, call `add_comment`
557
577
  with `${{ env.REFINE_MARKER }}`, then `${{ env.SAFE_OUTPUT_COMMENT_PREFIX }}`,
558
578
  then exactly one of these messages, based only on the `labels` array in the supplied issue
559
579
  context:
@@ -561,6 +581,10 @@ timeout-minutes: 40
561
581
  - If the array includes the exact label `future`: `Refinement complete. The implement label has been added. Implementation is paused until the future label is removed.`
562
582
  - Otherwise: `Refinement complete. The implement label has been added and the implement workflow will start shortly.`
563
583
 
584
+ If `update_issue` did not succeed, do not post either message: an issue that reads as
585
+ refined with its body untouched is worse than one that says the run failed. Call
586
+ `report_incomplete` with what the tool told you, and let the run be retried.
587
+
564
588
  **The story was split.** You estimated ${{ env.SPLIT_THRESHOLD }} or more and found real
565
589
  seams. Call `create_issue` once per child, then `update_issue` on the parent with the
566
590
  summary and the checklist, then `add_comment` with `${{ env.REFINE_MARKER }}`, then