autonomous-sdlc-harness 0.6.1 → 0.6.2

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.
@@ -191,13 +191,18 @@
191
191
  # a refusal can still be commented. It reads, only from the environment:
192
192
  # GITHUB_EVENT_NAME, GITHUB_EVENT_PATH the event; each field is read by `jq`
193
193
  # into a variable and is only ever an argument or file
194
- # bytes, never shell source
194
+ # bytes, never shell source; `.repository.owner.login`
195
+ # and `.repository.owner.type` only when
196
+ # `HARNESS_RUN_ACTORS` is unset
195
197
  # GITHUB_REPOSITORY, GITHUB_SERVER_URL, GITHUB_RUN_ID the `gh` target and
196
198
  # the URLs its comments name
197
199
  # HARNESS_REMOTE_STOP non-empty: every start is refused
198
200
  # HARNESS_TRIGGER_LABEL the trigger label; when empty, `DEFAULT_TRIGGER_LABEL`
199
201
  # or `LEGACY_TRIGGER_LABEL`, per the paragraph below
200
202
  # HARNESS_TRIGGER_ALLOWED_BOTS comma-separated bot logins allowed to start
203
+ # HARNESS_RUN_ACTORS comma-separated people allowed to act on a run, `*`
204
+ # for every writer; unset admits a `User` owner alone
205
+ # (`run_actor_listed`)
201
206
  # HARNESS_TRIGGER_LOOKUP_SECS seconds between run lookups; `5` when empty.
202
207
  # A test seam
203
208
  # RUNNER_TEMP where the prompt snapshot is written; a `mktemp -d`
@@ -226,7 +231,13 @@
226
231
  # 6. a `User` whose `collaborators/<login>/permission` is not `admin` or
227
232
  # `write` — `maintain` reads as `write` and `triage` as `read` there; a
228
233
  # failed call is "could not confirm write access", never a pass
229
- # Refusals 4 to 6 are `authorise_actor`, the one actor check `control` reuses.
234
+ # 7. a writer `HARNESS_RUN_ACTORS` does not admit — after refusal 6, so a
235
+ # non-writer is still told about write access
236
+ # Refusals 4 to 7 are `authorise_actor`, the one actor check `control` reuses.
237
+ # Before them, after refusal 2, a re-run (GITHUB_RUN_ATTEMPT above 1) whose
238
+ # GITHUB_TRIGGERING_ACTOR is not `github-actions[bot]` and is not admitted by
239
+ # `HARNESS_RUN_ACTORS` is refused (`rerun_actor_listed`), on both event kinds:
240
+ # a re-run replays the event, so its sender is whoever first acted.
230
241
  # Then it fetches `origin <defaultBranch>` (a failure tolerated), derives the
231
242
  # branch with `hr_derive_branch <title> issue_<number>`, passing `gh` so a name
232
243
  # with run-workflow history counts as taken (2 or 3 refused), writes
@@ -252,7 +263,10 @@
252
263
  # and exit 0 with no `gh` call; an empty or missing `title` is refused. It has
253
264
  # no labeller: GitHub sends one only for a fine-grained token with Contents
254
265
  # write or a classic token with `repo`, so the token holder is the authority,
255
- # and refusals 3 to 6 do not apply. The fallback name is `task_<GITHUB_RUN_ID>`
266
+ # and refusals 3 to 6 do not apply. After refusal 2 a `User` sender
267
+ # (`.sender.login`, `.sender.type`) that `HARNESS_RUN_ACTORS` does not admit is
268
+ # refused with no permission call, and so is a sender with no type unless the
269
+ # list is `*`, failing closed; any other sender type passes. The fallback name is `task_<GITHUB_RUN_ID>`
256
270
  # and the snapshot's provenance sentence names the event type, `source` when
257
271
  # set, and the time. There is no issue, so no comment and no label: every
258
272
  # outcome is printed to stdout and appended as a Markdown block to
@@ -280,9 +294,11 @@
280
294
  # label is a view, and the run list stays the authority. The state map:
281
295
  # `parked` and `park_loop` -> parked, `paused` -> paused, `resumed` -> running,
282
296
  # `failed` -> failed, `stopped` -> stopped, `round` (review's) -> running.
