autonomous-sdlc-harness 0.6.3 → 0.6.5
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.
- package/dist/commands/init.js +2 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/doctor/checks.js +24 -7
- package/dist/doctor/checks.js.map +1 -1
- package/dist/remote/githubActions.js +42 -1
- package/dist/remote/githubActions.js.map +1 -1
- package/package.json +1 -1
- package/templates/github/workflows/harness-control.yml +145 -16
- package/templates/github/workflows/harness-run.yml +10 -1
- package/templates/scripts/autonomous-watcher.sh +4 -1
- package/templates/scripts/push-branch.sh +45 -4
- package/templates/scripts/remote-run.sh +845 -103
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
# (always 0, 1 only on a usage error: its paragraph)
|
|
39
39
|
# remote-run.sh collect <branch> [--pr <n>] [--repo <root>]
|
|
40
40
|
# (always 0, 1 only on a usage error: its paragraph)
|
|
41
|
-
# remote-run.sh control [--repo <root>]
|
|
41
|
+
# remote-run.sh control [--needs-agent] [--repo <root>]
|
|
42
|
+
# (its own exit map: its paragraph)
|
|
42
43
|
# 0 sent (for stop: the action=stop marker was dispatched, and every
|
|
43
44
|
# queued, waiting or in-progress `harness run` run of that branch was
|
|
44
45
|
# asked to cancel, or there was none); for status and fetch: printed
|
|
@@ -311,8 +312,10 @@
|
|
|
311
312
|
# stays open; otherwise it says the draft stays open, to close to discard the
|
|
312
313
|
# run, or, when an issue is known, to re-apply the trigger label there for a
|
|
313
314
|
# new run on the next indexed branch. A plain `stopped` on a pull request says
|
|
314
|
-
# its draft stays open and closing it discards the run.
|
|
315
|
-
#
|
|
315
|
+
# its draft stays open and closing it discards the run. A `stopped` report also
|
|
316
|
+
# rewrites the progress comment of each pull request it labels, its
|
|
317
|
+
# `in progress` line becoming `stopped` (`forge_progress_stopped`). No other
|
|
318
|
+
# event changes a pull request's draft state.
|
|
316
319
|
# Every job event — `parked`, `park_loop`, `paused`, `resumed`, `round`,
|
|
317
320
|
# `failed` and `not_started` — posts nothing and sets no label when `remote_branch_stopped`,
|
|
318
321
|
# asked afresh, finds the branch stopped, so a job a stop overtook never
|
|
@@ -334,8 +337,11 @@
|
|
|
334
337
|
# `COMMAND_HANDLE answer <n>` over `<your answer>`; the marker adds
|
|
335
338
|
# `question=<n>`. With no question open it posts nothing, sets no label and
|
|
336
339
|
# prints one `::error::` line naming the branch. The label
|
|
337
|
-
# is set once per target, not per question.
|
|
338
|
-
#
|
|
340
|
+
# is set once per target, not per question. When the registry's `pause_reason`
|
|
341
|
+
# is `user` (a pause the job dropped and the run never honoured), `parked`
|
|
342
|
+
# appends `PAUSE_FOLDED_NOTE` to <note> and `park_loop` appends
|
|
343
|
+
# `PAUSE_FOLDED_HOLD_NOTE`, each after one blank line. On a public repository a
|
|
344
|
+
# question comment and its answer are public, as the artifact already is
|
|
339
345
|
# (`docs/remote-execution.md` -> `## 11. Security`, *What a reader of the
|
|
340
346
|
# repository's Actions runs can see*).
|
|
341
347
|
# `progress` (`forge_progress`) keeps ONE comment per run or round on the pull
|
|
@@ -356,7 +362,10 @@
|
|
|
356
362
|
# is one `forge_comment`; a body equal to the render (carriage returns and
|
|
357
363
|
# trailing newlines aside) is no call and one `already current` line; otherwise
|
|
358
364
|
# one `PATCH` of that comment — the only comment this file ever edits. A refused
|
|
359
|
-
# listing, create or edit is one `::warning::` line.
|
|
365
|
+
# listing, create or edit is one `::warning::` line. After a stop,
|
|
366
|
+
# `forge_progress_stopped` edits that same comment, so it is still the only
|
|
367
|
+
# comment this file edits, and a resumed job's first progress pass renders it
|
|
368
|
+
# from the ledger again.
|
|
360
369
|
# It never fails its caller: every problem is one line and exit 0.
|
|
361
370
|
#
|
|
362
371
|
# `open` OPENS THE RUN'S DRAFT PULL REQUEST at the run's start, so its issue
|
|
@@ -504,12 +513,17 @@
|
|
|
504
513
|
# into a variable (data, never shell source), plus `trigger`'s environment.
|
|
505
514
|
# Ignored, with one line and no `gh` call: an action other than `created`; a
|
|
506
515
|
# body carrying `COMMENT_MARKER` anywhere (the harness's own comment, whoever
|
|
507
|
-
# posted it); and a body
|
|
508
|
-
#
|
|
509
|
-
#
|
|
510
|
-
#
|
|
511
|
-
#
|
|
512
|
-
#
|
|
516
|
+
# posted it); and a body that does not hold `COMMAND_HANDLE` as a word
|
|
517
|
+
# anywhere, compared lowercase (the ERE `(^|[^a-z0-9])<handle>([^a-z0-9-]|$)`).
|
|
518
|
+
# So `pause` and `foo@sdlc-harnessx` start nothing. THE EXACT FORM is a first
|
|
519
|
+
# line — a trailing CR stripped, leading spaces and tabs skipped — opening with
|
|
520
|
+
# a word equal to `COMMAND_HANDLE`, compared lowercase, whose next word,
|
|
521
|
+
# lowercased, is a `COMMAND_VERBS` word: that is the verb, and `CONTROL_ARGS`
|
|
522
|
+
# the rest of that line. Any other body holding the handle is A MENTION
|
|
523
|
+
# (`CONTROL_MENTION`, no verb): `Let's @sdlc-harness pause`, `> @sdlc-harness
|
|
524
|
+
# pause`, the handle on a later line, and `@sdlc-harness check the question`
|
|
525
|
+
# alike, each read by MENTION below. Then refused, in this order, each a reply
|
|
526
|
+
# and exit 2:
|
|
513
527
|
# 1. `HARNESS_REMOTE_STOP` is set
|
|
514
528
|
# 2. `forge_on` fails — before any authorisation, so a disabled coupling asks
|
|
515
529
|
# GitHub nothing about the commenter
|
|
@@ -517,8 +531,101 @@
|
|
|
517
531
|
# refuses, before the event's own actor is checked
|
|
518
532
|
# 3. `authorise_actor` fails: `AUTH_WHY`, and who may command a run — a
|
|
519
533
|
# writer the `HARNESS_RUN_ACTORS` allow-list admits, or a listed bot
|
|
520
|
-
# 4. the verb is
|
|
521
|
-
#
|
|
534
|
+
# 4. the exact form's verb is one no arm carries out (`control_verb_handled`;
|
|
535
|
+
# no such word exists today): the reply lists every command and names
|
|
536
|
+
# `docs/github-run-control.md`. Skipped for a mention.
|
|
537
|
+
# Then THE BRANCH, and the exact form's arm (`control_run_verb`) or MENTION.
|
|
538
|
+
# `--needs-agent` answers only whether MENTION would start a session: the
|
|
539
|
+
# intake above, then refusals 1 to 3 (`control_gates`, shared with `control`),
|
|
540
|
+
# run alone, posting, dispatching and creating nothing; its one `gh` call is
|
|
541
|
+
# `authorise_actor`'s permission call, and the branch is never resolved. One
|
|
542
|
+
# stdout line each: exit 0 `needs-agent: yes, a mention by @<login> on #<n>`;
|
|
543
|
+
# exit 2 `needs-agent: no, <reason>` for an event other than `issue_comment`,
|
|
544
|
+
# an ignored comment (after its own line), the exact form (before any gate,
|
|
545
|
+
# no `gh` call), or a refusal, whose <reason> is that refusal's reply text;
|
|
546
|
+
# exit 3 `needs-agent: undecided, <AUTH_WHY>` when the permission call failed
|
|
547
|
+
# (`control` still refuses that with exit 2); exit 1 as `control`'s.
|
|
548
|
+
# `WORKFLOW_CONTROL_FILE` runs it before installing the agent; the act step,
|
|
549
|
+
# plain `control`, still re-checks everything.
|
|
550
|
+
# MENTION. `verb_control`'s first statements, for every event, copy `IN_OAUTH`
|
|
551
|
+
# / `IN_API` into the non-exported `MENTION_OAUTH` / `MENTION_API` and unset
|
|
552
|
+
# them, with `CLAUDE_CODE_OAUTH_TOKEN` / `ANTHROPIC_API_KEY`, so no `gh`, `git`
|
|
553
|
+
# or `control_child` process the job spawns inherits a credential; only the
|
|
554
|
+
# agent subshell receives it. After the gates and the branch, `control_mention`
|
|
555
|
+
# reads the state (`control_state_var`; a failed read is exit 3), then refuses
|
|
556
|
+
# with no session: no saved credential (exit 2, the reply listing every
|
|
557
|
+
# command); no `commands/<basename of MENTION_COMMAND>.md` under
|
|
558
|
+
# `HARNESS_MENTION_PLUGIN_DIR` (exit 3, naming `HARNESS_CLI_VERSION` in
|
|
559
|
+
# `harness-run.yml` and `docs/remote-execution.md` -> `### Upgrading`); and an
|
|
560
|
+
# agent binary, `${HARNESS_AGENT_CLI:-claude}`, that `command -v` does not
|
|
561
|
+
# resolve (exit 3). The plugin directory is a path only: nothing under it is
|
|
562
|
+
# sourced or run by this script. THE CONTEXT DIRECTORY, a fresh one under
|
|
563
|
+
# `RUNNER_TEMP` (removed on exit): `comment.md` (`handle: <COMMAND_HANDLE>`,
|
|
564
|
+
# `Comment by @<login> on <issue|pull request> #<n>:`, a blank line, the body
|
|
565
|
+
# verbatim), `run.md` (`branch:`, the `fetch` keys, `stopped: yes|no`, the next
|
|
566
|
+
# ledger entry and its section, or that every entry is ticked or the ledger
|
|
567
|
+
# could not be read), `questions/question_<n>.md` per open question,
|
|
568
|
+
# `item.md` (`kind:`, `number:`, `title:`, `author:`, `url:`, a blank line, the
|
|
569
|
+
# body, from `api repos/<repo>/issues/<n>`), `conversation.md` (the last
|
|
570
|
+
# `MENTION_COMMENTS_MAX` comments before the commenter's own, oldest first,
|
|
571
|
+
# each `### @<login> at <at>`, `(posted by the harness)` when it carries
|
|
572
|
+
# `COMMENT_MARKER`, then its body, from the paginated comment listing) and, on
|
|
573
|
+
# a pull request, `diff.patch` (`pr diff`, text only: nothing is checked out),
|
|
574
|
+
# each capped at `MENTION_FILE_MAX_BYTES` at a whole line with a `(cut at <n>
|
|
575
|
+
# bytes)` line; a failed read is said in the file, never refused. THE SESSION runs once, never retried, in a subshell `cd` into
|
|
576
|
+
# that directory with `GH_TOKEN`, `GITHUB_TOKEN` and `HARNESS_PR_TOKEN` unset
|
|
577
|
+
# and `CLAUDE_CODE_OAUTH_TOKEN` / `ANTHROPIC_API_KEY` exported from whichever
|
|
578
|
+
# saved value is set: `-p "$MENTION_COMMAND"` (no argument, so no comment text
|
|
579
|
+
# reaches argv) `--plugin-dir <dir> --add-dir <dir>/instructions
|
|
580
|
+
# --output-format json --json-schema <built by jq from MENTION_ACTIONS and
|
|
581
|
+
# COMMAND_VERBS> --tools Read,Grep,Glob --restricted --strict-mcp-config
|
|
582
|
+
# --no-session-persistence --permission-prompts none --model <agentModel>
|
|
583
|
+
# --max-budget-usd "$MENTION_MAX_BUDGET_USD"`, the model read through
|
|
584
|
+
# `hr_agent_model` (a command declares no `model:`) and the flag left off only
|
|
585
|
+
# when the configuration cannot be read; never `--bare`, which reads no
|
|
586
|
+
# OAuth token, and never `--disable-slash-commands`, which would stop the `-p`
|
|
587
|
+
# command expanding. The command is `plugin/commands/harness-read-mention.md`
|
|
588
|
+
# (`MENTION_COMMAND`), and it loads `plugin/instructions/mention_reading.md`,
|
|
589
|
+
# whose `## Output contract` restates the decision contract validated here: a
|
|
590
|
+
# field renamed on either side is an edit to both. A command declares no
|
|
591
|
+
# allowlist, so the read-only closure is the session's `--tools` and
|
|
592
|
+
# `--restricted` alone. EXTRACTION: a non-zero exit, stdout that is not one
|
|
593
|
+
# JSON object, `.is_error` true or a `.subtype` other than `success` is a
|
|
594
|
+
# failure, exit 3; the decision is `.structured_output` when an object, else
|
|
595
|
+
# `.result | fromjson?` when an object, and one stdout line logs it (`mention
|
|
596
|
+
# on #<n> by @<login> read as <action>[ <verb>] from <field>: <reason>`).
|
|
597
|
+
# VALIDATION, by `jq`, the sole authority whatever the schema did: `action` in
|
|
598
|
+
# `MENTION_ACTIONS`; `command` needs a `verb` in `COMMAND_VERBS`; `answer`
|
|
599
|
+
# needs a non-blank `answer` and an optional integer `question` of 1 or more;
|
|
600
|
+
# `reply` and `clarify` need a non-blank `text`; `reason` is a string. Unknown
|
|
601
|
+
# keys are ignored; an invalid decision is refused, exit 2, naming the first
|
|
602
|
+
# rule broken. Then, once and before any action is answered, a `text` or
|
|
603
|
+
# `answer` holding a saved credential value, or the job's `GH_TOKEN` raw or in
|
|
604
|
+
# the base64 form `actions/checkout` persists, verbatim, is an `::error::` line
|
|
605
|
+
# that never prints it, nothing posted, exit 3. THE ANSWERS, each through `control_post`:
|
|
606
|
+
# `none` posts nothing; `reply` / `clarify` post `@<login>: <text>` — `text`
|
|
607
|
+
# through `JQ_DEF_SANITISE` (`<!--` neutralised, every other `@<login>` but the
|
|
608
|
+
# commenter's and the handle given U+200B) and capped at
|
|
609
|
+
# `MENTION_TEXT_MAX_BYTES` with a `(cut)` line — then a footer saying an agent
|
|
610
|
+
# wrote it and changed nothing, listing the commands; `fixes` posts this
|
|
611
|
+
# script's text pointing at a review requesting changes or
|
|
612
|
+
# `/autonomous-sdlc-harness:branch-user-review`; `command` with a verb in
|
|
613
|
+
# `MENTION_ACT_VERBS` is carried out by that verb's own arm (`control_run_verb`)
|
|
614
|
+
# from the exact form's `CONTROL_ARGS` and `CONTROL_BODY` — for `answer`, the
|
|
615
|
+
# command line then the decision's `answer` below it, byte for byte — every
|
|
616
|
+
# reply the arm posts opening with `CONTROL_MENTION_NOTE`, `Read from your
|
|
617
|
+
# mention as` and that form, `answer` adding the answer fenced by
|
|
618
|
+
# `JQ_DEF_FENCE` after `JQ_DEF_SANITISE`; an `answer` carrying a credential
|
|
619
|
+
# value was refused above, so it reaches neither the answer file, the dispatch
|
|
620
|
+
# nor the note; a state the arm refuses is that arm's refusal and exit,
|
|
621
|
+
# unchanged. `command` with a verb in `MENTION_CONFIRM_VERBS` posts a
|
|
622
|
+
# confirmation request naming the exact form it was read as and carries out no
|
|
623
|
+
# verb: `stop` because it is destructive, `clear` because on GitHub typing it
|
|
624
|
+
# is `branch-resume`'s confirmation. Exits: 0 answered or nothing to do; 2
|
|
625
|
+
# refused; 3 a state read, plugin, binary or agent failure, or a credential
|
|
626
|
+
# value in `text` or `answer`; a carried-out verb, its arm's. The workflow's
|
|
627
|
+
# interface: `IN_OAUTH` / `IN_API`, `HARNESS_MENTION_PLUGIN_DIR`, and
|
|
628
|
+
# `control --needs-agent`'s exit 2, on which it skips installing the agent.
|
|
522
629
|
# THE BRANCH. On a pull request (`.issue.pull_request.url` set), its head, by
|
|
523
630
|
# `pr view`: a fork's pull request is refused, because this event carries the
|
|
524
631
|
# repository's secrets, and nothing from its head is checked out or run; one
|
|
@@ -717,7 +824,8 @@
|
|
|
717
824
|
# 5. `hr_branch_is_protected` does not answer 1
|
|
718
825
|
# 6. on `pull_request`, the head branch absent on origin (`remote_branch_exists`
|
|
719
826
|
# answers 1): GitHub closed the pull request because the branch was
|
|
720
|
-
# deleted, and the `delete` event's job stops the run
|
|
827
|
+
# deleted, and the `delete` event's job stops the run and reports it on
|
|
828
|
+
# this pull request; an `ls-remote` that
|
|
721
829
|
# cannot answer is one line and proceeds
|
|
722
830
|
# There is no harness-branch check (`forge_recognised`): a merged or deleted
|
|
723
831
|
# branch may no longer carry its task prompt or ledger, and a listed `harness run <b>` run is the
|
|
@@ -1064,7 +1172,11 @@
|
|
|
1064
1172
|
# task prompt at the newest `harness run <branch>` run's `headSha` through the
|
|
1065
1173
|
# contents API rather than from `origin/<branch>` (a failed read is one line
|
|
1066
1174
|
# and no issue), and its text says the branch was deleted, so the run cannot
|
|
1067
|
-
# be resumed, and that its workflow runs and artifacts are kept.
|
|
1175
|
+
# be resumed, and that its workflow runs and artifacts are kept. (4) also
|
|
1176
|
+
# reports on each pull request of the branch from this repository that is not
|
|
1177
|
+
# merged and still carries `running`, `parked` or `paused`: each gets the same
|
|
1178
|
+
# comment, the `stopped` label and its progress comment marked stopped
|
|
1179
|
+
# (`forge_gone_prs_var`, `forge_progress_stopped`). The issue read
|
|
1068
1180
|
# rests on GitHub serving a commit no branch points at, which is unverified
|
|
1069
1181
|
# (`docs/github-run-control.md` -> `## 8. What is not verified here`).
|
|
1070
1182
|
#
|
|
@@ -1077,7 +1189,8 @@
|
|
|
1077
1189
|
# repository from that checkout's remote. The configuration read is that
|
|
1078
1190
|
# root's `harness.config.json`.
|
|
1079
1191
|
#
|
|
1080
|
-
# WHAT IT NEVER DOES. It never launches a local session
|
|
1192
|
+
# WHAT IT NEVER DOES. It never launches a local session but `control`'s one
|
|
1193
|
+
# read-only mention session (MENTION, in `control`'s paragraph), never writes the
|
|
1081
1194
|
# inbox, and never watches a run it sent beyond the bounded lookup of its
|
|
1082
1195
|
# listing that `trigger` and `review` make. Only `start` and `review` push, and
|
|
1083
1196
|
# only through `create-worktree.sh` and `push-branch.sh`; `start`'s writes are
|
|
@@ -1103,8 +1216,9 @@
|
|
|
1103
1216
|
# comment files under `RUNNER_TEMP` (removed), at most one pull request, its
|
|
1104
1217
|
# `running` label and one comment on the issue. `deliver` writes its body and
|
|
1105
1218
|
# comment files under `RUNNER_TEMP` (removed), at most one pull request, one
|
|
1106
|
-
# comment and the state labels. `control` writes its reply file
|
|
1107
|
-
#
|
|
1219
|
+
# comment and the state labels. `control` writes its reply file, its `fetch`
|
|
1220
|
+
# directory and, for a mention, its context directory and the session's output
|
|
1221
|
+
# files under `RUNNER_TEMP` (removed) and one reply comment, plus
|
|
1108
1222
|
# what the child verb it runs writes, and the fetch of origin's <branch> that
|
|
1109
1223
|
# force-writes `refs/remotes/origin/<branch>` in its checkout. `collect` writes its round file and its
|
|
1110
1224
|
# settledness directory under `RUNNER_TEMP` (removed), at most one comment, on
|
|
@@ -1148,6 +1262,10 @@
|
|
|
1148
1262
|
# WORKFLOW_CONTROL_FILE mirrors WORKFLOW_CONTROL_FILE
|
|
1149
1263
|
# COMMAND_HANDLE mirrors COMMAND_HANDLE
|
|
1150
1264
|
# COMMAND_VERBS mirrors COMMAND_VERBS, space-separated
|
|
1265
|
+
# MENTION_ACTIONS mirrors MENTION_ACTIONS, space-separated, same order
|
|
1266
|
+
# MENTION_ACT_VERBS mirrors MENTION_ACT_VERBS, space-separated, same order
|
|
1267
|
+
# MENTION_CONFIRM_VERBS mirrors MENTION_CONFIRM_VERBS, space-separated, same order
|
|
1268
|
+
# MENTION_COMMAND mirrors MENTION_COMMAND, the plugin-qualified slash command
|
|
1151
1269
|
# COMMENT_MARKER mirrors COMMENT_MARKER
|
|
1152
1270
|
# REVIEW_ROUND_STATE mirrors REVIEW_ROUND_STATE
|
|
1153
1271
|
# STATE_LABEL_PREFIX mirrors STATE_LABEL_PREFIX
|
|
@@ -1421,8 +1539,27 @@
|
|
|
1421
1539
|
# pause bash scripts/remote-run.sh control -> 0; "$s.log" gains `workflow
|
|
1422
1540
|
# run harness-run.yml --ref feat_x -f action=pause -f branch=feat_x`,
|
|
1423
1541
|
# then one comment on 12 naming @alice
|
|
1424
|
-
# ignored c.json's body `
|
|
1425
|
-
#
|
|
1542
|
+
# ignored c.json's body `pause` -> 0, one line, "$s.log" unchanged
|
|
1543
|
+
# mention c.json's body `Let's @sdlc-harness pause`, with
|
|
1544
|
+
# HARNESS_MENTION_PLUGIN_DIR=<a throwaway directory holding an
|
|
1545
|
+
# empty commands/harness-read-mention.md>, IN_OAUTH=x and
|
|
1546
|
+
# HARNESS_AGENT_CLI=<a stub printing {"type":"result",
|
|
1547
|
+
# "subtype":"success","is_error":false,"structured_output":
|
|
1548
|
+
# {"action":"reply","text":"It is running.","reason":"r"}}>
|
|
1549
|
+
# -> 0; "$s.log" gains one comment on 12 opening `@alice: It is
|
|
1550
|
+
# running.`, and no `workflow run`
|
|
1551
|
+
# act the same with the stub's structured_output {"action":"command",
|
|
1552
|
+
# "verb":"pause","reason":"r"} -> 0; "$s.log" gains `workflow run
|
|
1553
|
+
# harness-run.yml --ref feat_x -f action=pause -f branch=feat_x`,
|
|
1554
|
+
# then one comment on 12 opening `Read from your mention as
|
|
1555
|
+
# `@sdlc-harness pause`.`
|
|
1556
|
+
# needs-agent the mention's c.json, `control --needs-agent` -> 0, one line
|
|
1557
|
+
# `needs-agent: yes, a mention by @alice on #12`; "$s.log" holds
|
|
1558
|
+
# only the permission call
|
|
1559
|
+
# needs-agent c.json's body `@sdlc-harness pause` -> 2, one `needs-agent: no`
|
|
1560
|
+
# exact line naming the exact form; "$s.log" unchanged
|
|
1561
|
+
# needs-agent the mention with HARNESS_RUN_ACTORS=bob -> 2, one `needs-agent:
|
|
1562
|
+
# unlisted no` line naming HARNESS_RUN_ACTORS; nothing posted
|
|
1426
1563
|
|
|
1427
1564
|
set -u
|
|
1428
1565
|
|
|
@@ -1445,6 +1582,10 @@ TRIGGER_DISPATCH_EVENT_TYPE='harness-task'
|
|
|
1445
1582
|
WORKFLOW_CONTROL_FILE='harness-control.yml'
|
|
1446
1583
|
COMMAND_HANDLE='@sdlc-harness'
|
|
1447
1584
|
COMMAND_VERBS='answer pause resume stop clear status'
|
|
1585
|
+
MENTION_ACTIONS='command reply clarify fixes none'
|
|
1586
|
+
MENTION_ACT_VERBS='answer pause resume status'
|
|
1587
|
+
MENTION_CONFIRM_VERBS='stop clear'
|
|
1588
|
+
MENTION_COMMAND='/autonomous-sdlc-harness:harness-read-mention'
|
|
1448
1589
|
COMMENT_MARKER='<!-- sdlc-harness'
|
|
1449
1590
|
REVIEW_ROUND_STATE='changes_requested'
|
|
1450
1591
|
STATE_LABEL_PREFIX='sdlc-harness: '
|
|
@@ -1456,6 +1597,18 @@ PR_CREATE_SETTING_PATH='Settings -> Actions -> General -> Workflow permissions'
|
|
|
1456
1597
|
# to be trusted (docs/github-integration-research.md -> S6); the margin is the
|
|
1457
1598
|
# framing lines and the marker.
|
|
1458
1599
|
QUESTION_COMMENT_MAX_BYTES=250000
|
|
1600
|
+
# The most bytes of each file in a mention's context directory, and of the
|
|
1601
|
+
# agent-written text a mention reply posts (under QUESTION_COMMENT_MAX_BYTES
|
|
1602
|
+
# with room for the prefix, the footer and the marker).
|
|
1603
|
+
MENTION_FILE_MAX_BYTES=200000
|
|
1604
|
+
MENTION_TEXT_MAX_BYTES=60000
|
|
1605
|
+
# How many of the comments before a mention its context's conversation.md keeps.
|
|
1606
|
+
MENTION_COMMENTS_MAX=30
|
|
1607
|
+
# The mention session's spend bound: a bound on one read, not a measured cost.
|
|
1608
|
+
MENTION_MAX_BUDGET_USD=1
|
|
1609
|
+
# Name no login: the job sees only the `harness pause` run, whose actor is the bot.
|
|
1610
|
+
PAUSE_FOLDED_NOTE='A pause was requested on this run before it parked, so it is folded into this park: the run waits for the answer and continues once it is answered, and no separate `paused` comment follows.'
|
|
1611
|
+
PAUSE_FOLDED_HOLD_NOTE='A pause was requested on this run before it was put on hold, so it is folded into this hold: the run waits for the hold to be cleared and continues once it is, and no separate `paused` comment follows.'
|
|
1459
1612
|
GH="${HARNESS_GH_CLI:-gh}"
|
|
1460
1613
|
|
|
1461
1614
|
# How many runs `status` prints, and how many `run list` returns for status
|
|
@@ -1531,7 +1684,7 @@ usage() {
|
|
|
1531
1684
|
echo " remote-run.sh open <branch> [--repo <root>]" >&2
|
|
1532
1685
|
echo " remote-run.sh deliver <branch> <bundle_dir> [--repo <root>]" >&2
|
|
1533
1686
|
echo " remote-run.sh collect <branch> [--pr <n>] [--repo <root>]" >&2
|
|
1534
|
-
echo " remote-run.sh control [--repo <root>]" >&2
|
|
1687
|
+
echo " remote-run.sh control [--needs-agent] [--repo <root>]" >&2
|
|
1535
1688
|
[ "${verb-}" != save ] || exit "$EXIT_OK"
|
|
1536
1689
|
exit "$EXIT_USAGE"
|
|
1537
1690
|
}
|
|
@@ -1631,6 +1784,7 @@ reviewers_arg=""
|
|
|
1631
1784
|
allow_no_run=0
|
|
1632
1785
|
pr_arg=""
|
|
1633
1786
|
branch_gone=0
|
|
1787
|
+
needs_agent=0
|
|
1634
1788
|
|
|
1635
1789
|
while [ "$#" -gt 0 ]; do
|
|
1636
1790
|
case "$1" in
|
|
@@ -1691,6 +1845,9 @@ while [ "$#" -gt 0 ]; do
|
|
|
1691
1845
|
--branch-gone)
|
|
1692
1846
|
[ "$verb" = stop ] || usage "$1 is a stop option"
|
|
1693
1847
|
branch_gone=1; shift ;;
|
|
1848
|
+
--needs-agent)
|
|
1849
|
+
[ "$verb" = control ] || usage "$1 is a control option"
|
|
1850
|
+
needs_agent=1; shift ;;
|
|
1694
1851
|
-*)
|
|
1695
1852
|
usage "unknown option '$1'" ;;
|
|
1696
1853
|
*)
|
|
@@ -2674,6 +2831,23 @@ ANSWERS_SHAPE='env.HARNESS_INPUT_ANSWERS | fromjson
|
|
|
2674
2831
|
and all(to_entries[]; (.value | type) == "string"
|
|
2675
2832
|
and (.key | explode | length > 0 and .[0] != 48 and all(.[]; . >= 48 and . <= 57)))'
|
|
2676
2833
|
|
|
2834
|
+
# sanitise($login; $handle), string to string, for agent-written text a reply
|
|
2835
|
+
# quotes: no `<!--` survives to read as a harness marker, and every `@<login>`
|
|
2836
|
+
# but the commenter's and the handle gets U+200B after the `@`, so it notifies
|
|
2837
|
+
# nobody.
|
|
2838
|
+
JQ_DEF_SANITISE='def sanitise($login; $handle):
|
|
2839
|
+
gsub("<!--"; "<!--")
|
|
2840
|
+
| gsub("@(?<l>[A-Za-z0-9][A-Za-z0-9-]*)";
|
|
2841
|
+
if (.l | ascii_downcase) == ($login | ascii_downcase)
|
|
2842
|
+
or (("@" + .l) | ascii_downcase) == ($handle | ascii_downcase)
|
|
2843
|
+
then "@" + .l else "@" + .l end);'
|
|
2844
|
+
|
|
2845
|
+
# fence, string to string: the code fence that quotes the input — one backtick
|
|
2846
|
+
# longer than its longest backtick run, at least three.
|
|
2847
|
+
JQ_DEF_FENCE='def fence:
|
|
2848
|
+
(([match("`+"; "g") | .length] | max) // 0) as $m
|
|
2849
|
+
| "`" * ([$m + 1, 3] | max);'
|
|
2850
|
+
|
|
2677
2851
|
verb_restore() {
|
|
2678
2852
|
local id download status_file clar n tmp status_source
|
|
2679
2853
|
if [ "$resume" = answer ]; then
|
|
@@ -4415,6 +4589,35 @@ forge_pr_var() {
|
|
|
4415
4589
|
return 0
|
|
4416
4590
|
}
|
|
4417
4591
|
|
|
4592
|
+
# forge_gone_prs_var <branch> — FORGE_GONE_PRS, the space-separated numbers of
|
|
4593
|
+
# the pull requests of <branch>, in any state, from this repository, not merged
|
|
4594
|
+
# and still labelled `STATE_LABEL_PREFIX` running, parked or paused: the states
|
|
4595
|
+
# control_close stops, so a pull request an earlier stop or a merge settled is
|
|
4596
|
+
# left out. Stands in for forge_recognised, which cannot read a deleted branch.
|
|
4597
|
+
# A failed listing, or one not the expected JSON, is one line and an empty list.
|
|
4598
|
+
# Always 0.
|
|
4599
|
+
FORGE_GONE_PRS=""
|
|
4600
|
+
forge_gone_prs_var() {
|
|
4601
|
+
local sel
|
|
4602
|
+
FORGE_GONE_PRS=""
|
|
4603
|
+
if ! gh_call pr list --repo "$FORGE_REPO" --head "$1" --state all --json number,isCrossRepository,mergedAt,labels --limit 10; then
|
|
4604
|
+
echo "remote-run.sh: report: listing the pull requests of $1 in every state failed: $GH_ERR" >&2
|
|
4605
|
+
return 0
|
|
4606
|
+
fi
|
|
4607
|
+
if ! sel=$(printf '%s' "$GH_OUT" | jq -r --arg p "$STATE_LABEL_PREFIX" '
|
|
4608
|
+
if type == "array" then
|
|
4609
|
+
[.[] | select(type == "object" and .isCrossRepository == false and .mergedAt == null
|
|
4610
|
+
and (.number | type) == "number"
|
|
4611
|
+
and ([.labels[]? | .name? | strings] | any(. == ($p + "running") or . == ($p + "parked") or . == ($p + "paused"))))
|
|
4612
|
+
| .number | tostring] | join(" ")
|
|
4613
|
+
else error end' 2>/dev/null); then
|
|
4614
|
+
echo "remote-run.sh: report: listing the pull requests of $1 in every state failed: its pr list is not the expected JSON" >&2
|
|
4615
|
+
return 0
|
|
4616
|
+
fi
|
|
4617
|
+
FORGE_GONE_PRS="$sel"
|
|
4618
|
+
return 0
|
|
4619
|
+
}
|
|
4620
|
+
|
|
4418
4621
|
# forge_dispatch_engine_var <branch> <created_at_iso> — FORGE_DISPATCH_ENGINE,
|
|
4419
4622
|
# the engine the dispatch that created the run at <created_at_iso> recorded in
|
|
4420
4623
|
# its comment on the branch's issue or open pull request, or empty. Only a
|
|
@@ -4589,10 +4792,12 @@ forge_question_body() {
|
|
|
4589
4792
|
# comment (on `parked`, one per open question) and the state label, by the
|
|
4590
4793
|
# target rule above. Read on `stopped` only: <pr>, an explicit pull request
|
|
4591
4794
|
# that is the target whatever its state; `gone`, the branch deleted on GitHub,
|
|
4592
|
-
# its issue read from the task prompt at <sha
|
|
4593
|
-
#
|
|
4594
|
-
#
|
|
4595
|
-
#
|
|
4795
|
+
# its issue read from the task prompt at <sha>, and then each pull request
|
|
4796
|
+
# forge_gone_prs_var lists given the same comment, the label and its progress
|
|
4797
|
+
# comment marked stopped; nothing is posted only when neither is known. Every
|
|
4798
|
+
# event but `stopped` is withheld when the branch's newest `harness stop` run is
|
|
4799
|
+
# newer than its newest `harness run` run, read from a fresh listing.
|
|
4800
|
+
# `not_started` reads REPORT_NOT_STARTED_STATE (`paused`, else `failed`) and
|
|
4596
4801
|
# REPORT_NOT_STARTED_ENGINE (empty or the recovered engine), set by its caller.
|
|
4597
4802
|
# Always 0.
|
|
4598
4803
|
REPORT_NOT_STARTED_STATE=""
|
|
@@ -4600,6 +4805,7 @@ REPORT_NOT_STARTED_ENGINE=""
|
|
|
4600
4805
|
forge_report() {
|
|
4601
4806
|
local event="$1" br="$2" note="${3-}" pr="${4-}" gone="${5-}" gone_sha="${6-}" state reason="" resume_at="" when registry_file
|
|
4602
4807
|
local target kind text tmp made_tmp="" file trigger_label stopped state_rel="" count n route engine=""
|
|
4808
|
+
local gone_prs="" reported="" folded=""
|
|
4603
4809
|
case "$event" in
|
|
4604
4810
|
parked|park_loop) state=parked ;;
|
|
4605
4811
|
paused) state=paused ;;
|
|
@@ -4648,8 +4854,11 @@ forge_report() {
|
|
|
4648
4854
|
if [ -n "$pr" ]; then
|
|
4649
4855
|
FORGE_PR="$pr"
|
|
4650
4856
|
elif [ -n "$gone" ]; then
|
|
4651
|
-
#
|
|
4857
|
+
# No pull request of a deleted head is open, so the run's unfinished ones
|
|
4858
|
+
# are found in every state by forge_gone_prs_var.
|
|
4652
4859
|
FORGE_PR=""
|
|
4860
|
+
forge_gone_prs_var "$br"
|
|
4861
|
+
gone_prs="$FORGE_GONE_PRS"
|
|
4653
4862
|
else
|
|
4654
4863
|
forge_pr_var "$br" || FORGE_PR=""
|
|
4655
4864
|
fi
|
|
@@ -4661,6 +4870,8 @@ forge_report() {
|
|
|
4661
4870
|
target="$FORGE_PR"; kind=pr
|
|
4662
4871
|
elif [ -n "$FORGE_ISSUE" ]; then
|
|
4663
4872
|
target="$FORGE_ISSUE"; kind=issue
|
|
4873
|
+
elif [ -n "$gone_prs" ]; then
|
|
4874
|
+
target=""; kind=pr
|
|
4664
4875
|
else
|
|
4665
4876
|
echo "remote-run.sh: report: $br has no open pull request and no issue it was started from; nothing posted"
|
|
4666
4877
|
return 0
|
|
@@ -4673,6 +4884,14 @@ forge_report() {
|
|
|
4673
4884
|
resume_at=$(hr_registry_get "$registry_file" "$br" usage_resume_at)
|
|
4674
4885
|
[ "$event" != failed ] || engine=$(hr_registry_get "$registry_file" "$br" engine)
|
|
4675
4886
|
fi
|
|
4887
|
+
case "$event" in
|
|
4888
|
+
parked|park_loop)
|
|
4889
|
+
if [ "$reason" = user ]; then
|
|
4890
|
+
folded="$PAUSE_FOLDED_NOTE"
|
|
4891
|
+
[ "$event" != park_loop ] || folded="$PAUSE_FOLDED_HOLD_NOTE"
|
|
4892
|
+
if [ -z "$note" ]; then note="$folded"; else note="$note"$'\n\n'"$folded"; fi
|
|
4893
|
+
fi ;;
|
|
4894
|
+
esac
|
|
4676
4895
|
|
|
4677
4896
|
case "$event" in
|
|
4678
4897
|
paused)
|
|
@@ -4758,22 +4977,75 @@ forge_report() {
|
|
|
4758
4977
|
echo "remote-run.sh: report: cannot create question $n's comment file for #$target; not posted" >&2
|
|
4759
4978
|
fi
|
|
4760
4979
|
done
|
|
4761
|
-
elif [ -n "$
|
|
4762
|
-
|
|
4763
|
-
printf '%s\n' "$text"
|
|
4764
|
-
[ -z "$note" ] || printf '\n%s\n' "$note"
|
|
4765
|
-
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
4766
|
-
} >"$file"
|
|
4767
|
-
forge_comment "$target" "$event" "$br" "$file" || :
|
|
4768
|
-
rm -f "$file"
|
|
4769
|
-
else
|
|
4770
|
-
echo "remote-run.sh: report: cannot create the comment file for #$target; no comment posted" >&2
|
|
4980
|
+
elif [ -n "$target" ]; then
|
|
4981
|
+
forge_report_text "$target" "$event" "$br" "$tmp" "$text" "$note"
|
|
4771
4982
|
fi
|
|
4772
|
-
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
4773
4983
|
|
|
4774
4984
|
[ -z "$FORGE_ISSUE" ] || forge_set_state "$FORGE_ISSUE" "$state" || :
|
|
4775
4985
|
[ -z "$FORGE_PR" ] || forge_set_state "$FORGE_PR" "$state" || :
|
|
4776
|
-
|
|
4986
|
+
if [ "$event" = stopped ]; then
|
|
4987
|
+
[ -z "$FORGE_PR" ] || forge_progress_stopped "$FORGE_PR" "$br"
|
|
4988
|
+
fi
|
|
4989
|
+
reported="${target:+#$target}"
|
|
4990
|
+
for n in $gone_prs; do
|
|
4991
|
+
forge_report_text "$n" "$event" "$br" "$tmp" "$text" "$note"
|
|
4992
|
+
forge_set_state "$n" "$state" || :
|
|
4993
|
+
forge_progress_stopped "$n" "$br"
|
|
4994
|
+
reported="${reported:+$reported, }#$n"
|
|
4995
|
+
done
|
|
4996
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
4997
|
+
echo "remote-run.sh: report: $event on $br reported on $reported"
|
|
4998
|
+
return 0
|
|
4999
|
+
}
|
|
5000
|
+
|
|
5001
|
+
# forge_report_text <number> <event> <branch> <tmp_dir> <text> <note> — post
|
|
5002
|
+
# forge_report's one comment, <text>, <note> and the run URL, on <number>.
|
|
5003
|
+
forge_report_text() {
|
|
5004
|
+
local number="$1" event="$2" br="$3" tmp="$4" text="$5" note="$6" file
|
|
5005
|
+
if [ -z "$tmp" ] || ! file=$(mktemp "$tmp/harness-report-comment.XXXXXX"); then
|
|
5006
|
+
echo "remote-run.sh: report: cannot create the comment file for #$number; no comment posted" >&2
|
|
5007
|
+
return 0
|
|
5008
|
+
fi
|
|
5009
|
+
{
|
|
5010
|
+
printf '%s\n' "$text"
|
|
5011
|
+
[ -z "$note" ] || printf '\n%s\n' "$note"
|
|
5012
|
+
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
5013
|
+
} >"$file"
|
|
5014
|
+
forge_comment "$number" "$event" "$br" "$file" || :
|
|
5015
|
+
rm -f "$file"
|
|
5016
|
+
}
|
|
5017
|
+
|
|
5018
|
+
# forge_progress_comment_var <pr> <marker> [any-round] — the one lookup of a
|
|
5019
|
+
# progress comment: one paginated listing of <pr>'s comments, then the newest
|
|
5020
|
+
# `github-actions[bot]` comment with a numeric id whose last non-empty line is
|
|
5021
|
+
# <marker>, or, with `any-round`, <marker> with ` round=<digits>` before its
|
|
5022
|
+
# ` -->`. Sets PROGRESS_COMMENT_ID and PROGRESS_COMMENT_BODY, both empty when
|
|
5023
|
+
# none is picked. 0 when the listing parsed, picked or not; 1 when gh refused it
|
|
5024
|
+
# (GH_ERR as gh_call set it); 2 when it is not the expected JSON. Prints
|
|
5025
|
+
# nothing. No temporary file: forge_progress needs its own before this lookup,
|
|
5026
|
+
# forge_progress_stopped only after it.
|
|
5027
|
+
PROGRESS_COMMENT_ID=""
|
|
5028
|
+
PROGRESS_COMMENT_BODY=""
|
|
5029
|
+
forge_progress_comment_var() {
|
|
5030
|
+
local pr="$1" marker="$2" any="${3-}" pick id body
|
|
5031
|
+
PROGRESS_COMMENT_ID=""
|
|
5032
|
+
PROGRESS_COMMENT_BODY=""
|
|
5033
|
+
gh_call api --paginate "repos/$FORGE_REPO/issues/$pr/comments" --jq '.[] | {id, login: .user.login, body}' || return 1
|
|
5034
|
+
pick=$(printf '%s' "$GH_OUT" | jq -s -c --arg m "$marker" --arg any "$any" '
|
|
5035
|
+
def hit: . == $m
|
|
5036
|
+
or ($any == "any-round"
|
|
5037
|
+
and (($m | sub(" -->$"; "")) as $stem
|
|
5038
|
+
| startswith($stem) and (.[($stem | length):] | test("^ round=[0-9]+ -->$"))));
|
|
5039
|
+
[.[] | select(type == "object" and .login == "github-actions[bot]" and (.id | type) == "number")
|
|
5040
|
+
| select((.body // "") | gsub("\r"; "") | split("\n") | map(select(test("^\\s*$") | not)) | last // "" | hit)]
|
|
5041
|
+
| max_by(.id) // empty
|
|
5042
|
+
| {id, body: (.body // "")}' 2>/dev/null) || return 2
|
|
5043
|
+
[ -n "$pick" ] || return 0
|
|
5044
|
+
id=$(printf '%s' "$pick" | jq -r '.id' 2>/dev/null) || return 2
|
|
5045
|
+
# The sentinel keeps the body's trailing newlines, which $(…) would strip.
|
|
5046
|
+
body=$(printf '%s' "$pick" | jq -j '.body' 2>/dev/null && printf x) || return 2
|
|
5047
|
+
PROGRESS_COMMENT_ID="$id"
|
|
5048
|
+
PROGRESS_COMMENT_BODY="${body%x}"
|
|
4777
5049
|
return 0
|
|
4778
5050
|
}
|
|
4779
5051
|
|
|
@@ -4783,7 +5055,7 @@ forge_report() {
|
|
|
4783
5055
|
# issue fallback, no label. Always 0.
|
|
4784
5056
|
forge_progress() {
|
|
4785
5057
|
local br="$1" status state_rel phases engine round p line i first=1 marker
|
|
4786
|
-
local tmp made_tmp="" file
|
|
5058
|
+
local tmp made_tmp="" file id same
|
|
4787
5059
|
local -a labels states
|
|
4788
5060
|
if ! forge_on; then
|
|
4789
5061
|
echo "remote-run.sh: report: the forge coupling is off (forge github and execution.target github-actions); nothing posted"
|
|
@@ -4856,24 +5128,23 @@ forge_progress() {
|
|
|
4856
5128
|
} >"$file"
|
|
4857
5129
|
{ cat "$file"; printf '\n%s\n' "$marker"; } >"$file.full"
|
|
4858
5130
|
|
|
4859
|
-
|
|
5131
|
+
status=0
|
|
5132
|
+
forge_progress_comment_var "$FORGE_PR" "$marker" || status=$?
|
|
5133
|
+
if [ "$status" -eq 1 ]; then
|
|
4860
5134
|
echo "::warning::remote-run.sh: report: listing the comments of #$FORGE_PR was refused, so the progress is not posted: $GH_ERR"
|
|
4861
|
-
elif
|
|
4862
|
-
def norm: gsub("\r"; "") | sub("\n+$"; "");
|
|
4863
|
-
[.[] | select(type == "object" and .login == "github-actions[bot]" and (.id | type) == "number")
|
|
4864
|
-
| select(((.body // "") | gsub("\r"; "") | split("\n") | map(select(test("^\\s*$") | not)) | last // "") == $m)]
|
|
4865
|
-
| max_by(.id) // empty
|
|
4866
|
-
| "\(.id) \(if ((.body // "") | norm) == ($want | norm) then "same" else "differs" end)"' 2>/dev/null); then
|
|
5135
|
+
elif [ "$status" -ne 0 ]; then
|
|
4867
5136
|
echo "::warning::remote-run.sh: report: the comments of #$FORGE_PR are not the expected JSON, so the progress is not posted"
|
|
4868
|
-
elif [ -z "$
|
|
5137
|
+
elif [ -z "$PROGRESS_COMMENT_ID" ]; then
|
|
4869
5138
|
if forge_comment "$FORGE_PR" progress "$br" "$file" "" "" "$round"; then
|
|
4870
5139
|
echo "remote-run.sh: report: progress on $br posted on #$FORGE_PR"
|
|
4871
5140
|
else
|
|
4872
5141
|
echo "::warning::remote-run.sh: report: posting the progress comment on #$FORGE_PR was refused: $GH_ERR"
|
|
4873
5142
|
fi
|
|
4874
5143
|
else
|
|
4875
|
-
id="$
|
|
4876
|
-
same
|
|
5144
|
+
id="$PROGRESS_COMMENT_ID"
|
|
5145
|
+
same=$(jq -n -r --arg body "$PROGRESS_COMMENT_BODY" --arg want "$(cat "$file.full")" '
|
|
5146
|
+
def norm: gsub("\r"; "") | sub("\n+$"; "");
|
|
5147
|
+
if ($body | norm) == ($want | norm) then "same" else "differs" end' 2>/dev/null) || same=differs
|
|
4877
5148
|
if [ "$same" = same ]; then
|
|
4878
5149
|
echo "remote-run.sh: report: the progress comment on #$FORGE_PR is already current"
|
|
4879
5150
|
elif gh_call api --method PATCH "repos/$FORGE_REPO/issues/comments/$id" -F "body=@$file.full"; then
|
|
@@ -4887,6 +5158,58 @@ forge_progress() {
|
|
|
4887
5158
|
return 0
|
|
4888
5159
|
}
|
|
4889
5160
|
|
|
5161
|
+
# forge_progress_stopped <pr> <branch> — after a stop, rewrite each
|
|
5162
|
+
# `- <label>: in progress` line of <branch>'s progress comment on <pr>, the task
|
|
5163
|
+
# run's or any round's, to `- <label>: stopped`, leaving every other byte; the
|
|
5164
|
+
# cancelled job's last progress pass is withheld, so nothing else does. Gated by
|
|
5165
|
+
# hr_progress_comments; no comment or no such line is no edit. Always 0.
|
|
5166
|
+
forge_progress_stopped() {
|
|
5167
|
+
local pr="$1" br="$2" status count tmp made_tmp="" file
|
|
5168
|
+
status=0
|
|
5169
|
+
hr_progress_comments "$root" || status=$?
|
|
5170
|
+
case "$status" in
|
|
5171
|
+
0) ;;
|
|
5172
|
+
1) echo "remote-run.sh: report: progress comments are off by execution.progressComments; nothing marked stopped"; return 0 ;;
|
|
5173
|
+
*) echo "remote-run.sh: report: execution.progressComments is unreadable; nothing marked stopped"; return 0 ;;
|
|
5174
|
+
esac
|
|
5175
|
+
status=0
|
|
5176
|
+
forge_progress_comment_var "$pr" "$(forge_marker progress "$br")" any-round || status=$?
|
|
5177
|
+
if [ "$status" -eq 1 ]; then
|
|
5178
|
+
echo "::warning::remote-run.sh: report: listing the comments of #$pr was refused, so its progress comment is not marked stopped: $GH_ERR"
|
|
5179
|
+
return 0
|
|
5180
|
+
elif [ "$status" -ne 0 ]; then
|
|
5181
|
+
echo "::warning::remote-run.sh: report: the comments of #$pr are not the expected JSON, so its progress comment is not marked stopped"
|
|
5182
|
+
return 0
|
|
5183
|
+
fi
|
|
5184
|
+
# A trailing carriage return is kept, so a CRLF body is rewritten in its own line endings.
|
|
5185
|
+
count=0
|
|
5186
|
+
[ -z "$PROGRESS_COMMENT_ID" ] \
|
|
5187
|
+
|| count=$(jq -n -r --arg b "$PROGRESS_COMMENT_BODY" \
|
|
5188
|
+
'[$b | split("\n")[] | select(test("^- .+: in progress\r?$"))] | length' 2>/dev/null) || count=0
|
|
5189
|
+
if [ "$count" = 0 ]; then
|
|
5190
|
+
echo "remote-run.sh: report: #$pr has no progress comment of $br in progress; nothing to mark stopped"
|
|
5191
|
+
return 0
|
|
5192
|
+
fi
|
|
5193
|
+
tmp="${RUNNER_TEMP-}"
|
|
5194
|
+
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
5195
|
+
tmp=$(mktemp -d) || tmp=""
|
|
5196
|
+
made_tmp="$tmp"
|
|
5197
|
+
fi
|
|
5198
|
+
if [ -z "$tmp" ] || ! file=$(mktemp "$tmp/harness-progress-stopped.XXXXXX"); then
|
|
5199
|
+
echo "::warning::remote-run.sh: report: cannot create the progress comment file for #$pr; nothing marked stopped"
|
|
5200
|
+
elif ! jq -n -j --arg b "$PROGRESS_COMMENT_BODY" '$b | split("\n")
|
|
5201
|
+
| map(sub("^(?<l>- .+): in progress(?<cr>\r?)$"; "\(.l): stopped\(.cr)")) | join("\n")' >"$file" 2>/dev/null; then
|
|
5202
|
+
echo "::warning::remote-run.sh: report: cannot write the stopped progress comment for #$pr; nothing marked stopped"
|
|
5203
|
+
elif gh_call api --method PATCH "repos/$FORGE_REPO/issues/comments/$PROGRESS_COMMENT_ID" -F "body=@$file"; then
|
|
5204
|
+
echo "remote-run.sh: report: progress of $br marked stopped in comment $PROGRESS_COMMENT_ID on #$pr"
|
|
5205
|
+
else
|
|
5206
|
+
echo "::warning::remote-run.sh: report: editing the progress comment $PROGRESS_COMMENT_ID on #$pr was refused: $GH_ERR"
|
|
5207
|
+
fi
|
|
5208
|
+
[ -z "${file-}" ] || rm -f "$file"
|
|
5209
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
5210
|
+
return 0
|
|
5211
|
+
}
|
|
5212
|
+
|
|
4890
5213
|
verb_report() {
|
|
4891
5214
|
if [ "$report_event" = progress ]; then
|
|
4892
5215
|
forge_progress "$branch"
|
|
@@ -5416,11 +5739,17 @@ control_cleanup() {
|
|
|
5416
5739
|
# non-empty value adds ` engine=<engine>` to every later reply's marker.
|
|
5417
5740
|
CONTROL_REPLY_ENGINE=""
|
|
5418
5741
|
|
|
5742
|
+
# How control_mention read the mention a verb arm is carrying out; non-empty,
|
|
5743
|
+
# it opens every reply, an arm's refusal included.
|
|
5744
|
+
CONTROL_MENTION_NOTE=""
|
|
5745
|
+
|
|
5419
5746
|
# control_post <text> — post <text> on CONTROL_NUMBER as a `reply` comment,
|
|
5420
|
-
#
|
|
5421
|
-
# it cannot
|
|
5747
|
+
# after CONTROL_MENTION_NOTE and a blank line when that is set, its marker
|
|
5748
|
+
# carrying CONTROL_REPLY_ENGINE; 1, after an `::error::` line, when it cannot
|
|
5749
|
+
# be posted.
|
|
5422
5750
|
control_post() {
|
|
5423
5751
|
local text="$1" file status=0
|
|
5752
|
+
[ -z "$CONTROL_MENTION_NOTE" ] || text="$CONTROL_MENTION_NOTE"$'\n\n'"$text"
|
|
5424
5753
|
if ! forge_repo_var; then
|
|
5425
5754
|
echo "::error::remote-run.sh: control: the reply on #$CONTROL_NUMBER could not be posted: $GH_ERR"
|
|
5426
5755
|
return 1
|
|
@@ -6313,10 +6642,9 @@ AUTHORS
|
|
|
6313
6642
|
RC_REVIEWERS=$(printf '%s' "$kept" | jq -r '
|
|
6314
6643
|
reduce (.reviews[] | .user.login) as $l ([]; if any(.[]; . == $l) then . else . + [$l] end) | join(",")')
|
|
6315
6644
|
|
|
6316
|
-
# A hunk
|
|
6317
|
-
#
|
|
6318
|
-
|
|
6319
|
-
if ! text=$(printf '%s' "$kept" | jq -j --arg pr "$pr" --arg at "$collected_at" --arg marker "$COMMENT_MARKER" --arg state "$REVIEW_ROUND_STATE" '
|
|
6645
|
+
# A hunk is fenced by JQ_DEF_FENCE. The trailing `x` keeps the text's final
|
|
6646
|
+
# newline through the substitution.
|
|
6647
|
+
if ! text=$(printf '%s' "$kept" | jq -j --arg pr "$pr" --arg at "$collected_at" --arg marker "$COMMENT_MARKER" --arg state "$REVIEW_ROUND_STATE" "$JQ_DEF_FENCE"'
|
|
6320
6648
|
def nl: if endswith("\n") then . else . + "\n" end;
|
|
6321
6649
|
.reviews as $rv | .comments as $cm
|
|
6322
6650
|
| ($rv | map(
|
|
@@ -6334,8 +6662,7 @@ AUTHORS
|
|
|
6334
6662
|
+ (if ($cm | length) == 0 then "" else
|
|
6335
6663
|
"\n## Inline comments\n" + ($cm | map(
|
|
6336
6664
|
(.diff_hunk // "") as $h
|
|
6337
|
-
| (
|
|
6338
|
-
| ("`" * ([$m + 1, 3] | max)) as $f
|
|
6665
|
+
| ($h | fence) as $f
|
|
6339
6666
|
| "\n### `" + (.path // "") + "`"
|
|
6340
6667
|
+ (if .line != null then ", line \(.line)"
|
|
6341
6668
|
elif .original_line != null then ", original line \(.original_line) (outdated)"
|
|
@@ -6550,11 +6877,13 @@ control_review_intake() {
|
|
|
6550
6877
|
}
|
|
6551
6878
|
|
|
6552
6879
|
# control_comment_intake — read an `issue_comment` event; returns 1 when it is
|
|
6553
|
-
# ignored, after one line.
|
|
6880
|
+
# ignored, after one line. The exact form sets CONTROL_VERB / CONTROL_ARGS; any
|
|
6881
|
+
# other mention of the handle as a word sets CONTROL_MENTION and no verb.
|
|
6554
6882
|
CONTROL_SENDER_TYPE=""
|
|
6555
6883
|
CONTROL_IS_PR=""
|
|
6884
|
+
CONTROL_MENTION=0
|
|
6556
6885
|
control_comment_intake() {
|
|
6557
|
-
local action first word rest handle
|
|
6886
|
+
local action first word rest handle verb body re
|
|
6558
6887
|
{ event_field '.action // ""' && action="$EVENT_VALUE" \
|
|
6559
6888
|
&& event_field '.comment.body // ""' && CONTROL_BODY="$EVENT_VALUE" \
|
|
6560
6889
|
&& event_field '.issue.number // ""' && CONTROL_NUMBER="$EVENT_VALUE" \
|
|
@@ -6581,15 +6910,27 @@ control_comment_intake() {
|
|
|
6581
6910
|
rest=${first#"$word"}
|
|
6582
6911
|
rest=${rest#"${rest%%[!$' \t']*}"}
|
|
6583
6912
|
handle=$(printf '%s' "$COMMAND_HANDLE" | tr '[:upper:]' '[:lower:]')
|
|
6584
|
-
if [ "$(printf '%s' "$word" | tr '[:upper:]' '[:lower:]')"
|
|
6585
|
-
|
|
6586
|
-
|
|
6913
|
+
if [ "$(printf '%s' "$word" | tr '[:upper:]' '[:lower:]')" = "$handle" ]; then
|
|
6914
|
+
word=${rest%%[$' \t']*}
|
|
6915
|
+
verb=$(printf '%s' "$word" | tr '[:upper:]' '[:lower:]')
|
|
6916
|
+
case " $COMMAND_VERBS " in
|
|
6917
|
+
*" $verb "*)
|
|
6918
|
+
if [ -n "$verb" ]; then
|
|
6919
|
+
CONTROL_VERB="$verb"
|
|
6920
|
+
CONTROL_ARGS=${rest#"$word"}
|
|
6921
|
+
CONTROL_ARGS=${CONTROL_ARGS#"${CONTROL_ARGS%%[!$' \t']*}"}
|
|
6922
|
+
return 0
|
|
6923
|
+
fi ;;
|
|
6924
|
+
esac
|
|
6587
6925
|
fi
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6926
|
+
body=$(printf '%s' "$CONTROL_BODY" | tr '[:upper:]' '[:lower:]')
|
|
6927
|
+
re="(^|[^a-z0-9])${handle}([^a-z0-9-]|\$)"
|
|
6928
|
+
if [[ $body =~ $re ]]; then
|
|
6929
|
+
CONTROL_MENTION=1
|
|
6930
|
+
return 0
|
|
6931
|
+
fi
|
|
6932
|
+
echo "remote-run.sh: control: ignored, the comment does not mention $COMMAND_HANDLE"
|
|
6933
|
+
return 1
|
|
6593
6934
|
}
|
|
6594
6935
|
|
|
6595
6936
|
# The close: CLOSE_KIND (`issue`, `pr_closed`, `pr_merged`, `deleted`) and
|
|
@@ -6730,10 +7071,11 @@ control_close() {
|
|
|
6730
7071
|
CONTROL_BRANCH="$b"
|
|
6731
7072
|
if [ "$CLOSE_KIND" = pr_closed ] || [ "$CLOSE_KIND" = pr_merged ]; then
|
|
6732
7073
|
# GitHub closes a pull request whose head is deleted; the `delete` event's
|
|
6733
|
-
# own job stops that run from the default branch
|
|
7074
|
+
# own job stops that run from the default branch and reports it on this
|
|
7075
|
+
# pull request, so this one stays quiet.
|
|
6734
7076
|
remote_branch_exists "$b"
|
|
6735
7077
|
case $? in
|
|
6736
|
-
1) control_close_ignore "the branch \`$b\` of pull request #$CONTROL_NUMBER is gone from origin; the deletion's own job stops the run" ;;
|
|
7078
|
+
1) control_close_ignore "the branch \`$b\` of pull request #$CONTROL_NUMBER is gone from origin; the deletion's own job stops the run and reports it here" ;;
|
|
6737
7079
|
2) echo "remote-run.sh: control: whether \`$b\` exists on origin could not be checked ($REMOTE_BRANCH_ERR); proceeding" ;;
|
|
6738
7080
|
esac
|
|
6739
7081
|
fi
|
|
@@ -6771,9 +7113,416 @@ control_close() {
|
|
|
6771
7113
|
exit "$EXIT_OK"
|
|
6772
7114
|
}
|
|
6773
7115
|
|
|
7116
|
+
# control_commands_way — prints the way on that lists every command and names
|
|
7117
|
+
# the documentation, the unknown-verb refusal's and every mention refusal's.
|
|
7118
|
+
control_commands_way() {
|
|
7119
|
+
local v verbs=""
|
|
7120
|
+
for v in $COMMAND_VERBS; do
|
|
7121
|
+
[ "$v" != answer ] || v="answer [<n>]"
|
|
7122
|
+
verbs="$verbs${verbs:+, }\`$COMMAND_HANDLE $v\`"
|
|
7123
|
+
done
|
|
7124
|
+
printf '%s' "The commands are $verbs; \`docs/github-run-control.md\` in the harness documentation states each."
|
|
7125
|
+
}
|
|
7126
|
+
|
|
7127
|
+
# The credential `verb_control`'s first statements take out of the environment;
|
|
7128
|
+
# never exported, so only the agent subshell in control_mention_session sees it.
|
|
7129
|
+
MENTION_OAUTH=""
|
|
7130
|
+
MENTION_API=""
|
|
7131
|
+
# The job's `GH_TOKEN` in the base64 form `actions/checkout` persists into the
|
|
7132
|
+
# checkout's git configuration; never exported.
|
|
7133
|
+
MENTION_JOB_TOKEN_B64=""
|
|
7134
|
+
|
|
7135
|
+
# mention_has_credential <string> — 0 when <string> contains, verbatim, a
|
|
7136
|
+
# non-empty saved credential value, or the job's `GH_TOKEN` raw or in its
|
|
7137
|
+
# persisted base64 form. An encoded or split copy is not caught.
|
|
7138
|
+
mention_has_credential() {
|
|
7139
|
+
if [ -n "$MENTION_OAUTH" ]; then
|
|
7140
|
+
case "$1" in *"$MENTION_OAUTH"*) return 0 ;; esac
|
|
7141
|
+
fi
|
|
7142
|
+
if [ -n "$MENTION_API" ]; then
|
|
7143
|
+
case "$1" in *"$MENTION_API"*) return 0 ;; esac
|
|
7144
|
+
fi
|
|
7145
|
+
if [ -n "${GH_TOKEN-}" ]; then
|
|
7146
|
+
case "$1" in *"$GH_TOKEN"*) return 0 ;; esac
|
|
7147
|
+
fi
|
|
7148
|
+
if [ -n "$MENTION_JOB_TOKEN_B64" ]; then
|
|
7149
|
+
case "$1" in *"$MENTION_JOB_TOKEN_B64"*) return 0 ;; esac
|
|
7150
|
+
fi
|
|
7151
|
+
return 1
|
|
7152
|
+
}
|
|
7153
|
+
|
|
7154
|
+
# mention_cap <file> <max bytes> <note> — over <max bytes>, <file> keeps its
|
|
7155
|
+
# first <max bytes> cut back to the last whole line, then the line <note>.
|
|
7156
|
+
mention_cap() {
|
|
7157
|
+
local file="$1" max="$2" note="$3" size
|
|
7158
|
+
size=$(wc -c <"$file") || return 1
|
|
7159
|
+
size=${size//[!0-9]/}
|
|
7160
|
+
[ "${size:-0}" -gt "$max" ] || return 0
|
|
7161
|
+
head -c "$max" "$file" >"$file.cut" || return 1
|
|
7162
|
+
if [ -n "$(tail -c 1 "$file.cut")" ]; then
|
|
7163
|
+
sed '$d' "$file.cut" >"$file.line" || return 1
|
|
7164
|
+
mv -f "$file.line" "$file.cut" || return 1
|
|
7165
|
+
fi
|
|
7166
|
+
printf '%s\n' "$note" >>"$file.cut"
|
|
7167
|
+
mv -f "$file.cut" "$file"
|
|
7168
|
+
}
|
|
7169
|
+
|
|
7170
|
+
# control_mention_context <dir> — comment.md, run.md and questions/ in <dir>,
|
|
7171
|
+
# every file capped at MENTION_FILE_MAX_BYTES; 1 when one cannot be written.
|
|
7172
|
+
control_mention_context() {
|
|
7173
|
+
local dir="$1" kind=issue n src f
|
|
7174
|
+
[ -z "$CONTROL_IS_PR" ] || kind="pull request"
|
|
7175
|
+
{
|
|
7176
|
+
printf 'handle: %s\n' "$COMMAND_HANDLE"
|
|
7177
|
+
printf 'Comment by @%s on %s #%s:\n\n' "$CONTROL_ACTOR" "$kind" "$CONTROL_NUMBER"
|
|
7178
|
+
printf '%s' "$CONTROL_BODY"
|
|
7179
|
+
} >"$dir/comment.md" || return 1
|
|
7180
|
+
{
|
|
7181
|
+
printf 'branch: %s\n' "$CONTROL_BRANCH"
|
|
7182
|
+
printf 'state: %s\n' "$CS_STATE"
|
|
7183
|
+
printf 'pause_reason: %s\n' "$CS_REASON"
|
|
7184
|
+
printf 'engine: %s\n' "$CS_ENGINE"
|
|
7185
|
+
printf 'open_questions: %s\n' "$CS_OPEN"
|
|
7186
|
+
printf 'detail: %s\n' "$CS_DETAIL"
|
|
7187
|
+
printf 'run_url: %s\n' "$CS_URL"
|
|
7188
|
+
printf 'run_status: %s\n' "$CS_RUN_STATUS"
|
|
7189
|
+
if [ "$CS_STOPPED" = 1 ]; then printf 'stopped: yes\n'; else printf 'stopped: no\n'; fi
|
|
7190
|
+
if ! control_ledger_next_var; then
|
|
7191
|
+
printf 'next: the flow-progress ledger could not be read\n'
|
|
7192
|
+
elif [ -z "$LEDGER_NEXT" ]; then
|
|
7193
|
+
printf 'next: every entry of the flow-progress ledger is ticked\n'
|
|
7194
|
+
else
|
|
7195
|
+
printf 'next: %s\n' "$LEDGER_NEXT"
|
|
7196
|
+
printf 'section: %s\n' "$LEDGER_SECTION"
|
|
7197
|
+
fi
|
|
7198
|
+
} >"$dir/run.md" || return 1
|
|
7199
|
+
mkdir "$dir/questions" || return 1
|
|
7200
|
+
for n in $CS_OPEN; do
|
|
7201
|
+
case "$n" in ''|*[!0-9]*) continue ;; esac
|
|
7202
|
+
src="$CS_DIR/clarifications/$CONTROL_BRANCH/question_$n.md"
|
|
7203
|
+
[ -f "$src" ] || continue
|
|
7204
|
+
cp "$src" "$dir/questions/question_$n.md" || return 1
|
|
7205
|
+
done
|
|
7206
|
+
for f in "$dir/comment.md" "$dir/run.md" "$dir"/questions/question_*.md; do
|
|
7207
|
+
[ -f "$f" ] || continue
|
|
7208
|
+
mention_cap "$f" "$MENTION_FILE_MAX_BYTES" "(cut at $MENTION_FILE_MAX_BYTES bytes)" || return 1
|
|
7209
|
+
done
|
|
7210
|
+
return 0
|
|
7211
|
+
}
|
|
7212
|
+
|
|
7213
|
+
# control_mention_context_extra <dir> — item.md, conversation.md and, on a pull
|
|
7214
|
+
# request, diff.patch in <dir>, each capped at MENTION_FILE_MAX_BYTES; a failed
|
|
7215
|
+
# read is one line in its file, never a refusal. 1 when a file cannot be
|
|
7216
|
+
# written. Every forge value reaches a file through `jq` or `printf '%s'` only.
|
|
7217
|
+
control_mention_context_extra() {
|
|
7218
|
+
local dir="$1" kind=issue own f
|
|
7219
|
+
[ -z "$CONTROL_IS_PR" ] || kind="pull request"
|
|
7220
|
+
if ! gh_call api "repos/$FORGE_REPO/issues/$CONTROL_NUMBER"; then
|
|
7221
|
+
printf 'The issue or pull request could not be read (%s).\n' "$GH_ERR" >"$dir/item.md" || return 1
|
|
7222
|
+
elif ! printf '%s' "$GH_OUT" | jq -j --arg kind "$kind" --arg n "$CONTROL_NUMBER" '
|
|
7223
|
+
"kind: \($kind)\nnumber: \($n)\ntitle: \(.title // "" | tostring)\nauthor: @\(.user.login // "" | tostring)\nurl: \(.html_url // "" | tostring)\n\n\(.body // "" | tostring)"' \
|
|
7224
|
+
>"$dir/item.md" 2>/dev/null; then
|
|
7225
|
+
printf 'The issue or pull request could not be read (its answer is not the expected JSON).\n' >"$dir/item.md" || return 1
|
|
7226
|
+
fi
|
|
7227
|
+
|
|
7228
|
+
# The commenter's own body goes to jq through a file: a comment can exceed
|
|
7229
|
+
# the kernel's bound on one argument.
|
|
7230
|
+
own="$dir.own"
|
|
7231
|
+
control_dirs="$control_dirs $own"
|
|
7232
|
+
printf '%s' "$CONTROL_BODY" >"$own" || return 1
|
|
7233
|
+
if ! gh_call api --paginate "repos/$FORGE_REPO/issues/$CONTROL_NUMBER/comments" --jq '.[] | {login: .user.login, at: .created_at, body: .body}'; then
|
|
7234
|
+
printf 'The comments of this %s could not be read (%s).\n' "$kind" "$GH_ERR" >"$dir/conversation.md" || return 1
|
|
7235
|
+
elif ! printf '%s' "$GH_OUT" | jq -s -j --arg actor "$CONTROL_ACTOR" --rawfile own "$own" \
|
|
7236
|
+
--arg marker "$COMMENT_MARKER" --arg kind "$kind" --argjson max "$MENTION_COMMENTS_MAX" '
|
|
7237
|
+
[.[] | objects] as $all
|
|
7238
|
+
| ([range(0; $all | length) | select($all[.].login == $actor and $all[.].body == $own)] | last) as $i
|
|
7239
|
+
| (if $i == null then $all else $all[:$i] end) as $before
|
|
7240
|
+
| (if ($before | length) > $max then $before[($before | length) - $max:] else $before end)
|
|
7241
|
+
| if length == 0 then "No comment precedes this one on the \($kind).\n"
|
|
7242
|
+
else map("### @\(.login // "" | tostring) at \(.at // "" | tostring)\n"
|
|
7243
|
+
+ (if ((.body // "" | tostring) | contains($marker)) then "(posted by the harness)\n" else "" end)
|
|
7244
|
+
+ "\n\(.body // "" | tostring)\n\n") | join("") end' \
|
|
7245
|
+
>"$dir/conversation.md" 2>/dev/null; then
|
|
7246
|
+
printf 'The comments of this %s could not be read (their listing is not the expected JSON).\n' "$kind" >"$dir/conversation.md" || return 1
|
|
7247
|
+
fi
|
|
7248
|
+
|
|
7249
|
+
if [ -n "$CONTROL_IS_PR" ]; then
|
|
7250
|
+
if ! gh_call pr diff "$CONTROL_NUMBER" --repo "$FORGE_REPO"; then
|
|
7251
|
+
printf "The pull request's diff could not be read (%s).\n" "$GH_ERR" >"$dir/diff.patch" || return 1
|
|
7252
|
+
else
|
|
7253
|
+
printf '%s\n' "$GH_OUT" >"$dir/diff.patch" || return 1
|
|
7254
|
+
fi
|
|
7255
|
+
fi
|
|
7256
|
+
|
|
7257
|
+
for f in "$dir/item.md" "$dir/conversation.md" "$dir/diff.patch"; do
|
|
7258
|
+
[ -f "$f" ] || continue
|
|
7259
|
+
mention_cap "$f" "$MENTION_FILE_MAX_BYTES" "(cut at $MENTION_FILE_MAX_BYTES bytes)" || return 1
|
|
7260
|
+
done
|
|
7261
|
+
return 0
|
|
7262
|
+
}
|
|
7263
|
+
|
|
7264
|
+
# control_mention_session <dir> <out> <err> — the one read-only session, run
|
|
7265
|
+
# once in a subshell in <dir>; AGENT_STATUS is its exit.
|
|
7266
|
+
AGENT_STATUS=0
|
|
7267
|
+
control_mention_session() {
|
|
7268
|
+
local dir="$1" out="$2" err="$3" schema model model_args
|
|
7269
|
+
AGENT_STATUS=0
|
|
7270
|
+
# Omitted rather than passed empty, so the CLI applies its own default; the
|
|
7271
|
+
# `${arr[@]+…}` form keeps an empty array safe under `set -u` on bash 3.2.
|
|
7272
|
+
model=$(hr_agent_model "$root" 2>/dev/null) || model=""
|
|
7273
|
+
model_args=()
|
|
7274
|
+
[ -z "$model" ] || model_args=(--model "$model")
|
|
7275
|
+
schema=$(jq -n -c --arg actions "$MENTION_ACTIONS" --arg verbs "$COMMAND_VERBS" '
|
|
7276
|
+
def words: split(" ") | map(select(length > 0));
|
|
7277
|
+
{ type: "object", additionalProperties: false, required: ["action", "reason"],
|
|
7278
|
+
properties: {
|
|
7279
|
+
action: { type: "string", enum: ($actions | words) },
|
|
7280
|
+
verb: { type: "string", enum: ($verbs | words) },
|
|
7281
|
+
question: { type: "integer", minimum: 1 },
|
|
7282
|
+
answer: { type: "string" },
|
|
7283
|
+
text: { type: "string" },
|
|
7284
|
+
reason: { type: "string" } } }') || { AGENT_STATUS=1; return 0; }
|
|
7285
|
+
(
|
|
7286
|
+
cd "$dir" || exit 1
|
|
7287
|
+
unset GH_TOKEN GITHUB_TOKEN HARNESS_PR_TOKEN
|
|
7288
|
+
[ -z "$MENTION_OAUTH" ] || export CLAUDE_CODE_OAUTH_TOKEN="$MENTION_OAUTH"
|
|
7289
|
+
[ -z "$MENTION_API" ] || export ANTHROPIC_API_KEY="$MENTION_API"
|
|
7290
|
+
exec "$AGENT_CLI" -p "$MENTION_COMMAND" \
|
|
7291
|
+
--plugin-dir "$HARNESS_MENTION_PLUGIN_DIR" \
|
|
7292
|
+
--add-dir "$HARNESS_MENTION_PLUGIN_DIR/instructions" \
|
|
7293
|
+
--output-format json \
|
|
7294
|
+
--json-schema "$schema" \
|
|
7295
|
+
--tools Read,Grep,Glob --restricted \
|
|
7296
|
+
--strict-mcp-config --no-session-persistence --permission-prompts none \
|
|
7297
|
+
${model_args[@]+"${model_args[@]}"} --max-budget-usd "$MENTION_MAX_BUDGET_USD"
|
|
7298
|
+
) </dev/null >"$out" 2>"$err" || AGENT_STATUS=$?
|
|
7299
|
+
return 0
|
|
7300
|
+
}
|
|
7301
|
+
|
|
7302
|
+
# control_mention — MENTION (the header): the state, the credential, the plugin
|
|
7303
|
+
# and the binary, the context directory, one session, then its decision
|
|
7304
|
+
# extracted, validated, checked for a credential and answered. Always exits.
|
|
7305
|
+
control_mention() {
|
|
7306
|
+
local base dir out err why decision from rule action verb question reason text answer file reply quote
|
|
7307
|
+
control_state_var "$CONTROL_BRANCH" \
|
|
7308
|
+
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
7309
|
+
control_state_word_var
|
|
7310
|
+
|
|
7311
|
+
if [ -z "$MENTION_OAUTH" ] && [ -z "$MENTION_API" ]; then
|
|
7312
|
+
control_refuse "$EXIT_REFUSED" "\`$WORKFLOW_CONTROL_FILE\` passes the agent that reads a mention no credential, so a mention is not read" \
|
|
7313
|
+
"$(control_commands_way)"
|
|
7314
|
+
fi
|
|
7315
|
+
base=${MENTION_COMMAND##*:}
|
|
7316
|
+
if [ -z "${HARNESS_MENTION_PLUGIN_DIR-}" ] || [ ! -f "$HARNESS_MENTION_PLUGIN_DIR/commands/$base.md" ]; then
|
|
7317
|
+
control_refuse "$EXIT_GH" "the harness plugin carrying the mention command is not available to this job" \
|
|
7318
|
+
"The control job fetches the plugin at the version \`HARNESS_CLI_VERSION\` pins in \`harness-run.yml\`; see \`docs/remote-execution.md\` → \`### Upgrading\`, then comment again, or comment a command. $(control_commands_way)"
|
|
7319
|
+
fi
|
|
7320
|
+
AGENT_CLI="${HARNESS_AGENT_CLI:-claude}"
|
|
7321
|
+
if ! command -v "$AGENT_CLI" >/dev/null 2>&1; then
|
|
7322
|
+
control_refuse "$EXIT_GH" "the agent binary \`$AGENT_CLI\` is not on this job's PATH" \
|
|
7323
|
+
"Comment a command instead. $(control_commands_way)"
|
|
7324
|
+
fi
|
|
7325
|
+
|
|
7326
|
+
if ! dir=$(mktemp -d "$control_tmp/harness-control-mention.XXXXXX"); then
|
|
7327
|
+
control_refuse "$EXIT_GH" "a context directory could not be created under '$control_tmp'" "Comment again to retry."
|
|
7328
|
+
fi
|
|
7329
|
+
out="$dir.out"
|
|
7330
|
+
err="$dir.err"
|
|
7331
|
+
control_dirs="$control_dirs $dir $out $err"
|
|
7332
|
+
{ control_mention_context "$dir" && control_mention_context_extra "$dir"; } \
|
|
7333
|
+
|| control_refuse "$EXIT_GH" "the mention's context could not be written under '$dir'" "Comment again to retry."
|
|
7334
|
+
|
|
7335
|
+
control_mention_session "$dir" "$out" "$err"
|
|
7336
|
+
|
|
7337
|
+
why=$(jq -r -s '
|
|
7338
|
+
if length != 1 or (.[0] | type) != "object" then "its output is not one JSON object"
|
|
7339
|
+
elif .[0].is_error == true then "it reported an error (\(.[0].subtype // "no subtype"))"
|
|
7340
|
+
elif (.[0].subtype // "") != "success" then "it ended as \(.[0].subtype // "no subtype")"
|
|
7341
|
+
else "" end' "$out" 2>/dev/null) || why="its output is not one JSON object"
|
|
7342
|
+
if [ "$AGENT_STATUS" -ne 0 ] || [ -n "$why" ]; then
|
|
7343
|
+
reason=$(grep -v '^[[:space:]]*$' "$err" 2>/dev/null | tail -n 1)
|
|
7344
|
+
! mention_has_credential "$reason" || reason=""
|
|
7345
|
+
[ "$AGENT_STATUS" -eq 0 ] || why="it exited $AGENT_STATUS${reason:+: $reason}"
|
|
7346
|
+
control_refuse "$EXIT_GH" "the agent that reads a mention failed: $why" \
|
|
7347
|
+
"Comment again, or comment a command. $(control_commands_way)"
|
|
7348
|
+
fi
|
|
7349
|
+
|
|
7350
|
+
decision=$(jq -c -s '.[0] as $r
|
|
7351
|
+
| if ($r.structured_output | type) == "object" then { from: "structured_output", d: $r.structured_output }
|
|
7352
|
+
else ([$r.result | strings | fromjson? | objects] | first) as $p
|
|
7353
|
+
| if $p != null then { from: "result", d: $p } else { from: "", d: null } end
|
|
7354
|
+
end' "$out") || decision='{"from":"","d":null}'
|
|
7355
|
+
from=$(printf '%s' "$decision" | jq -r '.from')
|
|
7356
|
+
if [ -z "$from" ]; then
|
|
7357
|
+
control_refuse "$EXIT_REFUSED" "the agent's reading of the mention carries no decision object" \
|
|
7358
|
+
"Comment again, or comment a command. $(control_commands_way)"
|
|
7359
|
+
fi
|
|
7360
|
+
action=$(printf '%s' "$decision" | jq -r '.d.action | if type == "string" then gsub("[\r\n]+"; " ") else tojson end')
|
|
7361
|
+
verb=$(printf '%s' "$decision" | jq -r '.d.verb | strings | gsub("[\r\n]+"; " ")')
|
|
7362
|
+
reason=$(printf '%s' "$decision" | jq -r '.d.reason | strings | gsub("[\r\n]+"; " ") | .[0:200]')
|
|
7363
|
+
! mention_has_credential "$reason" || reason="(withheld: it carries a credential value)"
|
|
7364
|
+
echo "remote-run.sh: control: mention on #$CONTROL_NUMBER by @$CONTROL_ACTOR read as $action${verb:+ $verb} from $from: $reason"
|
|
7365
|
+
|
|
7366
|
+
rule=$(printf '%s' "$decision" | jq -r --arg actions "$MENTION_ACTIONS" --arg verbs "$COMMAND_VERBS" '
|
|
7367
|
+
def words: split(" ") | map(select(length > 0));
|
|
7368
|
+
def nonblank: type == "string" and test("\\S");
|
|
7369
|
+
.d as $d
|
|
7370
|
+
| ($actions | words) as $A
|
|
7371
|
+
| ($verbs | words) as $V
|
|
7372
|
+
| if ($d | type) != "object" then "it is not an object"
|
|
7373
|
+
elif ($A | any(.[]; . == $d.action) | not) then "its `action` is not one of \($A | join(", "))"
|
|
7374
|
+
elif $d.action == "command" and ($V | any(.[]; . == $d.verb) | not) then "a `command` names no `verb` among \($V | join(", "))"
|
|
7375
|
+
elif $d.action == "command" and $d.verb == "answer" and ($d.answer | nonblank | not) then "an `answer` carries no non-blank `answer`"
|
|
7376
|
+
elif $d.action == "command" and $d.verb == "answer" and $d.question != null
|
|
7377
|
+
and (($d.question | type) != "number" or $d.question != ($d.question | floor) or $d.question < 1) then "its `question` is not an integer of 1 or more"
|
|
7378
|
+
elif ($d.action == "reply" or $d.action == "clarify") and ($d.text | nonblank | not) then "a `\($d.action)` carries no non-blank `text`"
|
|
7379
|
+
elif ($d.reason | type) != "string" then "it carries no `reason`"
|
|
7380
|
+
else "" end') || rule="it could not be read"
|
|
7381
|
+
if [ -n "$rule" ]; then
|
|
7382
|
+
control_refuse "$EXIT_REFUSED" "the agent's reading of the mention is not a valid decision: $rule" \
|
|
7383
|
+
"Comment again, or comment a command. $(control_commands_way)"
|
|
7384
|
+
fi
|
|
7385
|
+
|
|
7386
|
+
# Once, before any action is answered: no branch, this one or a later one,
|
|
7387
|
+
# sees a decision whose posted or dispatched text carries a credential.
|
|
7388
|
+
text=$(printf '%s' "$decision" | jq -r '.d.text | strings')
|
|
7389
|
+
answer=$(printf '%s' "$decision" | jq -r '.d.answer | strings')
|
|
7390
|
+
if mention_has_credential "$text" || mention_has_credential "$answer"; then
|
|
7391
|
+
echo "::error::remote-run.sh: control: the decision on the mention on #$CONTROL_NUMBER carries a credential value in its text or answer; nothing it wrote is posted"
|
|
7392
|
+
exit "$EXIT_GH"
|
|
7393
|
+
fi
|
|
7394
|
+
|
|
7395
|
+
case "$action" in
|
|
7396
|
+
none)
|
|
7397
|
+
echo "remote-run.sh: control: the mention on #$CONTROL_NUMBER needs no answer"
|
|
7398
|
+
exit "$EXIT_OK" ;;
|
|
7399
|
+
reply|clarify)
|
|
7400
|
+
file="$dir.text"
|
|
7401
|
+
control_dirs="$control_dirs $file"
|
|
7402
|
+
printf '%s' "$decision" | jq -r --arg login "$CONTROL_ACTOR" --arg handle "$COMMAND_HANDLE" \
|
|
7403
|
+
"$JQ_DEF_SANITISE"' .d.text | sanitise($login; $handle)' >"$file" \
|
|
7404
|
+
&& mention_cap "$file" "$MENTION_TEXT_MAX_BYTES" "(cut)" \
|
|
7405
|
+
|| control_refuse "$EXIT_GH" "the agent's reply could not be prepared under '$control_tmp'" "Comment again to retry."
|
|
7406
|
+
text=$(cat "$file")
|
|
7407
|
+
control_reply "$EXIT_OK" "@$CONTROL_ACTOR: $text"$'\n\n'"_Written by an agent that read your mention; it changed nothing. $(control_commands_way)_" ;;
|
|
7408
|
+
fixes)
|
|
7409
|
+
control_reply "$EXIT_OK" "@$CONTROL_ACTOR: a mention does not start a round of fixes. A review that requests changes on the run's pull request starts one (\`docs/github-run-control.md\` → \`## 2.\`). An author who cannot request changes on their own pull request starts the round locally with \`/autonomous-sdlc-harness:branch-user-review\` (\`## 4.\`)." ;;
|
|
7410
|
+
command)
|
|
7411
|
+
question=""
|
|
7412
|
+
[ "$verb" != answer ] || question=$(printf '%s' "$decision" | jq -r '.d.question | numbers | floor')
|
|
7413
|
+
case " $MENTION_CONFIRM_VERBS " in
|
|
7414
|
+
*" $verb "*)
|
|
7415
|
+
reply="@$CONTROL_ACTOR: your mention reads as \`$COMMAND_HANDLE $verb${question:+ $question}\`. Comment that command to carry it out"
|
|
7416
|
+
[ "$verb" != answer ] || reply="$reply, with the answer on the lines below it"
|
|
7417
|
+
control_reply "$EXIT_OK" "$reply." ;;
|
|
7418
|
+
esac
|
|
7419
|
+
case " $MENTION_ACT_VERBS " in
|
|
7420
|
+
*" $verb "*)
|
|
7421
|
+
# What control_answer reads from the exact form: the command line,
|
|
7422
|
+
# then the answer below it. The credential check above has already
|
|
7423
|
+
# refused an `answer` carrying a saved credential.
|
|
7424
|
+
CONTROL_BODY="$COMMAND_HANDLE $verb${question:+ $question}"
|
|
7425
|
+
reply="Read from your mention as \`$CONTROL_BODY\`."
|
|
7426
|
+
if [ "$verb" = answer ]; then
|
|
7427
|
+
# The `x` keeps the answer's final newlines through the substitution.
|
|
7428
|
+
answer=$(printf '%s' "$decision" | jq -j '.d.answer' && printf x) \
|
|
7429
|
+
|| control_refuse "$EXIT_GH" "the agent's answer could not be read" "Comment again to retry."
|
|
7430
|
+
answer=${answer%x}
|
|
7431
|
+
quote=$(printf '%s' "$decision" | jq -r --arg login "$CONTROL_ACTOR" --arg handle "$COMMAND_HANDLE" \
|
|
7432
|
+
"$JQ_DEF_SANITISE $JQ_DEF_FENCE"' .d.answer | sanitise($login; $handle)
|
|
7433
|
+
| (if endswith("\n") then . else . + "\n" end) as $q
|
|
7434
|
+
| ($q | fence) as $f
|
|
7435
|
+
| $f + "\n" + $q + $f') \
|
|
7436
|
+
|| control_refuse "$EXIT_GH" "the agent's answer could not be quoted" "Comment again to retry."
|
|
7437
|
+
CONTROL_BODY="$CONTROL_BODY"$'\n'"$answer"
|
|
7438
|
+
reply="Read from your mention as \`$COMMAND_HANDLE $verb${question:+ $question}\`, with this answer:"$'\n\n'"$quote"
|
|
7439
|
+
fi
|
|
7440
|
+
CONTROL_VERB="$verb"
|
|
7441
|
+
CONTROL_ARGS="$question"
|
|
7442
|
+
CONTROL_MENTION_NOTE="$reply"
|
|
7443
|
+
control_run_verb ;;
|
|
7444
|
+
esac ;;
|
|
7445
|
+
esac
|
|
7446
|
+
echo "::error::remote-run.sh: control: the validated action \`$action\` has no answer"
|
|
7447
|
+
exit "$EXIT_GH"
|
|
7448
|
+
}
|
|
7449
|
+
|
|
7450
|
+
# control_gates — refusals 1 to 3 of `control`, in its order, for both
|
|
7451
|
+
# `control` and `--needs-agent`. 0 when all pass; otherwise 1 with CG_EXIT,
|
|
7452
|
+
# CG_WHY and CG_WAY set as `control_refuse` takes them, and CG_AUTH the
|
|
7453
|
+
# `authorise_actor` status (0 when an earlier gate refused). Posts nothing.
|
|
7454
|
+
CG_EXIT=0
|
|
7455
|
+
CG_WHY=""
|
|
7456
|
+
CG_WAY=""
|
|
7457
|
+
CG_AUTH=0
|
|
7458
|
+
control_gates() {
|
|
7459
|
+
local forge target
|
|
7460
|
+
CG_EXIT="$EXIT_REFUSED"
|
|
7461
|
+
CG_WHY=""
|
|
7462
|
+
CG_WAY=""
|
|
7463
|
+
CG_AUTH=0
|
|
7464
|
+
if [ -n "${HARNESS_REMOTE_STOP-}" ]; then
|
|
7465
|
+
CG_WHY="the repository variable \`HARNESS_REMOTE_STOP\` is set, which stops every command"
|
|
7466
|
+
CG_WAY="Clear it under **Settings → Secrets and variables → Actions → Variables**, then comment again."
|
|
7467
|
+
return 1
|
|
7468
|
+
fi
|
|
7469
|
+
|
|
7470
|
+
forge=$(hr_forge "$root") || forge=""
|
|
7471
|
+
target=$(hr_execution_target "$root") || target=""
|
|
7472
|
+
if [ "$forge" != github ] || [ "$target" != github-actions ]; then
|
|
7473
|
+
CG_WHY="the default branch's \`harness.config.json\` does not turn run control on: it needs \`forge\` set to \`github\` (it is ${forge:-not set or unreadable}) and \`execution.target\` set to \`github-actions\` (it is ${target:-unreadable})"
|
|
7474
|
+
CG_WAY="Set both keys on the default branch, then comment again."
|
|
7475
|
+
return 1
|
|
7476
|
+
fi
|
|
7477
|
+
|
|
7478
|
+
if ! rerun_actor_listed; then
|
|
7479
|
+
CG_WHY="${RUN_ACTORS_WHY%.}"
|
|
7480
|
+
CG_WAY="Only a person the repository variable \`HARNESS_RUN_ACTORS\` admits may re-run this job; one of them can comment again."
|
|
7481
|
+
return 1
|
|
7482
|
+
fi
|
|
7483
|
+
|
|
7484
|
+
authorise_actor "$CONTROL_ACTOR" "$CONTROL_SENDER_TYPE" || CG_AUTH=$?
|
|
7485
|
+
if [ "$CG_AUTH" -ne 0 ]; then
|
|
7486
|
+
CG_WHY="${AUTH_WHY%.}"
|
|
7487
|
+
CG_WAY="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."
|
|
7488
|
+
return 1
|
|
7489
|
+
fi
|
|
7490
|
+
return 0
|
|
7491
|
+
}
|
|
7492
|
+
|
|
7493
|
+
# control_needs_agent_no <reason> — the `--needs-agent` answer that no session
|
|
7494
|
+
# starts: one line, exit 2.
|
|
7495
|
+
control_needs_agent_no() {
|
|
7496
|
+
echo "remote-run.sh: control: needs-agent: no, $1"
|
|
7497
|
+
exit "$EXIT_REFUSED"
|
|
7498
|
+
}
|
|
7499
|
+
|
|
7500
|
+
# control_needs_agent — `--needs-agent` after the comment intake: the exact
|
|
7501
|
+
# form, then `control_gates`, answered as one line and an exit. Never returns.
|
|
7502
|
+
control_needs_agent() {
|
|
7503
|
+
[ -z "$CONTROL_VERB" ] \
|
|
7504
|
+
|| control_needs_agent_no "the comment is the exact form \`$COMMAND_HANDLE $CONTROL_VERB\`"
|
|
7505
|
+
if ! control_gates; then
|
|
7506
|
+
if [ "$CG_AUTH" -eq 4 ]; then
|
|
7507
|
+
echo "remote-run.sh: control: needs-agent: undecided, $AUTH_WHY"
|
|
7508
|
+
exit "$EXIT_GH"
|
|
7509
|
+
fi
|
|
7510
|
+
control_needs_agent_no "$CG_WHY"
|
|
7511
|
+
fi
|
|
7512
|
+
echo "remote-run.sh: control: needs-agent: yes, a mention by @$CONTROL_ACTOR on #$CONTROL_NUMBER"
|
|
7513
|
+
exit "$EXIT_OK"
|
|
7514
|
+
}
|
|
7515
|
+
|
|
6774
7516
|
verb_control() {
|
|
6775
7517
|
local LC_ALL=C
|
|
6776
|
-
local review=0 close=0
|
|
7518
|
+
local review=0 close=0
|
|
7519
|
+
# Unset first so an inherited export of either name cannot keep it exported.
|
|
7520
|
+
unset MENTION_OAUTH MENTION_API MENTION_JOB_TOKEN_B64
|
|
7521
|
+
MENTION_OAUTH="${IN_OAUTH-}"
|
|
7522
|
+
MENTION_API="${IN_API-}"
|
|
7523
|
+
MENTION_JOB_TOKEN_B64=""
|
|
7524
|
+
[ -z "${GH_TOKEN-}" ] || MENTION_JOB_TOKEN_B64="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')"
|
|
7525
|
+
unset IN_OAUTH IN_API CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY
|
|
6777
7526
|
case "${GITHUB_EVENT_NAME-}" in
|
|
6778
7527
|
issue_comment) ;;
|
|
6779
7528
|
pull_request_review) review=1 ;;
|
|
@@ -6788,9 +7537,17 @@ verb_control() {
|
|
|
6788
7537
|
fi
|
|
6789
7538
|
hr_have_jq || { echo "remote-run.sh: control needs jq" >&2; exit "$EXIT_USAGE"; }
|
|
6790
7539
|
|
|
7540
|
+
if [ "$needs_agent" -eq 1 ] && [ "$GITHUB_EVENT_NAME" != issue_comment ]; then
|
|
7541
|
+
control_needs_agent_no "a \`$GITHUB_EVENT_NAME\` event is not a comment"
|
|
7542
|
+
fi
|
|
7543
|
+
|
|
6791
7544
|
case "$GITHUB_EVENT_NAME" in
|
|
6792
7545
|
pull_request_review) control_review_intake || return 0 ;;
|
|
6793
|
-
issue_comment)
|
|
7546
|
+
issue_comment)
|
|
7547
|
+
if ! control_comment_intake; then
|
|
7548
|
+
[ "$needs_agent" -eq 0 ] || control_needs_agent_no "the comment is ignored"
|
|
7549
|
+
return 0
|
|
7550
|
+
fi ;;
|
|
6794
7551
|
issues) control_issues_intake || return 0 ;;
|
|
6795
7552
|
pull_request) control_pull_request_intake || return 0 ;;
|
|
6796
7553
|
delete) control_delete_intake || return 0 ;;
|
|
@@ -6804,6 +7561,8 @@ verb_control() {
|
|
|
6804
7561
|
esac
|
|
6805
7562
|
fi
|
|
6806
7563
|
|
|
7564
|
+
[ "$needs_agent" -eq 0 ] || control_needs_agent
|
|
7565
|
+
|
|
6807
7566
|
control_tmp="${RUNNER_TEMP-}"
|
|
6808
7567
|
if [ -z "$control_tmp" ] || [ ! -d "$control_tmp" ]; then
|
|
6809
7568
|
control_tmp=$(mktemp -d) || { echo "remote-run.sh: control: mktemp failed" >&2; exit "$EXIT_USAGE"; }
|
|
@@ -6814,40 +7573,14 @@ verb_control() {
|
|
|
6814
7573
|
# Decided by the event name, never CONTROL_VERB: a comment can name `close`.
|
|
6815
7574
|
[ "$close" -eq 0 ] || control_close
|
|
6816
7575
|
|
|
6817
|
-
|
|
6818
|
-
control_refuse "$EXIT_REFUSED" "the repository variable \`HARNESS_REMOTE_STOP\` is set, which stops every command" \
|
|
6819
|
-
"Clear it under **Settings → Secrets and variables → Actions → Variables**, then comment again."
|
|
6820
|
-
fi
|
|
6821
|
-
|
|
6822
|
-
forge=$(hr_forge "$root") || forge=""
|
|
6823
|
-
target=$(hr_execution_target "$root") || target=""
|
|
6824
|
-
if [ "$forge" != github ] || [ "$target" != github-actions ]; then
|
|
6825
|
-
control_refuse "$EXIT_REFUSED" "the default branch's \`harness.config.json\` does not turn run control on: it needs \`forge\` set to \`github\` (it is ${forge:-not set or unreadable}) and \`execution.target\` set to \`github-actions\` (it is ${target:-unreadable})" \
|
|
6826
|
-
"Set both keys on the default branch, then comment again."
|
|
6827
|
-
fi
|
|
6828
|
-
|
|
6829
|
-
if ! rerun_actor_listed; then
|
|
6830
|
-
control_refuse "$EXIT_REFUSED" "${RUN_ACTORS_WHY%.}" \
|
|
6831
|
-
"Only a person the repository variable \`HARNESS_RUN_ACTORS\` admits may re-run this job; one of them can comment again."
|
|
6832
|
-
fi
|
|
7576
|
+
control_gates || control_refuse "$CG_EXIT" "$CG_WHY" "$CG_WAY"
|
|
6833
7577
|
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
control_refuse "$EXIT_REFUSED" "${AUTH_WHY%.}" \
|
|
6838
|
-
"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."
|
|
7578
|
+
if [ "$review" -eq 0 ] && [ "$CONTROL_MENTION" != 1 ] \
|
|
7579
|
+
&& { [ -z "$CONTROL_VERB" ] || ! control_verb_handled "$CONTROL_VERB"; }; then
|
|
7580
|
+
control_refuse "$EXIT_REFUSED" "it is not a command this harness carries out" "$(control_commands_way)"
|
|
6839
7581
|
fi
|
|
6840
7582
|
|
|
6841
|
-
|
|
6842
|
-
for v in $COMMAND_VERBS; do
|
|
6843
|
-
[ "$v" != answer ] || v="answer [<n>]"
|
|
6844
|
-
verbs="$verbs${verbs:+, }\`$COMMAND_HANDLE $v\`"
|
|
6845
|
-
done
|
|
6846
|
-
control_refuse "$EXIT_REFUSED" "it is not a command this harness carries out" \
|
|
6847
|
-
"The commands are $verbs; \`docs/github-run-control.md\` in the harness documentation states each."
|
|
6848
|
-
fi
|
|
6849
|
-
|
|
6850
|
-
forge_repo_var || control_reply "$EXIT_GH" "@$CONTROL_ACTOR: \`$CONTROL_VERB\` was not run: the repository's name could not be read."
|
|
7583
|
+
forge_repo_var || control_reply "$EXIT_GH" "@$CONTROL_ACTOR: \`${CONTROL_VERB:-$COMMAND_HANDLE}\` was not run: the repository's name could not be read."
|
|
6851
7584
|
if [ "$review" -eq 1 ]; then
|
|
6852
7585
|
control_check_branch "$REVIEW_HEAD"
|
|
6853
7586
|
elif [ -n "$CONTROL_IS_PR" ]; then
|
|
@@ -6856,7 +7589,16 @@ verb_control() {
|
|
|
6856
7589
|
control_branch_from_issue "$CONTROL_NUMBER"
|
|
6857
7590
|
fi
|
|
6858
7591
|
|
|
7592
|
+
if [ "$CONTROL_MENTION" = 1 ]; then
|
|
7593
|
+
echo "remote-run.sh: control: a mention on $CONTROL_BRANCH from @$CONTROL_ACTOR on #$CONTROL_NUMBER"
|
|
7594
|
+
control_mention
|
|
7595
|
+
fi
|
|
6859
7596
|
echo "remote-run.sh: control: $CONTROL_VERB on $CONTROL_BRANCH from @$CONTROL_ACTOR on #$CONTROL_NUMBER"
|
|
7597
|
+
control_run_verb
|
|
7598
|
+
}
|
|
7599
|
+
|
|
7600
|
+
# control_run_verb — the arm carrying out CONTROL_VERB.
|
|
7601
|
+
control_run_verb() {
|
|
6860
7602
|
case "$CONTROL_VERB" in
|
|
6861
7603
|
answer) control_answer ;;
|
|
6862
7604
|
pause) control_pause ;;
|