autonomous-sdlc-harness 0.6.2 → 0.6.4
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 +3 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +3 -2
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +1 -1
- package/dist/config/model.js.map +1 -1
- package/dist/doctor/checks.js +30 -12
- package/dist/doctor/checks.js.map +1 -1
- package/dist/remote/githubActions.js +45 -3
- package/dist/remote/githubActions.js.map +1 -1
- package/package.json +1 -1
- package/templates/github/workflows/harness-control.yml +114 -16
- package/templates/github/workflows/harness-resume.yml +6 -5
- package/templates/github/workflows/harness-run.yml +39 -8
- package/templates/scripts/autonomous-watcher.sh +125 -22
- package/templates/scripts/lib/harness-run-lib.sh +140 -13
- package/templates/scripts/push-branch.sh +113 -14
- package/templates/scripts/remote-run.sh +1972 -301
- package/templates/state-dir/user_reviews/README.md +1 -1
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
# remote-run.sh discard <dir> [--repo <root>]
|
|
33
33
|
# remote-run.sh report <event> <branch> [--note <text>] [--repo <root>]
|
|
34
34
|
# (always 0, 1 only on a usage error: its paragraph)
|
|
35
|
+
# remote-run.sh open <branch> [--repo <root>]
|
|
36
|
+
# (always 0, 1 only on a usage error: its paragraph)
|
|
35
37
|
# remote-run.sh deliver <branch> <bundle_dir> [--repo <root>]
|
|
36
38
|
# (always 0, 1 only on a usage error: its paragraph)
|
|
37
39
|
# remote-run.sh collect <branch> [--pr <n>] [--repo <root>]
|
|
@@ -53,9 +55,7 @@
|
|
|
53
55
|
# 1 usage error, or the library or the configuration could not be
|
|
54
56
|
# resolved; for fetch, <out_dir> is not an existing, empty directory;
|
|
55
57
|
# for discard, <dir>'s parent does not resolve or the removal failed;
|
|
56
|
-
# for sync and restore, a local copy or write failed
|
|
57
|
-
# pause-requested, also NO such run — a caller that reads 1 as "no
|
|
58
|
-
# pause" passes arguments it has already validated
|
|
58
|
+
# for sync and restore, a local copy or write failed
|
|
59
59
|
# 2 refused, nothing sent or written: execution.target is not
|
|
60
60
|
# github-actions (sending verbs, fetch, review and list); for
|
|
61
61
|
# review, a protected branch, a review file that is not a readable
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
# copy (the branch checked out in another working copy included), the
|
|
92
92
|
# fast-forward, the commit or the push — and nothing was dispatched; a
|
|
93
93
|
# copy it cut was removed
|
|
94
|
+
# 5 pause-requested only: the read succeeded and found no such run
|
|
94
95
|
#
|
|
95
96
|
# `start` IS THE ADAPTERS' ONE ENTRY: every trigger (an issue event, a forge
|
|
96
97
|
# dispatch, anything later) reduces to a branch and a task text and ends here.
|
|
@@ -285,17 +286,37 @@
|
|
|
285
286
|
# as `trigger` is, and outside the sending-verb gate: it does nothing, with one
|
|
286
287
|
# line, unless `hr_forge` is `github` and `hr_execution_target` is
|
|
287
288
|
# `github-actions`. The comment goes to the open same-repository pull request
|
|
288
|
-
# whose head is <branch> when origin's <branch> carries
|
|
289
|
-
# `<state_dir>/flow_progress/<branch>_progress.md
|
|
290
|
-
# the last `Started from <server>/<repo>/issues/<n> by @` line of its committed
|
|
289
|
+
# whose head is <branch> when origin's <branch> carries its task prompt or
|
|
290
|
+
# `<state_dir>/flow_progress/<branch>_progress.md` (`forge_recognised`), else to
|
|
291
|
+
# the issue named by the last `Started from <server>/<repo>/issues/<n> by @` line of its committed
|
|
291
292
|
# task prompt, else nowhere (`stop`'s `--pr` and `--branch-gone` change this
|
|
292
293
|
# for its own report: its paragraph). The label `STATE_LABEL_PREFIX<state>` replaces any
|
|
293
294
|
# other state label on that issue and that pull request, each when known; the
|
|
294
295
|
# label is a view, and the run list stays the authority. The state map:
|
|
295
296
|
# `parked` and `park_loop` -> parked, `paused` -> paused, `resumed` -> running,
|
|
296
|
-
# `failed` -> failed, `stopped` -> stopped, `round` (review's) -> running
|
|
297
|
-
#
|
|
298
|
-
# `
|
|
297
|
+
# `failed` -> failed, `stopped` -> stopped, `round` (review's) -> running,
|
|
298
|
+
# `not_started` (collect's) -> paused or failed, as its caller says. A
|
|
299
|
+
# `not_started` comment says GitHub did not start the run's job, so nothing ran
|
|
300
|
+
# and the branch is unchanged, and names its way on: `COMMAND_HANDLE resume`
|
|
301
|
+
# when the state is paused and an engine was recovered, else the **Run
|
|
302
|
+
# workflow** form with that engine, or with the three to choose from.
|
|
303
|
+
# On a pull-request target that is not a draft, `round` runs `gh pr ready
|
|
304
|
+
# --undo` before its comment, with the caller's own token, and its comment says
|
|
305
|
+
# the pull request is a draft again until the round completes; a refusal (a
|
|
306
|
+
# plan without drafts) is one `::warning::` line naming gh's error, and the
|
|
307
|
+
# comment then says turning it back was refused and it stays ready for review
|
|
308
|
+
# while the round works. A `failed` comment on a pull request reads the
|
|
309
|
+
# registry record's `engine`: for
|
|
310
|
+
# `user_review` it names a review requesting changes and says the pull request
|
|
311
|
+
# stays open; otherwise it says the draft stays open, to close to discard the
|
|
312
|
+
# run, or, when an issue is known, to re-apply the trigger label there for a
|
|
313
|
+
# 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. A `stopped` report also
|
|
315
|
+
# rewrites the progress comment of each pull request it labels, its
|
|
316
|
+
# `in progress` line becoming `stopped` (`forge_progress_stopped`). No other
|
|
317
|
+
# event changes a pull request's draft state.
|
|
318
|
+
# Every job event — `parked`, `park_loop`, `paused`, `resumed`, `round`,
|
|
319
|
+
# `failed` and `not_started` — posts nothing and sets no label when `remote_branch_stopped`,
|
|
299
320
|
# asked afresh, finds the branch stopped, so a job a stop overtook never
|
|
300
321
|
# overwrites `stopped`; a failed listing reports anyway, and `stopped` is never
|
|
301
322
|
# withheld. `completed` (deliver's) and `launched` (the trigger's
|
|
@@ -315,40 +336,137 @@
|
|
|
315
336
|
# `COMMAND_HANDLE answer <n>` over `<your answer>`; the marker adds
|
|
316
337
|
# `question=<n>`. With no question open it posts nothing, sets no label and
|
|
317
338
|
# prints one `::error::` line naming the branch. The label
|
|
318
|
-
# is set once per target, not per question.
|
|
319
|
-
#
|
|
339
|
+
# is set once per target, not per question. When the registry's `pause_reason`
|
|
340
|
+
# is `user` (a pause the job dropped and the run never honoured), `parked`
|
|
341
|
+
# appends `PAUSE_FOLDED_NOTE` to <note> and `park_loop` appends
|
|
342
|
+
# `PAUSE_FOLDED_HOLD_NOTE`, each after one blank line. On a public repository a
|
|
343
|
+
# question comment and its answer are public, as the artifact already is
|
|
320
344
|
# (`docs/remote-execution.md` -> `## 11. Security`, *What a reader of the
|
|
321
345
|
# repository's Actions runs can see*).
|
|
346
|
+
# `progress` (`forge_progress`) keeps ONE comment per run or round on the pull
|
|
347
|
+
# request, never on the issue, and sets no label. Gated in order, each stop one
|
|
348
|
+
# line: `forge_on`; `hr_progress_comments` (`execution.progressComments` false,
|
|
349
|
+
# or unreadable); `remote_branch_stopped`, as for the job events above; no task
|
|
350
|
+
# or user-review ledger at `$root`'s `<state_dir>/flow_progress/<branch>_progress.md`
|
|
351
|
+
# (`hr_ledger_phases` — a docs-engine ledger posts nothing); no open
|
|
352
|
+
# same-repository pull request that `forge_recognised` holds for. The body is
|
|
353
|
+
# *Progress of the harness run on `<branch>`:* or *Progress of user-review round
|
|
354
|
+
# <n> on `<branch>`:*, then `- <label>: done`, `in progress` (the first pending
|
|
355
|
+
# phase) or `not started` for `Planning`, `Implementation`, `Branch review`,
|
|
356
|
+
# `Done` (a round: `Fix plan`, `Fix implementation`, `Branch review`, `Done`) —
|
|
357
|
+
# plain list items, no timestamp, no run URL — then the marker
|
|
358
|
+
# `<!-- sdlc-harness event=progress branch=<branch> -->`, gaining ` round=<n>`
|
|
359
|
+
# for a round. One paginated comment listing picks the newest
|
|
360
|
+
# `github-actions[bot]` comment whose last non-empty line is that marker: none
|
|
361
|
+
# is one `forge_comment`; a body equal to the render (carriage returns and
|
|
362
|
+
# trailing newlines aside) is no call and one `already current` line; otherwise
|
|
363
|
+
# one `PATCH` of that comment — the only comment this file ever edits. A refused
|
|
364
|
+
# listing, create or edit is one `::warning::` line. After a stop,
|
|
365
|
+
# `forge_progress_stopped` edits that same comment, so it is still the only
|
|
366
|
+
# comment this file edits, and a resumed job's first progress pass renders it
|
|
367
|
+
# from the ledger again.
|
|
322
368
|
# It never fails its caller: every problem is one line and exit 0.
|
|
323
369
|
#
|
|
370
|
+
# `open` OPENS THE RUN'S DRAFT PULL REQUEST at the run's start, so its issue
|
|
371
|
+
# links it from the moment it exists. Job-side for its root and self-gated, as
|
|
372
|
+
# `deliver` is (`forge_on`, one line when off); it takes <branch> and no
|
|
373
|
+
# bundle. A pull-request lookup that fails is one `::warning::` line and opens
|
|
374
|
+
# nothing; an open same-repository pull request whose head is <branch> is one
|
|
375
|
+
# line, and none is opened. Otherwise `forge_open_pr` — the one opener, which
|
|
376
|
+
# `deliver` also calls — creates it against `defaultBranch` with `deliver`'s
|
|
377
|
+
# title, body, token and retry rules: a `--draft` create with
|
|
378
|
+
# `HARNESS_PR_TOKEN` as `GH_TOKEN` when set, no retry on `PR_CREATE_FORBIDDEN`,
|
|
379
|
+
# one retry without `--draft` on any other failure. Opened, the pull request is
|
|
380
|
+
# labelled `STATE_LABEL_PREFIX`running, and when the task prompt names an issue
|
|
381
|
+
# one comment there names it — as a draft, or as not a draft because the
|
|
382
|
+
# repository's plan may not offer drafts — says the run's questions, lifecycle
|
|
383
|
+
# comments and progress go to it from now on while `COMMAND_HANDLE` commands
|
|
384
|
+
# keep working on the issue, adds this run's URL when `GITHUB_RUN_ID` is set,
|
|
385
|
+
# and ends with the `opened` marker. With no issue nothing is posted. Not
|
|
386
|
+
# opened is one `::warning::` line naming why and saying the run goes on, its
|
|
387
|
+
# comments reach the issue and `deliver` tries again at the run's end; nothing
|
|
388
|
+
# is posted, since `deliver`'s `completed` comment is the one notification.
|
|
389
|
+
# It never fails its caller: every outcome but a usage error is exit 0.
|
|
390
|
+
#
|
|
324
391
|
# `deliver` HANDS A COMPLETED RUN TO REVIEW: the run workflow's step after the
|
|
325
392
|
# job's own `push-branch.sh`, which opens no pull request. Job-side for its
|
|
326
393
|
# root and self-gated, as `report` is (`forge_on`, one line when off). Anything
|
|
327
394
|
# but `status: completed` in <bundle_dir>/status.json, or no status.json, is
|
|
328
|
-
# one line and nothing sent.
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
#
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
#
|
|
336
|
-
#
|
|
337
|
-
#
|
|
338
|
-
#
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
#
|
|
342
|
-
#
|
|
343
|
-
# branch
|
|
344
|
-
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
#
|
|
348
|
-
#
|
|
349
|
-
#
|
|
350
|
-
#
|
|
351
|
-
#
|
|
395
|
+
# one line and nothing sent. The bundle's `engine` names what completed:
|
|
396
|
+
# `user_review` is a round, anything else a run. An open same-repository pull
|
|
397
|
+
# request whose head is <branch> is reused and no second one is opened. With
|
|
398
|
+
# none open and a lookup that succeeded, `forge_open_pr` is the fallback — one
|
|
399
|
+
# line says none was open at completion, because the start's attempt failed or
|
|
400
|
+
# the workflow predates `open`. It is created against `defaultBranch`, titled
|
|
401
|
+
# from the task prompt's `# ` first line (cut to `PR_TITLE_MAX_CHARS`, else
|
|
402
|
+
# <branch>), with a body naming the issue as `Started from #<n>.` — a plain
|
|
403
|
+
# mention, never a closing keyword — stating what a review requesting changes
|
|
404
|
+
# and the `COMMAND_HANDLE` commands do, and ending with the `pull-request`
|
|
405
|
+
# marker: a `--draft` create with `HARNESS_PR_TOKEN` as `GH_TOKEN` when set, so
|
|
406
|
+
# the adopter's CI runs without an approval click, no retry on
|
|
407
|
+
# `PR_CREATE_FORBIDDEN`, one retry without `--draft` on any other failure. A
|
|
408
|
+
# pull request so opened is authored by that token's owner, who therefore
|
|
409
|
+
# cannot request changes on it: use a machine account's token, another
|
|
410
|
+
# reviewer, or a local `branch-user-review` round. A lookup that fails opens
|
|
411
|
+
# nothing. Then the ready flip, with the job's token and never
|
|
412
|
+
# `HARNESS_PR_TOKEN`: a draft gets one `gh pr ready` (flipped), and a refusal
|
|
413
|
+
# is one `::warning::` line naming gh's error (refused); a pull request that is
|
|
414
|
+
# not a draft — no drafts on the plan, or a person marked it ready — gets no
|
|
415
|
+
# call (not a draft) and is left alone. A round's review threads are handled
|
|
416
|
+
# next, after the flip and before the comments, with the job's token, per THE
|
|
417
|
+
# REVIEW-COMMENT LINE below: only when the pull request is known, the
|
|
418
|
+
# highest-numbered round file on origin's tip is the highest-numbered marked
|
|
419
|
+
# one (`round_markers_read`) — a round placed by the local command consumes no
|
|
420
|
+
# pull-request item — and its marker's `comments=` is non-empty; otherwise one
|
|
421
|
+
# line. Its fix plan is `<branch>_fix_plan.md` and `<branch>_fix_plan/` for
|
|
422
|
+
# round 1, `<branch>_fix_plan_<n>.md` and `<branch>_fix_plan_<n>/` for round
|
|
423
|
+
# <n>, read from origin's tip. A verdict is kept only for an id the round
|
|
424
|
+
# collected: `fixed <sha>` for an implemented finding, `reason <text>` for an
|
|
425
|
+
# out-of-scope bullet; a collected id with none is one line, left alone. One
|
|
426
|
+
# paginated `api graphql` listing of the pull request's `reviewThreads` (`id`,
|
|
427
|
+
# `isResolved`, each comment's `databaseId`, `createdAt` and `body`); the
|
|
428
|
+
# thread holding a collected id is that id's thread. Left alone, one line
|
|
429
|
+
# each: a thread already resolved; a thread with a comment created after the
|
|
430
|
+
# round's `collected_at` that is not a collected id and carries no
|
|
431
|
+
# `COMMENT_MARKER` — the reviewer replied after the collection; and a thread
|
|
432
|
+
# already carrying this round's `thread` marker (` round=<n>` included), so a
|
|
433
|
+
# re-run of the same round replies nothing twice while a later round still
|
|
434
|
+
# handles a thread an earlier round replied to. Otherwise one reply, through
|
|
435
|
+
# `pulls/<pr>/comments/<id>/replies` to the thread's first comment, ending with
|
|
436
|
+
# `forge_marker thread <branch>` and the round's ` round=<n>` — whose
|
|
437
|
+
# `COMMENT_MARKER` keeps the next `round_collect` from collecting it: for
|
|
438
|
+
# `fixed`, ``Addressed in `<sha>`.`` and, only once that reply is posted, one
|
|
439
|
+
# `resolveReviewThread` mutation; for `reason`, `Not changed in this round:
|
|
440
|
+
# <reason>`, the thread left open. A refused listing, reply or resolve is one
|
|
441
|
+
# `::warning::` line. It never dismisses a review.
|
|
442
|
+
# THE REVIEW-COMMENT LINE is this file's contract, written by one writer,
|
|
443
|
+
# `plugin/agents/user-review-fix-plan-writer.md`, and read by `deliver` alone:
|
|
444
|
+
# - in a per-finding file `<state_dir>/user_reviews/<branch>_fix_plan[_<n>]/
|
|
445
|
+
# finding_<K>.md`, one line beginning `**Review comments:** ` followed by
|
|
446
|
+
# one or more comment ids separated by `, `, e.g.
|
|
447
|
+
# `**Review comments:** 2735551234, 2735551240`;
|
|
448
|
+
# - in the fix-plan index's `## Out of scope / verified-OK` section, a bullet
|
|
449
|
+
# (a line beginning `- `) ending with ` **Review comments:** <id>[, <id>…]`,
|
|
450
|
+
# its text before that suffix the reason the reply quotes;
|
|
451
|
+
# - the ids are the round marker's `comments=` ids, GitHub's review-comment
|
|
452
|
+
# `id`s; an observation from no inline comment carries no such line;
|
|
453
|
+
# - finding <K> is implemented when the index carries the literal
|
|
454
|
+
# `[x] **Finding <K>**`, and its fix commit is the oldest commit on
|
|
455
|
+
# `origin/<branch>` that introduced that literal into the index (`git log
|
|
456
|
+
# --reverse -S`), else origin's tip — the committer flips the entry in the
|
|
457
|
+
# fix's own commit.
|
|
458
|
+
# Then one `completed` comment
|
|
459
|
+
# on the pull request, its readiness clause by the flip's outcome, and one on
|
|
460
|
+
# the issue naming the pull request's number and URL, each when known; with no
|
|
461
|
+
# pull request, one comment on the issue alone naming why none could be opened
|
|
462
|
+
# — the Actions setting and `HARNESS_GIT_TOKEN` on `PR_CREATE_FORBIDDEN`, else
|
|
463
|
+
# gh's error — with the branch's compare URL; nowhere when neither is known.
|
|
464
|
+
# Each names reviewing and requesting changes as the next action, and with
|
|
465
|
+
# `phases.qa` true the local `branch-qa-test` still owed. Then
|
|
466
|
+
# `sdlc-harness: done` on the issue and the pull request, each when known. It
|
|
467
|
+
# writes at most one pull request, one flip, a round's thread replies and
|
|
468
|
+
# resolves, two comments and those labels, and never pushes. It never fails its caller: every problem, a refused flip
|
|
469
|
+
# included, is one line and exit 0.
|
|
352
470
|
#
|
|
353
471
|
# `collect` STARTS THE NEXT ROUND FROM THE REVIEWS COLLECTED DURING A RUN: the
|
|
354
472
|
# one step of the run workflow's `collect` job, which follows its `run` job, so
|
|
@@ -356,19 +474,27 @@
|
|
|
356
474
|
# Job-side for its root and self-gated, as `report` and `deliver` are
|
|
357
475
|
# (`forge_on`, one line when off). In order, each stop one line and exit 0:
|
|
358
476
|
# `HARNESS_REMOTE_STOP` set; `remote_branch_stopped` finding the branch stopped,
|
|
359
|
-
# or failing;
|
|
360
|
-
#
|
|
477
|
+
# or failing; `branch_settled_var`, read as `control` reads it, failing; the
|
|
478
|
+
# newest run's job never started — when that run is this one (or
|
|
479
|
+
# `GITHUB_RUN_ID` is unset), `report`'s `not_started` on its pull request or
|
|
480
|
+
# issue, with the state and engine the read derived, and one push notification,
|
|
481
|
+
# no round collected, since the reviews stay for the resumed run's own end;
|
|
482
|
+
# when it is another run, one line, since that run's own `collect` reports it;
|
|
483
|
+
# no pull request, from --pr or else `forge_pr_var` (a failed lookup
|
|
484
|
+
# included); the settledness read finding the
|
|
361
485
|
# branch in flight — a newer run listed, or this run ending `parked`,
|
|
362
486
|
# `park_loop` or `paused` (a budget chain's included), whose own end collects
|
|
363
|
-
# next
|
|
487
|
+
# next; `round_collect` finding no review requesting changes
|
|
364
488
|
# pending (inline comments alone start no round, as *Comment* starts none; they
|
|
365
489
|
# ride along in the next), or failing, a `::warning::` line. Otherwise `review
|
|
366
490
|
# <branch> --review-file <file> --allow-no-run --reviewers <logins> --source
|
|
367
491
|
# <pull request url>` runs as a child, as `control` runs it, placing,
|
|
368
492
|
# dispatching and reporting the round. A child that exits non-zero gets exactly
|
|
369
493
|
# one comment on the pull request, with the `reply` marker, naming its last
|
|
370
|
-
# stderr line and saying the reviews stay there and that
|
|
371
|
-
#
|
|
494
|
+
# stderr line and saying the reviews stay there and that, when `GITHUB_RUN_ID`
|
|
495
|
+
# is set, re-running this run's `collect` job retries with no new review, and
|
|
496
|
+
# that submitting a review requesting changes retries; there is no automatic
|
|
497
|
+
# retry. It never fails its
|
|
372
498
|
# caller: every outcome but a usage error is exit 0.
|
|
373
499
|
#
|
|
374
500
|
# `control` IS THE COMMENT AND REVIEW ADAPTER, the twin of `trigger` and the one
|
|
@@ -386,12 +512,17 @@
|
|
|
386
512
|
# into a variable (data, never shell source), plus `trigger`'s environment.
|
|
387
513
|
# Ignored, with one line and no `gh` call: an action other than `created`; a
|
|
388
514
|
# body carrying `COMMENT_MARKER` anywhere (the harness's own comment, whoever
|
|
389
|
-
# posted it); and a body
|
|
390
|
-
#
|
|
391
|
-
#
|
|
392
|
-
#
|
|
393
|
-
#
|
|
394
|
-
#
|
|
515
|
+
# posted it); and a body that does not hold `COMMAND_HANDLE` as a word
|
|
516
|
+
# anywhere, compared lowercase (the ERE `(^|[^a-z0-9])<handle>([^a-z0-9-]|$)`).
|
|
517
|
+
# So `pause` and `foo@sdlc-harnessx` start nothing. THE EXACT FORM is a first
|
|
518
|
+
# line — a trailing CR stripped, leading spaces and tabs skipped — opening with
|
|
519
|
+
# a word equal to `COMMAND_HANDLE`, compared lowercase, whose next word,
|
|
520
|
+
# lowercased, is a `COMMAND_VERBS` word: that is the verb, and `CONTROL_ARGS`
|
|
521
|
+
# the rest of that line. Any other body holding the handle is A MENTION
|
|
522
|
+
# (`CONTROL_MENTION`, no verb): `Let's @sdlc-harness pause`, `> @sdlc-harness
|
|
523
|
+
# pause`, the handle on a later line, and `@sdlc-harness check the question`
|
|
524
|
+
# alike, each read by MENTION below. Then refused, in this order, each a reply
|
|
525
|
+
# and exit 2:
|
|
395
526
|
# 1. `HARNESS_REMOTE_STOP` is set
|
|
396
527
|
# 2. `forge_on` fails — before any authorisation, so a disabled coupling asks
|
|
397
528
|
# GitHub nothing about the commenter
|
|
@@ -399,8 +530,89 @@
|
|
|
399
530
|
# refuses, before the event's own actor is checked
|
|
400
531
|
# 3. `authorise_actor` fails: `AUTH_WHY`, and who may command a run — a
|
|
401
532
|
# writer the `HARNESS_RUN_ACTORS` allow-list admits, or a listed bot
|
|
402
|
-
# 4. the verb is
|
|
403
|
-
#
|
|
533
|
+
# 4. the exact form's verb is one no arm carries out (`control_verb_handled`;
|
|
534
|
+
# no such word exists today): the reply lists every command and names
|
|
535
|
+
# `docs/github-run-control.md`. Skipped for a mention.
|
|
536
|
+
# Then THE BRANCH, and the exact form's arm (`control_run_verb`) or MENTION.
|
|
537
|
+
# MENTION. `verb_control`'s first statements, for every event, copy `IN_OAUTH`
|
|
538
|
+
# / `IN_API` into the non-exported `MENTION_OAUTH` / `MENTION_API` and unset
|
|
539
|
+
# them, with `CLAUDE_CODE_OAUTH_TOKEN` / `ANTHROPIC_API_KEY`, so no `gh`, `git`
|
|
540
|
+
# or `control_child` process the job spawns inherits a credential; only the
|
|
541
|
+
# agent subshell receives it. After the gates and the branch, `control_mention`
|
|
542
|
+
# reads the state (`control_state_var`; a failed read is exit 3), then refuses
|
|
543
|
+
# with no session: no saved credential (exit 2, the reply listing every
|
|
544
|
+
# command); no `commands/<basename of MENTION_COMMAND>.md` under
|
|
545
|
+
# `HARNESS_MENTION_PLUGIN_DIR` (exit 3, naming `HARNESS_CLI_VERSION` in
|
|
546
|
+
# `harness-run.yml` and `docs/remote-execution.md` -> `### Upgrading`); and an
|
|
547
|
+
# agent binary, `${HARNESS_AGENT_CLI:-claude}`, that `command -v` does not
|
|
548
|
+
# resolve (exit 3). The plugin directory is a path only: nothing under it is
|
|
549
|
+
# sourced or run by this script. THE CONTEXT DIRECTORY, a fresh one under
|
|
550
|
+
# `RUNNER_TEMP` (removed on exit): `comment.md` (`handle: <COMMAND_HANDLE>`,
|
|
551
|
+
# `Comment by @<login> on <issue|pull request> #<n>:`, a blank line, the body
|
|
552
|
+
# verbatim), `run.md` (`branch:`, the `fetch` keys, `stopped: yes|no`, the next
|
|
553
|
+
# ledger entry and its section, or that every entry is ticked or the ledger
|
|
554
|
+
# could not be read), `questions/question_<n>.md` per open question,
|
|
555
|
+
# `item.md` (`kind:`, `number:`, `title:`, `author:`, `url:`, a blank line, the
|
|
556
|
+
# body, from `api repos/<repo>/issues/<n>`), `conversation.md` (the last
|
|
557
|
+
# `MENTION_COMMENTS_MAX` comments before the commenter's own, oldest first,
|
|
558
|
+
# each `### @<login> at <at>`, `(posted by the harness)` when it carries
|
|
559
|
+
# `COMMENT_MARKER`, then its body, from the paginated comment listing) and, on
|
|
560
|
+
# a pull request, `diff.patch` (`pr diff`, text only: nothing is checked out),
|
|
561
|
+
# each capped at `MENTION_FILE_MAX_BYTES` at a whole line with a `(cut at <n>
|
|
562
|
+
# bytes)` line; a failed read is said in the file, never refused. THE SESSION runs once, never retried, in a subshell `cd` into
|
|
563
|
+
# that directory with `GH_TOKEN`, `GITHUB_TOKEN` and `HARNESS_PR_TOKEN` unset
|
|
564
|
+
# and `CLAUDE_CODE_OAUTH_TOKEN` / `ANTHROPIC_API_KEY` exported from whichever
|
|
565
|
+
# saved value is set: `-p "$MENTION_COMMAND"` (no argument, so no comment text
|
|
566
|
+
# reaches argv) `--plugin-dir <dir> --add-dir <dir>/instructions
|
|
567
|
+
# --output-format json --json-schema <built by jq from MENTION_ACTIONS and
|
|
568
|
+
# COMMAND_VERBS> --tools Read,Grep,Glob --restricted --strict-mcp-config
|
|
569
|
+
# --no-session-persistence --permission-prompts none --model <agentModel>
|
|
570
|
+
# --max-budget-usd "$MENTION_MAX_BUDGET_USD"`, the model read through
|
|
571
|
+
# `hr_agent_model` (a command declares no `model:`) and the flag left off only
|
|
572
|
+
# when the configuration cannot be read; never `--bare`, which reads no
|
|
573
|
+
# OAuth token, and never `--disable-slash-commands`, which would stop the `-p`
|
|
574
|
+
# command expanding. The command is `plugin/commands/harness-read-mention.md`
|
|
575
|
+
# (`MENTION_COMMAND`), and it loads `plugin/instructions/mention_reading.md`,
|
|
576
|
+
# whose `## Output contract` restates the decision contract validated here: a
|
|
577
|
+
# field renamed on either side is an edit to both. A command declares no
|
|
578
|
+
# allowlist, so the read-only closure is the session's `--tools` and
|
|
579
|
+
# `--restricted` alone. EXTRACTION: a non-zero exit, stdout that is not one
|
|
580
|
+
# JSON object, `.is_error` true or a `.subtype` other than `success` is a
|
|
581
|
+
# failure, exit 3; the decision is `.structured_output` when an object, else
|
|
582
|
+
# `.result | fromjson?` when an object, and one stdout line logs it (`mention
|
|
583
|
+
# on #<n> by @<login> read as <action>[ <verb>] from <field>: <reason>`).
|
|
584
|
+
# VALIDATION, by `jq`, the sole authority whatever the schema did: `action` in
|
|
585
|
+
# `MENTION_ACTIONS`; `command` needs a `verb` in `COMMAND_VERBS`; `answer`
|
|
586
|
+
# needs a non-blank `answer` and an optional integer `question` of 1 or more;
|
|
587
|
+
# `reply` and `clarify` need a non-blank `text`; `reason` is a string. Unknown
|
|
588
|
+
# keys are ignored; an invalid decision is refused, exit 2, naming the first
|
|
589
|
+
# rule broken. Then, once and before any action is answered, a `text` or
|
|
590
|
+
# `answer` holding a saved credential value, or the job's `GH_TOKEN` raw or in
|
|
591
|
+
# the base64 form `actions/checkout` persists, verbatim, is an `::error::` line
|
|
592
|
+
# that never prints it, nothing posted, exit 3. THE ANSWERS, each through `control_post`:
|
|
593
|
+
# `none` posts nothing; `reply` / `clarify` post `@<login>: <text>` — `text`
|
|
594
|
+
# through `JQ_DEF_SANITISE` (`<!--` neutralised, every other `@<login>` but the
|
|
595
|
+
# commenter's and the handle given U+200B) and capped at
|
|
596
|
+
# `MENTION_TEXT_MAX_BYTES` with a `(cut)` line — then a footer saying an agent
|
|
597
|
+
# wrote it and changed nothing, listing the commands; `fixes` posts this
|
|
598
|
+
# script's text pointing at a review requesting changes or
|
|
599
|
+
# `/autonomous-sdlc-harness:branch-user-review`; `command` with a verb in
|
|
600
|
+
# `MENTION_ACT_VERBS` is carried out by that verb's own arm (`control_run_verb`)
|
|
601
|
+
# from the exact form's `CONTROL_ARGS` and `CONTROL_BODY` — for `answer`, the
|
|
602
|
+
# command line then the decision's `answer` below it, byte for byte — every
|
|
603
|
+
# reply the arm posts opening with `CONTROL_MENTION_NOTE`, `Read from your
|
|
604
|
+
# mention as` and that form, `answer` adding the answer fenced by
|
|
605
|
+
# `JQ_DEF_FENCE` after `JQ_DEF_SANITISE`; an `answer` carrying a credential
|
|
606
|
+
# value was refused above, so it reaches neither the answer file, the dispatch
|
|
607
|
+
# nor the note; a state the arm refuses is that arm's refusal and exit,
|
|
608
|
+
# unchanged. `command` with a verb in `MENTION_CONFIRM_VERBS` posts a
|
|
609
|
+
# confirmation request naming the exact form it was read as and carries out no
|
|
610
|
+
# verb: `stop` because it is destructive, `clear` because on GitHub typing it
|
|
611
|
+
# is `branch-resume`'s confirmation. Exits: 0 answered or nothing to do; 2
|
|
612
|
+
# refused; 3 a state read, plugin, binary or agent failure, or a credential
|
|
613
|
+
# value in `text` or `answer`; a carried-out verb, its arm's. The workflow's
|
|
614
|
+
# interface: `IN_OAUTH` / `IN_API` and
|
|
615
|
+
# `HARNESS_MENTION_PLUGIN_DIR`.
|
|
404
616
|
# THE BRANCH. On a pull request (`.issue.pull_request.url` set), its head, by
|
|
405
617
|
# `pr view`: a fork's pull request is refused, because this event carries the
|
|
406
618
|
# repository's secrets, and nothing from its head is checked out or run; one
|
|
@@ -412,8 +624,20 @@
|
|
|
412
624
|
# marker quoted mid-body, in a comment not opening with that sentence, or by
|
|
413
625
|
# anyone else, is never trusted; none is a refusal. Both paths then pass
|
|
414
626
|
# `control_check_branch`: a branch `hr_branch_is_protected` does not answer 1
|
|
415
|
-
# for is refused, then it is fetched
|
|
416
|
-
# flow-progress ledger (`forge_recognised`)
|
|
627
|
+
# for is refused, then it is fetched. It is a harness branch when its origin
|
|
628
|
+
# tip carries its task prompt or the flow-progress ledger (`forge_recognised`)
|
|
629
|
+
# — the task prompt, `start`'s first commit, makes a pull request opened at
|
|
630
|
+
# the run's start commandable before the ledger's first push; or, for a command
|
|
631
|
+
# typed on an issue, when it is the branch that issue's genuine `started`
|
|
632
|
+
# marker names and it still exists on origin (`remote_branch_exists`) — the
|
|
633
|
+
# marker shows the harness started a run on it, so a first run GitHub never
|
|
634
|
+
# started, which committed only its task prompt, is still commandable; an
|
|
635
|
+
# issue keeps its marker after its branch is deleted, so a marker-named branch
|
|
636
|
+
# gone from origin is refused as deleted, and a failed existence check is a
|
|
637
|
+
# refusal naming the read; or when a `harness run <branch>` run is queued,
|
|
638
|
+
# waiting, requested, pending or in progress (`control_run_in_flight`), so a
|
|
639
|
+
# run is commandable from its first minute on its pull request too. Otherwise
|
|
640
|
+
# it is not a harness branch. A failed listing is a refusal naming the read.
|
|
417
641
|
# THE ARMS. `pause`: the state by a `fetch` child (`control_state_var`); only
|
|
418
642
|
# `running` sends `pause <branch>` as a child and replies that the run yields at
|
|
419
643
|
# its next clean checkpoint; any other state is a refusal naming it. `stop`:
|
|
@@ -428,9 +652,11 @@
|
|
|
428
652
|
# accepts only `park_loop`, the GitHub form of `branch-resume`'s confirmation;
|
|
429
653
|
# any other state is a refusal naming it. Each sends the local relay's dispatch,
|
|
430
654
|
# `dispatch <branch> --engine <the state's engine> --resume pause --chain 0`,
|
|
431
|
-
# `clear` with `park_loop_clear` set, and no other command sets it.
|
|
432
|
-
#
|
|
433
|
-
#
|
|
655
|
+
# `clear` with `park_loop_clear` set, and no other command sets it. A run whose
|
|
656
|
+
# job GitHub never started first takes the engine its dispatch's comment
|
|
657
|
+
# records (`forge_dispatch_engine_var`); an engine still empty after that is
|
|
658
|
+
# refused, never guessed (the `engine` input defaults to `task`), and the
|
|
659
|
+
# refusal names the Run workflow form. On 0 a reply, then `running` on the run's issue
|
|
434
660
|
# and pull request; on 2 a refusal and exit 2. `answer`: the first line is
|
|
435
661
|
# `answer <n>` and the answer every line below it, a trailing CR stripped from
|
|
436
662
|
# each line and its bytes otherwise unchanged; text after <n> on the first line
|
|
@@ -555,8 +781,10 @@
|
|
|
555
781
|
# submitting a review requesting changes retries now.
|
|
556
782
|
# A child's failure is a reply
|
|
557
783
|
# naming its last stderr line, and exit 3. Every reply goes to the item the comment was
|
|
558
|
-
# typed on, opens `@<login>`, and carries the `reply` marker; a
|
|
559
|
-
#
|
|
784
|
+
# typed on, opens `@<login>`, and carries the `reply` marker; a reply posted
|
|
785
|
+
# after a successful dispatch adds ` engine=<engine>` to that marker, the engine
|
|
786
|
+
# the dispatch named, so a run whose job never ran can still be resumed with
|
|
787
|
+
# it. A refusal reads `@<login>: `<verb>` was not run: <reason>. <way on>`.
|
|
560
788
|
# THE CLOSE. Whatever the run's phase, it is stopped when its issue is closed,
|
|
561
789
|
# its pull request closed or merged, or its branch deleted; the routing is by
|
|
562
790
|
# event name, never by verb, so a comment naming `close` is not one. `issues`
|
|
@@ -583,10 +811,11 @@
|
|
|
583
811
|
# 5. `hr_branch_is_protected` does not answer 1
|
|
584
812
|
# 6. on `pull_request`, the head branch absent on origin (`remote_branch_exists`
|
|
585
813
|
# answers 1): GitHub closed the pull request because the branch was
|
|
586
|
-
# deleted, and the `delete` event's job stops the run
|
|
814
|
+
# deleted, and the `delete` event's job stops the run and reports it on
|
|
815
|
+
# this pull request; an `ls-remote` that
|
|
587
816
|
# cannot answer is one line and proceeds
|
|
588
|
-
# There is no
|
|
589
|
-
# branch may no longer carry
|
|
817
|
+
# There is no harness-branch check (`forge_recognised`): a merged or deleted
|
|
818
|
+
# branch may no longer carry its task prompt or ledger, and a listed `harness run <b>` run is the
|
|
590
819
|
# proof. The state is read by `control_state_var`: only `running`, `parked`,
|
|
591
820
|
# `park_loop` and `paused` are acted on; `completed`, `failed` and `none` are
|
|
592
821
|
# `left alone: the run on <b> is <state>`, and one `remote_branch_stopped`
|
|
@@ -683,7 +912,9 @@
|
|
|
683
912
|
# HARNESS_MAX_CHAIN the automatic-dispatch limit; `24` when empty. Not a
|
|
684
913
|
# non-negative integer: nothing is dispatched
|
|
685
914
|
# HARNESS_POLL_MAX_DISPATCH_FAILURES `poll` only: failed re-dispatches of
|
|
686
|
-
# one paused run
|
|
915
|
+
# one paused run, and consecutive failed downloads of
|
|
916
|
+
# one run's listed bundle, each counted apart, before it
|
|
917
|
+
# gives up; `3` when empty
|
|
687
918
|
# HARNESS_POLL_GIVE_UP_AFTER_MINUTES `poll` only: minutes after a run's
|
|
688
919
|
# `usage_resume_at` past which a failed re-dispatch gives
|
|
689
920
|
# up; `360` when empty. Either one not a non-negative
|
|
@@ -694,7 +925,9 @@
|
|
|
694
925
|
# GITHUB_RUN_ID, GITHUB_SERVER_URL, GITHUB_REPOSITORY the run URL
|
|
695
926
|
# Notifications go through the sibling `autonomous-notify.sh`, as `paused` or
|
|
696
927
|
# `failed`, and each is then reported as `report` reports that event, with a
|
|
697
|
-
# note of its own that names no slash command and no shell command.
|
|
928
|
+
# note of its own that names no slash command and no shell command. The one
|
|
929
|
+
# exception is `poll`'s push-only `bundle_unreadable`, which posts no comment
|
|
930
|
+
# and sets no label. A re-dispatch is `dispatch <branch> --engine <status.json engine>
|
|
698
931
|
# --resume pause --chain <chain + 1>`, composed by `dispatch` itself.
|
|
699
932
|
#
|
|
700
933
|
# `chain` HAS ONE SOURCE: the bundle's `status.json`, whose `chain` is the
|
|
@@ -741,7 +974,8 @@
|
|
|
741
974
|
# than `GITHUB_RUN_ID` (bounded) carrying an unexpired `harness-poll-state`
|
|
742
975
|
# artifact is downloaded to `<state_dir>/autonomous_logs/poll_state/previous/`;
|
|
743
976
|
# any failure to find or read it is one line and an empty state. Its
|
|
744
|
-
# `poll_state.json` is {"<branch>": {"run_id", "failures", "notified"
|
|
977
|
+
# `poll_state.json` is {"<branch>": {"run_id", "failures", "notified",
|
|
978
|
+
# "download_failures"}}, the last absent until a download fails; an
|
|
745
979
|
# entry whose `run_id` is not the branch's newest `harness run` run is dropped,
|
|
746
980
|
# so a new run restarts the count. `poll` writes the state to `poll_state/
|
|
747
981
|
# current/` on every exit, `HARNESS_REMOTE_STOP` included, and the poller
|
|
@@ -756,9 +990,16 @@
|
|
|
756
990
|
# download that fails for it is one line and waiting. For a `completed` run,
|
|
757
991
|
# skipped, not waiting: a stopped branch, a branch absent on origin (its state
|
|
758
992
|
# entry dropped, so a later branch of the name starts clean; an `ls-remote`
|
|
759
|
-
# that cannot answer is one line and proceeds), a
|
|
760
|
-
#
|
|
761
|
-
#
|
|
993
|
+
# that cannot answer is one line and proceeds), a run whose `run` job GitHub
|
|
994
|
+
# never started (one line; its `collect` job reports it), and anything but
|
|
995
|
+
# `status: paused` / `pause_reason: usage` with an integer `usage_resume_at`,
|
|
996
|
+
# and a run whose state entry says `notified`. A failed artifact lookup on a
|
|
997
|
+
# `completed` run counts as a failed download of its bundle, below. A listed
|
|
998
|
+
# bundle that cannot be downloaded is waiting, counted per run in `download_failures` and reset by a
|
|
999
|
+
# later successful download; at `HARNESS_POLL_MAX_DISPATCH_FAILURES`
|
|
1000
|
+
# consecutive failures it sends exactly one `bundle_unreadable` push
|
|
1001
|
+
# notification, with no comment and no label, the entry marked `notified`, and
|
|
1002
|
+
# is no longer waiting. Due
|
|
762
1003
|
# (reset passed): re-dispatched under the same chain limit — a refusal is one
|
|
763
1004
|
# `failed` and not waiting; a success drops the branch's state entry. A
|
|
764
1005
|
# dispatch that fails counts one more failure for that run and is still
|
|
@@ -783,9 +1024,12 @@
|
|
|
783
1024
|
# `pause-requested` lists the branch's runs and exits 0 when one whose
|
|
784
1025
|
# `displayTitle` is exactly `harness pause <branch>` has a `createdAt` at or
|
|
785
1026
|
# after <since_epoch> — AT, because `createdAt` has one-second resolution and
|
|
786
|
-
# the caller
|
|
787
|
-
#
|
|
788
|
-
#
|
|
1027
|
+
# the caller passes a bound that starts at the job's own starting bound and
|
|
1028
|
+
# afterwards lags each query by an overlap (`CONTROL_POLL_OVERLAP_SECS` in
|
|
1029
|
+
# `autonomous-watcher.sh`), floored at that starting bound, so a pause created
|
|
1030
|
+
# later in a second already queried is still seen; seeing one twice is
|
|
1031
|
+
# harmless, since the caller drops PAUSE once. Exit 5 is "no pause"; exit 1 is
|
|
1032
|
+
# a refused call and never one. `run-created-at` prints
|
|
789
1033
|
# `gh run view <run_id> --json createdAt` as an epoch second.
|
|
790
1034
|
#
|
|
791
1035
|
# `status` AND `sync` READ THE RECORD, NOT THE KEY. A run keeps the execution
|
|
@@ -840,12 +1084,17 @@
|
|
|
840
1084
|
# `engine` when the bundle names `task`, `user_review` or `docs`. A
|
|
841
1085
|
# `mirror` restore places no planning draft
|
|
842
1086
|
# 4. no artifact, while some bundle exists (`remote_run_id` is set, or an
|
|
843
|
-
# older finished run carries one): a job that died before its upload
|
|
1087
|
+
# older finished run carries one): a job that died before its upload,
|
|
1088
|
+
# or that GitHub never started (the detail says so).
|
|
844
1089
|
# `paused` / `killed`, `remote_run_id` / `remote_run_url` re-pointed at
|
|
845
|
-
# THIS run,
|
|
1090
|
+
# THIS run, and, for a run GitHub never started whose dispatch's comment
|
|
1091
|
+
# records its engine, `engine` set to that engine; nothing restored — so
|
|
1092
|
+
# a later sync with no newer run is case 1
|
|
846
1093
|
# 5. no bundle in any run and an empty `remote_run_id`: `failed`. Not
|
|
847
1094
|
# `paused`: with no bundle anywhere a pause resume has nothing to restore,
|
|
848
|
-
# and re-dropping the artifact is the recovery
|
|
1095
|
+
# and re-dropping the artifact is the recovery, except a run whose job
|
|
1096
|
+
# GitHub never started and whose engine its dispatch's comment records,
|
|
1097
|
+
# which `sync` records as case 4 does, `paused (killed)`
|
|
849
1098
|
#
|
|
850
1099
|
# THE `killed` AND `expired` MAPPINGS. A finished run whose bundle still says
|
|
851
1100
|
# `running` (a kill, a timeout with no chain left) syncs as `status: paused`,
|
|
@@ -854,7 +1103,16 @@
|
|
|
854
1103
|
# never carries either, because `sync` derives them from the run and its
|
|
855
1104
|
# artifact list, never from a bundle. Both are `paused` rather than `failed`
|
|
856
1105
|
# because a `failed` record has no resume path, while the ledger on the branch
|
|
857
|
-
# is intact and a resume continues from it.
|
|
1106
|
+
# is intact and a resume continues from it. A finished run with no bundle
|
|
1107
|
+
# whose `run` job GitHub never started (it ended `cancelled` or `failure` with
|
|
1108
|
+
# no step run) maps to the same states as any other run with no bundle:
|
|
1109
|
+
# `paused` / `killed` when an older run carries a bundle, else `failed`. Its
|
|
1110
|
+
# detail names GitHub's reason instead of "killed, cancelled or replaced".
|
|
1111
|
+
# A never-started run with no bundle anywhere whose engine its dispatch's
|
|
1112
|
+
# comment records maps to `paused` / `killed` too, so `resume` accepts it.
|
|
1113
|
+
# When that run was the branch's first, the branch has no ledger yet:
|
|
1114
|
+
# `restore` finds no previous bundle and the resumed job starts as the
|
|
1115
|
+
# branch's first. With no such comment it stays `failed`.
|
|
858
1116
|
#
|
|
859
1117
|
# THE WORKFLOW INPUT CONTRACT (the workflow template declares the same inputs):
|
|
860
1118
|
#
|
|
@@ -901,7 +1159,11 @@
|
|
|
901
1159
|
# task prompt at the newest `harness run <branch>` run's `headSha` through the
|
|
902
1160
|
# contents API rather than from `origin/<branch>` (a failed read is one line
|
|
903
1161
|
# and no issue), and its text says the branch was deleted, so the run cannot
|
|
904
|
-
# be resumed, and that its workflow runs and artifacts are kept.
|
|
1162
|
+
# be resumed, and that its workflow runs and artifacts are kept. (4) also
|
|
1163
|
+
# reports on each pull request of the branch from this repository that is not
|
|
1164
|
+
# merged and still carries `running`, `parked` or `paused`: each gets the same
|
|
1165
|
+
# comment, the `stopped` label and its progress comment marked stopped
|
|
1166
|
+
# (`forge_gone_prs_var`, `forge_progress_stopped`). The issue read
|
|
905
1167
|
# rests on GitHub serving a commit no branch points at, which is unverified
|
|
906
1168
|
# (`docs/github-run-control.md` -> `## 8. What is not verified here`).
|
|
907
1169
|
#
|
|
@@ -914,42 +1176,62 @@
|
|
|
914
1176
|
# repository from that checkout's remote. The configuration read is that
|
|
915
1177
|
# root's `harness.config.json`.
|
|
916
1178
|
#
|
|
917
|
-
# WHAT IT NEVER DOES. It never launches a local session
|
|
1179
|
+
# WHAT IT NEVER DOES. It never launches a local session but `control`'s one
|
|
1180
|
+
# read-only mention session (MENTION, in `control`'s paragraph), never writes the
|
|
918
1181
|
# inbox, and never watches a run it sent beyond the bounded lookup of its
|
|
919
1182
|
# listing that `trigger` and `review` make. Only `start` and `review` push, and
|
|
920
1183
|
# only through `create-worktree.sh` and `push-branch.sh`; `start`'s writes are
|
|
921
1184
|
# the prompt committed on `origin/<branch>`, through a working copy and a local
|
|
922
|
-
# branch it removes before it returns
|
|
923
|
-
#
|
|
924
|
-
#
|
|
925
|
-
#
|
|
1185
|
+
# branch it removes before it returns, and, after a push that did not land, a
|
|
1186
|
+
# fetch that force-writes that copy's `refs/remotes/origin/<branch>`
|
|
1187
|
+
# (`hr_push_landed`'s, to name the commit the remote moved to); `review`'s are
|
|
1188
|
+
# the round committed on `origin/<branch>`, through the record's mirror or a
|
|
1189
|
+
# copy and a local branch it removes, and, after a push that did not land, the
|
|
1190
|
+
# same `hr_push_landed` fetch force-writing `refs/remotes/origin/<branch>` in
|
|
1191
|
+
# that mirror or copy, the record's `status` / `engine`, the bundle download
|
|
1192
|
+
# directory `sync` uses, and `report`'s writes for the round. Its settledness
|
|
1193
|
+
# read, when the newest run's job GitHub never started, also fetches origin's
|
|
1194
|
+
# <branch>, force-writing `refs/remotes/origin/<branch>` in the root checkout:
|
|
1195
|
+
# for a local `/autonomous-sdlc-harness:branch-user-review`, the main checkout. A user's chain-0 `dispatch --resume answer|pause`
|
|
926
1196
|
# writes the record's `status`, `resumed_at` and `resume_kind` in one write
|
|
927
1197
|
# when the main checkout's registry file exists and holds a record with
|
|
928
1198
|
# `execution: github-actions`; any other `dispatch` writes nothing. `trigger` writes its snapshot and comment
|
|
929
1199
|
# files under `RUNNER_TEMP`, one comment on the issue and the label removal, or
|
|
930
1200
|
# for a dispatch event a block in the step summary. `report` writes its comment
|
|
931
1201
|
# file under `RUNNER_TEMP` (removed), one comment and the state labels on the
|
|
932
|
-
# issue and the pull request, and nothing local. `
|
|
1202
|
+
# issue and the pull request, and nothing local. `open` writes its body and
|
|
1203
|
+
# comment files under `RUNNER_TEMP` (removed), at most one pull request, its
|
|
1204
|
+
# `running` label and one comment on the issue. `deliver` writes its body and
|
|
933
1205
|
# comment files under `RUNNER_TEMP` (removed), at most one pull request, one
|
|
934
|
-
# comment and the state labels. `control` writes its reply file
|
|
935
|
-
#
|
|
936
|
-
#
|
|
937
|
-
#
|
|
938
|
-
#
|
|
1206
|
+
# comment and the state labels. `control` writes its reply file, its `fetch`
|
|
1207
|
+
# directory and, for a mention, its context directory and the session's output
|
|
1208
|
+
# files under `RUNNER_TEMP` (removed) and one reply comment, plus
|
|
1209
|
+
# what the child verb it runs writes, and the fetch of origin's <branch> that
|
|
1210
|
+
# force-writes `refs/remotes/origin/<branch>` in its checkout. `collect` writes its round file and its
|
|
1211
|
+
# settledness directory under `RUNNER_TEMP` (removed), at most one comment, on
|
|
1212
|
+
# the pull request, or on the issue for a run whose job never started, and for
|
|
1213
|
+
# that event `report`'s state labels on both items and one push notification,
|
|
1214
|
+
# the fetch of origin's <branch> its settledness read makes for such a run,
|
|
1215
|
+
# force-writing `refs/remotes/origin/<branch>`, plus what its `review` child
|
|
1216
|
+
# writes. `stop`, `continue` and `poll` also make
|
|
939
1217
|
# `report`'s writes for each event they report. Every other verb's only writes are the
|
|
940
1218
|
# registry record (`stop`, `sync`) and, for `sync`, the download directory
|
|
941
1219
|
# `<state_dir>/autonomous_logs/remote_download/<branch>/<id>/` and
|
|
942
1220
|
# `<branch>.remote.log` in the main checkout, plus the mirror restore
|
|
943
|
-
# `hr_remote_bundle_restore` performs in the record's `worktree
|
|
1221
|
+
# `hr_remote_bundle_restore` performs in the record's `worktree`, and, when
|
|
1222
|
+
# the newest run's job GitHub never started, a fetch force-writing
|
|
1223
|
+
# `refs/remotes/origin/<branch>` in the main checkout; for
|
|
944
1224
|
# `restore`, that download directory, the job restore (the planning drafts
|
|
945
1225
|
# among it), `answer_<n>.md` and the
|
|
946
1226
|
# `park_loop_cycles` rewrite of `remote_status.json`, all in the job's
|
|
947
1227
|
# checkout; for `save`, <out_dir> and the step summary; for `poll`, its
|
|
948
1228
|
# download directories and `<state_dir>/autonomous_logs/poll_state/previous/`
|
|
949
|
-
# and `current/`. For `fetch`, <out_dir>
|
|
950
|
-
#
|
|
951
|
-
#
|
|
952
|
-
#
|
|
1229
|
+
# and `current/`. For `fetch`, <out_dir> and, when the newest run's job GitHub
|
|
1230
|
+
# never started, a fetch force-writing `refs/remotes/origin/<branch>` in the
|
|
1231
|
+
# root checkout. For `discard`, the removal of <dir> only. `pause-requested`,
|
|
1232
|
+
# `run-created-at` and `list` write nothing, and `status` writes nothing but,
|
|
1233
|
+
# with no record, that same fetch; a no-record `status` downloads into a
|
|
1234
|
+
# temporary directory it removes on exit.
|
|
953
1235
|
#
|
|
954
1236
|
# MIRRORS OF `cli/src/remote/githubActions.ts`, which owns these names; a
|
|
955
1237
|
# rename there is an edit here, byte for byte:
|
|
@@ -967,6 +1249,10 @@
|
|
|
967
1249
|
# WORKFLOW_CONTROL_FILE mirrors WORKFLOW_CONTROL_FILE
|
|
968
1250
|
# COMMAND_HANDLE mirrors COMMAND_HANDLE
|
|
969
1251
|
# COMMAND_VERBS mirrors COMMAND_VERBS, space-separated
|
|
1252
|
+
# MENTION_ACTIONS mirrors MENTION_ACTIONS, space-separated, same order
|
|
1253
|
+
# MENTION_ACT_VERBS mirrors MENTION_ACT_VERBS, space-separated, same order
|
|
1254
|
+
# MENTION_CONFIRM_VERBS mirrors MENTION_CONFIRM_VERBS, space-separated, same order
|
|
1255
|
+
# MENTION_COMMAND mirrors MENTION_COMMAND, the plugin-qualified slash command
|
|
970
1256
|
# COMMENT_MARKER mirrors COMMENT_MARKER
|
|
971
1257
|
# REVIEW_ROUND_STATE mirrors REVIEW_ROUND_STATE
|
|
972
1258
|
# STATE_LABEL_PREFIX mirrors STATE_LABEL_PREFIX
|
|
@@ -1139,7 +1425,9 @@
|
|
|
1139
1425
|
# the job's reads: a `run list` answer whose run has `displayTitle` `harness
|
|
1140
1426
|
# pause feat_x` and `createdAt` `2026-01-01T00:00:10Z` (epoch 1767225610):
|
|
1141
1427
|
# pause-requested bash scripts/remote-run.sh pause-requested feat_x 1767225600
|
|
1142
|
-
# -> 0; with 1767225620 ->
|
|
1428
|
+
# -> 0; with 1767225620 -> 5; a stub failing `run list` -> 3;
|
|
1429
|
+
# bash scripts/remote-run.sh pause-requested feat_x (no
|
|
1430
|
+
# <since_epoch>) -> 1
|
|
1143
1431
|
# run-created-at a stub answering `run view 42 --json createdAt` with
|
|
1144
1432
|
# {"createdAt":"2026-01-01T00:00:10Z"}: bash scripts/remote-run.sh
|
|
1145
1433
|
# run-created-at 42 -> prints 1767225610, 0; a failing stub -> 3
|
|
@@ -1178,15 +1466,45 @@
|
|
|
1178
1466
|
# naming `@sdlc-harness resume`, then `api --method POST
|
|
1179
1467
|
# repos/o/r/issues/7/labels -f labels[]=sdlc-harness: paused`
|
|
1180
1468
|
# forge off `"forge": "none"`, then the same -> 0, one line, "$s.log" unchanged
|
|
1469
|
+
# progress a task ledger at sdlc-harness/flow_progress/feat_x_progress.md in
|
|
1470
|
+
# the checkout and the stub answering `pr list` with [{"number":12,
|
|
1471
|
+
# "isCrossRepository":false}]: bash scripts/remote-run.sh report
|
|
1472
|
+
# progress feat_x -> 0; "$s.log" gains `api --paginate
|
|
1473
|
+
# repos/o/r/issues/12/comments ...`, then one POST on 12 ending in
|
|
1474
|
+
# `event=progress branch=feat_x -->`; with that comment listed, no
|
|
1475
|
+
# write; with the ledger changed, one `PATCH repos/o/r/issues/comments/<id>`
|
|
1181
1476
|
#
|
|
1182
1477
|
# deliver needs report's setup, a stub answering the create with
|
|
1183
1478
|
# https://github.com/o/r/pull/12, and <b> a bundle directory whose status.json
|
|
1184
1479
|
# carries schema "1" and status completed:
|
|
1185
1480
|
# deliver bash scripts/remote-run.sh deliver feat_x <b> -> 0; "$s.log" gains
|
|
1186
|
-
# a `--draft` create with `--base main --head feat_x`, then
|
|
1187
|
-
#
|
|
1188
|
-
# 7
|
|
1481
|
+
# a `--draft` create with `--base main --head feat_x`, then `pr
|
|
1482
|
+
# ready 12` with the job's token, then one comment on 12 saying
|
|
1483
|
+
# it is now marked ready, then one on issue 7 naming #12 and
|
|
1484
|
+
# /pull/12, then `sdlc-harness: done` on 7 and 12
|
|
1485
|
+
# reused the stub answering `pr list` with [{"number":12,
|
|
1486
|
+
# "isCrossRepository":false,"isDraft":false}] -> 0, no create, no
|
|
1487
|
+
# `pr ready`, the pull request's comment saying it is not a draft
|
|
1488
|
+
# refused the stub failing `pr ready` -> 0, one `::warning::` line, both
|
|
1489
|
+
# comments posted, the pull request's saying the flip was refused
|
|
1189
1490
|
# not done status parked, or an empty <b> -> 0, one line, "$s.log" unchanged
|
|
1491
|
+
# threads <b>'s engine user_review; on feat_x a `feat_x_review.md` ending in
|
|
1492
|
+
# the marker `comments=101,103`, a `feat_x_fix_plan.md` whose
|
|
1493
|
+
# `1. [x] **Finding 1**` lands in its own commit and whose `## Out
|
|
1494
|
+
# of scope / verified-OK` bullet ends ` **Review comments:** 103`,
|
|
1495
|
+
# and `feat_x_fix_plan/finding_1.md` carrying `**Review comments:**
|
|
1496
|
+
# 101`; a stub answering `api graphql` with one open thread per id
|
|
1497
|
+
# -> 0; "$s.log" gains a reply on comment 101 naming that commit,
|
|
1498
|
+
# then one `resolveReviewThread`, then a reply on 103 quoting the
|
|
1499
|
+
# bullet and no resolve; no dismissal call
|
|
1500
|
+
#
|
|
1501
|
+
# open needs deliver's setup without the bundle:
|
|
1502
|
+
# open bash scripts/remote-run.sh open feat_x -> 0; "$s.log" gains a
|
|
1503
|
+
# `--draft` create with `--base main --head feat_x`, then
|
|
1504
|
+
# `sdlc-harness: running` on 12, then one comment on issue 7
|
|
1505
|
+
# naming /pull/12 and ending in `event=opened branch=feat_x`
|
|
1506
|
+
# reused the stub answering `pr list` with [{"number":12,
|
|
1507
|
+
# "isCrossRepository":false}] -> 0, one line, no create, no comment
|
|
1190
1508
|
#
|
|
1191
1509
|
# collect needs deliver's setup, a story index on feat_x, a stub answering
|
|
1192
1510
|
# `pr list` with [{"number":12,"isCrossRepository":false}], `run list` with a
|
|
@@ -1208,8 +1526,20 @@
|
|
|
1208
1526
|
# pause bash scripts/remote-run.sh control -> 0; "$s.log" gains `workflow
|
|
1209
1527
|
# run harness-run.yml --ref feat_x -f action=pause -f branch=feat_x`,
|
|
1210
1528
|
# then one comment on 12 naming @alice
|
|
1211
|
-
# ignored c.json's body `
|
|
1212
|
-
#
|
|
1529
|
+
# ignored c.json's body `pause` -> 0, one line, "$s.log" unchanged
|
|
1530
|
+
# mention c.json's body `Let's @sdlc-harness pause`, with
|
|
1531
|
+
# HARNESS_MENTION_PLUGIN_DIR=<a throwaway directory holding an
|
|
1532
|
+
# empty commands/harness-read-mention.md>, IN_OAUTH=x and
|
|
1533
|
+
# HARNESS_AGENT_CLI=<a stub printing {"type":"result",
|
|
1534
|
+
# "subtype":"success","is_error":false,"structured_output":
|
|
1535
|
+
# {"action":"reply","text":"It is running.","reason":"r"}}>
|
|
1536
|
+
# -> 0; "$s.log" gains one comment on 12 opening `@alice: It is
|
|
1537
|
+
# running.`, and no `workflow run`
|
|
1538
|
+
# act the same with the stub's structured_output {"action":"command",
|
|
1539
|
+
# "verb":"pause","reason":"r"} -> 0; "$s.log" gains `workflow run
|
|
1540
|
+
# harness-run.yml --ref feat_x -f action=pause -f branch=feat_x`,
|
|
1541
|
+
# then one comment on 12 opening `Read from your mention as
|
|
1542
|
+
# `@sdlc-harness pause`.`
|
|
1213
1543
|
|
|
1214
1544
|
set -u
|
|
1215
1545
|
|
|
@@ -1232,6 +1562,10 @@ TRIGGER_DISPATCH_EVENT_TYPE='harness-task'
|
|
|
1232
1562
|
WORKFLOW_CONTROL_FILE='harness-control.yml'
|
|
1233
1563
|
COMMAND_HANDLE='@sdlc-harness'
|
|
1234
1564
|
COMMAND_VERBS='answer pause resume stop clear status'
|
|
1565
|
+
MENTION_ACTIONS='command reply clarify fixes none'
|
|
1566
|
+
MENTION_ACT_VERBS='answer pause resume status'
|
|
1567
|
+
MENTION_CONFIRM_VERBS='stop clear'
|
|
1568
|
+
MENTION_COMMAND='/autonomous-sdlc-harness:harness-read-mention'
|
|
1235
1569
|
COMMENT_MARKER='<!-- sdlc-harness'
|
|
1236
1570
|
REVIEW_ROUND_STATE='changes_requested'
|
|
1237
1571
|
STATE_LABEL_PREFIX='sdlc-harness: '
|
|
@@ -1243,6 +1577,18 @@ PR_CREATE_SETTING_PATH='Settings -> Actions -> General -> Workflow permissions'
|
|
|
1243
1577
|
# to be trusted (docs/github-integration-research.md -> S6); the margin is the
|
|
1244
1578
|
# framing lines and the marker.
|
|
1245
1579
|
QUESTION_COMMENT_MAX_BYTES=250000
|
|
1580
|
+
# The most bytes of each file in a mention's context directory, and of the
|
|
1581
|
+
# agent-written text a mention reply posts (under QUESTION_COMMENT_MAX_BYTES
|
|
1582
|
+
# with room for the prefix, the footer and the marker).
|
|
1583
|
+
MENTION_FILE_MAX_BYTES=200000
|
|
1584
|
+
MENTION_TEXT_MAX_BYTES=60000
|
|
1585
|
+
# How many of the comments before a mention its context's conversation.md keeps.
|
|
1586
|
+
MENTION_COMMENTS_MAX=30
|
|
1587
|
+
# The mention session's spend bound: a bound on one read, not a measured cost.
|
|
1588
|
+
MENTION_MAX_BUDGET_USD=1
|
|
1589
|
+
# Name no login: the job sees only the `harness pause` run, whose actor is the bot.
|
|
1590
|
+
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.'
|
|
1591
|
+
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.'
|
|
1246
1592
|
GH="${HARNESS_GH_CLI:-gh}"
|
|
1247
1593
|
|
|
1248
1594
|
# How many runs `status` prints, and how many `run list` returns for status
|
|
@@ -1274,6 +1620,11 @@ RUN_JOB_NAME='run'
|
|
|
1274
1620
|
# How far before the previous round's `collected_at` `round_collect` lists
|
|
1275
1621
|
# from, absorbing runner-clock skew; the id check drops what it re-lists.
|
|
1276
1622
|
ROUND_OVERLAP_SECS=300
|
|
1623
|
+
# How far before a run's `createdAt` `forge_dispatch_engine_var` still trusts
|
|
1624
|
+
# a dispatcher's comment: the comment is posted after the dispatch request and
|
|
1625
|
+
# GitHub creates the run asynchronously, so either may carry the earlier
|
|
1626
|
+
# timestamp. Small enough that an older dispatch's comment falls outside it.
|
|
1627
|
+
DISPATCH_MARKER_SLACK_SECS=120
|
|
1277
1628
|
|
|
1278
1629
|
# GitHub's documented limit on a `workflow_dispatch` inputs payload: "The
|
|
1279
1630
|
# maximum payload for inputs is 65,535 characters."
|
|
@@ -1285,8 +1636,9 @@ EXIT_USAGE=1
|
|
|
1285
1636
|
EXIT_REFUSED=2
|
|
1286
1637
|
EXIT_GH=3
|
|
1287
1638
|
EXIT_PLACEMENT=4
|
|
1288
|
-
# pause-requested only: the read succeeded and found no pause
|
|
1289
|
-
|
|
1639
|
+
# pause-requested only: the read succeeded and found no pause; distinct from
|
|
1640
|
+
# EXIT_USAGE so a caller never reads a refused call as no pause.
|
|
1641
|
+
EXIT_NO_PAUSE=5
|
|
1290
1642
|
|
|
1291
1643
|
usage() {
|
|
1292
1644
|
echo "remote-run.sh: $1" >&2
|
|
@@ -1309,6 +1661,7 @@ usage() {
|
|
|
1309
1661
|
echo " remote-run.sh list [--repo <root>]" >&2
|
|
1310
1662
|
echo " remote-run.sh discard <dir> [--repo <root>]" >&2
|
|
1311
1663
|
echo " remote-run.sh report <event> <branch> [--note <text>] [--repo <root>]" >&2
|
|
1664
|
+
echo " remote-run.sh open <branch> [--repo <root>]" >&2
|
|
1312
1665
|
echo " remote-run.sh deliver <branch> <bundle_dir> [--repo <root>]" >&2
|
|
1313
1666
|
echo " remote-run.sh collect <branch> [--pr <n>] [--repo <root>]" >&2
|
|
1314
1667
|
echo " remote-run.sh control [--repo <root>]" >&2
|
|
@@ -1381,7 +1734,7 @@ verb=""
|
|
|
1381
1734
|
verb="$1"
|
|
1382
1735
|
shift
|
|
1383
1736
|
case "$verb" in
|
|
1384
|
-
dispatch|pause|warm|stop|status|sync|fetch|restore|save|continue|poll|pause-requested|run-created-at|start|review|trigger|list|discard|report|deliver|collect|control) ;;
|
|
1737
|
+
dispatch|pause|warm|stop|status|sync|fetch|restore|save|continue|poll|pause-requested|run-created-at|start|review|trigger|list|discard|report|open|deliver|collect|control) ;;
|
|
1385
1738
|
*) usage "unknown verb '$verb'" ;;
|
|
1386
1739
|
esac
|
|
1387
1740
|
|
|
@@ -1606,10 +1959,11 @@ fi
|
|
|
1606
1959
|
# ---------------------------------------------------------------------------
|
|
1607
1960
|
|
|
1608
1961
|
# setup_fail <message> — a configuration problem: exit 1, except for save,
|
|
1609
|
-
# report, deliver and collect, which never fail the step that calls them.
|
|
1962
|
+
# report, open, deliver and collect, which never fail the step that calls them.
|
|
1610
1963
|
setup_fail() {
|
|
1611
1964
|
echo "remote-run.sh: $1" >&2
|
|
1612
|
-
[ "$verb" != save ] && [ "$verb" != report ] && [ "$verb" !=
|
|
1965
|
+
[ "$verb" != save ] && [ "$verb" != report ] && [ "$verb" != open ] && [ "$verb" != deliver ] \
|
|
1966
|
+
&& [ "$verb" != collect ] || exit "$EXIT_OK"
|
|
1613
1967
|
exit "$EXIT_USAGE"
|
|
1614
1968
|
}
|
|
1615
1969
|
|
|
@@ -1617,8 +1971,8 @@ if [ -n "$repo_arg" ]; then
|
|
|
1617
1971
|
root=$(hr_repo_root "$repo_arg") || setup_fail "'$repo_arg' is not a git repository"
|
|
1618
1972
|
elif [ "$verb" = restore ] || [ "$verb" = save ] || [ "$verb" = continue ] || [ "$verb" = poll ] \
|
|
1619
1973
|
|| [ "$verb" = pause-requested ] || [ "$verb" = run-created-at ] || [ "$verb" = trigger ] \
|
|
1620
|
-
|| [ "$verb" = discard ] || [ "$verb" = report ] || [ "$verb" =
|
|
1621
|
-
|| [ "$verb" = control ]; then
|
|
1974
|
+
|| [ "$verb" = discard ] || [ "$verb" = report ] || [ "$verb" = open ] || [ "$verb" = deliver ] \
|
|
1975
|
+
|| [ "$verb" = collect ] || [ "$verb" = control ]; then
|
|
1622
1976
|
root=$(hr_repo_root "${PWD-.}") || setup_fail "'${PWD-.}' is not inside a git repository"
|
|
1623
1977
|
else
|
|
1624
1978
|
root=$(hr_main_repo "${PWD-.}") || setup_fail "'${PWD-.}' is not inside a git repository"
|
|
@@ -1637,7 +1991,7 @@ case "$verb" in
|
|
|
1637
1991
|
trigger|control)
|
|
1638
1992
|
# Gates itself, after reading the event, so a refusal can still be commented.
|
|
1639
1993
|
;;
|
|
1640
|
-
report|deliver|collect)
|
|
1994
|
+
report|open|deliver|collect)
|
|
1641
1995
|
# Gates itself (`forge_on`) and exits 0 on every outcome.
|
|
1642
1996
|
;;
|
|
1643
1997
|
status|sync)
|
|
@@ -1917,6 +2271,54 @@ expired_line() {
|
|
|
1917
2271
|
printf '%s' "the state bundle of run $1 expired on $BUNDLE_EXPIRES_AT: resume from the committed ledger with $RESUME_HINT $branch, or re-drop the task"
|
|
1918
2272
|
}
|
|
1919
2273
|
|
|
2274
|
+
# run_not_started_var <run_id> — whether GitHub never started the run's
|
|
2275
|
+
# RUN_JOB_NAME job: 0 when that job's `conclusion` is `cancelled` or `failure`
|
|
2276
|
+
# and its `steps` array is absent or empty, with NOT_STARTED_REASON set; 1 when
|
|
2277
|
+
# the job started or none of that name is listed; 2 with GH_ERR set when the
|
|
2278
|
+
# jobs lookup failed or was not the expected JSON. Never exits. The reason is
|
|
2279
|
+
# the first message of the job's check-run annotations (a job's `id` is its
|
|
2280
|
+
# check run's id); that lookup is best-effort, and a failed or empty one falls
|
|
2281
|
+
# back to a line naming the conclusion. Replaces GH_OUT.
|
|
2282
|
+
NOT_STARTED_REASON=""
|
|
2283
|
+
run_not_started_var() {
|
|
2284
|
+
local answer job_id conclusion message
|
|
2285
|
+
NOT_STARTED_REASON=""
|
|
2286
|
+
gh_call api "repos/{owner}/{repo}/actions/runs/$1/jobs" || return 2
|
|
2287
|
+
answer=$(printf '%s' "$GH_OUT" | jq -r --arg n "$RUN_JOB_NAME" '
|
|
2288
|
+
if type == "object" and (.jobs | type) == "array" then
|
|
2289
|
+
([.jobs[] | select(type == "object" and .name == $n)] | first) as $j
|
|
2290
|
+
| if $j == null then "absent"
|
|
2291
|
+
elif ($j.conclusion == "cancelled" or $j.conclusion == "failure")
|
|
2292
|
+
and (($j.steps // []) | if type == "array" then length else 1 end) == 0
|
|
2293
|
+
then "not_started\t" + ($j.id | tostring) + "\t" + $j.conclusion
|
|
2294
|
+
else "started" end
|
|
2295
|
+
else "invalid" end' 2>/dev/null)
|
|
2296
|
+
case "$answer" in
|
|
2297
|
+
absent|started) return 1 ;;
|
|
2298
|
+
not_started$'\t'*) ;;
|
|
2299
|
+
*) GH_ERR="its job list is not the expected JSON"; return 2 ;;
|
|
2300
|
+
esac
|
|
2301
|
+
answer="${answer#*$'\t'}"
|
|
2302
|
+
job_id="${answer%%$'\t'*}"
|
|
2303
|
+
conclusion="${answer#*$'\t'}"
|
|
2304
|
+
message=""
|
|
2305
|
+
case "$job_id" in
|
|
2306
|
+
''|*[!0-9]*) ;;
|
|
2307
|
+
*)
|
|
2308
|
+
if gh_call api "repos/{owner}/{repo}/check-runs/$job_id/annotations"; then
|
|
2309
|
+
message=$(printf '%s' "$GH_OUT" | jq -r '
|
|
2310
|
+
[.[]? | .message? | select(type == "string" and length > 0) | split("\n")[0]] | first // ""' 2>/dev/null) || message=""
|
|
2311
|
+
fi
|
|
2312
|
+
;;
|
|
2313
|
+
esac
|
|
2314
|
+
if [ -n "$message" ]; then
|
|
2315
|
+
NOT_STARTED_REASON="$message"
|
|
2316
|
+
else
|
|
2317
|
+
NOT_STARTED_REASON="its \`$RUN_JOB_NAME\` job ended \`$conclusion\` with no step run"
|
|
2318
|
+
fi
|
|
2319
|
+
return 0
|
|
2320
|
+
}
|
|
2321
|
+
|
|
1920
2322
|
set_or_fail() {
|
|
1921
2323
|
hr_registry_set "$registry" "$branch" "$1" "$2" || {
|
|
1922
2324
|
echo "remote-run.sh: writing $1 of $branch to '$registry' failed" >&2
|
|
@@ -2035,10 +2437,17 @@ sync_expired() {
|
|
|
2035
2437
|
# RS_BUNDLE is then 1. <applied_run_id>, when set, also counts as a bundle
|
|
2036
2438
|
# existing for case 4. <finished> 1 reads the newest run as finished whatever
|
|
2037
2439
|
# its `status`: `branch_settled_var` passes it once that run's `run` job has
|
|
2038
|
-
# completed.
|
|
2440
|
+
# completed. RS_RUN_CREATED_AT is the newest run's `createdAt` (ISO 8601).
|
|
2441
|
+
# RS_NOT_STARTED is 1 only in cases 4 and 5, when `run_not_started_var` finds
|
|
2442
|
+
# GitHub never started that run's RUN_JOB_NAME job; RS_ENGINE is then
|
|
2443
|
+
# `forge_dispatch_engine_var`'s answer, and case 5 with one is `paused` /
|
|
2444
|
+
# `killed`. Replaces GH_OUT in cases 4 and 5. Exits 3 when gh fails, 2
|
|
2445
|
+
# for an unrecognised bundle.
|
|
2039
2446
|
RS_RUNS=""
|
|
2040
2447
|
RS_RUN_ID=""
|
|
2041
2448
|
RS_RUN_URL=""
|
|
2449
|
+
RS_RUN_CREATED_AT=""
|
|
2450
|
+
RS_NOT_STARTED=0
|
|
2042
2451
|
RS_GH_STATUS=""
|
|
2043
2452
|
RS_STATE=""
|
|
2044
2453
|
RS_PAUSE_REASON=""
|
|
@@ -2052,7 +2461,8 @@ remote_state() {
|
|
|
2052
2461
|
local download="${1-}" applied="${2-}" finished="${3-0}" newest status_file older bundle_exists=0
|
|
2053
2462
|
RS_RUNS=""; RS_RUN_ID=""; RS_RUN_URL=""; RS_GH_STATUS=""; RS_STATE=""
|
|
2054
2463
|
RS_PAUSE_REASON=""; RS_DETAIL=""; RS_ENGINE=""; RS_USAGE_RESUME_AT=""
|
|
2055
|
-
RS_PARK_LOOP_CYCLES=""; RS_BUNDLE=0; RS_DOWNLOAD=""
|
|
2464
|
+
RS_PARK_LOOP_CYCLES=""; RS_BUNDLE=0; RS_DOWNLOAD=""; RS_RUN_CREATED_AT=""
|
|
2465
|
+
RS_NOT_STARTED=0
|
|
2056
2466
|
RS_RUNS=$(titled_runs "harness run $branch") || RS_RUNS='[]'
|
|
2057
2467
|
newest=$(printf '%s' "$RS_RUNS" | jq -c '.[0] // empty')
|
|
2058
2468
|
if [ -z "$newest" ]; then
|
|
@@ -2062,6 +2472,7 @@ remote_state() {
|
|
|
2062
2472
|
RS_RUN_ID=$(printf '%s' "$newest" | jq -r '.databaseId | tostring')
|
|
2063
2473
|
RS_GH_STATUS=$(printf '%s' "$newest" | jq -r '.status // ""')
|
|
2064
2474
|
RS_RUN_URL=$(printf '%s' "$newest" | jq -r '.url // ""')
|
|
2475
|
+
RS_RUN_CREATED_AT=$(printf '%s' "$newest" | jq -r '.createdAt // ""')
|
|
2065
2476
|
if [ "$RS_GH_STATUS" != completed ] && [ "$finished" != 1 ]; then
|
|
2066
2477
|
RS_STATE=running
|
|
2067
2478
|
return 0
|
|
@@ -2129,12 +2540,32 @@ remote_state() {
|
|
|
2129
2540
|
RS_STATE=paused
|
|
2130
2541
|
RS_PAUSE_REASON=killed
|
|
2131
2542
|
RS_DETAIL="run $RS_RUN_ID ended with no state bundle (killed, cancelled or replaced): $RS_RUN_URL"
|
|
2132
|
-
|
|
2543
|
+
else
|
|
2544
|
+
# Case 5 — no bundle in any run.
|
|
2545
|
+
RS_STATE=failed
|
|
2546
|
+
RS_DETAIL="no run of $branch ever uploaded a state bundle; newest: $RS_RUN_URL"
|
|
2133
2547
|
fi
|
|
2134
2548
|
|
|
2135
|
-
#
|
|
2136
|
-
|
|
2137
|
-
|
|
2549
|
+
# Cases 4 and 5 — a job GitHub never started keeps its case's state; only
|
|
2550
|
+
# the detail changes. A failed jobs lookup changes nothing but says so.
|
|
2551
|
+
run_not_started_var "$RS_RUN_ID"
|
|
2552
|
+
case $? in
|
|
2553
|
+
0)
|
|
2554
|
+
RS_NOT_STARTED=1
|
|
2555
|
+
RS_DETAIL="GitHub did not start the job of run $RS_RUN_ID ($NOT_STARTED_REASON): $RS_RUN_URL"
|
|
2556
|
+
# No bundle names the engine, so the dispatch's own comment does; with
|
|
2557
|
+
# one, case 5 becomes resumable from the ledger as case 4 is.
|
|
2558
|
+
if [ -z "$RS_ENGINE" ]; then
|
|
2559
|
+
forge_dispatch_engine_var "$branch" "$RS_RUN_CREATED_AT" || :
|
|
2560
|
+
RS_ENGINE="$FORGE_DISPATCH_ENGINE"
|
|
2561
|
+
if [ "$RS_STATE" = failed ] && [ -n "$RS_ENGINE" ]; then
|
|
2562
|
+
RS_STATE=paused
|
|
2563
|
+
RS_PAUSE_REASON=killed
|
|
2564
|
+
fi
|
|
2565
|
+
fi
|
|
2566
|
+
;;
|
|
2567
|
+
2) echo "remote-run.sh: could not tell whether GitHub started the job of run $RS_RUN_ID: $GH_ERR" >&2 ;;
|
|
2568
|
+
esac
|
|
2138
2569
|
return 0
|
|
2139
2570
|
}
|
|
2140
2571
|
|
|
@@ -2264,10 +2695,16 @@ verb_sync() {
|
|
|
2264
2695
|
return 0
|
|
2265
2696
|
fi
|
|
2266
2697
|
|
|
2267
|
-
# Case 4 — a newer run with no bundle, while some bundle exists
|
|
2698
|
+
# Case 4 — a newer run with no bundle, while some bundle exists; also a run
|
|
2699
|
+
# GitHub never started whose engine its dispatch's comment records.
|
|
2268
2700
|
if [ "$RS_STATE" = paused ]; then
|
|
2269
|
-
|
|
2270
|
-
|
|
2701
|
+
if [ "$RS_NOT_STARTED" = 1 ] && valid_engine "$RS_ENGINE"; then
|
|
2702
|
+
set_many_or_fail status paused pause_reason killed remote_run_id "$id" remote_run_url "$url" \
|
|
2703
|
+
remote_detail "$RS_DETAIL" remote_synced_at "$now" engine "$RS_ENGINE"
|
|
2704
|
+
else
|
|
2705
|
+
set_many_or_fail status paused pause_reason killed remote_run_id "$id" remote_run_url "$url" \
|
|
2706
|
+
remote_detail "$RS_DETAIL" remote_synced_at "$now"
|
|
2707
|
+
fi
|
|
2271
2708
|
echo "remote-run.sh: run $id of $branch left no bundle; the record is paused (killed), nothing restored"
|
|
2272
2709
|
return 0
|
|
2273
2710
|
fi
|
|
@@ -2370,6 +2807,23 @@ ANSWERS_SHAPE='env.HARNESS_INPUT_ANSWERS | fromjson
|
|
|
2370
2807
|
and all(to_entries[]; (.value | type) == "string"
|
|
2371
2808
|
and (.key | explode | length > 0 and .[0] != 48 and all(.[]; . >= 48 and . <= 57)))'
|
|
2372
2809
|
|
|
2810
|
+
# sanitise($login; $handle), string to string, for agent-written text a reply
|
|
2811
|
+
# quotes: no `<!--` survives to read as a harness marker, and every `@<login>`
|
|
2812
|
+
# but the commenter's and the handle gets U+200B after the `@`, so it notifies
|
|
2813
|
+
# nobody.
|
|
2814
|
+
JQ_DEF_SANITISE='def sanitise($login; $handle):
|
|
2815
|
+
gsub("<!--"; "<!--")
|
|
2816
|
+
| gsub("@(?<l>[A-Za-z0-9][A-Za-z0-9-]*)";
|
|
2817
|
+
if (.l | ascii_downcase) == ($login | ascii_downcase)
|
|
2818
|
+
or (("@" + .l) | ascii_downcase) == ($handle | ascii_downcase)
|
|
2819
|
+
then "@" + .l else "@" + .l end);'
|
|
2820
|
+
|
|
2821
|
+
# fence, string to string: the code fence that quotes the input — one backtick
|
|
2822
|
+
# longer than its longest backtick run, at least three.
|
|
2823
|
+
JQ_DEF_FENCE='def fence:
|
|
2824
|
+
(([match("`+"; "g") | .length] | max) // 0) as $m
|
|
2825
|
+
| "`" * ([$m + 1, 3] | max);'
|
|
2826
|
+
|
|
2373
2827
|
verb_restore() {
|
|
2374
2828
|
local id download status_file clar n tmp status_source
|
|
2375
2829
|
if [ "$resume" = answer ]; then
|
|
@@ -2521,12 +2975,9 @@ verb_save() {
|
|
|
2521
2975
|
return 0
|
|
2522
2976
|
}
|
|
2523
2977
|
|
|
2524
|
-
#
|
|
2525
|
-
#
|
|
2526
|
-
|
|
2527
|
-
# <forge_note> is the comment's, naming no slash command and no shell command,
|
|
2528
|
-
# and states only what happened, since `forge_report` adds the next action.
|
|
2529
|
-
notify() {
|
|
2978
|
+
# notify_push <event> <branch> <detail> — the push notification alone: it
|
|
2979
|
+
# posts no comment and sets no label. Never fails.
|
|
2980
|
+
notify_push() {
|
|
2530
2981
|
if [ -n "${HARNESS_REMOTE_SLUG-}" ]; then
|
|
2531
2982
|
HARNESS_REPO_SLUG="$HARNESS_REMOTE_SLUG"
|
|
2532
2983
|
export HARNESS_REPO_SLUG
|
|
@@ -2534,6 +2985,15 @@ notify() {
|
|
|
2534
2985
|
bash "$script_dir/autonomous-notify.sh" "$1" "$2" "" "$3" \
|
|
2535
2986
|
|| echo "remote-run.sh: the $1 notification for $2 could not be sent" >&2
|
|
2536
2987
|
echo "remote-run.sh: notified $1 for $2: $3"
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
# notify <event> <branch> <detail> <forge_note> — notify_push, then
|
|
2991
|
+
# `forge_report` of the same event; never fails. Every call site supplies
|
|
2992
|
+
# both texts: <detail> is the push notification's, slash commands included;
|
|
2993
|
+
# <forge_note> is the comment's, naming no slash command and no shell command,
|
|
2994
|
+
# and states only what happened, since `forge_report` adds the next action.
|
|
2995
|
+
notify() {
|
|
2996
|
+
notify_push "$1" "$2" "$3"
|
|
2537
2997
|
# stdin closed: `poll` calls this inside a loop reading its run list.
|
|
2538
2998
|
forge_report "$1" "$2" "$4" </dev/null
|
|
2539
2999
|
}
|
|
@@ -2730,7 +3190,10 @@ verb_continue() {
|
|
|
2730
3190
|
}
|
|
2731
3191
|
|
|
2732
3192
|
# POLL_STATE — the poller state carried between ticks, one object keyed by
|
|
2733
|
-
# branch: {"<branch>": {"run_id", "failures", "notified"}},
|
|
3193
|
+
# branch: {"<branch>": {"run_id", "failures", "notified", "download_failures"}},
|
|
3194
|
+
# every value a string. `failures` counts failed re-dispatches only;
|
|
3195
|
+
# `download_failures`, absent until a download fails, counts consecutive failed
|
|
3196
|
+
# downloads of the run's listed bundle.
|
|
2734
3197
|
POLL_STATE='{}'
|
|
2735
3198
|
POLL_STATE_FILE_NAME='poll_state.json'
|
|
2736
3199
|
|
|
@@ -2738,9 +3201,21 @@ poll_state_get() {
|
|
|
2738
3201
|
printf '%s' "$POLL_STATE" | jq -r --arg b "$1" --arg f "$2" '.[$b][$f] // "" | tostring'
|
|
2739
3202
|
}
|
|
2740
3203
|
|
|
3204
|
+
# poll_state_put <branch> <run_id> <failures> <notified> — keeps the entry's
|
|
3205
|
+
# `download_failures` when its `run_id` is the same.
|
|
2741
3206
|
poll_state_put() {
|
|
2742
|
-
POLL_STATE=$(printf '%s' "$POLL_STATE" | jq -c --arg b "$1" --arg r "$2" --arg f "$3" --arg n "$4"
|
|
2743
|
-
|
|
3207
|
+
POLL_STATE=$(printf '%s' "$POLL_STATE" | jq -c --arg b "$1" --arg r "$2" --arg f "$3" --arg n "$4" '
|
|
3208
|
+
(.[$b] // {}) as $o
|
|
3209
|
+
| .[$b] = {run_id: $r, failures: $f, notified: $n}
|
|
3210
|
+
+ (if $o.run_id == $r and ($o | has("download_failures")) then {download_failures: $o.download_failures} else {} end)')
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
# poll_state_downloads_put <branch> <run_id> <count> — sets `download_failures`,
|
|
3214
|
+
# on a fresh entry when the stored `run_id` differs.
|
|
3215
|
+
poll_state_downloads_put() {
|
|
3216
|
+
POLL_STATE=$(printf '%s' "$POLL_STATE" | jq -c --arg b "$1" --arg r "$2" --arg c "$3" '
|
|
3217
|
+
(.[$b] // {}) as $o
|
|
3218
|
+
| .[$b] = (if $o.run_id == $r then $o else {run_id: $r, failures: "", notified: ""} end) + {download_failures: $c}')
|
|
2744
3219
|
}
|
|
2745
3220
|
|
|
2746
3221
|
poll_state_drop() {
|
|
@@ -2859,7 +3334,7 @@ poll_usage_paused() {
|
|
|
2859
3334
|
# With may_dispatch 0 nothing is sent and nothing notified: a due run that
|
|
2860
3335
|
# would be dispatched counts as waiting, one that would be refused does not.
|
|
2861
3336
|
poll_branch() {
|
|
2862
|
-
local id="$1" state="$2" may_dispatch="$3" at engine_value now failures
|
|
3337
|
+
local id="$1" state="$2" may_dispatch="$3" at engine_value now failures downloads listed
|
|
2863
3338
|
if [ "$may_dispatch" -eq 1 ] && [ -n "$(poll_state_get "$branch" run_id)" ] \
|
|
2864
3339
|
&& [ "$(poll_state_get "$branch" run_id)" != "$id" ]; then
|
|
2865
3340
|
poll_state_drop "$branch"
|
|
@@ -2894,10 +3369,40 @@ poll_branch() {
|
|
|
2894
3369
|
echo "remote-run.sh: poll: $branch's run $id was already reported as not re-dispatchable; skipped"
|
|
2895
3370
|
return 1
|
|
2896
3371
|
fi
|
|
2897
|
-
|
|
2898
|
-
|
|
3372
|
+
bundle_listed "$id"
|
|
3373
|
+
listed=$?
|
|
3374
|
+
if [ "$listed" -eq 1 ]; then
|
|
3375
|
+
if run_not_started_var "$id"; then
|
|
3376
|
+
echo "remote-run.sh: poll: run $id of $branch never started ($NOT_STARTED_REASON); its collect job reports it; not waiting"
|
|
3377
|
+
else
|
|
3378
|
+
echo "remote-run.sh: poll: $branch skipped"
|
|
3379
|
+
fi
|
|
2899
3380
|
return 1
|
|
2900
3381
|
fi
|
|
3382
|
+
[ "$listed" -ne 2 ] || echo "remote-run.sh: poll: reading the artifacts of run $id ($branch) failed ($GH_ERR)"
|
|
3383
|
+
downloads=""
|
|
3384
|
+
[ "$(poll_state_get "$branch" run_id)" != "$id" ] || downloads=$(poll_state_get "$branch" download_failures)
|
|
3385
|
+
if [ "$listed" -eq 2 ] || ! poll_fetch "$id"; then
|
|
3386
|
+
case "$downloads" in
|
|
3387
|
+
''|*[!0-9]*) downloads=0 ;;
|
|
3388
|
+
esac
|
|
3389
|
+
downloads=$((10#$downloads + 1))
|
|
3390
|
+
if [ "$downloads" -lt "$((10#$POLL_MAX_FAILURES))" ]; then
|
|
3391
|
+
[ "$may_dispatch" -eq 0 ] || poll_state_downloads_put "$branch" "$id" "$downloads"
|
|
3392
|
+
echo "remote-run.sh: poll: the bundle of run $id ($branch) could not be downloaded, attempt $downloads of $POLL_MAX_FAILURES; still waiting"
|
|
3393
|
+
return 0
|
|
3394
|
+
fi
|
|
3395
|
+
if [ "$may_dispatch" -eq 1 ]; then
|
|
3396
|
+
poll_state_put "$branch" "$id" "$(poll_state_get "$branch" failures)" 1
|
|
3397
|
+
# Push only, under a word outside autonomous-notify.sh's events: the run's
|
|
3398
|
+
# state is unknown, so neither a `failed` report nor a label is true of it.
|
|
3399
|
+
notify_push bundle_unreadable "$branch" "The resume poller could not download the state bundle of run $id after $downloads attempts ($GH_ERR); it no longer watches $branch. Its state is unknown: check the run, then run $RESUME_HINT $branch if it is paused; $(hr_github_resume_route "$branch" "")."
|
|
3400
|
+
fi
|
|
3401
|
+
return 1
|
|
3402
|
+
fi
|
|
3403
|
+
if [ -n "$downloads" ] && [ "$may_dispatch" -eq 1 ]; then
|
|
3404
|
+
poll_state_downloads_put "$branch" "$id" ""
|
|
3405
|
+
fi
|
|
2901
3406
|
poll_usage_paused || return 1
|
|
2902
3407
|
at=$(hr_remote_status_get "$POLL_STATUS_FILE" usage_resume_at) || at=""
|
|
2903
3408
|
case "$at" in
|
|
@@ -3131,8 +3636,12 @@ verb_start() {
|
|
|
3131
3636
|
status=0
|
|
3132
3637
|
hr_commit_placed "$script_dir/commit-on-branch.sh" "$worktree" "$rel" "$subject" >&2 || status=$?
|
|
3133
3638
|
[ "$status" -ne 1 ] || placement_fail "committing '$rel'"
|
|
3134
|
-
|
|
3135
|
-
|
|
3639
|
+
status=0
|
|
3640
|
+
hr_push_landed "$script_dir/push-branch.sh" "$worktree" "$branch" >&2 || status=$?
|
|
3641
|
+
[ "$status" -ne 2 ] \
|
|
3642
|
+
|| placement_fail "pushing $branch: origin/$branch moved to $HR_PUSH_REMOTE_TIP, which this branch does not have (something else pushed)"
|
|
3643
|
+
[ "$status" -eq 0 ] \
|
|
3644
|
+
|| placement_fail "pushing $branch: the remote refused the push (push-branch.sh's lines in this job's log name why)"
|
|
3136
3645
|
start_remove_copy
|
|
3137
3646
|
trap - EXIT
|
|
3138
3647
|
|
|
@@ -3285,8 +3794,12 @@ NAMES
|
|
|
3285
3794
|
status=0
|
|
3286
3795
|
hr_commit_placed "$script_dir/commit-on-branch.sh" "$worktree" "$rel" "$(hr_user_review_subject "$branch")" >&2 || status=$?
|
|
3287
3796
|
[ "$status" -eq 0 ] || review_fail "committing '$rel'"
|
|
3288
|
-
|
|
3289
|
-
|
|
3797
|
+
status=0
|
|
3798
|
+
hr_push_landed "$script_dir/push-branch.sh" "$worktree" "$branch" >&2 || status=$?
|
|
3799
|
+
[ "$status" -ne 2 ] \
|
|
3800
|
+
|| review_fail "pushing $branch: origin/$branch moved to $HR_PUSH_REMOTE_TIP, which this branch does not have (something else pushed)"
|
|
3801
|
+
[ "$status" -eq 0 ] \
|
|
3802
|
+
|| review_fail "pushing $branch: the remote refused the push (push-branch.sh's lines in this job's log name why)"
|
|
3290
3803
|
pushed=$(git -C "$worktree" rev-parse HEAD 2>/dev/null) || pushed=""
|
|
3291
3804
|
if [ "$use_mirror" -eq 0 ]; then
|
|
3292
3805
|
start_remove_copy
|
|
@@ -3865,9 +4378,12 @@ The task is the dispatch's \`client_payload\` title and body. Sending the same d
|
|
|
3865
4378
|
# never exits, and reports a failure as one `remote-run.sh: …` line on stderr.
|
|
3866
4379
|
#
|
|
3867
4380
|
# THE TARGET RULE. A comment goes to the open same-repository pull request
|
|
3868
|
-
# whose head is the branch when `forge_recognised` holds for that branch
|
|
3869
|
-
#
|
|
3870
|
-
#
|
|
4381
|
+
# whose head is the branch when `forge_recognised` holds for that branch — its
|
|
4382
|
+
# origin tip carries its task prompt or its flow-progress ledger — else to the
|
|
4383
|
+
# issue the run was started from (`FORGE_ISSUE`), else nowhere. The task prompt
|
|
4384
|
+
# counts so that a pull request opened at the run's start is a target before
|
|
4385
|
+
# the ledger's first push. The state label goes on that issue and on that pull
|
|
4386
|
+
# request, each when known.
|
|
3871
4387
|
#
|
|
3872
4388
|
# THE LABEL IS A VIEW, NEVER AN AUTHORITY. The run list is the authority
|
|
3873
4389
|
# (`remote_state`); the harness overwrites any state label set by hand, and
|
|
@@ -3926,14 +4442,12 @@ forge_fetch_branch() {
|
|
|
3926
4442
|
return 0
|
|
3927
4443
|
}
|
|
3928
4444
|
|
|
3929
|
-
# forge_marker <event> <branch> [<question>] — the one
|
|
3930
|
-
# marker line, built from COMMENT_MARKER
|
|
4445
|
+
# forge_marker <event> <branch> [<question> [<engine> [<round>]]] — the one
|
|
4446
|
+
# producer of a comment's marker line, built from COMMENT_MARKER: ` question=<n>`,
|
|
4447
|
+
# ` engine=<engine>` then ` round=<n>`, each only when non-empty, before ` -->`.
|
|
3931
4448
|
forge_marker() {
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
else
|
|
3935
|
-
printf '%s event=%s branch=%s -->\n' "$COMMENT_MARKER" "$1" "$2"
|
|
3936
|
-
fi
|
|
4449
|
+
printf '%s event=%s branch=%s%s%s%s -->\n' "$COMMENT_MARKER" "$1" "$2" \
|
|
4450
|
+
"${3:+ question=$3}" "${4:+ engine=$4}" "${5:+ round=$5}"
|
|
3937
4451
|
}
|
|
3938
4452
|
|
|
3939
4453
|
# forge_issue_var <branch> — FORGE_ISSUE from the last provenance line
|
|
@@ -4014,38 +4528,134 @@ forge_provenance_parse() {
|
|
|
4014
4528
|
}
|
|
4015
4529
|
|
|
4016
4530
|
# forge_recognised <branch> — the harness-branch test, read from committed
|
|
4017
|
-
# state: 0 when origin's copy of the branch carries its
|
|
4531
|
+
# state: 0 when origin's copy of the branch carries its task prompt or its
|
|
4532
|
+
# flow-progress ledger, 1 otherwise.
|
|
4018
4533
|
forge_recognised() {
|
|
4019
4534
|
local state_rel
|
|
4020
4535
|
state_rel=$(hr_state_dir "$root" 2>/dev/null) || return 1
|
|
4021
4536
|
[ -n "$state_rel" ] || return 1
|
|
4022
|
-
git -C "$root" cat-file -e "refs/remotes/origin/$1:${state_rel%/}/flow_progress/$1_progress.md" 2>/dev/null
|
|
4537
|
+
git -C "$root" cat-file -e "refs/remotes/origin/$1:${state_rel%/}/flow_progress/$1_progress.md" 2>/dev/null \
|
|
4538
|
+
|| git -C "$root" cat-file -e "refs/remotes/origin/$1:$(hr_task_prompt_rel "$state_rel" "$1")" 2>/dev/null
|
|
4023
4539
|
}
|
|
4024
4540
|
|
|
4025
4541
|
# forge_pr_var <branch> — FORGE_PR, the open pull request whose head is
|
|
4026
|
-
# <branch> in this repository (a fork's same-named head is skipped), or empty
|
|
4542
|
+
# <branch> in this repository (a fork's same-named head is skipped), or empty;
|
|
4543
|
+
# FORGE_PR_DRAFT, that same pull request's `isDraft` as `true` or `false`, or
|
|
4544
|
+
# empty when FORGE_PR is.
|
|
4027
4545
|
FORGE_PR=""
|
|
4546
|
+
FORGE_PR_DRAFT=""
|
|
4028
4547
|
forge_pr_var() {
|
|
4548
|
+
local sel
|
|
4029
4549
|
FORGE_PR=""
|
|
4030
|
-
|
|
4550
|
+
FORGE_PR_DRAFT=""
|
|
4551
|
+
if ! gh_call pr list --repo "$FORGE_REPO" --head "$1" --state open --json number,isCrossRepository,isDraft --limit 10; then
|
|
4031
4552
|
echo "remote-run.sh: listing the open pull requests of $1 failed: $GH_ERR" >&2
|
|
4032
4553
|
return 1
|
|
4033
4554
|
fi
|
|
4034
|
-
if !
|
|
4035
|
-
'if type == "array" then [.[] | select(.isCrossRepository == false
|
|
4036
|
-
FORGE_PR=""
|
|
4555
|
+
if ! sel=$(printf '%s' "$GH_OUT" | jq -r \
|
|
4556
|
+
'if type == "array" then [.[] | select(.isCrossRepository == false and (.number | type) == "number")] | first // empty | "\(.number) \(.isDraft == true)" else error end' 2>/dev/null); then
|
|
4037
4557
|
GH_ERR="its pr list is not the expected JSON"
|
|
4038
4558
|
echo "remote-run.sh: listing the open pull requests of $1 failed: $GH_ERR" >&2
|
|
4039
4559
|
return 1
|
|
4040
4560
|
fi
|
|
4561
|
+
if [ -n "$sel" ]; then
|
|
4562
|
+
FORGE_PR="${sel%% *}"
|
|
4563
|
+
FORGE_PR_DRAFT="${sel#* }"
|
|
4564
|
+
fi
|
|
4565
|
+
return 0
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
# forge_gone_prs_var <branch> — FORGE_GONE_PRS, the space-separated numbers of
|
|
4569
|
+
# the pull requests of <branch>, in any state, from this repository, not merged
|
|
4570
|
+
# and still labelled `STATE_LABEL_PREFIX` running, parked or paused: the states
|
|
4571
|
+
# control_close stops, so a pull request an earlier stop or a merge settled is
|
|
4572
|
+
# left out. Stands in for forge_recognised, which cannot read a deleted branch.
|
|
4573
|
+
# A failed listing, or one not the expected JSON, is one line and an empty list.
|
|
4574
|
+
# Always 0.
|
|
4575
|
+
FORGE_GONE_PRS=""
|
|
4576
|
+
forge_gone_prs_var() {
|
|
4577
|
+
local sel
|
|
4578
|
+
FORGE_GONE_PRS=""
|
|
4579
|
+
if ! gh_call pr list --repo "$FORGE_REPO" --head "$1" --state all --json number,isCrossRepository,mergedAt,labels --limit 10; then
|
|
4580
|
+
echo "remote-run.sh: report: listing the pull requests of $1 in every state failed: $GH_ERR" >&2
|
|
4581
|
+
return 0
|
|
4582
|
+
fi
|
|
4583
|
+
if ! sel=$(printf '%s' "$GH_OUT" | jq -r --arg p "$STATE_LABEL_PREFIX" '
|
|
4584
|
+
if type == "array" then
|
|
4585
|
+
[.[] | select(type == "object" and .isCrossRepository == false and .mergedAt == null
|
|
4586
|
+
and (.number | type) == "number"
|
|
4587
|
+
and ([.labels[]? | .name? | strings] | any(. == ($p + "running") or . == ($p + "parked") or . == ($p + "paused"))))
|
|
4588
|
+
| .number | tostring] | join(" ")
|
|
4589
|
+
else error end' 2>/dev/null); then
|
|
4590
|
+
echo "remote-run.sh: report: listing the pull requests of $1 in every state failed: its pr list is not the expected JSON" >&2
|
|
4591
|
+
return 0
|
|
4592
|
+
fi
|
|
4593
|
+
FORGE_GONE_PRS="$sel"
|
|
4594
|
+
return 0
|
|
4595
|
+
}
|
|
4596
|
+
|
|
4597
|
+
# forge_dispatch_engine_var <branch> <created_at_iso> — FORGE_DISPATCH_ENGINE,
|
|
4598
|
+
# the engine the dispatch that created the run at <created_at_iso> recorded in
|
|
4599
|
+
# its comment on the branch's issue or open pull request, or empty. Only a
|
|
4600
|
+
# `github-actions[bot]` comment counts, whose last non-empty line is exactly a
|
|
4601
|
+
# `started` or `round` marker for <branch>, either with or without an engine,
|
|
4602
|
+
# or a `reply` marker for <branch> carrying an engine, and whose `created_at`
|
|
4603
|
+
# is no earlier than <created_at_iso> less DISPATCH_MARKER_SLACK_SECS. The
|
|
4604
|
+
# newest one answers: `started` is `task`, `round` is `user_review`, `reply`
|
|
4605
|
+
# its own engine. 1 when the forge is off or the repository is unknown. Fetches
|
|
4606
|
+
# origin's <branch> into the root checkout; replaces GH_OUT.
|
|
4607
|
+
FORGE_DISPATCH_ENGINE=""
|
|
4608
|
+
forge_dispatch_engine_var() {
|
|
4609
|
+
local b="$1" created="${2-}" since markers listed="" n engine answer
|
|
4610
|
+
FORGE_DISPATCH_ENGINE=""
|
|
4611
|
+
forge_on || return 1
|
|
4612
|
+
forge_repo_var || return 1
|
|
4613
|
+
if ! since=$(jq -n -r --arg t "$created" --argjson o "$DISPATCH_MARKER_SLACK_SECS" '($t | fromdateiso8601) - $o' 2>/dev/null) \
|
|
4614
|
+
|| [ -z "$since" ]; then
|
|
4615
|
+
echo "remote-run.sh: the newest run of $b has no readable createdAt ('$created'); its dispatch's engine is not read" >&2
|
|
4616
|
+
return 0
|
|
4617
|
+
fi
|
|
4618
|
+
# Every marker line that qualifies, each with the engine it answers.
|
|
4619
|
+
markers=""
|
|
4620
|
+
for engine in task user_review docs; do
|
|
4621
|
+
markers="$markers$(forge_marker started "$b" "" "$engine")"$'\t'task$'\n'
|
|
4622
|
+
markers="$markers$(forge_marker round "$b" "" "$engine")"$'\t'user_review$'\n'
|
|
4623
|
+
markers="$markers$(forge_marker reply "$b" "" "$engine")"$'\t'"$engine"$'\n'
|
|
4624
|
+
done
|
|
4625
|
+
markers="$markers$(forge_marker started "$b")"$'\t'task$'\n'
|
|
4626
|
+
markers="$markers$(forge_marker round "$b")"$'\t'user_review$'\n'
|
|
4627
|
+
markers=$(printf '%s' "$markers" | jq -R -s -c \
|
|
4628
|
+
'[split("\n")[] | select(length > 0) | split("\t") | {(.[0]): .[1]}] | add')
|
|
4629
|
+
forge_fetch_branch "$b"
|
|
4630
|
+
forge_issue_var "$b" || FORGE_ISSUE=""
|
|
4631
|
+
forge_pr_var "$b" || FORGE_PR=""
|
|
4632
|
+
for n in $FORGE_ISSUE $FORGE_PR; do
|
|
4633
|
+
if ! gh_call api --paginate "repos/$FORGE_REPO/issues/$n/comments" --jq '.[] | {login: .user.login, at: .created_at, body: .body}'; then
|
|
4634
|
+
echo "remote-run.sh: listing the comments of #$n failed, so the engine of $b's dispatch is not read: $GH_ERR" >&2
|
|
4635
|
+
return 0
|
|
4636
|
+
fi
|
|
4637
|
+
listed="$listed$GH_OUT"$'\n'
|
|
4638
|
+
done
|
|
4639
|
+
if ! answer=$(printf '%s' "$listed" | jq -s -r --argjson m "$markers" --argjson since "$since" '
|
|
4640
|
+
[.[] | select(type == "object" and .login == "github-actions[bot]" and (.at | type) == "string")
|
|
4641
|
+
| ((.body // "") | gsub("\r"; "") | split("\n") | map(select(test("^\\s*$") | not)) | last // "") as $last
|
|
4642
|
+
| select($m[$last] != null)
|
|
4643
|
+
| (.at | try fromdateiso8601 catch null) as $t
|
|
4644
|
+
| select($t != null and $t >= $since)
|
|
4645
|
+
| {t: $t, e: $m[$last]}]
|
|
4646
|
+
| sort_by(.t) | last | .e // ""' 2>/dev/null); then
|
|
4647
|
+
echo "remote-run.sh: the comments of $b's issue and pull request are not the expected JSON, so the engine of its dispatch is not read" >&2
|
|
4648
|
+
return 0
|
|
4649
|
+
fi
|
|
4650
|
+
FORGE_DISPATCH_ENGINE="$answer"
|
|
4041
4651
|
return 0
|
|
4042
4652
|
}
|
|
4043
4653
|
|
|
4044
|
-
# forge_comment <number> <event> <branch> <body_file> [<question>
|
|
4045
|
-
# marker to <body_file> and post it on issue or pull request <number>.
|
|
4654
|
+
# forge_comment <number> <event> <branch> <body_file> [<question> [<engine> [<round>]]]
|
|
4655
|
+
# — append the marker to <body_file> and post it on issue or pull request <number>.
|
|
4046
4656
|
forge_comment() {
|
|
4047
|
-
local number="$1" event="$2" branch="$3" file="$4" question="${5-}" status
|
|
4048
|
-
if ! { printf '\n'; forge_marker "$event" "$branch" "$question"; } >>"$file"; then
|
|
4657
|
+
local number="$1" event="$2" branch="$3" file="$4" question="${5-}" engine="${6-}" round="${7-}" status
|
|
4658
|
+
if ! { printf '\n'; forge_marker "$event" "$branch" "$question" "$engine" "$round"; } >>"$file"; then
|
|
4049
4659
|
echo "remote-run.sh: cannot append the marker to '$file'" >&2
|
|
4050
4660
|
return 1
|
|
4051
4661
|
fi
|
|
@@ -4158,15 +4768,25 @@ forge_question_body() {
|
|
|
4158
4768
|
# comment (on `parked`, one per open question) and the state label, by the
|
|
4159
4769
|
# target rule above. Read on `stopped` only: <pr>, an explicit pull request
|
|
4160
4770
|
# that is the target whatever its state; `gone`, the branch deleted on GitHub,
|
|
4161
|
-
# its issue read from the task prompt at <sha
|
|
4162
|
-
#
|
|
4163
|
-
#
|
|
4771
|
+
# its issue read from the task prompt at <sha>, and then each pull request
|
|
4772
|
+
# forge_gone_prs_var lists given the same comment, the label and its progress
|
|
4773
|
+
# comment marked stopped; nothing is posted only when neither is known. Every
|
|
4774
|
+
# event but `stopped` is withheld when the branch's newest `harness stop` run is
|
|
4775
|
+
# newer than its newest `harness run` run, read from a fresh listing.
|
|
4776
|
+
# `not_started` reads REPORT_NOT_STARTED_STATE (`paused`, else `failed`) and
|
|
4777
|
+
# REPORT_NOT_STARTED_ENGINE (empty or the recovered engine), set by its caller.
|
|
4778
|
+
# Always 0.
|
|
4779
|
+
REPORT_NOT_STARTED_STATE=""
|
|
4780
|
+
REPORT_NOT_STARTED_ENGINE=""
|
|
4164
4781
|
forge_report() {
|
|
4165
4782
|
local event="$1" br="$2" note="${3-}" pr="${4-}" gone="${5-}" gone_sha="${6-}" state reason="" resume_at="" when registry_file
|
|
4166
|
-
local target kind text tmp made_tmp="" file trigger_label stopped state_rel="" count n
|
|
4783
|
+
local target kind text tmp made_tmp="" file trigger_label stopped state_rel="" count n route engine=""
|
|
4784
|
+
local gone_prs="" reported="" folded=""
|
|
4167
4785
|
case "$event" in
|
|
4168
4786
|
parked|park_loop) state=parked ;;
|
|
4169
4787
|
paused) state=paused ;;
|
|
4788
|
+
not_started)
|
|
4789
|
+
if [ "$REPORT_NOT_STARTED_STATE" = paused ]; then state=paused; else state=failed; fi ;;
|
|
4170
4790
|
resumed) state=running ;;
|
|
4171
4791
|
failed) state=failed ;;
|
|
4172
4792
|
stopped) state=stopped ;;
|
|
@@ -4210,19 +4830,24 @@ forge_report() {
|
|
|
4210
4830
|
if [ -n "$pr" ]; then
|
|
4211
4831
|
FORGE_PR="$pr"
|
|
4212
4832
|
elif [ -n "$gone" ]; then
|
|
4213
|
-
#
|
|
4833
|
+
# No pull request of a deleted head is open, so the run's unfinished ones
|
|
4834
|
+
# are found in every state by forge_gone_prs_var.
|
|
4214
4835
|
FORGE_PR=""
|
|
4836
|
+
forge_gone_prs_var "$br"
|
|
4837
|
+
gone_prs="$FORGE_GONE_PRS"
|
|
4215
4838
|
else
|
|
4216
4839
|
forge_pr_var "$br" || FORGE_PR=""
|
|
4217
4840
|
fi
|
|
4218
4841
|
if [ -z "$pr" ] && [ -n "$FORGE_PR" ] && ! forge_recognised "$br"; then
|
|
4219
|
-
echo "remote-run.sh: report: pull request #$FORGE_PR's head carries
|
|
4842
|
+
echo "remote-run.sh: report: pull request #$FORGE_PR's head carries neither a task prompt nor a flow-progress ledger; it is not a target"
|
|
4220
4843
|
FORGE_PR=""
|
|
4221
4844
|
fi
|
|
4222
4845
|
if [ -n "$FORGE_PR" ]; then
|
|
4223
4846
|
target="$FORGE_PR"; kind=pr
|
|
4224
4847
|
elif [ -n "$FORGE_ISSUE" ]; then
|
|
4225
4848
|
target="$FORGE_ISSUE"; kind=issue
|
|
4849
|
+
elif [ -n "$gone_prs" ]; then
|
|
4850
|
+
target=""; kind=pr
|
|
4226
4851
|
else
|
|
4227
4852
|
echo "remote-run.sh: report: $br has no open pull request and no issue it was started from; nothing posted"
|
|
4228
4853
|
return 0
|
|
@@ -4233,7 +4858,16 @@ forge_report() {
|
|
|
4233
4858
|
if [ -n "$registry_file" ] && [ -f "$registry_file" ]; then
|
|
4234
4859
|
reason=$(hr_registry_get "$registry_file" "$br" pause_reason)
|
|
4235
4860
|
resume_at=$(hr_registry_get "$registry_file" "$br" usage_resume_at)
|
|
4861
|
+
[ "$event" != failed ] || engine=$(hr_registry_get "$registry_file" "$br" engine)
|
|
4236
4862
|
fi
|
|
4863
|
+
case "$event" in
|
|
4864
|
+
parked|park_loop)
|
|
4865
|
+
if [ "$reason" = user ]; then
|
|
4866
|
+
folded="$PAUSE_FOLDED_NOTE"
|
|
4867
|
+
[ "$event" != park_loop ] || folded="$PAUSE_FOLDED_HOLD_NOTE"
|
|
4868
|
+
if [ -z "$note" ]; then note="$folded"; else note="$note"$'\n\n'"$folded"; fi
|
|
4869
|
+
fi ;;
|
|
4870
|
+
esac
|
|
4237
4871
|
|
|
4238
4872
|
case "$event" in
|
|
4239
4873
|
paused)
|
|
@@ -4248,10 +4882,13 @@ forge_report() {
|
|
|
4248
4882
|
resumed)
|
|
4249
4883
|
text="The harness run on \`$br\` resumed." ;;
|
|
4250
4884
|
failed)
|
|
4251
|
-
|
|
4252
|
-
|
|
4885
|
+
trigger_label="${FORGE_TRIGGER_LABEL:-${HARNESS_TRIGGER_LABEL:-$DEFAULT_TRIGGER_LABEL}}"
|
|
4886
|
+
if [ "$kind" = pr ] && [ "$engine" = user_review ]; then
|
|
4887
|
+
text="The harness run on \`$br\` failed. Its log is \`run.log\` in the run's \`$STATE_ARTIFACT_NAME\` artifact. To start again, submit a review on this pull request requesting changes. This pull request stays open."
|
|
4888
|
+
elif [ "$kind" = pr ]; then
|
|
4889
|
+
# No review route: a round would start over a task run that never completed.
|
|
4890
|
+
text="The harness run on \`$br\` failed. Its log is \`run.log\` in the run's \`$STATE_ARTIFACT_NAME\` artifact. This draft pull request stays open: close it to discard the run${FORGE_ISSUE:+, or re-apply the label \`$trigger_label\` to issue #$FORGE_ISSUE to start a new run on the next indexed branch}."
|
|
4253
4891
|
else
|
|
4254
|
-
trigger_label="${FORGE_TRIGGER_LABEL:-${HARNESS_TRIGGER_LABEL:-$DEFAULT_TRIGGER_LABEL}}"
|
|
4255
4892
|
text="The harness run on \`$br\` failed. Its log is \`run.log\` in the run's \`$STATE_ARTIFACT_NAME\` artifact. To start again, re-apply the label \`$trigger_label\` to this issue; that starts a new run, on the next indexed branch."
|
|
4256
4893
|
fi ;;
|
|
4257
4894
|
stopped)
|
|
@@ -4262,9 +4899,28 @@ forge_report() {
|
|
|
4262
4899
|
text="The harness run on \`$br\` was stopped. While the branch exists, reopen this pull request and comment \`${COMMAND_HANDLE} resume\` here, or comment it on the run's issue, to continue it from its committed ledger. A merged pull request cannot be reopened; after a merge, use the issue."
|
|
4263
4900
|
else
|
|
4264
4901
|
text="The harness run on \`$br\` was stopped. Comment \`${COMMAND_HANDLE} resume\` to continue it from its committed ledger. A review that requests changes is collected now, and its round starts once the resumed run finishes."
|
|
4902
|
+
[ "$kind" != pr ] || text="$text Its draft pull request stays open; closing it discards the run."
|
|
4265
4903
|
fi ;;
|
|
4266
4904
|
round)
|
|
4267
|
-
text="A user-review round started on \`$br\`; a \`completed\` comment follows when the branch is ready for review again."
|
|
4905
|
+
text="A user-review round started on \`$br\`; a \`completed\` comment follows when the branch is ready for review again."
|
|
4906
|
+
# The undo runs before the comment is written, so the comment states its outcome.
|
|
4907
|
+
if [ "$kind" = pr ] && [ "$FORGE_PR_DRAFT" = false ]; then
|
|
4908
|
+
if gh_call pr ready "$FORGE_PR" --repo "$FORGE_REPO" --undo; then
|
|
4909
|
+
text="$text This pull request is a draft again until the round completes."
|
|
4910
|
+
else
|
|
4911
|
+
echo "::warning::remote-run.sh: report: turning pull request #$FORGE_PR back to a draft was refused: $GH_ERR"
|
|
4912
|
+
text="$text Turning this pull request back to a draft was refused, so it stays ready for review while the round works."
|
|
4913
|
+
fi
|
|
4914
|
+
fi ;;
|
|
4915
|
+
not_started)
|
|
4916
|
+
text="GitHub did not start the job of the harness run on \`$br\`, so nothing ran and the branch is unchanged."
|
|
4917
|
+
if [ "$state" = paused ] && [ -n "$REPORT_NOT_STARTED_ENGINE" ]; then
|
|
4918
|
+
text="$text Comment \`${COMMAND_HANDLE} resume\` to start it again."
|
|
4919
|
+
else
|
|
4920
|
+
# A job that never started uploaded no artifact to read an engine from.
|
|
4921
|
+
route=$(hr_github_resume_route "$br" "${REPORT_NOT_STARTED_ENGINE:-<task, user_review or docs: the one the run was started with>}")
|
|
4922
|
+
text="$text Start it again with the **Run workflow** form: ${route#or from GitHub: }."
|
|
4923
|
+
fi ;;
|
|
4268
4924
|
esac
|
|
4269
4925
|
|
|
4270
4926
|
OPEN_QUESTIONS=""
|
|
@@ -4297,27 +4953,245 @@ forge_report() {
|
|
|
4297
4953
|
echo "remote-run.sh: report: cannot create question $n's comment file for #$target; not posted" >&2
|
|
4298
4954
|
fi
|
|
4299
4955
|
done
|
|
4300
|
-
elif [ -n "$
|
|
4301
|
-
|
|
4302
|
-
printf '%s\n' "$text"
|
|
4303
|
-
[ -z "$note" ] || printf '\n%s\n' "$note"
|
|
4304
|
-
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
4305
|
-
} >"$file"
|
|
4306
|
-
forge_comment "$target" "$event" "$br" "$file" || :
|
|
4307
|
-
rm -f "$file"
|
|
4308
|
-
else
|
|
4309
|
-
echo "remote-run.sh: report: cannot create the comment file for #$target; no comment posted" >&2
|
|
4956
|
+
elif [ -n "$target" ]; then
|
|
4957
|
+
forge_report_text "$target" "$event" "$br" "$tmp" "$text" "$note"
|
|
4310
4958
|
fi
|
|
4311
|
-
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
4312
4959
|
|
|
4313
4960
|
[ -z "$FORGE_ISSUE" ] || forge_set_state "$FORGE_ISSUE" "$state" || :
|
|
4314
4961
|
[ -z "$FORGE_PR" ] || forge_set_state "$FORGE_PR" "$state" || :
|
|
4315
|
-
|
|
4962
|
+
if [ "$event" = stopped ]; then
|
|
4963
|
+
[ -z "$FORGE_PR" ] || forge_progress_stopped "$FORGE_PR" "$br"
|
|
4964
|
+
fi
|
|
4965
|
+
reported="${target:+#$target}"
|
|
4966
|
+
for n in $gone_prs; do
|
|
4967
|
+
forge_report_text "$n" "$event" "$br" "$tmp" "$text" "$note"
|
|
4968
|
+
forge_set_state "$n" "$state" || :
|
|
4969
|
+
forge_progress_stopped "$n" "$br"
|
|
4970
|
+
reported="${reported:+$reported, }#$n"
|
|
4971
|
+
done
|
|
4972
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
4973
|
+
echo "remote-run.sh: report: $event on $br reported on $reported"
|
|
4974
|
+
return 0
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4977
|
+
# forge_report_text <number> <event> <branch> <tmp_dir> <text> <note> — post
|
|
4978
|
+
# forge_report's one comment, <text>, <note> and the run URL, on <number>.
|
|
4979
|
+
forge_report_text() {
|
|
4980
|
+
local number="$1" event="$2" br="$3" tmp="$4" text="$5" note="$6" file
|
|
4981
|
+
if [ -z "$tmp" ] || ! file=$(mktemp "$tmp/harness-report-comment.XXXXXX"); then
|
|
4982
|
+
echo "remote-run.sh: report: cannot create the comment file for #$number; no comment posted" >&2
|
|
4983
|
+
return 0
|
|
4984
|
+
fi
|
|
4985
|
+
{
|
|
4986
|
+
printf '%s\n' "$text"
|
|
4987
|
+
[ -z "$note" ] || printf '\n%s\n' "$note"
|
|
4988
|
+
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
4989
|
+
} >"$file"
|
|
4990
|
+
forge_comment "$number" "$event" "$br" "$file" || :
|
|
4991
|
+
rm -f "$file"
|
|
4992
|
+
}
|
|
4993
|
+
|
|
4994
|
+
# forge_progress_comment_var <pr> <marker> [any-round] — the one lookup of a
|
|
4995
|
+
# progress comment: one paginated listing of <pr>'s comments, then the newest
|
|
4996
|
+
# `github-actions[bot]` comment with a numeric id whose last non-empty line is
|
|
4997
|
+
# <marker>, or, with `any-round`, <marker> with ` round=<digits>` before its
|
|
4998
|
+
# ` -->`. Sets PROGRESS_COMMENT_ID and PROGRESS_COMMENT_BODY, both empty when
|
|
4999
|
+
# none is picked. 0 when the listing parsed, picked or not; 1 when gh refused it
|
|
5000
|
+
# (GH_ERR as gh_call set it); 2 when it is not the expected JSON. Prints
|
|
5001
|
+
# nothing. No temporary file: forge_progress needs its own before this lookup,
|
|
5002
|
+
# forge_progress_stopped only after it.
|
|
5003
|
+
PROGRESS_COMMENT_ID=""
|
|
5004
|
+
PROGRESS_COMMENT_BODY=""
|
|
5005
|
+
forge_progress_comment_var() {
|
|
5006
|
+
local pr="$1" marker="$2" any="${3-}" pick id body
|
|
5007
|
+
PROGRESS_COMMENT_ID=""
|
|
5008
|
+
PROGRESS_COMMENT_BODY=""
|
|
5009
|
+
gh_call api --paginate "repos/$FORGE_REPO/issues/$pr/comments" --jq '.[] | {id, login: .user.login, body}' || return 1
|
|
5010
|
+
pick=$(printf '%s' "$GH_OUT" | jq -s -c --arg m "$marker" --arg any "$any" '
|
|
5011
|
+
def hit: . == $m
|
|
5012
|
+
or ($any == "any-round"
|
|
5013
|
+
and (($m | sub(" -->$"; "")) as $stem
|
|
5014
|
+
| startswith($stem) and (.[($stem | length):] | test("^ round=[0-9]+ -->$"))));
|
|
5015
|
+
[.[] | select(type == "object" and .login == "github-actions[bot]" and (.id | type) == "number")
|
|
5016
|
+
| select((.body // "") | gsub("\r"; "") | split("\n") | map(select(test("^\\s*$") | not)) | last // "" | hit)]
|
|
5017
|
+
| max_by(.id) // empty
|
|
5018
|
+
| {id, body: (.body // "")}' 2>/dev/null) || return 2
|
|
5019
|
+
[ -n "$pick" ] || return 0
|
|
5020
|
+
id=$(printf '%s' "$pick" | jq -r '.id' 2>/dev/null) || return 2
|
|
5021
|
+
# The sentinel keeps the body's trailing newlines, which $(…) would strip.
|
|
5022
|
+
body=$(printf '%s' "$pick" | jq -j '.body' 2>/dev/null && printf x) || return 2
|
|
5023
|
+
PROGRESS_COMMENT_ID="$id"
|
|
5024
|
+
PROGRESS_COMMENT_BODY="${body%x}"
|
|
5025
|
+
return 0
|
|
5026
|
+
}
|
|
5027
|
+
|
|
5028
|
+
# forge_progress <branch> — upsert the one progress comment of the run or round
|
|
5029
|
+
# on its pull request, rendered from the job checkout's flow-progress ledger:
|
|
5030
|
+
# created when none is listed, edited in place only when its body differs. No
|
|
5031
|
+
# issue fallback, no label. Always 0.
|
|
5032
|
+
forge_progress() {
|
|
5033
|
+
local br="$1" status state_rel phases engine round p line i first=1 marker
|
|
5034
|
+
local tmp made_tmp="" file id same
|
|
5035
|
+
local -a labels states
|
|
5036
|
+
if ! forge_on; then
|
|
5037
|
+
echo "remote-run.sh: report: the forge coupling is off (forge github and execution.target github-actions); nothing posted"
|
|
5038
|
+
return 0
|
|
5039
|
+
fi
|
|
5040
|
+
status=0
|
|
5041
|
+
hr_progress_comments "$root" || status=$?
|
|
5042
|
+
case "$status" in
|
|
5043
|
+
0) ;;
|
|
5044
|
+
1) echo "remote-run.sh: report: progress comments are off by execution.progressComments; nothing posted"; return 0 ;;
|
|
5045
|
+
*) echo "remote-run.sh: report: execution.progressComments is unreadable; nothing posted"; return 0 ;;
|
|
5046
|
+
esac
|
|
5047
|
+
forge_repo_var || return 0
|
|
5048
|
+
ALL_RUNS_LISTED=0
|
|
5049
|
+
status=0
|
|
5050
|
+
remote_branch_stopped "$br" || status=$?
|
|
5051
|
+
if [ "$status" -eq 0 ]; then
|
|
5052
|
+
echo "remote-run.sh: report: $br was stopped, and the stop already reported the run; nothing posted"
|
|
5053
|
+
return 0
|
|
5054
|
+
fi
|
|
5055
|
+
[ "$status" -eq 1 ] \
|
|
5056
|
+
|| echo "remote-run.sh: report: whether $br was stopped is unknown ($GH_ERR); reporting the progress" >&2
|
|
5057
|
+
state_rel=$(hr_state_dir "$root" 2>/dev/null) || state_rel=""
|
|
5058
|
+
if [ -z "$state_rel" ] || ! phases=$(hr_ledger_phases "$root/${state_rel%/}/flow_progress/${br}_progress.md"); then
|
|
5059
|
+
echo "remote-run.sh: report: $br has no task or user-review ledger; nothing posted"
|
|
5060
|
+
return 0
|
|
5061
|
+
fi
|
|
5062
|
+
read -r engine round states[0] states[1] states[2] states[3] <<<"$phases"
|
|
5063
|
+
forge_fetch_branch "$br"
|
|
5064
|
+
forge_pr_var "$br" || FORGE_PR=""
|
|
5065
|
+
if [ -z "$FORGE_PR" ] || ! forge_recognised "$br"; then
|
|
5066
|
+
echo "remote-run.sh: report: $br has no recognised open pull request; nothing posted"
|
|
5067
|
+
return 0
|
|
5068
|
+
fi
|
|
5069
|
+
|
|
5070
|
+
if [ "$engine" = user_review ]; then
|
|
5071
|
+
labels=('Fix plan' 'Fix implementation' 'Branch review' 'Done')
|
|
5072
|
+
line="Progress of user-review round $round on \`$br\`:"
|
|
5073
|
+
else
|
|
5074
|
+
labels=('Planning' 'Implementation' 'Branch review' 'Done')
|
|
5075
|
+
line="Progress of the harness run on \`$br\`:"
|
|
5076
|
+
round=""
|
|
5077
|
+
fi
|
|
5078
|
+
marker=$(forge_marker progress "$br" "" "" "$round")
|
|
5079
|
+
|
|
5080
|
+
tmp="${RUNNER_TEMP-}"
|
|
5081
|
+
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
5082
|
+
tmp=$(mktemp -d) || tmp=""
|
|
5083
|
+
made_tmp="$tmp"
|
|
5084
|
+
fi
|
|
5085
|
+
if [ -z "$tmp" ] || ! file=$(mktemp "$tmp/harness-progress-comment.XXXXXX"); then
|
|
5086
|
+
echo "::warning::remote-run.sh: report: cannot create the progress comment file for #$FORGE_PR; nothing posted"
|
|
5087
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
5088
|
+
return 0
|
|
5089
|
+
fi
|
|
5090
|
+
# No timestamp and no run URL: an unchanged ledger must render byte-identical.
|
|
5091
|
+
# Plain list items: a `- [ ]` box is clickable by anyone with write access.
|
|
5092
|
+
{
|
|
5093
|
+
printf '%s\n\n' "$line"
|
|
5094
|
+
for i in 0 1 2 3; do
|
|
5095
|
+
if [ "${states[$i]}" = done ]; then
|
|
5096
|
+
p=done
|
|
5097
|
+
elif [ "$first" -eq 1 ]; then
|
|
5098
|
+
p="in progress"; first=0
|
|
5099
|
+
else
|
|
5100
|
+
p="not started"
|
|
5101
|
+
fi
|
|
5102
|
+
printf -- '- %s: %s\n' "${labels[$i]}" "$p"
|
|
5103
|
+
done
|
|
5104
|
+
} >"$file"
|
|
5105
|
+
{ cat "$file"; printf '\n%s\n' "$marker"; } >"$file.full"
|
|
5106
|
+
|
|
5107
|
+
status=0
|
|
5108
|
+
forge_progress_comment_var "$FORGE_PR" "$marker" || status=$?
|
|
5109
|
+
if [ "$status" -eq 1 ]; then
|
|
5110
|
+
echo "::warning::remote-run.sh: report: listing the comments of #$FORGE_PR was refused, so the progress is not posted: $GH_ERR"
|
|
5111
|
+
elif [ "$status" -ne 0 ]; then
|
|
5112
|
+
echo "::warning::remote-run.sh: report: the comments of #$FORGE_PR are not the expected JSON, so the progress is not posted"
|
|
5113
|
+
elif [ -z "$PROGRESS_COMMENT_ID" ]; then
|
|
5114
|
+
if forge_comment "$FORGE_PR" progress "$br" "$file" "" "" "$round"; then
|
|
5115
|
+
echo "remote-run.sh: report: progress on $br posted on #$FORGE_PR"
|
|
5116
|
+
else
|
|
5117
|
+
echo "::warning::remote-run.sh: report: posting the progress comment on #$FORGE_PR was refused: $GH_ERR"
|
|
5118
|
+
fi
|
|
5119
|
+
else
|
|
5120
|
+
id="$PROGRESS_COMMENT_ID"
|
|
5121
|
+
same=$(jq -n -r --arg body "$PROGRESS_COMMENT_BODY" --arg want "$(cat "$file.full")" '
|
|
5122
|
+
def norm: gsub("\r"; "") | sub("\n+$"; "");
|
|
5123
|
+
if ($body | norm) == ($want | norm) then "same" else "differs" end' 2>/dev/null) || same=differs
|
|
5124
|
+
if [ "$same" = same ]; then
|
|
5125
|
+
echo "remote-run.sh: report: the progress comment on #$FORGE_PR is already current"
|
|
5126
|
+
elif gh_call api --method PATCH "repos/$FORGE_REPO/issues/comments/$id" -F "body=@$file.full"; then
|
|
5127
|
+
echo "remote-run.sh: report: progress on $br edited in comment $id on #$FORGE_PR"
|
|
5128
|
+
else
|
|
5129
|
+
echo "::warning::remote-run.sh: report: editing the progress comment $id on #$FORGE_PR was refused: $GH_ERR"
|
|
5130
|
+
fi
|
|
5131
|
+
fi
|
|
5132
|
+
rm -f "$file" "$file.full"
|
|
5133
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
5134
|
+
return 0
|
|
5135
|
+
}
|
|
5136
|
+
|
|
5137
|
+
# forge_progress_stopped <pr> <branch> — after a stop, rewrite each
|
|
5138
|
+
# `- <label>: in progress` line of <branch>'s progress comment on <pr>, the task
|
|
5139
|
+
# run's or any round's, to `- <label>: stopped`, leaving every other byte; the
|
|
5140
|
+
# cancelled job's last progress pass is withheld, so nothing else does. Gated by
|
|
5141
|
+
# hr_progress_comments; no comment or no such line is no edit. Always 0.
|
|
5142
|
+
forge_progress_stopped() {
|
|
5143
|
+
local pr="$1" br="$2" status count tmp made_tmp="" file
|
|
5144
|
+
status=0
|
|
5145
|
+
hr_progress_comments "$root" || status=$?
|
|
5146
|
+
case "$status" in
|
|
5147
|
+
0) ;;
|
|
5148
|
+
1) echo "remote-run.sh: report: progress comments are off by execution.progressComments; nothing marked stopped"; return 0 ;;
|
|
5149
|
+
*) echo "remote-run.sh: report: execution.progressComments is unreadable; nothing marked stopped"; return 0 ;;
|
|
5150
|
+
esac
|
|
5151
|
+
status=0
|
|
5152
|
+
forge_progress_comment_var "$pr" "$(forge_marker progress "$br")" any-round || status=$?
|
|
5153
|
+
if [ "$status" -eq 1 ]; then
|
|
5154
|
+
echo "::warning::remote-run.sh: report: listing the comments of #$pr was refused, so its progress comment is not marked stopped: $GH_ERR"
|
|
5155
|
+
return 0
|
|
5156
|
+
elif [ "$status" -ne 0 ]; then
|
|
5157
|
+
echo "::warning::remote-run.sh: report: the comments of #$pr are not the expected JSON, so its progress comment is not marked stopped"
|
|
5158
|
+
return 0
|
|
5159
|
+
fi
|
|
5160
|
+
# A trailing carriage return is kept, so a CRLF body is rewritten in its own line endings.
|
|
5161
|
+
count=0
|
|
5162
|
+
[ -z "$PROGRESS_COMMENT_ID" ] \
|
|
5163
|
+
|| count=$(jq -n -r --arg b "$PROGRESS_COMMENT_BODY" \
|
|
5164
|
+
'[$b | split("\n")[] | select(test("^- .+: in progress\r?$"))] | length' 2>/dev/null) || count=0
|
|
5165
|
+
if [ "$count" = 0 ]; then
|
|
5166
|
+
echo "remote-run.sh: report: #$pr has no progress comment of $br in progress; nothing to mark stopped"
|
|
5167
|
+
return 0
|
|
5168
|
+
fi
|
|
5169
|
+
tmp="${RUNNER_TEMP-}"
|
|
5170
|
+
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
5171
|
+
tmp=$(mktemp -d) || tmp=""
|
|
5172
|
+
made_tmp="$tmp"
|
|
5173
|
+
fi
|
|
5174
|
+
if [ -z "$tmp" ] || ! file=$(mktemp "$tmp/harness-progress-stopped.XXXXXX"); then
|
|
5175
|
+
echo "::warning::remote-run.sh: report: cannot create the progress comment file for #$pr; nothing marked stopped"
|
|
5176
|
+
elif ! jq -n -j --arg b "$PROGRESS_COMMENT_BODY" '$b | split("\n")
|
|
5177
|
+
| map(sub("^(?<l>- .+): in progress(?<cr>\r?)$"; "\(.l): stopped\(.cr)")) | join("\n")' >"$file" 2>/dev/null; then
|
|
5178
|
+
echo "::warning::remote-run.sh: report: cannot write the stopped progress comment for #$pr; nothing marked stopped"
|
|
5179
|
+
elif gh_call api --method PATCH "repos/$FORGE_REPO/issues/comments/$PROGRESS_COMMENT_ID" -F "body=@$file"; then
|
|
5180
|
+
echo "remote-run.sh: report: progress of $br marked stopped in comment $PROGRESS_COMMENT_ID on #$pr"
|
|
5181
|
+
else
|
|
5182
|
+
echo "::warning::remote-run.sh: report: editing the progress comment $PROGRESS_COMMENT_ID on #$pr was refused: $GH_ERR"
|
|
5183
|
+
fi
|
|
5184
|
+
[ -z "${file-}" ] || rm -f "$file"
|
|
5185
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
4316
5186
|
return 0
|
|
4317
5187
|
}
|
|
4318
5188
|
|
|
4319
5189
|
verb_report() {
|
|
4320
|
-
|
|
5190
|
+
if [ "$report_event" = progress ]; then
|
|
5191
|
+
forge_progress "$branch"
|
|
5192
|
+
else
|
|
5193
|
+
forge_report "$report_event" "$branch" "$report_note"
|
|
5194
|
+
fi
|
|
4321
5195
|
exit "$EXIT_OK"
|
|
4322
5196
|
}
|
|
4323
5197
|
|
|
@@ -4352,7 +5226,7 @@ deliver_pr_body() {
|
|
|
4352
5226
|
verbs="$verbs${verbs:+, }\`$COMMAND_HANDLE $v\`"
|
|
4353
5227
|
done
|
|
4354
5228
|
{
|
|
4355
|
-
printf 'This pull request carries the harness run on `%s
|
|
5229
|
+
printf 'This pull request carries the harness run on `%s`. It stays a draft while the run works, and the harness marks it ready for review when the run completes; the harness never merges it.\n' "$2"
|
|
4356
5230
|
[ -z "$FORGE_ISSUE" ] || printf '\nStarted from #%s.\n' "$FORGE_ISSUE"
|
|
4357
5231
|
printf '\nA review that requests changes starts a user-review round on this branch.\n'
|
|
4358
5232
|
printf 'Comment %s to act on the run; each says when it applies (docs/github-run-control.md in the harness documentation).\n' "$verbs"
|
|
@@ -4374,11 +5248,280 @@ deliver_create() {
|
|
|
4374
5248
|
fi
|
|
4375
5249
|
}
|
|
4376
5250
|
|
|
4377
|
-
#
|
|
4378
|
-
#
|
|
5251
|
+
# forge_open_pr <branch> — the one opener of a run's pull request, against
|
|
5252
|
+
# `defaultBranch`: a `--draft` create, then on any failure but
|
|
5253
|
+
# PR_CREATE_FORBIDDEN one retry without `--draft`. The caller has set
|
|
5254
|
+
# FORGE_REPO, FORGE_SERVER and FORGE_ISSUE. 0 when opened, setting FORGE_PR,
|
|
5255
|
+
# FORGE_PR_URL and FORGE_PR_DRAFT (`false` after the retry); 1 when not,
|
|
5256
|
+
# setting OPEN_ERR (one line) and OPEN_FORBIDDEN (1 on PR_CREATE_FORBIDDEN).
|
|
5257
|
+
# Always sets OPEN_BASE, empty when unreadable. Never posts, never exits.
|
|
5258
|
+
FORGE_PR_URL=""
|
|
5259
|
+
OPEN_ERR=""
|
|
5260
|
+
OPEN_FORBIDDEN=0
|
|
5261
|
+
OPEN_BASE=""
|
|
5262
|
+
forge_open_pr() {
|
|
5263
|
+
local br="$1" tmp made_tmp="" body="" draft=""
|
|
5264
|
+
FORGE_PR=""
|
|
5265
|
+
FORGE_PR_URL=""
|
|
5266
|
+
FORGE_PR_DRAFT=""
|
|
5267
|
+
OPEN_ERR=""
|
|
5268
|
+
OPEN_FORBIDDEN=0
|
|
5269
|
+
OPEN_BASE=$(hr_default_branch "$root" 2>/dev/null) || OPEN_BASE=""
|
|
5270
|
+
deliver_title_var "$br"
|
|
5271
|
+
if [ -z "$OPEN_BASE" ]; then
|
|
5272
|
+
OPEN_ERR="the configured defaultBranch could not be read"
|
|
5273
|
+
return 1
|
|
5274
|
+
fi
|
|
5275
|
+
tmp="${RUNNER_TEMP-}"
|
|
5276
|
+
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
5277
|
+
tmp=$(mktemp -d) || tmp=""
|
|
5278
|
+
made_tmp="$tmp"
|
|
5279
|
+
fi
|
|
5280
|
+
if [ -z "$tmp" ] || ! body=$(mktemp "$tmp/harness-pr-body.XXXXXX") || ! deliver_pr_body "$body" "$br"; then
|
|
5281
|
+
OPEN_ERR="its body file could not be written"
|
|
5282
|
+
elif deliver_create "$OPEN_BASE" "$br" "$body" --draft; then
|
|
5283
|
+
draft=true
|
|
5284
|
+
else
|
|
5285
|
+
OPEN_ERR="$GH_ERR"
|
|
5286
|
+
case "$GH_ERR" in
|
|
5287
|
+
*"$PR_CREATE_FORBIDDEN"*) OPEN_FORBIDDEN=1 ;;
|
|
5288
|
+
*)
|
|
5289
|
+
# Drafts depend on the account's plan (C3, not measured): one retry as ready.
|
|
5290
|
+
echo "remote-run.sh: $verb: opening a draft pull request failed ($GH_ERR); retrying once without --draft" >&2
|
|
5291
|
+
if deliver_create "$OPEN_BASE" "$br" "$body"; then
|
|
5292
|
+
draft=false
|
|
5293
|
+
else
|
|
5294
|
+
OPEN_ERR="$GH_ERR"
|
|
5295
|
+
fi ;;
|
|
5296
|
+
esac
|
|
5297
|
+
fi
|
|
5298
|
+
[ -z "$body" ] || rm -f "$body"
|
|
5299
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
5300
|
+
[ -n "$draft" ] || return 1
|
|
5301
|
+
FORGE_PR=$(printf '%s\n' "$GH_OUT" | sed -n 's|.*/pull/\([0-9][0-9]*\).*|\1|p' | tail -n 1)
|
|
5302
|
+
if [ -z "$FORGE_PR" ]; then
|
|
5303
|
+
OPEN_ERR="gh printed no pull request URL"
|
|
5304
|
+
return 1
|
|
5305
|
+
fi
|
|
5306
|
+
FORGE_PR_URL="$FORGE_SERVER/$FORGE_REPO/pull/$FORGE_PR"
|
|
5307
|
+
FORGE_PR_DRAFT="$draft"
|
|
5308
|
+
return 0
|
|
5309
|
+
}
|
|
5310
|
+
|
|
5311
|
+
# deliver_comment <number> <tmp> <text> — one `completed` comment on <number>:
|
|
5312
|
+
# <text>, the `phases.qa` sentence when that phase is on, this run's URL when
|
|
5313
|
+
# GITHUB_RUN_ID is set, then the marker. A failure is one line.
|
|
5314
|
+
deliver_comment() {
|
|
5315
|
+
local number="$1" tmp="$2" text="$3" file
|
|
5316
|
+
if hr_phase_enabled "$root" qa; then
|
|
5317
|
+
text="$text
|
|
5318
|
+
|
|
5319
|
+
The interactive-test phase was skipped on GitHub Actions. Before merging, run \`/autonomous-sdlc-harness:branch-qa-test $branch\` locally."
|
|
5320
|
+
fi
|
|
5321
|
+
if [ -z "$tmp" ] || ! file=$(mktemp "$tmp/harness-deliver-comment.XXXXXX"); then
|
|
5322
|
+
echo "remote-run.sh: deliver: cannot create the comment file for #$number; no comment posted" >&2
|
|
5323
|
+
return 0
|
|
5324
|
+
fi
|
|
5325
|
+
{
|
|
5326
|
+
printf '%s\n' "$text"
|
|
5327
|
+
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
5328
|
+
} >"$file"
|
|
5329
|
+
forge_comment "$number" completed "$branch" "$file" || :
|
|
5330
|
+
rm -f "$file"
|
|
5331
|
+
}
|
|
5332
|
+
|
|
5333
|
+
# deliver_thread_reply <tmp> <comment_id> <text> — one reply to the review
|
|
5334
|
+
# thread holding <comment_id>, <text> then the `thread` marker. 0 when posted;
|
|
5335
|
+
# otherwise one `::warning::` line and 1.
|
|
5336
|
+
deliver_thread_reply() {
|
|
5337
|
+
local tmp="$1" id="$2" text="$3" file
|
|
5338
|
+
if [ -z "$tmp" ] || ! file=$(mktemp "$tmp/harness-thread-reply.XXXXXX"); then
|
|
5339
|
+
echo "::warning::remote-run.sh: deliver: cannot create the reply file for comment $id; no reply posted"
|
|
5340
|
+
return 1
|
|
5341
|
+
fi
|
|
5342
|
+
if ! { printf '%s\n\n' "$text"; forge_marker thread "$branch" "" "" "$RC_MARKED_N"; } >"$file"; then
|
|
5343
|
+
rm -f "$file"
|
|
5344
|
+
echo "::warning::remote-run.sh: deliver: cannot write the reply file for comment $id; no reply posted"
|
|
5345
|
+
return 1
|
|
5346
|
+
fi
|
|
5347
|
+
if ! gh_call api --method POST "repos/$FORGE_REPO/pulls/$FORGE_PR/comments/$id/replies" -F "body=@$file"; then
|
|
5348
|
+
rm -f "$file"
|
|
5349
|
+
echo "::warning::remote-run.sh: deliver: the reply to review comment $id was refused: $GH_ERR"
|
|
5350
|
+
return 1
|
|
5351
|
+
fi
|
|
5352
|
+
rm -f "$file"
|
|
5353
|
+
return 0
|
|
5354
|
+
}
|
|
5355
|
+
|
|
5356
|
+
# deliver_round_threads <tmp> — at a round's completion, handle each inline
|
|
5357
|
+
# review thread the round collected by the fix plan's verdict on its comment:
|
|
5358
|
+
# `fixed` is a reply naming the fix commit, then the thread resolved; `reason`
|
|
5359
|
+
# is a reply quoting it, the thread left open. The wire it reads is the
|
|
5360
|
+
# header's THE REVIEW-COMMENT LINE. Never dismisses a review; never fails.
|
|
5361
|
+
deliver_round_threads() {
|
|
5362
|
+
local tmp="$1" plan index_rel dir_rel index names path name k line rest id ids
|
|
5363
|
+
local sha tip since pages threads row t_id t_resolved t_ids t_root t_later t_own
|
|
5364
|
+
local i found handled="," own_marker re
|
|
5365
|
+
local -a v_id=() v_kind=() v_val=()
|
|
5366
|
+
if ! round_markers_read ""; then
|
|
5367
|
+
echo "::warning::remote-run.sh: deliver: the round's review threads were not read: $RC_ERR"
|
|
5368
|
+
return 0
|
|
5369
|
+
fi
|
|
5370
|
+
if [ "$RC_NEWEST_N" -eq 0 ]; then
|
|
5371
|
+
echo "remote-run.sh: deliver: $branch has no round file; no review thread handled"
|
|
5372
|
+
return 0
|
|
5373
|
+
fi
|
|
5374
|
+
if [ "$RC_NEWEST_N" -ne "$RC_MARKED_N" ]; then
|
|
5375
|
+
echo "remote-run.sh: deliver: round $RC_NEWEST_N of $branch was not placed from a pull-request review; no review thread handled"
|
|
5376
|
+
return 0
|
|
5377
|
+
fi
|
|
5378
|
+
if [ -z "$RC_MARKED_C" ]; then
|
|
5379
|
+
echo "remote-run.sh: deliver: round $RC_MARKED_N of $branch collected no inline comment; no review thread handled"
|
|
5380
|
+
return 0
|
|
5381
|
+
fi
|
|
5382
|
+
|
|
5383
|
+
plan="${branch}_fix_plan"
|
|
5384
|
+
[ "$RC_MARKED_N" -eq 1 ] || plan="${plan}_$RC_MARKED_N"
|
|
5385
|
+
index_rel="$RC_STATE_REL/user_reviews/$plan.md"
|
|
5386
|
+
dir_rel="$RC_STATE_REL/user_reviews/$plan"
|
|
5387
|
+
if ! index=$(git -C "$root" show "refs/remotes/origin/$branch:$index_rel" 2>/dev/null); then
|
|
5388
|
+
echo "remote-run.sh: deliver: round $RC_MARKED_N of $branch has no fix plan at $index_rel; no review thread handled"
|
|
5389
|
+
return 0
|
|
5390
|
+
fi
|
|
5391
|
+
tip=$(git -C "$root" rev-parse "refs/remotes/origin/$branch" 2>/dev/null) || tip=""
|
|
5392
|
+
|
|
5393
|
+
# Verdicts, kept only for ids the round collected; the first verdict an id gets wins.
|
|
5394
|
+
names=$(git -C "$root" ls-tree --name-only "refs/remotes/origin/$branch" -- "$dir_rel/" 2>/dev/null) || names=""
|
|
5395
|
+
while IFS= read -r path; do
|
|
5396
|
+
name="${path##*/}"
|
|
5397
|
+
[[ "$name" =~ ^finding_([0-9]+)\.md$ ]] || continue
|
|
5398
|
+
k=$((10#${BASH_REMATCH[1]}))
|
|
5399
|
+
case "$index" in *"[x] **Finding $k**"*) ;; *) continue ;; esac
|
|
5400
|
+
line=$(git -C "$root" show "refs/remotes/origin/$branch:$path" 2>/dev/null | grep -E '^\*\*Review comments:\*\* ' | head -n 1) || line=""
|
|
5401
|
+
[ -n "$line" ] || continue
|
|
5402
|
+
sha=$(git -C "$root" log --reverse --format=%H -S "[x] **Finding $k**" "refs/remotes/origin/$branch" -- "$index_rel" 2>/dev/null | head -n 1) || sha=""
|
|
5403
|
+
[ -n "$sha" ] || sha="$tip"
|
|
5404
|
+
rest="${line#'**Review comments:** '}"
|
|
5405
|
+
rest="${rest%$'\r'}"
|
|
5406
|
+
ids="${rest//[[:space:]]/}"
|
|
5407
|
+
for id in ${ids//,/ }; do
|
|
5408
|
+
[[ "$id" =~ ^[0-9]+$ ]] || continue
|
|
5409
|
+
v_id+=("$id"); v_kind+=(fixed); v_val+=("$sha")
|
|
5410
|
+
done
|
|
5411
|
+
done <<NAMES
|
|
5412
|
+
$names
|
|
5413
|
+
NAMES
|
|
5414
|
+
re='^- (.*) \*\*Review comments:\*\* ([0-9][0-9, ]*)$'
|
|
5415
|
+
found=0
|
|
5416
|
+
while IFS= read -r line; do
|
|
5417
|
+
line="${line%$'\r'}"
|
|
5418
|
+
case "$line" in
|
|
5419
|
+
'## Out of scope / verified-OK'*) found=1; continue ;;
|
|
5420
|
+
'## '*) found=0; continue ;;
|
|
5421
|
+
esac
|
|
5422
|
+
[ "$found" -eq 1 ] || continue
|
|
5423
|
+
[[ "$line" =~ $re ]] || continue
|
|
5424
|
+
rest="${BASH_REMATCH[1]}"
|
|
5425
|
+
ids="${BASH_REMATCH[2]//[[:space:]]/}"
|
|
5426
|
+
for id in ${ids//,/ }; do
|
|
5427
|
+
[[ "$id" =~ ^[0-9]+$ ]] || continue
|
|
5428
|
+
v_id+=("$id"); v_kind+=(reason); v_val+=("$rest")
|
|
5429
|
+
done
|
|
5430
|
+
done <<INDEX
|
|
5431
|
+
$index
|
|
5432
|
+
INDEX
|
|
5433
|
+
|
|
5434
|
+
if ! since=$(jq -n -r --arg t "$RC_MARKED_AT" '$t | fromdateiso8601' 2>/dev/null) || [ -z "$since" ]; then
|
|
5435
|
+
echo "::warning::remote-run.sh: deliver: round $RC_MARKED_N's collected_at '$RC_MARKED_AT' is not a UTC time; no review thread handled"
|
|
5436
|
+
return 0
|
|
5437
|
+
fi
|
|
5438
|
+
if ! gh_call api graphql --paginate -f "owner=${FORGE_REPO%%/*}" -f "name=${FORGE_REPO#*/}" -F "number=$FORGE_PR" \
|
|
5439
|
+
-f 'query=query($owner: String!, $name: String!, $number: Int!, $endCursor: String) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { reviewThreads(first: 100, after: $endCursor) { pageInfo { hasNextPage endCursor } nodes { id isResolved comments(first: 100) { nodes { databaseId createdAt body } } } } } } }'; then
|
|
5440
|
+
echo "::warning::remote-run.sh: deliver: listing the review threads of pull request #$FORGE_PR was refused: $GH_ERR"
|
|
5441
|
+
return 0
|
|
5442
|
+
fi
|
|
5443
|
+
pages="$GH_OUT"
|
|
5444
|
+
own_marker=$(forge_marker thread "$branch" "" "" "$RC_MARKED_N")
|
|
5445
|
+
# One `|`-joined row per thread (a GraphQL node id carries no `|`): id,
|
|
5446
|
+
# resolved, its comment ids, its first comment's id, whether a comment came
|
|
5447
|
+
# after the round, whether it carries this harness's reply.
|
|
5448
|
+
if ! threads=$(printf '%s' "$pages" | jq -s -r --argjson since "$since" --arg collected ",$RC_MARKED_C," \
|
|
5449
|
+
--arg marker "$COMMENT_MARKER" --arg own "$own_marker" '
|
|
5450
|
+
[ .[] | .data.repository.pullRequest.reviewThreads.nodes[] ]
|
|
5451
|
+
| .[] | (.comments.nodes // []) as $c
|
|
5452
|
+
| [ .id, (.isResolved == true | tostring),
|
|
5453
|
+
($c | map(.databaseId | tostring) | join(",")),
|
|
5454
|
+
(($c[0].databaseId // "") | tostring),
|
|
5455
|
+
(any($c[]; ("," + (.databaseId | tostring) + ",") as $k
|
|
5456
|
+
| ((.createdAt // "") | try fromdateiso8601 catch 0) > $since
|
|
5457
|
+
and (($collected | contains($k)) | not)
|
|
5458
|
+
and (((.body // "") | contains($marker)) | not)) | tostring),
|
|
5459
|
+
(any($c[]; (.body // "") | contains($own)) | tostring) ]
|
|
5460
|
+
| join("|")' 2>/dev/null); then
|
|
5461
|
+
echo "::warning::remote-run.sh: deliver: the review threads of pull request #$FORGE_PR are not the expected JSON; no review thread handled"
|
|
5462
|
+
return 0
|
|
5463
|
+
fi
|
|
5464
|
+
|
|
5465
|
+
for id in ${RC_MARKED_C//,/ }; do
|
|
5466
|
+
i=0; found=""
|
|
5467
|
+
while [ "$i" -lt "${#v_id[@]}" ]; do
|
|
5468
|
+
if [ "${v_id[$i]}" = "$id" ]; then found="$i"; break; fi
|
|
5469
|
+
i=$((i + 1))
|
|
5470
|
+
done
|
|
5471
|
+
if [ -z "$found" ]; then
|
|
5472
|
+
echo "remote-run.sh: deliver: review comment $id has no verdict in the fix plan; left alone"
|
|
5473
|
+
continue
|
|
5474
|
+
fi
|
|
5475
|
+
row=""
|
|
5476
|
+
while IFS= read -r line; do
|
|
5477
|
+
[ -n "$line" ] || continue
|
|
5478
|
+
IFS='|' read -r t_id t_resolved t_ids t_root t_later t_own <<<"$line"
|
|
5479
|
+
case ",$t_ids," in *",$id,"*) row="$line"; break ;; esac
|
|
5480
|
+
done <<THREADS
|
|
5481
|
+
$threads
|
|
5482
|
+
THREADS
|
|
5483
|
+
if [ -z "$row" ]; then
|
|
5484
|
+
echo "remote-run.sh: deliver: review comment $id is in no review thread of pull request #$FORGE_PR; left alone"
|
|
5485
|
+
continue
|
|
5486
|
+
fi
|
|
5487
|
+
case "$handled" in
|
|
5488
|
+
*",$t_id,"*) echo "remote-run.sh: deliver: review comment $id's thread was handled for another comment; left alone"; continue ;;
|
|
5489
|
+
esac
|
|
5490
|
+
handled="$handled$t_id,"
|
|
5491
|
+
if [ "$t_resolved" = true ]; then
|
|
5492
|
+
echo "remote-run.sh: deliver: review comment $id's thread is already resolved; left alone"
|
|
5493
|
+
continue
|
|
5494
|
+
fi
|
|
5495
|
+
if [ "$t_later" = true ]; then
|
|
5496
|
+
echo "remote-run.sh: deliver: review comment $id's thread has a reply newer than round $RC_MARKED_N; left alone"
|
|
5497
|
+
continue
|
|
5498
|
+
fi
|
|
5499
|
+
if [ "$t_own" = true ]; then
|
|
5500
|
+
echo "remote-run.sh: deliver: review comment $id's thread already carries this harness's reply; left alone"
|
|
5501
|
+
continue
|
|
5502
|
+
fi
|
|
5503
|
+
# Replies attach to a thread's first comment: GitHub does not take a reply to a reply.
|
|
5504
|
+
if [ "${v_kind[$found]}" = fixed ]; then
|
|
5505
|
+
deliver_thread_reply "$tmp" "$t_root" "Addressed in \`${v_val[$found]}\`." || continue
|
|
5506
|
+
if gh_call api graphql -f 'query=mutation($id: ID!) { resolveReviewThread(input: {threadId: $id}) { thread { isResolved } } }' -f "id=$t_id"; then
|
|
5507
|
+
echo "remote-run.sh: deliver: replied to review comment $id and resolved its thread"
|
|
5508
|
+
else
|
|
5509
|
+
echo "::warning::remote-run.sh: deliver: resolving review comment $id's thread was refused: $GH_ERR"
|
|
5510
|
+
fi
|
|
5511
|
+
else
|
|
5512
|
+
deliver_thread_reply "$tmp" "$t_root" "Not changed in this round: ${v_val[$found]}" || continue
|
|
5513
|
+
echo "remote-run.sh: deliver: replied to review comment $id with the reason; its thread stays open"
|
|
5514
|
+
fi
|
|
5515
|
+
done
|
|
5516
|
+
return 0
|
|
5517
|
+
}
|
|
5518
|
+
|
|
5519
|
+
# verb_deliver — after a `completed` bundle: reuse the branch's pull request or
|
|
5520
|
+
# open it, mark it ready, post `completed` on it and on the issue, and set
|
|
5521
|
+
# `done`. Always exit 0.
|
|
4379
5522
|
verb_deliver() {
|
|
4380
|
-
local status
|
|
4381
|
-
local tmp made_tmp=""
|
|
5523
|
+
local status engine noun verb_done pr_url="" lookup_failed=0 create_err="" forbidden=0 base=""
|
|
5524
|
+
local tmp made_tmp="" ready="" text readiness compare posted=""
|
|
4382
5525
|
if ! forge_on; then
|
|
4383
5526
|
echo "remote-run.sh: deliver: the forge coupling is off (forge github and execution.target github-actions); nothing posted"
|
|
4384
5527
|
exit "$EXIT_OK"
|
|
@@ -4389,99 +5532,159 @@ verb_deliver() {
|
|
|
4389
5532
|
echo "remote-run.sh: deliver: the bundle's status is '${status:-unreadable}', not completed; nothing posted"
|
|
4390
5533
|
exit "$EXIT_OK"
|
|
4391
5534
|
fi
|
|
5535
|
+
engine=$(hr_remote_status_get "$bundle_dir/$HR_REMOTE_STATUS_FILE" engine 2>/dev/null) || engine=""
|
|
5536
|
+
if [ "$engine" = user_review ]; then
|
|
5537
|
+
noun=round; verb_done=finished
|
|
5538
|
+
else
|
|
5539
|
+
noun=run; verb_done=completed
|
|
5540
|
+
fi
|
|
4392
5541
|
forge_repo_var || exit "$EXIT_OK"
|
|
4393
5542
|
|
|
4394
5543
|
forge_fetch_branch "$branch"
|
|
4395
5544
|
forge_issue_var "$branch" || FORGE_ISSUE=""
|
|
4396
|
-
forge_pr_var "$branch" || { FORGE_PR=""; lookup_failed=1; create_err="whether a pull request is already open could not be read ($GH_ERR)"; }
|
|
4397
|
-
|
|
4398
|
-
tmp="${RUNNER_TEMP-}"
|
|
4399
|
-
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
4400
|
-
tmp=$(mktemp -d) || tmp=""
|
|
4401
|
-
made_tmp="$tmp"
|
|
4402
|
-
fi
|
|
5545
|
+
forge_pr_var "$branch" || { FORGE_PR=""; FORGE_PR_DRAFT=""; lookup_failed=1; create_err="whether a pull request is already open could not be read ($GH_ERR)"; }
|
|
4403
5546
|
|
|
4404
5547
|
if [ -n "$FORGE_PR" ]; then
|
|
4405
5548
|
pr_url="$FORGE_SERVER/$FORGE_REPO/pull/$FORGE_PR"
|
|
4406
5549
|
echo "remote-run.sh: deliver: $branch already has pull request #$FORGE_PR; none opened"
|
|
4407
5550
|
elif [ "$lookup_failed" -eq 0 ]; then
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
elif [ -z "$tmp" ] || ! body=$(mktemp "$tmp/harness-deliver-body.XXXXXX") || ! deliver_pr_body "$body" "$branch"; then
|
|
4413
|
-
create_err="its body file could not be written"
|
|
4414
|
-
elif deliver_create "$base" "$branch" "$body" --draft; then
|
|
4415
|
-
opened=1
|
|
5551
|
+
echo "remote-run.sh: deliver: $branch has no open pull request at completion — the start's attempt failed or the workflow predates the open step; opening it now"
|
|
5552
|
+
if forge_open_pr "$branch"; then
|
|
5553
|
+
pr_url="$FORGE_PR_URL"
|
|
5554
|
+
echo "remote-run.sh: deliver: opened pull request #$FORGE_PR for $branch"
|
|
4416
5555
|
else
|
|
4417
|
-
create_err="$
|
|
4418
|
-
|
|
4419
|
-
*"$PR_CREATE_FORBIDDEN"*) forbidden=1 ;;
|
|
4420
|
-
*)
|
|
4421
|
-
# Drafts depend on the account's plan (C3, not measured): one retry as ready.
|
|
4422
|
-
echo "remote-run.sh: deliver: opening a draft pull request failed ($GH_ERR); retrying once without --draft" >&2
|
|
4423
|
-
if deliver_create "$base" "$branch" "$body"; then
|
|
4424
|
-
opened=1
|
|
4425
|
-
else
|
|
4426
|
-
create_err="$GH_ERR"
|
|
4427
|
-
fi ;;
|
|
4428
|
-
esac
|
|
5556
|
+
create_err="$OPEN_ERR"
|
|
5557
|
+
forbidden="$OPEN_FORBIDDEN"
|
|
4429
5558
|
fi
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
5559
|
+
base="$OPEN_BASE"
|
|
5560
|
+
fi
|
|
5561
|
+
|
|
5562
|
+
# The flip runs with the job's token, never HARNESS_PR_TOKEN.
|
|
5563
|
+
if [ -n "$FORGE_PR" ]; then
|
|
5564
|
+
if [ "$FORGE_PR_DRAFT" = true ]; then
|
|
5565
|
+
if gh_call pr ready "$FORGE_PR" --repo "$FORGE_REPO"; then
|
|
5566
|
+
ready=flipped
|
|
5567
|
+
echo "remote-run.sh: deliver: marked pull request #$FORGE_PR ready for review"
|
|
4436
5568
|
else
|
|
4437
|
-
|
|
4438
|
-
|
|
5569
|
+
ready=refused
|
|
5570
|
+
echo "::warning::remote-run.sh: deliver: marking pull request #$FORGE_PR ready for review was refused: $GH_ERR"
|
|
4439
5571
|
fi
|
|
5572
|
+
else
|
|
5573
|
+
ready=not-draft
|
|
4440
5574
|
fi
|
|
4441
5575
|
fi
|
|
4442
5576
|
|
|
4443
|
-
|
|
4444
|
-
if [ -z "$
|
|
4445
|
-
|
|
5577
|
+
tmp="${RUNNER_TEMP-}"
|
|
5578
|
+
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
5579
|
+
tmp=$(mktemp -d) || tmp=""
|
|
5580
|
+
made_tmp="$tmp"
|
|
5581
|
+
fi
|
|
5582
|
+
|
|
5583
|
+
# After the flip, before the comments.
|
|
5584
|
+
if [ "$noun" = round ] && [ -n "$FORGE_PR" ]; then
|
|
5585
|
+
deliver_round_threads "$tmp"
|
|
5586
|
+
fi
|
|
5587
|
+
|
|
5588
|
+
if [ -n "$FORGE_PR" ]; then
|
|
5589
|
+
case "$ready" in
|
|
5590
|
+
flipped)
|
|
5591
|
+
if [ "$noun" = round ]; then
|
|
5592
|
+
readiness="The harness round on \`$branch\` finished, and this pull request is marked ready for review again."
|
|
5593
|
+
else
|
|
5594
|
+
readiness="The harness run on \`$branch\` completed, and this pull request is now marked ready for your review."
|
|
5595
|
+
fi ;;
|
|
5596
|
+
not-draft)
|
|
5597
|
+
readiness="The harness $noun on \`$branch\` $verb_done. This pull request is not a draft — drafts may not be available on this repository's plan, or someone marked it ready — so it was left as it is." ;;
|
|
5598
|
+
*)
|
|
5599
|
+
readiness="The harness $noun on \`$branch\` $verb_done. Marking this draft ready for review was refused; mark it ready by hand." ;;
|
|
5600
|
+
esac
|
|
5601
|
+
deliver_comment "$FORGE_PR" "$tmp" "$readiness A review that requests changes starts another round."
|
|
5602
|
+
posted="#$FORGE_PR"
|
|
5603
|
+
if [ -n "$FORGE_ISSUE" ]; then
|
|
5604
|
+
if [ "$ready" = refused ]; then
|
|
5605
|
+
text="The harness $noun on \`$branch\` $verb_done. Its pull request #$FORGE_PR is still a draft — marking it ready for review was refused, so mark it ready by hand: $pr_url"
|
|
5606
|
+
elif [ "$noun" = round ]; then
|
|
5607
|
+
text="The harness round on \`$branch\` finished. Pull request #$FORGE_PR is ready for review again: $pr_url"
|
|
5608
|
+
else
|
|
5609
|
+
text="The harness run on \`$branch\` completed. Its pull request #$FORGE_PR is ready for your review: $pr_url"
|
|
5610
|
+
fi
|
|
5611
|
+
deliver_comment "$FORGE_ISSUE" "$tmp" "$text
|
|
5612
|
+
Review it there; a review that requests changes starts another round."
|
|
5613
|
+
posted="$posted and #$FORGE_ISSUE"
|
|
5614
|
+
fi
|
|
5615
|
+
elif [ -n "$FORGE_ISSUE" ]; then
|
|
5616
|
+
compare="$FORGE_SERVER/$FORGE_REPO/compare/${base:-<default branch>}...$branch?expand=1"
|
|
4446
5617
|
if [ "$forbidden" -eq 1 ]; then
|
|
4447
|
-
text="The harness
|
|
5618
|
+
text="The harness $noun on \`$branch\` $verb_done, but its pull request could not be opened: GitHub Actions is not permitted to create pull requests in this repository. Turn on *$PR_CREATE_SETTING* under $PR_CREATE_SETTING_PATH, or set the \`HARNESS_GIT_TOKEN\` secret, for the next run. For this one, open the pull request from the branch: $compare"
|
|
4448
5619
|
else
|
|
4449
|
-
text="The harness
|
|
5620
|
+
text="The harness $noun on \`$branch\` $verb_done, but its pull request could not be opened: $create_err. Open it by hand from the branch: $compare"
|
|
4450
5621
|
fi
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
text="The harness round on \`$branch\` finished. Review this pull request; a review that requests changes starts another round."
|
|
4454
|
-
elif [ -n "$FORGE_ISSUE" ]; then
|
|
4455
|
-
target="$FORGE_ISSUE"
|
|
4456
|
-
text="The harness run on \`$branch\` completed. Its pull request is ready for your review: $pr_url
|
|
4457
|
-
Review it there; a review that requests changes starts another round."
|
|
5622
|
+
deliver_comment "$FORGE_ISSUE" "$tmp" "$text"
|
|
5623
|
+
posted="#$FORGE_ISSUE"
|
|
4458
5624
|
else
|
|
4459
|
-
|
|
4460
|
-
text="The harness run on \`$branch\` completed and opened this pull request. Review it; a review that requests changes starts another round."
|
|
5625
|
+
echo "remote-run.sh: deliver: $branch has no pull request and no issue it was started from; nothing posted"
|
|
4461
5626
|
fi
|
|
4462
|
-
|
|
4463
|
-
text="$text
|
|
5627
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
4464
5628
|
|
|
4465
|
-
|
|
4466
|
-
|
|
5629
|
+
[ -z "$FORGE_ISSUE" ] || forge_set_state "$FORGE_ISSUE" done || :
|
|
5630
|
+
[ -z "$FORGE_PR" ] || forge_set_state "$FORGE_PR" done || :
|
|
5631
|
+
echo "remote-run.sh: deliver: completed on $branch reported${posted:+ on $posted}"
|
|
5632
|
+
exit "$EXIT_OK"
|
|
5633
|
+
}
|
|
4467
5634
|
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
5635
|
+
# verb_open — at the run's start: open the branch's draft pull request when none
|
|
5636
|
+
# is open, set it `running`, and name it once on the source issue. Always exit 0.
|
|
5637
|
+
verb_open() {
|
|
5638
|
+
local tmp made_tmp="" file
|
|
5639
|
+
if ! forge_on; then
|
|
5640
|
+
echo "remote-run.sh: open: the forge coupling is off (forge github and execution.target github-actions); nothing opened"
|
|
5641
|
+
exit "$EXIT_OK"
|
|
5642
|
+
fi
|
|
5643
|
+
forge_repo_var || exit "$EXIT_OK"
|
|
5644
|
+
forge_fetch_branch "$branch"
|
|
5645
|
+
forge_issue_var "$branch" || FORGE_ISSUE=""
|
|
5646
|
+
if ! forge_pr_var "$branch"; then
|
|
5647
|
+
echo "::warning::remote-run.sh: open: whether $branch already has a pull request could not be read ($GH_ERR); none opened"
|
|
5648
|
+
exit "$EXIT_OK"
|
|
5649
|
+
fi
|
|
5650
|
+
if [ -n "$FORGE_PR" ]; then
|
|
5651
|
+
echo "remote-run.sh: open: $branch already has pull request #$FORGE_PR; none opened"
|
|
5652
|
+
exit "$EXIT_OK"
|
|
5653
|
+
fi
|
|
5654
|
+
if ! forge_open_pr "$branch"; then
|
|
5655
|
+
echo "::warning::remote-run.sh: open: the pull request of $branch could not be opened: $OPEN_ERR. The run goes on and its comments reach its issue; deliver tries again when the run completes"
|
|
5656
|
+
exit "$EXIT_OK"
|
|
5657
|
+
fi
|
|
5658
|
+
echo "remote-run.sh: open: opened pull request #$FORGE_PR for $branch"
|
|
5659
|
+
forge_set_state "$FORGE_PR" running || :
|
|
5660
|
+
if [ -z "$FORGE_ISSUE" ]; then
|
|
5661
|
+
echo "remote-run.sh: open: $branch was started from no issue; nothing posted"
|
|
5662
|
+
exit "$EXIT_OK"
|
|
5663
|
+
fi
|
|
5664
|
+
tmp="${RUNNER_TEMP-}"
|
|
5665
|
+
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
5666
|
+
tmp=$(mktemp -d) || tmp=""
|
|
5667
|
+
made_tmp="$tmp"
|
|
5668
|
+
fi
|
|
5669
|
+
if [ -n "$tmp" ] && file=$(mktemp "$tmp/harness-open-comment.XXXXXX"); then
|
|
4471
5670
|
{
|
|
4472
|
-
|
|
5671
|
+
if [ "$FORGE_PR_DRAFT" = false ]; then
|
|
5672
|
+
printf 'The harness run on `%s` opened its pull request #%s, not as a draft — this repository'"'"'s plan may not offer drafts: %s\n' \
|
|
5673
|
+
"$branch" "$FORGE_PR" "$FORGE_PR_URL"
|
|
5674
|
+
else
|
|
5675
|
+
printf 'The harness run on `%s` opened its draft pull request #%s: %s\n' "$branch" "$FORGE_PR" "$FORGE_PR_URL"
|
|
5676
|
+
fi
|
|
5677
|
+
printf '\nThe run'"'"'s questions, lifecycle comments and progress are posted there from now on; `%s` commands keep working on this issue.\n' \
|
|
5678
|
+
"$COMMAND_HANDLE"
|
|
4473
5679
|
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
4474
5680
|
} >"$file"
|
|
4475
|
-
forge_comment "$
|
|
5681
|
+
forge_comment "$FORGE_ISSUE" opened "$branch" "$file" || :
|
|
4476
5682
|
rm -f "$file"
|
|
4477
5683
|
else
|
|
4478
|
-
echo "remote-run.sh:
|
|
5684
|
+
echo "remote-run.sh: open: cannot create the comment file for #$FORGE_ISSUE; no comment posted" >&2
|
|
4479
5685
|
fi
|
|
4480
5686
|
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
4481
|
-
|
|
4482
|
-
[ -z "$FORGE_ISSUE" ] || forge_set_state "$FORGE_ISSUE" done || :
|
|
4483
|
-
[ -z "$FORGE_PR" ] || forge_set_state "$FORGE_PR" done || :
|
|
4484
|
-
echo "remote-run.sh: deliver: completed on $branch reported${target:+ on #$target}"
|
|
5687
|
+
echo "remote-run.sh: open: pull request #$FORGE_PR named on #$FORGE_ISSUE"
|
|
4485
5688
|
exit "$EXIT_OK"
|
|
4486
5689
|
}
|
|
4487
5690
|
|
|
@@ -4508,10 +5711,21 @@ control_cleanup() {
|
|
|
4508
5711
|
return 0
|
|
4509
5712
|
}
|
|
4510
5713
|
|
|
4511
|
-
#
|
|
4512
|
-
#
|
|
5714
|
+
# The engine a dispatch child named, set only once that child exited 0; a
|
|
5715
|
+
# non-empty value adds ` engine=<engine>` to every later reply's marker.
|
|
5716
|
+
CONTROL_REPLY_ENGINE=""
|
|
5717
|
+
|
|
5718
|
+
# How control_mention read the mention a verb arm is carrying out; non-empty,
|
|
5719
|
+
# it opens every reply, an arm's refusal included.
|
|
5720
|
+
CONTROL_MENTION_NOTE=""
|
|
5721
|
+
|
|
5722
|
+
# control_post <text> — post <text> on CONTROL_NUMBER as a `reply` comment,
|
|
5723
|
+
# after CONTROL_MENTION_NOTE and a blank line when that is set, its marker
|
|
5724
|
+
# carrying CONTROL_REPLY_ENGINE; 1, after an `::error::` line, when it cannot
|
|
5725
|
+
# be posted.
|
|
4513
5726
|
control_post() {
|
|
4514
5727
|
local text="$1" file status=0
|
|
5728
|
+
[ -z "$CONTROL_MENTION_NOTE" ] || text="$CONTROL_MENTION_NOTE"$'\n\n'"$text"
|
|
4515
5729
|
if ! forge_repo_var; then
|
|
4516
5730
|
echo "::error::remote-run.sh: control: the reply on #$CONTROL_NUMBER could not be posted: $GH_ERR"
|
|
4517
5731
|
return 1
|
|
@@ -4524,7 +5738,7 @@ control_post() {
|
|
|
4524
5738
|
printf '%s\n' "$text"
|
|
4525
5739
|
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
4526
5740
|
} >"$file"
|
|
4527
|
-
if ! forge_comment "$CONTROL_NUMBER" reply "$CONTROL_BRANCH" "$file"; then
|
|
5741
|
+
if ! forge_comment "$CONTROL_NUMBER" reply "$CONTROL_BRANCH" "$file" "" "$CONTROL_REPLY_ENGINE"; then
|
|
4528
5742
|
echo "::error::remote-run.sh: control: the reply on #$CONTROL_NUMBER could not be posted: $GH_ERR"
|
|
4529
5743
|
status=1
|
|
4530
5744
|
fi
|
|
@@ -4661,11 +5875,47 @@ control_stopped_refuse() {
|
|
|
4661
5875
|
esac
|
|
4662
5876
|
}
|
|
4663
5877
|
|
|
4664
|
-
#
|
|
4665
|
-
#
|
|
4666
|
-
#
|
|
5878
|
+
# control_run_in_flight <branch> — 0 when a run titled exactly
|
|
5879
|
+
# `harness run <branch>` is queued, in_progress, waiting, requested or
|
|
5880
|
+
# pending; 1 when none is; 2 with GH_ERR set when the listing failed or was
|
|
5881
|
+
# not a JSON array. Passes <branch> explicitly rather than reading the global
|
|
5882
|
+
# `branch` list_runs reads, and leaves GH_OUT as it found it.
|
|
5883
|
+
control_run_in_flight() {
|
|
5884
|
+
local saved="$GH_OUT" verdict status=0
|
|
5885
|
+
if ! gh_call run list --workflow "$WORKFLOW_RUN_FILE" --branch "$1" \
|
|
5886
|
+
--json displayTitle,status --limit "$RUN_LIST_LIMIT"; then
|
|
5887
|
+
GH_OUT="$saved"
|
|
5888
|
+
return 2
|
|
5889
|
+
fi
|
|
5890
|
+
verdict=$(printf '%s' "$GH_OUT" | jq -r --arg t "harness run $1" '
|
|
5891
|
+
if type != "array" then error("not an array") else
|
|
5892
|
+
if any(.[]; .displayTitle == $t
|
|
5893
|
+
and (.status == "queued" or .status == "in_progress" or .status == "waiting"
|
|
5894
|
+
or .status == "requested" or .status == "pending"))
|
|
5895
|
+
then "yes" else "no" end
|
|
5896
|
+
end' 2>/dev/null) || verdict=""
|
|
5897
|
+
GH_OUT="$saved"
|
|
5898
|
+
case "$verdict" in
|
|
5899
|
+
yes) status=0 ;;
|
|
5900
|
+
no) status=1 ;;
|
|
5901
|
+
*) GH_ERR="its run list is not the expected JSON"; status=2 ;;
|
|
5902
|
+
esac
|
|
5903
|
+
return "$status"
|
|
5904
|
+
}
|
|
5905
|
+
|
|
5906
|
+
# control_check_branch <branch> [started] — the refusals every path shares, in
|
|
5907
|
+
# order: a branch not answered 1 by hr_branch_is_protected, then (after a
|
|
5908
|
+
# fetch) one that is not a harness branch. A branch passes with its task
|
|
5909
|
+
# prompt or the flow-progress ledger on its origin tip (`forge_recognised`);
|
|
5910
|
+
# or, with `started` (passed only by
|
|
5911
|
+
# control_branch_from_issue, whose issue's genuine `started` marker names it),
|
|
5912
|
+
# while it still exists on origin — gone is refused as deleted, a failed
|
|
5913
|
+
# existence check names the read, and no run list is read; or with a
|
|
5914
|
+
# `harness run <branch>` run in flight. Otherwise the refusal reads "neither a
|
|
5915
|
+
# task prompt nor a ledger, and no `harness run` in flight", and a failed listing names the read. Each
|
|
5916
|
+
# refusal is a reply and exit 2, or 3 for a failed read.
|
|
4667
5917
|
control_check_branch() {
|
|
4668
|
-
local b="$1" protected=0
|
|
5918
|
+
local b="$1" protected=0 exists=0 inflight=0
|
|
4669
5919
|
if ! valid_branch "$b" || ! git check-ref-format --branch "$b" >/dev/null 2>&1; then
|
|
4670
5920
|
control_refuse "$EXIT_REFUSED" "\`$b\` is not a valid branch name" "Comment on the pull request of the run's branch instead."
|
|
4671
5921
|
fi
|
|
@@ -4679,8 +5929,25 @@ control_check_branch() {
|
|
|
4679
5929
|
esac
|
|
4680
5930
|
forge_fetch_branch "$b"
|
|
4681
5931
|
if ! forge_recognised "$b"; then
|
|
4682
|
-
|
|
4683
|
-
"
|
|
5932
|
+
if [ "${2-}" = started ]; then
|
|
5933
|
+
remote_branch_exists "$b" || exists=$?
|
|
5934
|
+
case "$exists" in
|
|
5935
|
+
0) ;;
|
|
5936
|
+
1) control_refuse "$EXIT_REFUSED" "\`$b\` no longer exists on origin, so its run cannot be resumed or commanded" \
|
|
5937
|
+
"Start a new run from a new issue or the Run workflow form." ;;
|
|
5938
|
+
*) control_refuse "$EXIT_GH" "whether \`$b\` still exists on origin could not be read ($REMOTE_BRANCH_ERR)" \
|
|
5939
|
+
"Comment again to retry." ;;
|
|
5940
|
+
esac
|
|
5941
|
+
else
|
|
5942
|
+
control_run_in_flight "$b" || inflight=$?
|
|
5943
|
+
case "$inflight" in
|
|
5944
|
+
0) ;;
|
|
5945
|
+
1) control_refuse "$EXIT_REFUSED" "\`$b\` is not a harness branch: its tip carries neither a task prompt nor a flow-progress ledger, and no \`harness run $b\` run is queued or in progress" \
|
|
5946
|
+
"Only a branch a harness run works on can be commanded." ;;
|
|
5947
|
+
*) control_refuse "$EXIT_GH" "whether \`$b\` has a harness run in flight could not be read ($GH_ERR)" \
|
|
5948
|
+
"Comment again to retry." ;;
|
|
5949
|
+
esac
|
|
5950
|
+
fi
|
|
4684
5951
|
fi
|
|
4685
5952
|
CONTROL_BRANCH="$b"
|
|
4686
5953
|
}
|
|
@@ -4756,7 +6023,8 @@ control_issue_branch_var() {
|
|
|
4756
6023
|
}
|
|
4757
6024
|
|
|
4758
6025
|
# control_branch_from_issue <number> — control_issue_branch_var, its failures
|
|
4759
|
-
# refused, then control_check_branch
|
|
6026
|
+
# refused, then control_check_branch with `started`: the verified marker makes
|
|
6027
|
+
# the branch a harness branch while it still exists on origin.
|
|
4760
6028
|
control_branch_from_issue() {
|
|
4761
6029
|
if ! control_issue_branch_var "$1"; then
|
|
4762
6030
|
if [ -n "$ISSUE_BRANCH_ERR" ]; then
|
|
@@ -4765,7 +6033,7 @@ control_branch_from_issue() {
|
|
|
4765
6033
|
control_refuse "$EXIT_REFUSED" "no harness run was started from this issue" \
|
|
4766
6034
|
"Comment on the pull request of the run's branch instead."
|
|
4767
6035
|
fi
|
|
4768
|
-
control_check_branch "$ISSUE_BRANCH"
|
|
6036
|
+
control_check_branch "$ISSUE_BRANCH" started
|
|
4769
6037
|
}
|
|
4770
6038
|
|
|
4771
6039
|
# control_verb_handled <verb> — 0 when an arm below carries out <verb>.
|
|
@@ -4800,6 +6068,7 @@ control_resume_dispatch() {
|
|
|
4800
6068
|
*) control_refuse "$EXIT_GH" "the dispatch could not be sent ($CHILD_LAST)" "Comment \`$COMMAND_HANDLE $CONTROL_VERB\` again to retry." ;;
|
|
4801
6069
|
esac
|
|
4802
6070
|
status="$EXIT_OK"
|
|
6071
|
+
CONTROL_REPLY_ENGINE="$CS_ENGINE"
|
|
4803
6072
|
control_post "$done_text" || status="$EXIT_GH"
|
|
4804
6073
|
# The job posts its own `resumed` comment; the labels say `running` now.
|
|
4805
6074
|
forge_issue_var "$CONTROL_BRANCH" || FORGE_ISSUE=""
|
|
@@ -5102,6 +6371,7 @@ control_answer() {
|
|
|
5102
6371
|
control_refuse "$EXIT_GH" "the dispatch could not be sent ($CHILD_LAST)" \
|
|
5103
6372
|
"Comment \`$COMMAND_HANDLE answer $n\` again to retry." ;;
|
|
5104
6373
|
esac
|
|
6374
|
+
CONTROL_REPLY_ENGINE="$CS_ENGINE"
|
|
5105
6375
|
|
|
5106
6376
|
cmds=""
|
|
5107
6377
|
for v in $CS_OPEN; do
|
|
@@ -5177,18 +6447,27 @@ RC_ERR=""
|
|
|
5177
6447
|
# origin's tip consumed, from their marker lines: RC_SEEN_R and RC_SEEN_C (the
|
|
5178
6448
|
# recorded review and comment ids, comma-wrapped), RC_MARKED_AT (the
|
|
5179
6449
|
# highest-numbered marked round's `collected_at`, empty when none is marked),
|
|
5180
|
-
#
|
|
5181
|
-
#
|
|
5182
|
-
#
|
|
6450
|
+
# RC_MARKED_N and RC_MARKED_C (that round's number, 0 when none, and its
|
|
6451
|
+
# `comments=` ids as written), RC_NEWEST_N (the highest round number of any
|
|
6452
|
+
# round file, marked or not, 0 when none), RC_EVENT_ROUND (the round recording
|
|
6453
|
+
# <event_review_id>), and RC_STATE_REL. 4 with RC_ERR when a round cannot be
|
|
6454
|
+
# listed or read. `control` also calls it on its own, to name the round a
|
|
6455
|
+
# review in flight is already part of, and `deliver` to find a round's threads.
|
|
5183
6456
|
RC_SEEN_R=","
|
|
5184
6457
|
RC_SEEN_C=","
|
|
5185
6458
|
RC_MARKED_AT=""
|
|
6459
|
+
RC_MARKED_N=0
|
|
6460
|
+
RC_MARKED_C=""
|
|
6461
|
+
RC_NEWEST_N=0
|
|
5186
6462
|
RC_STATE_REL=""
|
|
5187
6463
|
round_markers_read() {
|
|
5188
|
-
local event_id="${1-}" names name path n line
|
|
6464
|
+
local event_id="${1-}" names name path n line
|
|
5189
6465
|
RC_SEEN_R=","
|
|
5190
6466
|
RC_SEEN_C=","
|
|
5191
6467
|
RC_MARKED_AT=""
|
|
6468
|
+
RC_MARKED_N=0
|
|
6469
|
+
RC_MARKED_C=""
|
|
6470
|
+
RC_NEWEST_N=0
|
|
5192
6471
|
RC_EVENT_ROUND=""
|
|
5193
6472
|
RC_STATE_REL=$(hr_state_dir "$root" 2>/dev/null) || RC_STATE_REL=""
|
|
5194
6473
|
RC_STATE_REL="${RC_STATE_REL%/}"
|
|
@@ -5206,6 +6485,7 @@ round_markers_read() {
|
|
|
5206
6485
|
[ "${BASH_REMATCH[1]}" = "$branch" ] || continue
|
|
5207
6486
|
n="${BASH_REMATCH[3]:-1}"
|
|
5208
6487
|
n=$((10#$n))
|
|
6488
|
+
[ "$n" -le "$RC_NEWEST_N" ] || RC_NEWEST_N="$n"
|
|
5209
6489
|
if ! line=$(git -C "$root" show "refs/remotes/origin/$branch:$path" 2>/dev/null); then
|
|
5210
6490
|
RC_ERR="the previous round \`$path\` could not be read"
|
|
5211
6491
|
return 4
|
|
@@ -5215,9 +6495,10 @@ round_markers_read() {
|
|
|
5215
6495
|
[[ "$line" =~ ^"$COMMENT_MARKER round collected_at="([0-9T:Z-]+)" reviews="([0-9,]*)" comments="([0-9,]*)" -->"$ ]] || continue
|
|
5216
6496
|
RC_SEEN_R="$RC_SEEN_R${BASH_REMATCH[2]}${BASH_REMATCH[2]:+,}"
|
|
5217
6497
|
RC_SEEN_C="$RC_SEEN_C${BASH_REMATCH[3]}${BASH_REMATCH[3]:+,}"
|
|
5218
|
-
if [ "$n" -gt "$
|
|
5219
|
-
|
|
6498
|
+
if [ "$n" -gt "$RC_MARKED_N" ]; then
|
|
6499
|
+
RC_MARKED_N="$n"
|
|
5220
6500
|
RC_MARKED_AT="${BASH_REMATCH[1]}"
|
|
6501
|
+
RC_MARKED_C="${BASH_REMATCH[3]}"
|
|
5221
6502
|
fi
|
|
5222
6503
|
case ",${BASH_REMATCH[2]}," in
|
|
5223
6504
|
*",$event_id,"*) [ -z "$event_id" ] || RC_EVENT_ROUND="$n" ;;
|
|
@@ -5337,10 +6618,9 @@ AUTHORS
|
|
|
5337
6618
|
RC_REVIEWERS=$(printf '%s' "$kept" | jq -r '
|
|
5338
6619
|
reduce (.reviews[] | .user.login) as $l ([]; if any(.[]; . == $l) then . else . + [$l] end) | join(",")')
|
|
5339
6620
|
|
|
5340
|
-
# A hunk
|
|
5341
|
-
#
|
|
5342
|
-
|
|
5343
|
-
if ! text=$(printf '%s' "$kept" | jq -j --arg pr "$pr" --arg at "$collected_at" --arg marker "$COMMENT_MARKER" --arg state "$REVIEW_ROUND_STATE" '
|
|
6621
|
+
# A hunk is fenced by JQ_DEF_FENCE. The trailing `x` keeps the text's final
|
|
6622
|
+
# newline through the substitution.
|
|
6623
|
+
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"'
|
|
5344
6624
|
def nl: if endswith("\n") then . else . + "\n" end;
|
|
5345
6625
|
.reviews as $rv | .comments as $cm
|
|
5346
6626
|
| ($rv | map(
|
|
@@ -5358,8 +6638,7 @@ AUTHORS
|
|
|
5358
6638
|
+ (if ($cm | length) == 0 then "" else
|
|
5359
6639
|
"\n## Inline comments\n" + ($cm | map(
|
|
5360
6640
|
(.diff_hunk // "") as $h
|
|
5361
|
-
| (
|
|
5362
|
-
| ("`" * ([$m + 1, 3] | max)) as $f
|
|
6641
|
+
| ($h | fence) as $f
|
|
5363
6642
|
| "\n### `" + (.path // "") + "`"
|
|
5364
6643
|
+ (if .line != null then ", line \(.line)"
|
|
5365
6644
|
elif .original_line != null then ", original line \(.original_line) (outdated)"
|
|
@@ -5391,12 +6670,16 @@ REVIEW_RETRY_WAY="The reviews stay on the pull request and are collected by the
|
|
|
5391
6670
|
# control_settled_var — `branch_settled_var` for CONTROL_BRANCH, with no run
|
|
5392
6671
|
# counted as settled, run in a command substitution so that its exit on a
|
|
5393
6672
|
# failed read reaches this process as a status, not as an exit with no reply:
|
|
5394
|
-
# BS_SETTLED, BS_STATE and BS_REASON
|
|
5395
|
-
#
|
|
6673
|
+
# BS_SETTLED, BS_STATE and BS_REASON, and RS_NOT_STARTED, RS_RUN_ID, RS_ENGINE
|
|
6674
|
+
# and RS_DETAIL as BS_NOT_STARTED, BS_RUN_ID, BS_ENGINE and BS_DETAIL; 1 with
|
|
6675
|
+
# BS_ERR, its last stderr line, on a failed read. BS_DETAIL is read last, so a
|
|
6676
|
+
# `|` inside it shifts no other field.
|
|
5396
6677
|
BS_SETTLED=0; BS_STATE=""; BS_REASON=""; BS_ERR=""
|
|
6678
|
+
BS_NOT_STARTED=0; BS_RUN_ID=""; BS_ENGINE=""; BS_DETAIL=""
|
|
5397
6679
|
control_settled_var() {
|
|
5398
6680
|
local dir errfile line status=0
|
|
5399
6681
|
BS_SETTLED=0; BS_STATE=""; BS_REASON=""; BS_ERR=""
|
|
6682
|
+
BS_NOT_STARTED=0; BS_RUN_ID=""; BS_ENGINE=""; BS_DETAIL=""
|
|
5400
6683
|
if ! dir=$(mktemp -d "$control_tmp/harness-control-settled.XXXXXX"); then
|
|
5401
6684
|
BS_ERR="a state directory could not be created under '$control_tmp'"
|
|
5402
6685
|
return 1
|
|
@@ -5405,14 +6688,15 @@ control_settled_var() {
|
|
|
5405
6688
|
control_dirs="$control_dirs $dir $errfile"
|
|
5406
6689
|
branch="$CONTROL_BRANCH"
|
|
5407
6690
|
line=$(branch_settled_var "$dir" 1 >/dev/null 2>"$errfile" \
|
|
5408
|
-
&& printf '%s|%s|%s\n' "$SETTLED" "$RS_STATE" "$RS_PAUSE_REASON"
|
|
6691
|
+
&& printf '%s|%s|%s|%s|%s|%s|%s\n' "$SETTLED" "$RS_STATE" "$RS_PAUSE_REASON" \
|
|
6692
|
+
"$RS_NOT_STARTED" "$RS_RUN_ID" "$RS_ENGINE" "$RS_DETAIL") || status=$?
|
|
5409
6693
|
cat "$errfile" >&2 2>/dev/null || :
|
|
5410
6694
|
if [ "$status" -ne 0 ] || [ -z "$line" ]; then
|
|
5411
6695
|
BS_ERR=$(grep -v '^[[:space:]]*$' "$errfile" 2>/dev/null | tail -n 1)
|
|
5412
6696
|
[ -n "$BS_ERR" ] || BS_ERR="exit $status, no message"
|
|
5413
6697
|
return 1
|
|
5414
6698
|
fi
|
|
5415
|
-
IFS='|' read -r BS_SETTLED BS_STATE BS_REASON <<<"$line"
|
|
6699
|
+
IFS='|' read -r BS_SETTLED BS_STATE BS_REASON BS_NOT_STARTED BS_RUN_ID BS_ENGINE BS_DETAIL <<<"$line"
|
|
5416
6700
|
return 0
|
|
5417
6701
|
}
|
|
5418
6702
|
|
|
@@ -5569,11 +6853,13 @@ control_review_intake() {
|
|
|
5569
6853
|
}
|
|
5570
6854
|
|
|
5571
6855
|
# control_comment_intake — read an `issue_comment` event; returns 1 when it is
|
|
5572
|
-
# ignored, after one line.
|
|
6856
|
+
# ignored, after one line. The exact form sets CONTROL_VERB / CONTROL_ARGS; any
|
|
6857
|
+
# other mention of the handle as a word sets CONTROL_MENTION and no verb.
|
|
5573
6858
|
CONTROL_SENDER_TYPE=""
|
|
5574
6859
|
CONTROL_IS_PR=""
|
|
6860
|
+
CONTROL_MENTION=0
|
|
5575
6861
|
control_comment_intake() {
|
|
5576
|
-
local action first word rest handle
|
|
6862
|
+
local action first word rest handle verb body re
|
|
5577
6863
|
{ event_field '.action // ""' && action="$EVENT_VALUE" \
|
|
5578
6864
|
&& event_field '.comment.body // ""' && CONTROL_BODY="$EVENT_VALUE" \
|
|
5579
6865
|
&& event_field '.issue.number // ""' && CONTROL_NUMBER="$EVENT_VALUE" \
|
|
@@ -5600,15 +6886,27 @@ control_comment_intake() {
|
|
|
5600
6886
|
rest=${first#"$word"}
|
|
5601
6887
|
rest=${rest#"${rest%%[!$' \t']*}"}
|
|
5602
6888
|
handle=$(printf '%s' "$COMMAND_HANDLE" | tr '[:upper:]' '[:lower:]')
|
|
5603
|
-
if [ "$(printf '%s' "$word" | tr '[:upper:]' '[:lower:]')"
|
|
5604
|
-
|
|
5605
|
-
|
|
6889
|
+
if [ "$(printf '%s' "$word" | tr '[:upper:]' '[:lower:]')" = "$handle" ]; then
|
|
6890
|
+
word=${rest%%[$' \t']*}
|
|
6891
|
+
verb=$(printf '%s' "$word" | tr '[:upper:]' '[:lower:]')
|
|
6892
|
+
case " $COMMAND_VERBS " in
|
|
6893
|
+
*" $verb "*)
|
|
6894
|
+
if [ -n "$verb" ]; then
|
|
6895
|
+
CONTROL_VERB="$verb"
|
|
6896
|
+
CONTROL_ARGS=${rest#"$word"}
|
|
6897
|
+
CONTROL_ARGS=${CONTROL_ARGS#"${CONTROL_ARGS%%[!$' \t']*}"}
|
|
6898
|
+
return 0
|
|
6899
|
+
fi ;;
|
|
6900
|
+
esac
|
|
5606
6901
|
fi
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
6902
|
+
body=$(printf '%s' "$CONTROL_BODY" | tr '[:upper:]' '[:lower:]')
|
|
6903
|
+
re="(^|[^a-z0-9])${handle}([^a-z0-9-]|\$)"
|
|
6904
|
+
if [[ $body =~ $re ]]; then
|
|
6905
|
+
CONTROL_MENTION=1
|
|
6906
|
+
return 0
|
|
6907
|
+
fi
|
|
6908
|
+
echo "remote-run.sh: control: ignored, the comment does not mention $COMMAND_HANDLE"
|
|
6909
|
+
return 1
|
|
5612
6910
|
}
|
|
5613
6911
|
|
|
5614
6912
|
# The close: CLOSE_KIND (`issue`, `pr_closed`, `pr_merged`, `deleted`) and
|
|
@@ -5749,16 +7047,17 @@ control_close() {
|
|
|
5749
7047
|
CONTROL_BRANCH="$b"
|
|
5750
7048
|
if [ "$CLOSE_KIND" = pr_closed ] || [ "$CLOSE_KIND" = pr_merged ]; then
|
|
5751
7049
|
# GitHub closes a pull request whose head is deleted; the `delete` event's
|
|
5752
|
-
# own job stops that run from the default branch
|
|
7050
|
+
# own job stops that run from the default branch and reports it on this
|
|
7051
|
+
# pull request, so this one stays quiet.
|
|
5753
7052
|
remote_branch_exists "$b"
|
|
5754
7053
|
case $? in
|
|
5755
|
-
1) control_close_ignore "the branch \`$b\` of pull request #$CONTROL_NUMBER is gone from origin; the deletion's own job stops the run" ;;
|
|
7054
|
+
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" ;;
|
|
5756
7055
|
2) echo "remote-run.sh: control: whether \`$b\` exists on origin could not be checked ($REMOTE_BRANCH_ERR); proceeding" ;;
|
|
5757
7056
|
esac
|
|
5758
7057
|
fi
|
|
5759
7058
|
|
|
5760
7059
|
# No forge_recognised check: a merged or deleted branch may no longer carry
|
|
5761
|
-
# its ledger, and a listed `harness run <b>` run is what proves a harness run.
|
|
7060
|
+
# its task prompt or ledger, and a listed `harness run <b>` run is what proves a harness run.
|
|
5762
7061
|
if ! control_state_var "$b"; then
|
|
5763
7062
|
echo "::error::remote-run.sh: control: the state of the run on \`$b\` could not be read ($CS_ERR)"
|
|
5764
7063
|
exit "$EXIT_GH"
|
|
@@ -5790,9 +7089,350 @@ control_close() {
|
|
|
5790
7089
|
exit "$EXIT_OK"
|
|
5791
7090
|
}
|
|
5792
7091
|
|
|
7092
|
+
# control_commands_way — prints the way on that lists every command and names
|
|
7093
|
+
# the documentation, the unknown-verb refusal's and every mention refusal's.
|
|
7094
|
+
control_commands_way() {
|
|
7095
|
+
local v verbs=""
|
|
7096
|
+
for v in $COMMAND_VERBS; do
|
|
7097
|
+
[ "$v" != answer ] || v="answer [<n>]"
|
|
7098
|
+
verbs="$verbs${verbs:+, }\`$COMMAND_HANDLE $v\`"
|
|
7099
|
+
done
|
|
7100
|
+
printf '%s' "The commands are $verbs; \`docs/github-run-control.md\` in the harness documentation states each."
|
|
7101
|
+
}
|
|
7102
|
+
|
|
7103
|
+
# The credential `verb_control`'s first statements take out of the environment;
|
|
7104
|
+
# never exported, so only the agent subshell in control_mention_session sees it.
|
|
7105
|
+
MENTION_OAUTH=""
|
|
7106
|
+
MENTION_API=""
|
|
7107
|
+
# The job's `GH_TOKEN` in the base64 form `actions/checkout` persists into the
|
|
7108
|
+
# checkout's git configuration; never exported.
|
|
7109
|
+
MENTION_JOB_TOKEN_B64=""
|
|
7110
|
+
|
|
7111
|
+
# mention_has_credential <string> — 0 when <string> contains, verbatim, a
|
|
7112
|
+
# non-empty saved credential value, or the job's `GH_TOKEN` raw or in its
|
|
7113
|
+
# persisted base64 form. An encoded or split copy is not caught.
|
|
7114
|
+
mention_has_credential() {
|
|
7115
|
+
if [ -n "$MENTION_OAUTH" ]; then
|
|
7116
|
+
case "$1" in *"$MENTION_OAUTH"*) return 0 ;; esac
|
|
7117
|
+
fi
|
|
7118
|
+
if [ -n "$MENTION_API" ]; then
|
|
7119
|
+
case "$1" in *"$MENTION_API"*) return 0 ;; esac
|
|
7120
|
+
fi
|
|
7121
|
+
if [ -n "${GH_TOKEN-}" ]; then
|
|
7122
|
+
case "$1" in *"$GH_TOKEN"*) return 0 ;; esac
|
|
7123
|
+
fi
|
|
7124
|
+
if [ -n "$MENTION_JOB_TOKEN_B64" ]; then
|
|
7125
|
+
case "$1" in *"$MENTION_JOB_TOKEN_B64"*) return 0 ;; esac
|
|
7126
|
+
fi
|
|
7127
|
+
return 1
|
|
7128
|
+
}
|
|
7129
|
+
|
|
7130
|
+
# mention_cap <file> <max bytes> <note> — over <max bytes>, <file> keeps its
|
|
7131
|
+
# first <max bytes> cut back to the last whole line, then the line <note>.
|
|
7132
|
+
mention_cap() {
|
|
7133
|
+
local file="$1" max="$2" note="$3" size
|
|
7134
|
+
size=$(wc -c <"$file") || return 1
|
|
7135
|
+
size=${size//[!0-9]/}
|
|
7136
|
+
[ "${size:-0}" -gt "$max" ] || return 0
|
|
7137
|
+
head -c "$max" "$file" >"$file.cut" || return 1
|
|
7138
|
+
if [ -n "$(tail -c 1 "$file.cut")" ]; then
|
|
7139
|
+
sed '$d' "$file.cut" >"$file.line" || return 1
|
|
7140
|
+
mv -f "$file.line" "$file.cut" || return 1
|
|
7141
|
+
fi
|
|
7142
|
+
printf '%s\n' "$note" >>"$file.cut"
|
|
7143
|
+
mv -f "$file.cut" "$file"
|
|
7144
|
+
}
|
|
7145
|
+
|
|
7146
|
+
# control_mention_context <dir> — comment.md, run.md and questions/ in <dir>,
|
|
7147
|
+
# every file capped at MENTION_FILE_MAX_BYTES; 1 when one cannot be written.
|
|
7148
|
+
control_mention_context() {
|
|
7149
|
+
local dir="$1" kind=issue n src f
|
|
7150
|
+
[ -z "$CONTROL_IS_PR" ] || kind="pull request"
|
|
7151
|
+
{
|
|
7152
|
+
printf 'handle: %s\n' "$COMMAND_HANDLE"
|
|
7153
|
+
printf 'Comment by @%s on %s #%s:\n\n' "$CONTROL_ACTOR" "$kind" "$CONTROL_NUMBER"
|
|
7154
|
+
printf '%s' "$CONTROL_BODY"
|
|
7155
|
+
} >"$dir/comment.md" || return 1
|
|
7156
|
+
{
|
|
7157
|
+
printf 'branch: %s\n' "$CONTROL_BRANCH"
|
|
7158
|
+
printf 'state: %s\n' "$CS_STATE"
|
|
7159
|
+
printf 'pause_reason: %s\n' "$CS_REASON"
|
|
7160
|
+
printf 'engine: %s\n' "$CS_ENGINE"
|
|
7161
|
+
printf 'open_questions: %s\n' "$CS_OPEN"
|
|
7162
|
+
printf 'detail: %s\n' "$CS_DETAIL"
|
|
7163
|
+
printf 'run_url: %s\n' "$CS_URL"
|
|
7164
|
+
printf 'run_status: %s\n' "$CS_RUN_STATUS"
|
|
7165
|
+
if [ "$CS_STOPPED" = 1 ]; then printf 'stopped: yes\n'; else printf 'stopped: no\n'; fi
|
|
7166
|
+
if ! control_ledger_next_var; then
|
|
7167
|
+
printf 'next: the flow-progress ledger could not be read\n'
|
|
7168
|
+
elif [ -z "$LEDGER_NEXT" ]; then
|
|
7169
|
+
printf 'next: every entry of the flow-progress ledger is ticked\n'
|
|
7170
|
+
else
|
|
7171
|
+
printf 'next: %s\n' "$LEDGER_NEXT"
|
|
7172
|
+
printf 'section: %s\n' "$LEDGER_SECTION"
|
|
7173
|
+
fi
|
|
7174
|
+
} >"$dir/run.md" || return 1
|
|
7175
|
+
mkdir "$dir/questions" || return 1
|
|
7176
|
+
for n in $CS_OPEN; do
|
|
7177
|
+
case "$n" in ''|*[!0-9]*) continue ;; esac
|
|
7178
|
+
src="$CS_DIR/clarifications/$CONTROL_BRANCH/question_$n.md"
|
|
7179
|
+
[ -f "$src" ] || continue
|
|
7180
|
+
cp "$src" "$dir/questions/question_$n.md" || return 1
|
|
7181
|
+
done
|
|
7182
|
+
for f in "$dir/comment.md" "$dir/run.md" "$dir"/questions/question_*.md; do
|
|
7183
|
+
[ -f "$f" ] || continue
|
|
7184
|
+
mention_cap "$f" "$MENTION_FILE_MAX_BYTES" "(cut at $MENTION_FILE_MAX_BYTES bytes)" || return 1
|
|
7185
|
+
done
|
|
7186
|
+
return 0
|
|
7187
|
+
}
|
|
7188
|
+
|
|
7189
|
+
# control_mention_context_extra <dir> — item.md, conversation.md and, on a pull
|
|
7190
|
+
# request, diff.patch in <dir>, each capped at MENTION_FILE_MAX_BYTES; a failed
|
|
7191
|
+
# read is one line in its file, never a refusal. 1 when a file cannot be
|
|
7192
|
+
# written. Every forge value reaches a file through `jq` or `printf '%s'` only.
|
|
7193
|
+
control_mention_context_extra() {
|
|
7194
|
+
local dir="$1" kind=issue own f
|
|
7195
|
+
[ -z "$CONTROL_IS_PR" ] || kind="pull request"
|
|
7196
|
+
if ! gh_call api "repos/$FORGE_REPO/issues/$CONTROL_NUMBER"; then
|
|
7197
|
+
printf 'The issue or pull request could not be read (%s).\n' "$GH_ERR" >"$dir/item.md" || return 1
|
|
7198
|
+
elif ! printf '%s' "$GH_OUT" | jq -j --arg kind "$kind" --arg n "$CONTROL_NUMBER" '
|
|
7199
|
+
"kind: \($kind)\nnumber: \($n)\ntitle: \(.title // "" | tostring)\nauthor: @\(.user.login // "" | tostring)\nurl: \(.html_url // "" | tostring)\n\n\(.body // "" | tostring)"' \
|
|
7200
|
+
>"$dir/item.md" 2>/dev/null; then
|
|
7201
|
+
printf 'The issue or pull request could not be read (its answer is not the expected JSON).\n' >"$dir/item.md" || return 1
|
|
7202
|
+
fi
|
|
7203
|
+
|
|
7204
|
+
# The commenter's own body goes to jq through a file: a comment can exceed
|
|
7205
|
+
# the kernel's bound on one argument.
|
|
7206
|
+
own="$dir.own"
|
|
7207
|
+
control_dirs="$control_dirs $own"
|
|
7208
|
+
printf '%s' "$CONTROL_BODY" >"$own" || return 1
|
|
7209
|
+
if ! gh_call api --paginate "repos/$FORGE_REPO/issues/$CONTROL_NUMBER/comments" --jq '.[] | {login: .user.login, at: .created_at, body: .body}'; then
|
|
7210
|
+
printf 'The comments of this %s could not be read (%s).\n' "$kind" "$GH_ERR" >"$dir/conversation.md" || return 1
|
|
7211
|
+
elif ! printf '%s' "$GH_OUT" | jq -s -j --arg actor "$CONTROL_ACTOR" --rawfile own "$own" \
|
|
7212
|
+
--arg marker "$COMMENT_MARKER" --arg kind "$kind" --argjson max "$MENTION_COMMENTS_MAX" '
|
|
7213
|
+
[.[] | objects] as $all
|
|
7214
|
+
| ([range(0; $all | length) | select($all[.].login == $actor and $all[.].body == $own)] | last) as $i
|
|
7215
|
+
| (if $i == null then $all else $all[:$i] end) as $before
|
|
7216
|
+
| (if ($before | length) > $max then $before[($before | length) - $max:] else $before end)
|
|
7217
|
+
| if length == 0 then "No comment precedes this one on the \($kind).\n"
|
|
7218
|
+
else map("### @\(.login // "" | tostring) at \(.at // "" | tostring)\n"
|
|
7219
|
+
+ (if ((.body // "" | tostring) | contains($marker)) then "(posted by the harness)\n" else "" end)
|
|
7220
|
+
+ "\n\(.body // "" | tostring)\n\n") | join("") end' \
|
|
7221
|
+
>"$dir/conversation.md" 2>/dev/null; then
|
|
7222
|
+
printf 'The comments of this %s could not be read (their listing is not the expected JSON).\n' "$kind" >"$dir/conversation.md" || return 1
|
|
7223
|
+
fi
|
|
7224
|
+
|
|
7225
|
+
if [ -n "$CONTROL_IS_PR" ]; then
|
|
7226
|
+
if ! gh_call pr diff "$CONTROL_NUMBER" --repo "$FORGE_REPO"; then
|
|
7227
|
+
printf "The pull request's diff could not be read (%s).\n" "$GH_ERR" >"$dir/diff.patch" || return 1
|
|
7228
|
+
else
|
|
7229
|
+
printf '%s\n' "$GH_OUT" >"$dir/diff.patch" || return 1
|
|
7230
|
+
fi
|
|
7231
|
+
fi
|
|
7232
|
+
|
|
7233
|
+
for f in "$dir/item.md" "$dir/conversation.md" "$dir/diff.patch"; do
|
|
7234
|
+
[ -f "$f" ] || continue
|
|
7235
|
+
mention_cap "$f" "$MENTION_FILE_MAX_BYTES" "(cut at $MENTION_FILE_MAX_BYTES bytes)" || return 1
|
|
7236
|
+
done
|
|
7237
|
+
return 0
|
|
7238
|
+
}
|
|
7239
|
+
|
|
7240
|
+
# control_mention_session <dir> <out> <err> — the one read-only session, run
|
|
7241
|
+
# once in a subshell in <dir>; AGENT_STATUS is its exit.
|
|
7242
|
+
AGENT_STATUS=0
|
|
7243
|
+
control_mention_session() {
|
|
7244
|
+
local dir="$1" out="$2" err="$3" schema model model_args
|
|
7245
|
+
AGENT_STATUS=0
|
|
7246
|
+
# Omitted rather than passed empty, so the CLI applies its own default; the
|
|
7247
|
+
# `${arr[@]+…}` form keeps an empty array safe under `set -u` on bash 3.2.
|
|
7248
|
+
model=$(hr_agent_model "$root" 2>/dev/null) || model=""
|
|
7249
|
+
model_args=()
|
|
7250
|
+
[ -z "$model" ] || model_args=(--model "$model")
|
|
7251
|
+
schema=$(jq -n -c --arg actions "$MENTION_ACTIONS" --arg verbs "$COMMAND_VERBS" '
|
|
7252
|
+
def words: split(" ") | map(select(length > 0));
|
|
7253
|
+
{ type: "object", additionalProperties: false, required: ["action", "reason"],
|
|
7254
|
+
properties: {
|
|
7255
|
+
action: { type: "string", enum: ($actions | words) },
|
|
7256
|
+
verb: { type: "string", enum: ($verbs | words) },
|
|
7257
|
+
question: { type: "integer", minimum: 1 },
|
|
7258
|
+
answer: { type: "string" },
|
|
7259
|
+
text: { type: "string" },
|
|
7260
|
+
reason: { type: "string" } } }') || { AGENT_STATUS=1; return 0; }
|
|
7261
|
+
(
|
|
7262
|
+
cd "$dir" || exit 1
|
|
7263
|
+
unset GH_TOKEN GITHUB_TOKEN HARNESS_PR_TOKEN
|
|
7264
|
+
[ -z "$MENTION_OAUTH" ] || export CLAUDE_CODE_OAUTH_TOKEN="$MENTION_OAUTH"
|
|
7265
|
+
[ -z "$MENTION_API" ] || export ANTHROPIC_API_KEY="$MENTION_API"
|
|
7266
|
+
exec "$AGENT_CLI" -p "$MENTION_COMMAND" \
|
|
7267
|
+
--plugin-dir "$HARNESS_MENTION_PLUGIN_DIR" \
|
|
7268
|
+
--add-dir "$HARNESS_MENTION_PLUGIN_DIR/instructions" \
|
|
7269
|
+
--output-format json \
|
|
7270
|
+
--json-schema "$schema" \
|
|
7271
|
+
--tools Read,Grep,Glob --restricted \
|
|
7272
|
+
--strict-mcp-config --no-session-persistence --permission-prompts none \
|
|
7273
|
+
${model_args[@]+"${model_args[@]}"} --max-budget-usd "$MENTION_MAX_BUDGET_USD"
|
|
7274
|
+
) </dev/null >"$out" 2>"$err" || AGENT_STATUS=$?
|
|
7275
|
+
return 0
|
|
7276
|
+
}
|
|
7277
|
+
|
|
7278
|
+
# control_mention — MENTION (the header): the state, the credential, the plugin
|
|
7279
|
+
# and the binary, the context directory, one session, then its decision
|
|
7280
|
+
# extracted, validated, checked for a credential and answered. Always exits.
|
|
7281
|
+
control_mention() {
|
|
7282
|
+
local base dir out err why decision from rule action verb question reason text answer file reply quote
|
|
7283
|
+
control_state_var "$CONTROL_BRANCH" \
|
|
7284
|
+
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
7285
|
+
control_state_word_var
|
|
7286
|
+
|
|
7287
|
+
if [ -z "$MENTION_OAUTH" ] && [ -z "$MENTION_API" ]; then
|
|
7288
|
+
control_refuse "$EXIT_REFUSED" "\`$WORKFLOW_CONTROL_FILE\` passes the agent that reads a mention no credential, so a mention is not read" \
|
|
7289
|
+
"$(control_commands_way)"
|
|
7290
|
+
fi
|
|
7291
|
+
base=${MENTION_COMMAND##*:}
|
|
7292
|
+
if [ -z "${HARNESS_MENTION_PLUGIN_DIR-}" ] || [ ! -f "$HARNESS_MENTION_PLUGIN_DIR/commands/$base.md" ]; then
|
|
7293
|
+
control_refuse "$EXIT_GH" "the harness plugin carrying the mention command is not available to this job" \
|
|
7294
|
+
"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)"
|
|
7295
|
+
fi
|
|
7296
|
+
AGENT_CLI="${HARNESS_AGENT_CLI:-claude}"
|
|
7297
|
+
if ! command -v "$AGENT_CLI" >/dev/null 2>&1; then
|
|
7298
|
+
control_refuse "$EXIT_GH" "the agent binary \`$AGENT_CLI\` is not on this job's PATH" \
|
|
7299
|
+
"Comment a command instead. $(control_commands_way)"
|
|
7300
|
+
fi
|
|
7301
|
+
|
|
7302
|
+
if ! dir=$(mktemp -d "$control_tmp/harness-control-mention.XXXXXX"); then
|
|
7303
|
+
control_refuse "$EXIT_GH" "a context directory could not be created under '$control_tmp'" "Comment again to retry."
|
|
7304
|
+
fi
|
|
7305
|
+
out="$dir.out"
|
|
7306
|
+
err="$dir.err"
|
|
7307
|
+
control_dirs="$control_dirs $dir $out $err"
|
|
7308
|
+
{ control_mention_context "$dir" && control_mention_context_extra "$dir"; } \
|
|
7309
|
+
|| control_refuse "$EXIT_GH" "the mention's context could not be written under '$dir'" "Comment again to retry."
|
|
7310
|
+
|
|
7311
|
+
control_mention_session "$dir" "$out" "$err"
|
|
7312
|
+
|
|
7313
|
+
why=$(jq -r -s '
|
|
7314
|
+
if length != 1 or (.[0] | type) != "object" then "its output is not one JSON object"
|
|
7315
|
+
elif .[0].is_error == true then "it reported an error (\(.[0].subtype // "no subtype"))"
|
|
7316
|
+
elif (.[0].subtype // "") != "success" then "it ended as \(.[0].subtype // "no subtype")"
|
|
7317
|
+
else "" end' "$out" 2>/dev/null) || why="its output is not one JSON object"
|
|
7318
|
+
if [ "$AGENT_STATUS" -ne 0 ] || [ -n "$why" ]; then
|
|
7319
|
+
reason=$(grep -v '^[[:space:]]*$' "$err" 2>/dev/null | tail -n 1)
|
|
7320
|
+
! mention_has_credential "$reason" || reason=""
|
|
7321
|
+
[ "$AGENT_STATUS" -eq 0 ] || why="it exited $AGENT_STATUS${reason:+: $reason}"
|
|
7322
|
+
control_refuse "$EXIT_GH" "the agent that reads a mention failed: $why" \
|
|
7323
|
+
"Comment again, or comment a command. $(control_commands_way)"
|
|
7324
|
+
fi
|
|
7325
|
+
|
|
7326
|
+
decision=$(jq -c -s '.[0] as $r
|
|
7327
|
+
| if ($r.structured_output | type) == "object" then { from: "structured_output", d: $r.structured_output }
|
|
7328
|
+
else ([$r.result | strings | fromjson? | objects] | first) as $p
|
|
7329
|
+
| if $p != null then { from: "result", d: $p } else { from: "", d: null } end
|
|
7330
|
+
end' "$out") || decision='{"from":"","d":null}'
|
|
7331
|
+
from=$(printf '%s' "$decision" | jq -r '.from')
|
|
7332
|
+
if [ -z "$from" ]; then
|
|
7333
|
+
control_refuse "$EXIT_REFUSED" "the agent's reading of the mention carries no decision object" \
|
|
7334
|
+
"Comment again, or comment a command. $(control_commands_way)"
|
|
7335
|
+
fi
|
|
7336
|
+
action=$(printf '%s' "$decision" | jq -r '.d.action | if type == "string" then gsub("[\r\n]+"; " ") else tojson end')
|
|
7337
|
+
verb=$(printf '%s' "$decision" | jq -r '.d.verb | strings | gsub("[\r\n]+"; " ")')
|
|
7338
|
+
reason=$(printf '%s' "$decision" | jq -r '.d.reason | strings | gsub("[\r\n]+"; " ") | .[0:200]')
|
|
7339
|
+
! mention_has_credential "$reason" || reason="(withheld: it carries a credential value)"
|
|
7340
|
+
echo "remote-run.sh: control: mention on #$CONTROL_NUMBER by @$CONTROL_ACTOR read as $action${verb:+ $verb} from $from: $reason"
|
|
7341
|
+
|
|
7342
|
+
rule=$(printf '%s' "$decision" | jq -r --arg actions "$MENTION_ACTIONS" --arg verbs "$COMMAND_VERBS" '
|
|
7343
|
+
def words: split(" ") | map(select(length > 0));
|
|
7344
|
+
def nonblank: type == "string" and test("\\S");
|
|
7345
|
+
.d as $d
|
|
7346
|
+
| ($actions | words) as $A
|
|
7347
|
+
| ($verbs | words) as $V
|
|
7348
|
+
| if ($d | type) != "object" then "it is not an object"
|
|
7349
|
+
elif ($A | any(.[]; . == $d.action) | not) then "its `action` is not one of \($A | join(", "))"
|
|
7350
|
+
elif $d.action == "command" and ($V | any(.[]; . == $d.verb) | not) then "a `command` names no `verb` among \($V | join(", "))"
|
|
7351
|
+
elif $d.action == "command" and $d.verb == "answer" and ($d.answer | nonblank | not) then "an `answer` carries no non-blank `answer`"
|
|
7352
|
+
elif $d.action == "command" and $d.verb == "answer" and $d.question != null
|
|
7353
|
+
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"
|
|
7354
|
+
elif ($d.action == "reply" or $d.action == "clarify") and ($d.text | nonblank | not) then "a `\($d.action)` carries no non-blank `text`"
|
|
7355
|
+
elif ($d.reason | type) != "string" then "it carries no `reason`"
|
|
7356
|
+
else "" end') || rule="it could not be read"
|
|
7357
|
+
if [ -n "$rule" ]; then
|
|
7358
|
+
control_refuse "$EXIT_REFUSED" "the agent's reading of the mention is not a valid decision: $rule" \
|
|
7359
|
+
"Comment again, or comment a command. $(control_commands_way)"
|
|
7360
|
+
fi
|
|
7361
|
+
|
|
7362
|
+
# Once, before any action is answered: no branch, this one or a later one,
|
|
7363
|
+
# sees a decision whose posted or dispatched text carries a credential.
|
|
7364
|
+
text=$(printf '%s' "$decision" | jq -r '.d.text | strings')
|
|
7365
|
+
answer=$(printf '%s' "$decision" | jq -r '.d.answer | strings')
|
|
7366
|
+
if mention_has_credential "$text" || mention_has_credential "$answer"; then
|
|
7367
|
+
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"
|
|
7368
|
+
exit "$EXIT_GH"
|
|
7369
|
+
fi
|
|
7370
|
+
|
|
7371
|
+
case "$action" in
|
|
7372
|
+
none)
|
|
7373
|
+
echo "remote-run.sh: control: the mention on #$CONTROL_NUMBER needs no answer"
|
|
7374
|
+
exit "$EXIT_OK" ;;
|
|
7375
|
+
reply|clarify)
|
|
7376
|
+
file="$dir.text"
|
|
7377
|
+
control_dirs="$control_dirs $file"
|
|
7378
|
+
printf '%s' "$decision" | jq -r --arg login "$CONTROL_ACTOR" --arg handle "$COMMAND_HANDLE" \
|
|
7379
|
+
"$JQ_DEF_SANITISE"' .d.text | sanitise($login; $handle)' >"$file" \
|
|
7380
|
+
&& mention_cap "$file" "$MENTION_TEXT_MAX_BYTES" "(cut)" \
|
|
7381
|
+
|| control_refuse "$EXIT_GH" "the agent's reply could not be prepared under '$control_tmp'" "Comment again to retry."
|
|
7382
|
+
text=$(cat "$file")
|
|
7383
|
+
control_reply "$EXIT_OK" "@$CONTROL_ACTOR: $text"$'\n\n'"_Written by an agent that read your mention; it changed nothing. $(control_commands_way)_" ;;
|
|
7384
|
+
fixes)
|
|
7385
|
+
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.\`)." ;;
|
|
7386
|
+
command)
|
|
7387
|
+
question=""
|
|
7388
|
+
[ "$verb" != answer ] || question=$(printf '%s' "$decision" | jq -r '.d.question | numbers | floor')
|
|
7389
|
+
case " $MENTION_CONFIRM_VERBS " in
|
|
7390
|
+
*" $verb "*)
|
|
7391
|
+
reply="@$CONTROL_ACTOR: your mention reads as \`$COMMAND_HANDLE $verb${question:+ $question}\`. Comment that command to carry it out"
|
|
7392
|
+
[ "$verb" != answer ] || reply="$reply, with the answer on the lines below it"
|
|
7393
|
+
control_reply "$EXIT_OK" "$reply." ;;
|
|
7394
|
+
esac
|
|
7395
|
+
case " $MENTION_ACT_VERBS " in
|
|
7396
|
+
*" $verb "*)
|
|
7397
|
+
# What control_answer reads from the exact form: the command line,
|
|
7398
|
+
# then the answer below it. The credential check above has already
|
|
7399
|
+
# refused an `answer` carrying a saved credential.
|
|
7400
|
+
CONTROL_BODY="$COMMAND_HANDLE $verb${question:+ $question}"
|
|
7401
|
+
reply="Read from your mention as \`$CONTROL_BODY\`."
|
|
7402
|
+
if [ "$verb" = answer ]; then
|
|
7403
|
+
# The `x` keeps the answer's final newlines through the substitution.
|
|
7404
|
+
answer=$(printf '%s' "$decision" | jq -j '.d.answer' && printf x) \
|
|
7405
|
+
|| control_refuse "$EXIT_GH" "the agent's answer could not be read" "Comment again to retry."
|
|
7406
|
+
answer=${answer%x}
|
|
7407
|
+
quote=$(printf '%s' "$decision" | jq -r --arg login "$CONTROL_ACTOR" --arg handle "$COMMAND_HANDLE" \
|
|
7408
|
+
"$JQ_DEF_SANITISE $JQ_DEF_FENCE"' .d.answer | sanitise($login; $handle)
|
|
7409
|
+
| (if endswith("\n") then . else . + "\n" end) as $q
|
|
7410
|
+
| ($q | fence) as $f
|
|
7411
|
+
| $f + "\n" + $q + $f') \
|
|
7412
|
+
|| control_refuse "$EXIT_GH" "the agent's answer could not be quoted" "Comment again to retry."
|
|
7413
|
+
CONTROL_BODY="$CONTROL_BODY"$'\n'"$answer"
|
|
7414
|
+
reply="Read from your mention as \`$COMMAND_HANDLE $verb${question:+ $question}\`, with this answer:"$'\n\n'"$quote"
|
|
7415
|
+
fi
|
|
7416
|
+
CONTROL_VERB="$verb"
|
|
7417
|
+
CONTROL_ARGS="$question"
|
|
7418
|
+
CONTROL_MENTION_NOTE="$reply"
|
|
7419
|
+
control_run_verb ;;
|
|
7420
|
+
esac ;;
|
|
7421
|
+
esac
|
|
7422
|
+
echo "::error::remote-run.sh: control: the validated action \`$action\` has no answer"
|
|
7423
|
+
exit "$EXIT_GH"
|
|
7424
|
+
}
|
|
7425
|
+
|
|
5793
7426
|
verb_control() {
|
|
5794
7427
|
local LC_ALL=C
|
|
5795
|
-
local review=0 close=0 forge="" target="" status
|
|
7428
|
+
local review=0 close=0 forge="" target="" status
|
|
7429
|
+
# Unset first so an inherited export of either name cannot keep it exported.
|
|
7430
|
+
unset MENTION_OAUTH MENTION_API MENTION_JOB_TOKEN_B64
|
|
7431
|
+
MENTION_OAUTH="${IN_OAUTH-}"
|
|
7432
|
+
MENTION_API="${IN_API-}"
|
|
7433
|
+
MENTION_JOB_TOKEN_B64=""
|
|
7434
|
+
[ -z "${GH_TOKEN-}" ] || MENTION_JOB_TOKEN_B64="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')"
|
|
7435
|
+
unset IN_OAUTH IN_API CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY
|
|
5796
7436
|
case "${GITHUB_EVENT_NAME-}" in
|
|
5797
7437
|
issue_comment) ;;
|
|
5798
7438
|
pull_request_review) review=1 ;;
|
|
@@ -5857,16 +7497,12 @@ verb_control() {
|
|
|
5857
7497
|
"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."
|
|
5858
7498
|
fi
|
|
5859
7499
|
|
|
5860
|
-
if [ "$review" -eq 0 ] &&
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
verbs="$verbs${verbs:+, }\`$COMMAND_HANDLE $v\`"
|
|
5864
|
-
done
|
|
5865
|
-
control_refuse "$EXIT_REFUSED" "it is not a command this harness carries out" \
|
|
5866
|
-
"The commands are $verbs; \`docs/github-run-control.md\` in the harness documentation states each."
|
|
7500
|
+
if [ "$review" -eq 0 ] && [ "$CONTROL_MENTION" != 1 ] \
|
|
7501
|
+
&& { [ -z "$CONTROL_VERB" ] || ! control_verb_handled "$CONTROL_VERB"; }; then
|
|
7502
|
+
control_refuse "$EXIT_REFUSED" "it is not a command this harness carries out" "$(control_commands_way)"
|
|
5867
7503
|
fi
|
|
5868
7504
|
|
|
5869
|
-
forge_repo_var || control_reply "$EXIT_GH" "@$CONTROL_ACTOR: \`$CONTROL_VERB\` was not run: the repository's name could not be read."
|
|
7505
|
+
forge_repo_var || control_reply "$EXIT_GH" "@$CONTROL_ACTOR: \`${CONTROL_VERB:-$COMMAND_HANDLE}\` was not run: the repository's name could not be read."
|
|
5870
7506
|
if [ "$review" -eq 1 ]; then
|
|
5871
7507
|
control_check_branch "$REVIEW_HEAD"
|
|
5872
7508
|
elif [ -n "$CONTROL_IS_PR" ]; then
|
|
@@ -5875,7 +7511,16 @@ verb_control() {
|
|
|
5875
7511
|
control_branch_from_issue "$CONTROL_NUMBER"
|
|
5876
7512
|
fi
|
|
5877
7513
|
|
|
7514
|
+
if [ "$CONTROL_MENTION" = 1 ]; then
|
|
7515
|
+
echo "remote-run.sh: control: a mention on $CONTROL_BRANCH from @$CONTROL_ACTOR on #$CONTROL_NUMBER"
|
|
7516
|
+
control_mention
|
|
7517
|
+
fi
|
|
5878
7518
|
echo "remote-run.sh: control: $CONTROL_VERB on $CONTROL_BRANCH from @$CONTROL_ACTOR on #$CONTROL_NUMBER"
|
|
7519
|
+
control_run_verb
|
|
7520
|
+
}
|
|
7521
|
+
|
|
7522
|
+
# control_run_verb — the arm carrying out CONTROL_VERB.
|
|
7523
|
+
control_run_verb() {
|
|
5879
7524
|
case "$CONTROL_VERB" in
|
|
5880
7525
|
answer) control_answer ;;
|
|
5881
7526
|
pause) control_pause ;;
|
|
@@ -5910,7 +7555,7 @@ collect_notify() {
|
|
|
5910
7555
|
}
|
|
5911
7556
|
|
|
5912
7557
|
verb_collect() {
|
|
5913
|
-
local status=0 dir file out
|
|
7558
|
+
local status=0 dir file out reason route way
|
|
5914
7559
|
if ! forge_on; then
|
|
5915
7560
|
echo "remote-run.sh: collect: the forge coupling is off (forge github and execution.target github-actions); nothing collected"
|
|
5916
7561
|
exit "$EXIT_OK"
|
|
@@ -5935,17 +7580,7 @@ verb_collect() {
|
|
|
5935
7580
|
exit "$EXIT_OK" ;;
|
|
5936
7581
|
esac
|
|
5937
7582
|
|
|
5938
|
-
|
|
5939
|
-
FORGE_PR="$pr_arg"
|
|
5940
|
-
elif ! forge_pr_var "$branch"; then
|
|
5941
|
-
echo "remote-run.sh: collect: the pull request of $branch could not be read; nothing collected"
|
|
5942
|
-
exit "$EXIT_OK"
|
|
5943
|
-
fi
|
|
5944
|
-
if [ -z "$FORGE_PR" ]; then
|
|
5945
|
-
echo "remote-run.sh: collect: no open pull request; nothing to collect"
|
|
5946
|
-
exit "$EXIT_OK"
|
|
5947
|
-
fi
|
|
5948
|
-
|
|
7583
|
+
# Read before the pull request is required: a first run has none.
|
|
5949
7584
|
control_tmp="${RUNNER_TEMP-}"
|
|
5950
7585
|
if [ -z "$control_tmp" ] || [ ! -d "$control_tmp" ]; then
|
|
5951
7586
|
control_tmp=$(mktemp -d) || { echo "remote-run.sh: collect: mktemp failed; nothing collected"; exit "$EXIT_OK"; }
|
|
@@ -5958,6 +7593,37 @@ verb_collect() {
|
|
|
5958
7593
|
echo "remote-run.sh: collect: the state of the run on $branch could not be read ($BS_ERR); no round started"
|
|
5959
7594
|
exit "$EXIT_OK"
|
|
5960
7595
|
fi
|
|
7596
|
+
if [ "$BS_NOT_STARTED" = 1 ]; then
|
|
7597
|
+
if [ -n "${GITHUB_RUN_ID-}" ] && [ "$BS_RUN_ID" != "$GITHUB_RUN_ID" ]; then
|
|
7598
|
+
echo "remote-run.sh: collect: run $BS_RUN_ID of $branch never started; its own collect reports it"
|
|
7599
|
+
exit "$EXIT_OK"
|
|
7600
|
+
fi
|
|
7601
|
+
reason="${BS_DETAIL#"GitHub did not start the job of run $BS_RUN_ID ("}"
|
|
7602
|
+
if [ "$reason" = "$BS_DETAIL" ]; then reason=""; else reason="${reason%"): "*}"; fi
|
|
7603
|
+
REPORT_NOT_STARTED_STATE="$BS_STATE"
|
|
7604
|
+
REPORT_NOT_STARTED_ENGINE="$BS_ENGINE"
|
|
7605
|
+
route=$(hr_github_resume_route "$branch" "${BS_ENGINE:-<task, user_review or docs: the one the run was started with>}")
|
|
7606
|
+
if [ "$BS_STATE" = paused ]; then
|
|
7607
|
+
way="Run $RESUME_HINT $branch to start it again; $route."
|
|
7608
|
+
else
|
|
7609
|
+
way="Start it again ${route#or }."
|
|
7610
|
+
fi
|
|
7611
|
+
notify not_started "$branch" "$BS_DETAIL. $way" "$reason"
|
|
7612
|
+
# The reviews stay for the resumed run's own end.
|
|
7613
|
+
echo "remote-run.sh: collect: run ${BS_RUN_ID} of $branch never started; reported, and no round collected"
|
|
7614
|
+
exit "$EXIT_OK"
|
|
7615
|
+
fi
|
|
7616
|
+
|
|
7617
|
+
if [ -n "$pr_arg" ]; then
|
|
7618
|
+
FORGE_PR="$pr_arg"
|
|
7619
|
+
elif ! forge_pr_var "$branch"; then
|
|
7620
|
+
echo "remote-run.sh: collect: the pull request of $branch could not be read; nothing collected"
|
|
7621
|
+
exit "$EXIT_OK"
|
|
7622
|
+
fi
|
|
7623
|
+
if [ -z "$FORGE_PR" ]; then
|
|
7624
|
+
echo "remote-run.sh: collect: no open pull request; nothing to collect"
|
|
7625
|
+
exit "$EXIT_OK"
|
|
7626
|
+
fi
|
|
5961
7627
|
if [ "$BS_SETTLED" != 1 ]; then
|
|
5962
7628
|
echo "remote-run.sh: collect: $branch is ${BS_STATE:-in flight}${BS_REASON:+ ($BS_REASON)}; that run's own end collects"
|
|
5963
7629
|
exit "$EXIT_OK"
|
|
@@ -5994,7 +7660,11 @@ verb_collect() {
|
|
|
5994
7660
|
echo "remote-run.sh: collect: started the next round of $branch from @${RC_REVIEWERS//,/, @}"
|
|
5995
7661
|
exit "$EXIT_OK"
|
|
5996
7662
|
fi
|
|
5997
|
-
|
|
7663
|
+
if [ -n "${GITHUB_RUN_ID-}" ]; then
|
|
7664
|
+
collect_notify "The reviews requesting changes collected during the run on \`$branch\` could not start the next round: ${CHILD_LAST%.}. They stay on the pull request. To retry, re-run this run's \`collect\` job (no new review is needed), or submit a review requesting changes."
|
|
7665
|
+
else
|
|
7666
|
+
collect_notify "The reviews requesting changes collected during the run on \`$branch\` could not start the next round: ${CHILD_LAST%.}. They stay on the pull request; submit a review requesting changes to retry."
|
|
7667
|
+
fi
|
|
5998
7668
|
echo "remote-run.sh: collect: review exited $CHILD_STATUS; the pull request was told, and nothing is retried"
|
|
5999
7669
|
exit "$EXIT_OK"
|
|
6000
7670
|
}
|
|
@@ -6078,6 +7748,7 @@ case "$verb" in
|
|
|
6078
7748
|
list) verb_list ;;
|
|
6079
7749
|
discard) verb_discard ;;
|
|
6080
7750
|
report) verb_report ;;
|
|
7751
|
+
open) verb_open ;;
|
|
6081
7752
|
deliver) verb_deliver ;;
|
|
6082
7753
|
collect) verb_collect ;;
|
|
6083
7754
|
control) verb_control ;;
|