283
- # `failed` posts nothing when
284
- # `remote_branch_stopped` finds the branch stopped, so a cancelled job never
285
- # overwrites `stopped`. `completed` (deliver's) and `launched` (the trigger's
297
+ # Every job event — `parked`, `park_loop`, `paused`, `resumed`, `round` and
298
+ # `failed` — posts nothing and sets no label when `remote_branch_stopped`,
299
+ # asked afresh, finds the branch stopped, so a job a stop overtook never
300
+ # overwrites `stopped`; a failed listing reports anyway, and `stopped` is never
301
+ # withheld. `completed` (deliver's) and `launched` (the trigger's
286
302
  # own comment) are one line each, as is any other event. The comment names the
287
303
  # next GitHub action — never a slash command — then <note> byte for byte, then
288
304
  # this run's URL when `GITHUB_RUN_ID` is set, then the marker line
@@ -379,7 +395,10 @@
379
395
  # 1. `HARNESS_REMOTE_STOP` is set
380
396
  # 2. `forge_on` fails — before any authorisation, so a disabled coupling asks
381
397
  # GitHub nothing about the commenter
382
- # 3. `authorise_actor` fails: `AUTH_WHY`, and who may command a run
398
+ # then a re-run whose GITHUB_TRIGGERING_ACTOR `rerun_actor_listed`
399
+ # refuses, before the event's own actor is checked
400
+ # 3. `authorise_actor` fails: `AUTH_WHY`, and who may command a run — a
401
+ # writer the `HARNESS_RUN_ACTORS` allow-list admits, or a listed bot
383
402
  # 4. the verb is empty, not a `COMMAND_VERBS` word, or one no arm carries out
384
403
  # yet: the reply lists every command and names `docs/github-run-control.md`
385
404
  # THE BRANCH. On a pull request (`.issue.pull_request.url` set), its head, by
@@ -451,8 +470,12 @@
451
470
  # first `- [ ] ` entry of the flow-progress ledger on origin's tip with the
452
471
  # `## ` heading above it, and replies once — the state by `CS_WORD` with its
453
472
  # pause reason, the state underneath a stopped one, an expired bundle's detail,
454
- # the next ledger entry (or that every entry is ticked, or that the ledger
455
- # could not be read), each open question with its `answer <n>` form, and the
473
+ # the next ledger entry (or, on a fully ticked ledger, that a user-review round
474
+ # has started and its ledger is not written yet, when origin's tip carries the
475
+ # round's `chore: add user review for <branch>` commit after the ledger's last
476
+ # change; else, for a `running` run, that it is still finishing or starting a
477
+ # stage, never that every entry is ticked; else that every entry is ticked; or
478
+ # that the ledger could not be read), each open question with its `answer <n>` form, and the
456
479
  # latest run's URL. It sets no label and runs no child verb but `fetch`; a
457
480
  # failed state read is a refusal and exit 3.
458
481
  # THE REVIEW. A `pull_request_review` event reads `.action`, `.review.state`,
@@ -496,9 +519,10 @@
496
519
  # inline comment, by any author and whatever its review's state, whose id is
497
520
  # unrecorded, whose body carries no `COMMENT_MARKER`, and whose `created_at` is
498
521
  # at or after the boundary or whose review is pending. Every distinct author
499
- # of a pending item passes `authorise_actor`; a refused author's items are
500
- # dropped with one line naming the login, `AUTH_WHY` and the count, and a
501
- # failed permission call fails the collection. A round is placed only when at
522
+ # of a pending item passes `authorise_actor`, the `HARNESS_RUN_ACTORS`
523
+ # allow-list included; a refused author's items — a writer the list refuses
524
+ # among them — are dropped with one line naming the login, `AUTH_WHY` and the
525
+ # count, and a failed permission call (status 4) fails the collection. A round is placed only when at
502
526
  # least one kept review has state `REVIEW_ROUND_STATE`; a *Comment* or
503
527
  # *Approve* review rides along in the next round one requesting changes starts.
504
528
  # The file: one `## Review by @<login>` section per pending review, oldest
@@ -548,9 +572,11 @@
548
572
  # 1. `HARNESS_REMOTE_STOP` is set
549
573
  # 2. `forge_on` fails
550
574
  # 3. the actor: on `issues` and `pull_request`, `authorise_actor` refused
551
- # (statuses 1–3), the line naming the login and `AUTH_WHY`; on `delete`, only a `Bot`
552
- # sender `trigger_bot_listed` does not list, since deleting a branch
553
- # already needs write access
575
+ # (statuses 1–3 and 5, so a writer the `HARNESS_RUN_ACTORS` allow-list
576
+ # refuses is ignored too), the line naming the login and `AUTH_WHY`; on
577
+ # `delete`, only a `Bot` sender `trigger_bot_listed` does not list, and
578
+ # never the allow-list: deleting a branch already needs write access, the
579
+ # run's branch is gone, and a stop spends no credential
554
580
  # 4. no branch: an issue's from `control_issue_branch_var` (no genuine start
555
581
  # comment), a pull request's head ref, a deletion's `.ref`; then not a
556
582
  # valid branch name
@@ -936,6 +962,7 @@
936
962
  # DEFAULT_TRIGGER_LABEL mirrors DEFAULT_TRIGGER_LABEL
937
963
  # LEGACY_TRIGGER_LABEL mirrors LEGACY_TRIGGER_LABEL
938
964
  # HARNESS_TRIGGER_ALLOWED_BOTS mirrors TRIGGER_ALLOWED_BOTS_VARIABLE
965
+ # HARNESS_RUN_ACTORS mirrors RUN_ACTORS_VARIABLE
939
966
  # TRIGGER_DISPATCH_EVENT_TYPE mirrors TRIGGER_DISPATCH_EVENT_TYPE ('harness-task')
940
967
  # WORKFLOW_CONTROL_FILE mirrors WORKFLOW_CONTROL_FILE
941
968
  # COMMAND_HANDLE mirrors COMMAND_HANDLE
@@ -1123,13 +1150,18 @@
1123
1150
  # "html_url":"https://github.com/o/r/issues/7","state":"open"}}, and a stub
1124
1151
  # answering `api repos/o/r/collaborators/alice/permission` with
1125
1152
  # {"permission":"write"}; export GITHUB_EVENT_NAME=issues GITHUB_EVENT_PATH=e.json
1126
- # GITHUB_REPOSITORY=o/r HARNESS_TRIGGER_LOOKUP_SECS=0:
1153
+ # GITHUB_REPOSITORY=o/r HARNESS_TRIGGER_LOOKUP_SECS=0 HARNESS_RUN_ACTORS='*':
1127
1154
  # trigger bash scripts/remote-run.sh trigger -> 0; origin/add_comments gains
1128
1155
  # the prompt commit, "$s.log" gains `workflow run harness-run.yml
1129
1156
  # --ref add_comments ...`, `issue comment 7 ...` naming the branch,
1130
1157
  # then `issue edit 7 ... --remove-label sdlc-harness`
1131
1158
  # read the permission answer {"permission":"read"} -> 2, no `workflow
1132
1159
  # run`, one comment naming write access, the label removed
1160
+ # not listed HARNESS_RUN_ACTORS=bob -> 2, no `workflow run`, one comment
1161
+ # naming HARNESS_RUN_ACTORS, the label removed
1162
+ # unset HARNESS_RUN_ACTORS= and e.json gaining
1163
+ # "repository":{"owner":{"login":"alice","type":"User"}} -> 0, as
1164
+ # `trigger`; the owner `bob` instead -> 2, the comment naming @bob
1133
1165
  # ignored e.json's label name `bug` -> 0, one line, "$s.log" unchanged
1134
1166
  # dispatch GITHUB_EVENT_NAME=repository_dispatch GITHUB_RUN_ID=9
1135
1167
  # GITHUB_STEP_SUMMARY=/tmp/s, e.json {"action":<TRIGGER_DISPATCH_EVENT_TYPE>,
@@ -3472,13 +3504,73 @@ trigger_bot_listed() {
3472
3504
  return 1
3473
3505
  }
3474
3506
 
3507
+ # run_actor_listed <login> — 0 when HARNESS_RUN_ACTORS admits <login>: split on
3508
+ # `,`, each entry trimmed, empty entries dropped, matched case-insensitively,
3509
+ # `*` admitting everyone, an empty <login> admitted by `*` alone. A list with no
3510
+ # entries is unset, and admits only `.repository.owner.login` of the event file
3511
+ # when `.repository.owner.type` is `User`; any other owner, or an event file or
3512
+ # field that cannot be read, admits nobody. Otherwise 1, with RUN_ACTORS_WHY
3513
+ # holding one sentence. Lowercased with `tr`, never a parameter expansion:
3514
+ # bash 3.2 has no case-changing one.
3515
+ RUN_ACTORS_WHY=""
3516
+ run_actor_listed() {
3517
+ local login="$1" want rest="${HARNESS_RUN_ACTORS-}," entry listed="" owner="" owner_type=""
3518
+ RUN_ACTORS_WHY=""
3519
+ want=$(printf '%s' "$login" | tr '[:upper:]' '[:lower:]')
3520
+ while [ -n "$rest" ]; do
3521
+ entry=${rest%%,*}
3522
+ rest=${rest#*,}
3523
+ entry=${entry#"${entry%%[![:space:]]*}"}
3524
+ entry=${entry%"${entry##*[![:space:]]}"}
3525
+ [ -n "$entry" ] || continue
3526
+ listed=1
3527
+ [ "$entry" = '*' ] && return 0
3528
+ [ -n "$want" ] && [ "$(printf '%s' "$entry" | tr '[:upper:]' '[:lower:]')" = "$want" ] && return 0
3529
+ done
3530
+ if [ -n "$listed" ]; then
3531
+ RUN_ACTORS_WHY="@$login is not on the repository variable HARNESS_RUN_ACTORS, the allow-list of who may start, command, answer or review a run."
3532
+ return 1
3533
+ fi
3534
+ if [ -n "${GITHUB_EVENT_PATH-}" ] && [ -r "$GITHUB_EVENT_PATH" ]; then
3535
+ event_field '.repository.owner.type // ""' && owner_type="$EVENT_VALUE"
3536
+ event_field '.repository.owner.login // ""' && owner="$EVENT_VALUE"
3537
+ fi
3538
+ if [ "$owner_type" = User ] && [ -n "$owner" ]; then
3539
+ [ -n "$want" ] && [ "$(printf '%s' "$owner" | tr '[:upper:]' '[:lower:]')" = "$want" ] && return 0
3540
+ RUN_ACTORS_WHY="@$login is not the repository owner, and the repository variable HARNESS_RUN_ACTORS is unset, which admits the owner, @$owner, alone."
3541
+ return 1
3542
+ fi
3543
+ RUN_ACTORS_WHY="the repository variable HARNESS_RUN_ACTORS is unset, and this repository has no single owner to admit (its owner is ${owner_type:-unreadable}), so it admits nobody until it names the logins allowed, or * for every writer."
3544
+ return 1
3545
+ }
3546
+
3547
+ # rerun_actor_listed — 0 unless this job is a re-run (GITHUB_RUN_ATTEMPT above
3548
+ # 1) whose GITHUB_TRIGGERING_ACTOR is neither `github-actions[bot]` nor
3549
+ # admitted by run_actor_listed; then 1, with RUN_ACTORS_WHY naming the
3550
+ # re-runner. A re-run replays the original event, whose sender is the person
3551
+ # who first acted, so authorise_actor alone would check the wrong account.
3552
+ rerun_actor_listed() {
3553
+ local attempt="${GITHUB_RUN_ATTEMPT-}" who="${GITHUB_TRIGGERING_ACTOR-}"
3554
+ RUN_ACTORS_WHY=""
3555
+ case "$attempt" in ''|*[!0-9]*) return 0 ;; esac
3556
+ [ "$attempt" -gt 1 ] || return 0
3557
+ [ "$who" != 'github-actions[bot]' ] || return 0
3558
+ run_actor_listed "$who" && return 0
3559
+ RUN_ACTORS_WHY="this job is a re-run by @${who:-(no actor)}, and the re-runner is checked rather than the event's sender: $RUN_ACTORS_WHY"
3560
+ return 1
3561
+ }
3562
+
3475
3563
  # authorise_actor <login> <type> — the one actor check, shared by `trigger` and
3476
3564
  # `control`: 0 when authorised. Otherwise AUTH_WHY holds one sentence and the
3477
3565
  # status names the arm: 1 `ghost`, empty or not a login shape; 2 a non-`User`
3478
3566
  # not listed in HARNESS_TRIGGER_ALLOWED_BOTS, decided with no permission call,
3479
- # because the permission API answers `none` or 404 for a bot; 3 a `User` whose
3480
- # permission is not `admin` or `write` (AUTH_PERMISSION holds it); 4 that
3481
- # permission call failed (GH_ERR holds why). Prints nothing and never posts.
3567
+ # because the permission API answers `none` or 404 for a bot, and never
3568
+ # consulting the run-actor list; 3 a `User` whose permission is not `admin` or
3569
+ # `write` (AUTH_PERMISSION holds it); 4 that permission call failed (GH_ERR
3570
+ # holds why); 5 a writer `run_actor_listed` refuses. The list is checked after
3571
+ # the permission call so a non-writer's refusal keeps naming write access, and
3572
+ # 5 is kept for a writer the maintainer has not named. Prints nothing and
3573
+ # never posts.
3482
3574
  AUTH_WHY=""
3483
3575
  AUTH_PERMISSION=""
3484
3576
  authorise_actor() {
@@ -3502,7 +3594,10 @@ authorise_actor() {
3502
3594
  fi
3503
3595
  AUTH_PERMISSION=$(printf '%s' "$GH_OUT" | jq -r '.permission // empty' 2>/dev/null) || AUTH_PERMISSION=""
3504
3596
  case "$AUTH_PERMISSION" in
3505
- admin|write) return 0 ;;
3597
+ admin|write)
3598
+ run_actor_listed "$login" && return 0
3599
+ AUTH_WHY="$RUN_ACTORS_WHY"
3600
+ return 5 ;;
3506
3601
  esac
3507
3602
  AUTH_WHY="GitHub reports the permission of @$login as ${AUTH_PERMISSION:-nothing}, not write or admin."
3508
3603
  return 3
@@ -3602,7 +3697,9 @@ verb_trigger() {
3602
3697
  { event_field '.action // ""' && action="$EVENT_VALUE" \
3603
3698
  && event_field '.client_payload.title // ""' && title="$EVENT_VALUE" \
3604
3699
  && event_field '.client_payload.body // ""' && body="$EVENT_VALUE" \
3605
- && event_field '.client_payload.source // ""' && source="$EVENT_VALUE"; } || {
3700
+ && event_field '.client_payload.source // ""' && source="$EVENT_VALUE" \
3701
+ && event_field '.sender.login // ""' && login="$EVENT_VALUE" \
3702
+ && event_field '.sender.type // ""' && sender_type="$EVENT_VALUE"; } || {
3606
3703
  echo "remote-run.sh: trigger: '$GITHUB_EVENT_PATH' is not a readable event" >&2
3607
3704
  exit "$EXIT_USAGE"
3608
3705
  }
@@ -3638,6 +3735,26 @@ verb_trigger() {
3638
3735
  "Set both keys on the default branch, then $retry_then."
3639
3736
  fi
3640
3737
 
3738
+ # A re-run replays the event's sender; the re-runner is held to the list.
3739
+ if ! rerun_actor_listed; then
3740
+ trigger_refuse "$RUN_ACTORS_WHY" \
3741
+ "Only a person the repository variable \`HARNESS_RUN_ACTORS\` admits may re-run this job; one of them can $retry_then."
3742
+ fi
3743
+
3744
+ # A dispatch's `User` sender is held to the allow-list with no permission
3745
+ # call; a sender with no type fails closed, since an empty login is admitted
3746
+ # by `*` alone; any other sender type is the token's, and passes.
3747
+ if [ "$trigger_source" = dispatch ]; then
3748
+ if [ -z "$sender_type" ] && ! run_actor_listed ""; then
3749
+ trigger_refuse "the dispatch's sender carries no account type, so it cannot be checked against the allow-list." \
3750
+ "Set the repository variable \`HARNESS_RUN_ACTORS\` to \`*\` to admit every token holder, then $retry_then."
3751
+ fi
3752
+ if [ "$sender_type" = User ] && ! run_actor_listed "$login"; then
3753
+ trigger_refuse "$RUN_ACTORS_WHY" \
3754
+ "Add \`$login\` to the comma-separated repository variable \`HARNESS_RUN_ACTORS\`, or set it to \`*\` to admit every writer, then $retry_then."
3755
+ fi
3756
+ fi
3757
+
3641
3758
  if [ "$trigger_source" = issue ]; then
3642
3759
  if [ "$state" != open ]; then
3643
3760
  trigger_refuse "this issue is not open." "Reopen it, then re-apply the label \`$trigger_label\`."
@@ -3653,6 +3770,8 @@ verb_trigger() {
3653
3770
  "Add \`$login\` to that comma-separated list to let it start runs, or have a collaborator with write access apply the label \`$trigger_label\`." ;;
3654
3771
  3) trigger_refuse "could not confirm write access for @$login: GitHub reports their permission as \`${AUTH_PERMISSION:-nothing}\`." \
3655
3772
  "Only a collaborator with write, maintain or admin access starts a run by labelling an issue; one of them can re-apply the label \`$trigger_label\`." ;;
3773
+ 5) trigger_refuse "$AUTH_WHY" \
3774
+ "Add \`$login\` to the comma-separated repository variable \`HARNESS_RUN_ACTORS\`, or set it to \`*\` to admit every writer, then re-apply the label \`$trigger_label\`." ;;
3656
3775
  *) trigger_refuse "could not confirm write access for @$login: the permission check failed ($GH_ERR)." \
3657
3776
  "Re-apply the label \`$trigger_label\` to try again." ;;
3658
3777
  esac
@@ -4039,7 +4158,9 @@ forge_question_body() {
4039
4158
  # comment (on `parked`, one per open question) and the state label, by the
4040
4159
  # target rule above. Read on `stopped` only: <pr>, an explicit pull request
4041
4160
  # that is the target whatever its state; `gone`, the branch deleted on GitHub,
4042
- # its issue read from the task prompt at <sha>. Always 0.
4161
+ # its issue read from the task prompt at <sha>. Every event but `stopped` is
4162
+ # withheld when the branch's newest `harness stop` run is newer than its newest
4163
+ # `harness run` run, read from a fresh listing. Always 0.
4043
4164
  forge_report() {
4044
4165
  local event="$1" br="$2" note="${3-}" pr="${4-}" gone="${5-}" gone_sha="${6-}" state reason="" resume_at="" when registry_file
4045
4166
  local target kind text tmp made_tmp="" file trigger_label stopped state_rel="" count n
@@ -4066,7 +4187,9 @@ forge_report() {
4066
4187
  fi
4067
4188
  forge_repo_var || return 0
4068
4189
 
4069
- if [ "$event" = failed ]; then
4190
+ if [ "$event" != stopped ]; then
4191
+ # Re-listed: `review` dispatches a run and then reports `round` in one invocation.
4192
+ ALL_RUNS_LISTED=0
4070
4193
  remote_branch_stopped "$br"
4071
4194
  stopped=$?
4072
4195
  if [ "$stopped" -eq 0 ]; then
@@ -4074,7 +4197,7 @@ forge_report() {
4074
4197
  return 0
4075
4198
  fi
4076
4199
  [ "$stopped" -eq 1 ] \
4077
- || echo "remote-run.sh: report: whether $br was stopped is unknown ($GH_ERR); reporting the failure" >&2
4200
+ || echo "remote-run.sh: report: whether $br was stopped is unknown ($GH_ERR); reporting the $event" >&2
4078
4201
  fi
4079
4202
 
4080
4203
  [ "$event" = stopped ] || { pr=""; gone=""; }
@@ -4799,6 +4922,26 @@ control_ledger_next_var() {
4799
4922
  return 0
4800
4923
  }
4801
4924
 
4925
+ # control_round_newer_than_ledger — whether origin's tip of CONTROL_BRANCH
4926
+ # carries a user-review round commit newer than the ledger's last change: the
4927
+ # round's job writes its own ledger only after that commit. 0 when it does; 1
4928
+ # otherwise, a failed git read included. Subjects are compared as whole lines.
4929
+ control_round_newer_than_ledger() {
4930
+ local state_rel ledger_sha subjects subject line
4931
+ state_rel=$(hr_state_dir "$root" 2>/dev/null) || return 1
4932
+ [ -n "$state_rel" ] || return 1
4933
+ ledger_sha=$(git -C "$root" log -1 --format=%H "refs/remotes/origin/$CONTROL_BRANCH" \
4934
+ -- "${state_rel%/}/flow_progress/${CONTROL_BRANCH}_progress.md" 2>/dev/null) || return 1
4935
+ [ -n "$ledger_sha" ] || return 1
4936
+ subjects=$(git -C "$root" log --format=%s "$ledger_sha..refs/remotes/origin/$CONTROL_BRANCH" 2>/dev/null) \
4937
+ || return 1
4938
+ subject=$(hr_user_review_subject "$CONTROL_BRANCH")
4939
+ while IFS= read -r line; do
4940
+ [ "$line" != "$subject" ] || return 0
4941
+ done <<<"$subjects"
4942
+ return 1
4943
+ }
4944
+
4802
4945
  # control_status — the read-only reply: the state, the next ledger entry, the
4803
4946
  # open questions and the latest run. Sets no label and dispatches nothing.
4804
4947
  control_status() {
@@ -4824,6 +4967,10 @@ control_status() {
4824
4967
  if control_ledger_next_var; then
4825
4968
  if [ -n "$LEDGER_NEXT" ]; then
4826
4969
  text="$text${para}Next in the flow-progress ledger: $LEDGER_NEXT${LEDGER_SECTION:+ (under \`$LEDGER_SECTION\`)}"
4970
+ elif control_round_newer_than_ledger; then
4971
+ text="$text${para}A user-review round has started on \`$CONTROL_BRANCH\`, and its flow-progress ledger is not written yet."
4972
+ elif [ "$CS_WORD" = running ]; then
4973
+ text="$text${para}The run is still running, and its flow-progress ledger has no open entry: it is finishing its last step, or a new stage has not written its ledger yet."
4827
4974
  else
4828
4975
  text="$text${para}Every entry of the flow-progress ledger is ticked."
4829
4976
  fi
@@ -5006,7 +5153,9 @@ control_review_story() {
5006
5153
  # `branch`'s pull request <pr_number>, shared by `control` and `collect`: every
5007
5154
  # submitted review (never `PENDING`) that requests changes or carries a
5008
5155
  # non-blank body, and every inline comment, no earlier round consumed, by
5009
- # every author `authorise_actor` accepts. What earlier rounds consumed is
5156
+ # every author `authorise_actor` accepts, the `HARNESS_RUN_ACTORS` allow-list
5157
+ # included; a refused author's items are dropped with one line, while a
5158
+ # failed permission call (status 4) fails the collection. What earlier rounds consumed is
5010
5159
  # read from the marker lines of their files on origin's tip; with none marked,
5011
5160
  # the boundary is the committer time of the newest round file. A comment
5012
5161
  # belonging to a pending review is pending whatever its `created_at`: a draft
@@ -5551,7 +5700,8 @@ control_close() {
5551
5700
  fi
5552
5701
  forge_on || control_close_ignore "the default branch's harness.config.json does not turn run control on"
5553
5702
  if [ "$CLOSE_KIND" = deleted ]; then
5554
- # Deleting a branch already needs write access; only a bot is screened.
5703
+ # Deleting a branch already needs write access; only a bot is screened,
5704
+ # never HARNESS_RUN_ACTORS (the header, THE CLOSE gate 3).
5555
5705
  if [ "$CONTROL_SENDER_TYPE" = Bot ] && ! trigger_bot_listed "$CONTROL_ACTOR"; then
5556
5706
  control_close_ignore "@$CONTROL_ACTOR is a bot not listed in HARNESS_TRIGGER_ALLOWED_BOTS"
5557
5707
  fi
@@ -5695,11 +5845,16 @@ verb_control() {
5695
5845
  "Set both keys on the default branch, then comment again."
5696
5846
  fi
5697
5847
 
5848
+ if ! rerun_actor_listed; then
5849
+ control_refuse "$EXIT_REFUSED" "${RUN_ACTORS_WHY%.}" \
5850
+ "Only a person the repository variable \`HARNESS_RUN_ACTORS\` admits may re-run this job; one of them can comment again."
5851
+ fi
5852
+
5698
5853
  status=0
5699
5854
  authorise_actor "$CONTROL_ACTOR" "$CONTROL_SENDER_TYPE" || status=$?
5700
5855
  if [ "$status" -ne 0 ]; then
5701
5856
  control_refuse "$EXIT_REFUSED" "${AUTH_WHY%.}" \
5702
- "Only a collaborator with write, maintain or admin access, or a bot listed in the repository variable \`HARNESS_TRIGGER_ALLOWED_BOTS\`, commands a run."
5857
+ "Only a collaborator with write, maintain or admin access whom the repository variable \`HARNESS_RUN_ACTORS\` admits (when unset, the owner alone of a repository a personal account owns, and nobody in an organisation-owned one), or a bot listed in \`HARNESS_TRIGGER_ALLOWED_BOTS\`, commands a run."
5703
5858
  fi
5704
5859
 
5705
5860
  if [ "$review" -eq 0 ] && { [ -z "$CONTROL_VERB" ] || ! control_verb_handled "$CONTROL_VERB"; }; then