autonomous-sdlc-harness 0.6.4 → 0.6.6
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 +65 -5
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +10 -1
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +13 -0
- package/dist/config/model.js.map +1 -1
- package/dist/core/paths.js +1 -1
- package/dist/core/writer.js +1 -1
- package/dist/detect/signals.js +1 -1
- package/dist/doctor/checks.js +7 -7
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +3 -3
- package/dist/generators/githubWorkflows.js.map +1 -1
- package/dist/generators/harnessConfig.js +10 -5
- package/dist/generators/harnessConfig.js.map +1 -1
- package/dist/generators/scripts.js +1 -1
- package/dist/machine/plugins.js +1 -1
- package/dist/machine/registry.js +2 -2
- package/dist/retrieval/models.js +10 -3
- package/dist/retrieval/models.js.map +1 -1
- package/dist/retrieval/refresh.js +6 -2
- package/dist/retrieval/refresh.js.map +1 -1
- package/package.json +1 -1
- package/templates/github/workflows/harness-control.yml +85 -27
- package/templates/github/workflows/harness-resume.yml +41 -22
- package/templates/github/workflows/harness-run.yml +95 -34
- package/templates/github/workflows/harness-trigger.yml +31 -18
- package/templates/scripts/autonomous-watcher.sh +6 -6
- package/templates/scripts/cleanup-merged-worktrees.sh +1 -1
- package/templates/scripts/lib/harness-run-lib.sh +2 -2
- package/templates/scripts/remote-run.sh +108 -30
- package/templates/state-dir/autonomous_inbox/README.md +1 -1
|
@@ -113,20 +113,23 @@
|
|
|
113
113
|
# The credential secrets and HARNESS_GIT_TOKEN are read only by later steps.
|
|
114
114
|
#
|
|
115
115
|
# ACTION PINS.
|
|
116
|
-
# actions/checkout
|
|
117
|
-
# actions/setup-node
|
|
118
|
-
# actions/cache/restore
|
|
119
|
-
# actions/cache
|
|
120
|
-
# actions/upload-artifact
|
|
121
|
-
# Each
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
116
|
+
# actions/checkout v5.1.0
|
|
117
|
+
# actions/setup-node v5.0.0
|
|
118
|
+
# actions/cache/restore v5.1.0
|
|
119
|
+
# actions/cache v5.1.0
|
|
120
|
+
# actions/upload-artifact v6.0.0
|
|
121
|
+
# Each `uses:` names the full commit sha of the release on the comment line
|
|
122
|
+
# above it. Each release is in the lowest major whose own action.yml declares
|
|
123
|
+
# `runs.using: node24`, per a maintainer's lookups of each action's action.yml
|
|
124
|
+
# and release notes on 2026-09-29T08:21Z (UTC). Each of those majors' release
|
|
125
|
+
# notes requires Actions Runner 2.327.1 or newer; a GitHub-hosted runner already
|
|
126
|
+
# has it, and a self-hosted one must run it (docs/remote-execution.md, section
|
|
127
|
+
# 8). A sha, not a tag: a tag is mutable, and this job holds the Claude
|
|
128
|
+
# credential and write tokens. A pin moves when
|
|
129
|
+
# `npx autonomous-sdlc-harness@<version> init --upgrade-workflows` re-renders
|
|
130
|
+
# this file at a newer release's pins; a hand bump replaces the sha and its
|
|
131
|
+
# comment line together. The version stays on its own line, never trailing the
|
|
132
|
+
# `uses:` value: a value carrying ` #` is never a plain scalar.
|
|
130
133
|
#
|
|
131
134
|
# WHAT IT READS.
|
|
132
135
|
# Secrets: CLAUDE_CODE_OAUTH_TOKEN and/or ANTHROPIC_API_KEY (one is required;
|
|
@@ -145,13 +148,52 @@
|
|
|
145
148
|
# harness.config.json, at run time: `scriptsDir` and whether docs retrieval
|
|
146
149
|
# applies. No configured value is frozen into this file.
|
|
147
150
|
#
|
|
148
|
-
# THE PERMISSIONS.
|
|
149
|
-
# it does not list to `none
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
151
|
+
# THE PERMISSIONS. `permissions: {}` at workflow level grants nothing, and each
|
|
152
|
+
# job's own block sets every permission it does not list to `none`. Each grant
|
|
153
|
+
# is what that job's own steps run:
|
|
154
|
+
# wrong-ref none it fails the dispatch from its step's `env:` alone
|
|
155
|
+
# run contents push-branch.sh pushes the branch
|
|
156
|
+
# actions remote-run.sh `restore` downloads the previous
|
|
157
|
+
# bundle, `continue` dispatches and enables the poller
|
|
158
|
+
# issues lifecycle comments and state labels, on the issue
|
|
159
|
+
# and the pull request, which both use the issues API
|
|
160
|
+
# pull-requests remote-run.sh `open` and `deliver` open, update and
|
|
161
|
+
# label the run's pull request
|
|
162
|
+
# collect contents `collect` runs `review`, whose hr_push_landed
|
|
163
|
+
# pushes the round through push-branch.sh
|
|
164
|
+
# actions the settledness read lists runs and downloads the
|
|
165
|
+
# bundle; `review` dispatches the round (verb_dispatch)
|
|
166
|
+
# issues forge_report's `round` and `not_started` comments
|
|
167
|
+
# and labels, collect_notify's comment, and the label
|
|
168
|
+
# create forge_set_state retries with
|
|
169
|
+
# pull-requests forge_pr_var and round_collect read the pull
|
|
170
|
+
# request's reviews and comments, and `round` runs
|
|
171
|
+
# `gh pr ready --undo` on a ready pull request
|
|
172
|
+
# warm contents read the checkout. Its actions/cache restore and save
|
|
173
|
+
# use the runner's cache token, not GITHUB_TOKEN, and
|
|
174
|
+
# its `init` calls no GitHub API
|
|
175
|
+
# A grant narrower than a job's need fails only on GitHub, never locally.
|
|
176
|
+
#
|
|
177
|
+
# THE CHECKOUT CREDENTIAL. The `warm` checkout sets `persist-credentials: false`:
|
|
178
|
+
# that job pushes nothing. The `run` and `collect` checkouts keep the credential
|
|
179
|
+
# persisted, because both push through push-branch.sh with it (`run` with
|
|
180
|
+
# HARNESS_GIT_TOKEN or the job token, `collect` with the job token), so
|
|
181
|
+
# zizmor's `artipacked` finding stands on those two. The one upload, `Upload
|
|
182
|
+
# the state bundle`, takes its path from the runner's temp directory, outside
|
|
183
|
+
# GITHUB_WORKSPACE, so no artifact carries the checkout's .git/config. An edit
|
|
184
|
+
# keeps it so: never point an upload-artifact `path:` inside the workspace while
|
|
185
|
+
# a checkout persists its credential.
|
|
186
|
+
#
|
|
187
|
+
# THE CLAUDE CLI. `Install the claude CLI when absent` installs
|
|
188
|
+
# @anthropic-ai/claude-code unpinned, and only when the runner has no `claude`.
|
|
189
|
+
# The latest is wanted: the agent runner tracks the model API, and
|
|
190
|
+
# harness-control.yml installs it the same way but is never re-rendered by
|
|
191
|
+
# `--upgrade-workflows` (cli/src/generators/githubWorkflows.ts, choice 5), so a
|
|
192
|
+
# version frozen into the templates would stay frozen there. zizmor's
|
|
193
|
+
# `adhoc-packages` finding stands on it, and pinning does not clear it: it flags
|
|
194
|
+
# `npm install -g @anthropic-ai/claude-code@2.1.284` too, measured 2026-10-09.
|
|
195
|
+
# The audit-silent alternatives, `npx --yes` and `curl | bash`, are the same
|
|
196
|
+
# install with less integrity checking.
|
|
155
197
|
#
|
|
156
198
|
# WHY THE REPORT STEP MAY FAIL. `Open the pull request and report` runs
|
|
157
199
|
# `remote-run.sh deliver` after the push, so the pull request's head carries the
|
|
@@ -279,11 +321,7 @@ on:
|
|
|
279
321
|
type: number
|
|
280
322
|
default: 0
|
|
281
323
|
|
|
282
|
-
permissions:
|
|
283
|
-
contents: write
|
|
284
|
-
actions: write
|
|
285
|
-
issues: write
|
|
286
|
-
pull-requests: write
|
|
324
|
+
permissions: {}
|
|
287
325
|
|
|
288
326
|
defaults:
|
|
289
327
|
run:
|
|
@@ -293,6 +331,7 @@ jobs:
|
|
|
293
331
|
wrong-ref:
|
|
294
332
|
if: (inputs.action == 'run' || inputs.action == 'pause') && github.ref_name != inputs.branch
|
|
295
333
|
runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
|
|
334
|
+
permissions: {}
|
|
296
335
|
steps:
|
|
297
336
|
- name: Refuse a dispatch from another ref
|
|
298
337
|
env:
|
|
@@ -305,6 +344,11 @@ jobs:
|
|
|
305
344
|
run:
|
|
306
345
|
if: inputs.action == 'run' && github.ref_name == inputs.branch
|
|
307
346
|
runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
|
|
347
|
+
permissions:
|
|
348
|
+
contents: write
|
|
349
|
+
actions: write
|
|
350
|
+
issues: write
|
|
351
|
+
pull-requests: write
|
|
308
352
|
# The self-hosted job limit, 5 days; a hosted job stops at its own limit.
|
|
309
353
|
timeout-minutes: 7200
|
|
310
354
|
concurrency:
|
|
@@ -434,7 +478,8 @@ jobs:
|
|
|
434
478
|
|
|
435
479
|
- name: Check out the run's branch
|
|
436
480
|
if: env.HARNESS_STOPPED != '1'
|
|
437
|
-
|
|
481
|
+
# actions/checkout v5.1.0
|
|
482
|
+
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
|
|
438
483
|
with:
|
|
439
484
|
ref: ${{ inputs.branch }}
|
|
440
485
|
fetch-depth: 0
|
|
@@ -466,7 +511,8 @@ jobs:
|
|
|
466
511
|
|
|
467
512
|
- name: Set up Node
|
|
468
513
|
if: env.HARNESS_STOPPED != '1'
|
|
469
|
-
|
|
514
|
+
# actions/setup-node v5.0.0
|
|
515
|
+
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444
|
|
470
516
|
with:
|
|
471
517
|
node-version: '22'
|
|
472
518
|
# v5 caches by package.json's packageManager; the job installs nothing through npm's cache, and a lockfile-less repository must not fail this step.
|
|
@@ -522,7 +568,8 @@ jobs:
|
|
|
522
568
|
|
|
523
569
|
- name: Restore the docs-retrieval cache
|
|
524
570
|
if: env.HARNESS_STOPPED != '1' && steps.config.outputs.retrieval == 'true'
|
|
525
|
-
|
|
571
|
+
# actions/cache/restore v5.1.0
|
|
572
|
+
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25
|
|
526
573
|
with:
|
|
527
574
|
path: ${{ env.HARNESS_RETRIEVAL_CACHE }}
|
|
528
575
|
key: harness-retrieval-${{ runner.os }}-${{ env.HARNESS_CLI_VERSION }}
|
|
@@ -582,7 +629,8 @@ jobs:
|
|
|
582
629
|
|
|
583
630
|
- name: Upload the state bundle
|
|
584
631
|
if: always() && env.SCRIPTS_DIR != ''
|
|
585
|
-
|
|
632
|
+
# actions/upload-artifact v6.0.0
|
|
633
|
+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
|
586
634
|
with:
|
|
587
635
|
name: harness-state
|
|
588
636
|
path: ${{ runner.temp }}/harness-state
|
|
@@ -612,6 +660,11 @@ jobs:
|
|
|
612
660
|
needs: run
|
|
613
661
|
if: ${{ inputs.action == 'run' && !cancelled() && github.ref_name == inputs.branch }}
|
|
614
662
|
runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
|
|
663
|
+
permissions:
|
|
664
|
+
contents: write
|
|
665
|
+
actions: write
|
|
666
|
+
issues: write
|
|
667
|
+
pull-requests: write
|
|
615
668
|
concurrency:
|
|
616
669
|
group: harness-review-${{ inputs.branch }}
|
|
617
670
|
cancel-in-progress: false
|
|
@@ -669,7 +722,8 @@ jobs:
|
|
|
669
722
|
exit 1
|
|
670
723
|
|
|
671
724
|
- name: Check out the default branch for collecting
|
|
672
|
-
|
|
725
|
+
# actions/checkout v5.1.0
|
|
726
|
+
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
|
|
673
727
|
with:
|
|
674
728
|
ref: ${{ github.event.repository.default_branch }}
|
|
675
729
|
token: ${{ github.token }}
|
|
@@ -703,6 +757,8 @@ jobs:
|
|
|
703
757
|
warm:
|
|
704
758
|
if: inputs.action == 'warm'
|
|
705
759
|
runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
|
|
760
|
+
permissions:
|
|
761
|
+
contents: read
|
|
706
762
|
env:
|
|
707
763
|
HARNESS_CLI_VERSION: '{{cliVersion}}'
|
|
708
764
|
HARNESS_REMOTE_STOP: ${{ vars.HARNESS_REMOTE_STOP }}
|
|
@@ -716,7 +772,10 @@ jobs:
|
|
|
716
772
|
|
|
717
773
|
- name: Check out the dispatched ref
|
|
718
774
|
if: env.HARNESS_STOPPED != '1'
|
|
719
|
-
|
|
775
|
+
# actions/checkout v5.1.0
|
|
776
|
+
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
|
|
777
|
+
with:
|
|
778
|
+
persist-credentials: false
|
|
720
779
|
|
|
721
780
|
- name: Read the configuration
|
|
722
781
|
id: config
|
|
@@ -735,7 +794,8 @@ jobs:
|
|
|
735
794
|
|
|
736
795
|
- name: Set up Node
|
|
737
796
|
if: env.HARNESS_STOPPED != '1' && steps.config.outputs.retrieval == 'true'
|
|
738
|
-
|
|
797
|
+
# actions/setup-node v5.0.0
|
|
798
|
+
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444
|
|
739
799
|
with:
|
|
740
800
|
node-version: '22'
|
|
741
801
|
# As in the run job: no npm cache is used, and a lockfile-less repository must not fail this step.
|
|
@@ -743,7 +803,8 @@ jobs:
|
|
|
743
803
|
|
|
744
804
|
- name: Restore and save the docs-retrieval cache
|
|
745
805
|
if: env.HARNESS_STOPPED != '1' && steps.config.outputs.retrieval == 'true'
|
|
746
|
-
|
|
806
|
+
# actions/cache v5.1.0
|
|
807
|
+
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25
|
|
747
808
|
with:
|
|
748
809
|
path: ${{ env.HARNESS_RETRIEVAL_CACHE }}
|
|
749
810
|
key: harness-retrieval-${{ runner.os }}-${{ env.HARNESS_CLI_VERSION }}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# `forge` is `github` and its `execution.target` is `github-actions`. Written
|
|
6
6
|
# create-if-absent: from then on it is yours to tune, and a re-run of `init`
|
|
7
7
|
# keeps your copy. `init --upgrade-workflows` does NOT re-render it: it carries
|
|
8
|
-
# no version pin and calls the scripts on the default branch, so it shares
|
|
8
|
+
# no CLI version pin and calls the scripts on the default branch, so it shares
|
|
9
9
|
# their re-run contract — `init --force` replaces it after a `.bak`.
|
|
10
10
|
#
|
|
11
11
|
# WHAT IT DOES. One job runs `remote-run.sh trigger`, which does everything
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
# and the order its refusals are made in. Nothing here pre-filters on it.
|
|
39
39
|
#
|
|
40
40
|
# THE PERMISSIONS, declared because the default token may be read-only
|
|
41
|
-
# (docs/github-integration-research.md, T2):
|
|
41
|
+
# (docs/github-integration-research.md, T2). `permissions: {}` at workflow
|
|
42
|
+
# level grants nothing, and the `trigger` job's own block sets every permission
|
|
43
|
+
# it does not list to `none`, so a job added later inherits nothing:
|
|
42
44
|
# contents: write push the new branch carrying the task prompt
|
|
43
45
|
# actions: write dispatch harness-run.yml and look up the run it created
|
|
44
46
|
# issues: write comment on the issue and remove the trigger label
|
|
@@ -49,7 +51,13 @@
|
|
|
49
51
|
# HARNESS_GIT_TOKEN: a branch pushed with GITHUB_TOKEN starts no workflow
|
|
50
52
|
# (docs/remote-execution.md, `### Verified in Gate 12 round 3`), so the prompt
|
|
51
53
|
# commit does not run your CI. A full fetch, because pushing a new branch from
|
|
52
|
-
# a shallow clone has not been measured anywhere.
|
|
54
|
+
# a shallow clone has not been measured anywhere. The checkout keeps the
|
|
55
|
+
# credential persisted, so zizmor's `artipacked` finding stands on it:
|
|
56
|
+
# `remote-run.sh trigger` runs `start`, whose `hr_push_landed` pushes the new
|
|
57
|
+
# branch through push-branch.sh with that credential. The job uploads no
|
|
58
|
+
# artifact, so nothing carries the checkout's .git/config off the runner; an
|
|
59
|
+
# edit adding an upload never points its `path:` at the workspace root or at
|
|
60
|
+
# `.git/`.
|
|
53
61
|
#
|
|
54
62
|
# NO CONCURRENCY GROUP. A group keeps at most one pending run and cancels an
|
|
55
63
|
# earlier pending one, which would drop a trigger. Two starts that race for one
|
|
@@ -75,16 +83,19 @@
|
|
|
75
83
|
# cli/src/config/model.ts DEFAULTS.scriptsDir
|
|
76
84
|
#
|
|
77
85
|
# ACTION PINS.
|
|
78
|
-
# actions/checkout
|
|
79
|
-
# The
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
86
|
+
# actions/checkout v5.1.0
|
|
87
|
+
# The `uses:` names the full commit sha of the release on the comment line
|
|
88
|
+
# above it. The release is in the lowest major whose own action.yml declares
|
|
89
|
+
# `runs.using: node24`, per a maintainer's lookups of its action.yml and release
|
|
90
|
+
# notes on 2026-09-29T08:21Z (UTC). That major's release notes require Actions
|
|
91
|
+
# Runner 2.327.1 or newer; a GitHub-hosted runner already has it, and a
|
|
92
|
+
# self-hosted one must run it (docs/remote-execution.md, section 8). A sha, not
|
|
93
|
+
# a tag: a tag is mutable, and this job holds a write token while it reads
|
|
94
|
+
# untrusted issue text. `init --upgrade-workflows` does not re-render this file
|
|
95
|
+
# (see WHO WRITES IT), so its pin moves only when `init --force` replaces it
|
|
96
|
+
# after a `.bak`, or by a hand bump, which replaces the sha and its comment line
|
|
97
|
+
# together. The version stays on its own line, never trailing the `uses:` value:
|
|
98
|
+
# a value carrying ` #` is never a plain scalar.
|
|
88
99
|
|
|
89
100
|
name: harness-trigger
|
|
90
101
|
run-name: harness trigger ${{ github.event.issue.number || github.event.action }}
|
|
@@ -95,10 +106,7 @@ on:
|
|
|
95
106
|
repository_dispatch:
|
|
96
107
|
types: [harness-task]
|
|
97
108
|
|
|
98
|
-
permissions:
|
|
99
|
-
contents: write
|
|
100
|
-
actions: write
|
|
101
|
-
issues: write
|
|
109
|
+
permissions: {}
|
|
102
110
|
|
|
103
111
|
defaults:
|
|
104
112
|
run:
|
|
@@ -108,6 +116,10 @@ jobs:
|
|
|
108
116
|
trigger:
|
|
109
117
|
if: github.event_name == 'repository_dispatch' || github.event.label.name == (vars.HARNESS_TRIGGER_LABEL || 'sdlc-harness')
|
|
110
118
|
runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
|
|
119
|
+
permissions:
|
|
120
|
+
contents: write
|
|
121
|
+
actions: write
|
|
122
|
+
issues: write
|
|
111
123
|
env:
|
|
112
124
|
GH_TOKEN: ${{ github.token }}
|
|
113
125
|
HARNESS_REMOTE_STOP: ${{ vars.HARNESS_REMOTE_STOP }}
|
|
@@ -117,7 +129,8 @@ jobs:
|
|
|
117
129
|
HARNESS_RUN_ACTORS: ${{ vars.HARNESS_RUN_ACTORS }}
|
|
118
130
|
steps:
|
|
119
131
|
- name: Check out the default branch
|
|
120
|
-
|
|
132
|
+
# actions/checkout v5.1.0
|
|
133
|
+
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
|
|
121
134
|
with:
|
|
122
135
|
token: ${{ github.token }}
|
|
123
136
|
fetch-depth: 0
|
|
@@ -249,7 +249,7 @@
|
|
|
249
249
|
# would watch a directory nobody drops files into, log where nobody tails, and
|
|
250
250
|
# honor a kill switch nobody can reach — silently, for as long as it runs. So an
|
|
251
251
|
# unreadable library, a location outside a repository, or a `harness.config.json`
|
|
252
|
-
# that is absent,
|
|
252
|
+
# that is absent, unparsable, multi-document, missing `defaultBranch` or beyond
|
|
253
253
|
# the `jq` floor ends the process with ONE line on stderr and a non-zero status,
|
|
254
254
|
# before anything is created. Those lines go to stderr rather than to the watcher
|
|
255
255
|
# log, because the log's location is exactly what could not be resolved; the
|
|
@@ -1574,7 +1574,7 @@ EOF
|
|
|
1574
1574
|
|
|
1575
1575
|
# The report itself: every registered repository with its state, model, effort,
|
|
1576
1576
|
# live-run count and per-repository cap, then one summary. Reading fails open in
|
|
1577
|
-
# §7's sense — absent, unreadable,
|
|
1577
|
+
# §7's sense — absent, unreadable, unparsable, non-object or unrecognised-schema
|
|
1578
1578
|
# reads as "no repositories are registered", and a malformed entry is dropped
|
|
1579
1579
|
# rather than hiding the rest. Never a shell error, never a non-zero status,
|
|
1580
1580
|
# never a write.
|
|
@@ -2001,7 +2001,7 @@ ${GLOBAL_STOP}. End at 'branch ready for review' — never merge, never push to
|
|
|
2001
2001
|
# an --add-dir, because a job session was refused reads under a root the profile
|
|
2002
2002
|
# file already granted. `init` stays the one producer of the list. File order;
|
|
2003
2003
|
# an empty entry and one equal to the two directories already passed are
|
|
2004
|
-
# skipped. An absent,
|
|
2004
|
+
# skipped. An absent, unparsable or keyless profile adds nothing and never
|
|
2005
2005
|
# blocks the launch — `doctor --remote-job` refuses an unusable profile earlier.
|
|
2006
2006
|
local extra_dir_args extra_dirs_logged
|
|
2007
2007
|
extra_dir_args=()
|
|
@@ -3106,7 +3106,7 @@ remote_commit_and_push() {
|
|
|
3106
3106
|
# ^(.+)_docs\.md$ -> docs engine, a fresh working copy
|
|
3107
3107
|
#
|
|
3108
3108
|
# The library owns these patterns and their order: `hr_inbox_route_var` in
|
|
3109
|
-
# lib/harness-run-lib.sh, whose comment states why they cannot
|
|
3109
|
+
# lib/harness-run-lib.sh, whose comment states why they cannot misroute.
|
|
3110
3110
|
#
|
|
3111
3111
|
# A filename matching none of the three is logged and ARCHIVED rather than left
|
|
3112
3112
|
# where it is, so it is not re-logged on every pass for as long as the watcher
|
|
@@ -3583,14 +3583,14 @@ usage_read_run() {
|
|
|
3583
3583
|
sf="${lp%.log}.stream.jsonl"
|
|
3584
3584
|
[ -f "$sf" ] || return 0
|
|
3585
3585
|
tail -n 8000 "$sf" 2>/dev/null | grep '"type":"rate_limit_event"' |
|
|
3586
|
-
jq -rs --argjson
|
|
3586
|
+
jq -rs --argjson threshold "$USAGE_SEVEN_DAY_PAUSE_PCT" '
|
|
3587
3587
|
[ .[] | select(.rate_limit_info) | .rate_limit_info ] as $ev
|
|
3588
3588
|
| [ ($ev | map(select(.rateLimitType == "five_hour")) | last),
|
|
3589
3589
|
($ev | map(select(.rateLimitType == "seven_day")) | last),
|
|
3590
3590
|
($ev | map(select(.rateLimitType != "five_hour" and .rateLimitType != "seven_day")) | last) ]
|
|
3591
3591
|
| map(select(. != null))[]
|
|
3592
3592
|
| (.status // "unknown") as $st0
|
|
3593
|
-
| (if (.rateLimitType == "seven_day") and ($st0 == "allowed_warning") and (((.utilization // 0)) < $
|
|
3593
|
+
| (if (.rateLimitType == "seven_day") and ($st0 == "allowed_warning") and (((.utilization // 0)) < $threshold)
|
|
3594
3594
|
then "allowed" else $st0 end) as $st
|
|
3595
3595
|
| "\($st) \(.isUsingOverage // false) \(.resetsAt // 0) \(.overageResetsAt // 0)"' 2>/dev/null
|
|
3596
3596
|
}
|
|
@@ -325,7 +325,7 @@ run_bounded() {
|
|
|
325
325
|
# string `0`, and `[ 0 -ge 00 ]` is true on the first iteration, so every sweep
|
|
326
326
|
# kills its own fetch before it starts, blames the network, and never cleans up
|
|
327
327
|
# another branch. Comparing the VALUE closes both: `[ ]` returns non-zero for an
|
|
328
|
-
#
|
|
328
|
+
# unparsable or out-of-range string as readily as for a number that fails the
|
|
329
329
|
# test, and the default is what survives either way. The upper clamp is what keeps
|
|
330
330
|
# a fat-fingered value from expressing "wait forever" in seconds.
|
|
331
331
|
fetch_timeout="${HARNESS_FETCH_TIMEOUT:-60}"
|
|
@@ -1100,7 +1100,7 @@ hr_docs_retrieval_backend() {
|
|
|
1100
1100
|
# anchored SUFFIX regexes are mutually exclusive by construction: a filename
|
|
1101
1101
|
# cannot end in more than one of `_task_prompt.md` / `_review[_<n>].md` /
|
|
1102
1102
|
# `_docs.md`, so a branch whose own name contains `review` or `task_prompt`
|
|
1103
|
-
# cannot be
|
|
1103
|
+
# cannot be misrouted — `foo_review_task_prompt.md` is the task engine on branch
|
|
1104
1104
|
# `foo_review`, and `foo_task_prompt_review.md` is the review engine on branch
|
|
1105
1105
|
# `foo_task_prompt`. POSIX leftmost-longest matching of the greedy `(.+)` derives
|
|
1106
1106
|
# the right branch from a round-suffixed name: `foo_review_2.md` -> branch `foo`
|
|
@@ -2501,7 +2501,7 @@ EOF
|
|
|
2501
2501
|
# reset would pin the file for the life of the machine.
|
|
2502
2502
|
#
|
|
2503
2503
|
# FAIL OPEN ON THE STATE, CLOSED ON THE LANE. An absent, unreadable or
|
|
2504
|
-
#
|
|
2504
|
+
# unparsable `usage-state.json` reads as `unknown 0`, which defers nobody:
|
|
2505
2505
|
# pausing on an unreadable file would put a machine-level fault in charge of run
|
|
2506
2506
|
# state, and each repository's own gate is what pauses its runs. A lock directory
|
|
2507
2507
|
# that cannot be read or created is NOT assumed free: `hr_lane_acquire` returns
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
# (always 0, 1 only on a usage error: its paragraph)
|
|
39
39
|
# remote-run.sh collect <branch> [--pr <n>] [--repo <root>]
|
|
40
40
|
# (always 0, 1 only on a usage error: its paragraph)
|
|
41
|
-
# remote-run.sh control [--repo <root>]
|
|
41
|
+
# remote-run.sh control [--needs-agent] [--repo <root>]
|
|
42
|
+
# (its own exit map: its paragraph)
|
|
42
43
|
# 0 sent (for stop: the action=stop marker was dispatched, and every
|
|
43
44
|
# queued, waiting or in-progress `harness run` run of that branch was
|
|
44
45
|
# asked to cancel, or there was none); for status and fetch: printed
|
|
@@ -534,6 +535,18 @@
|
|
|
534
535
|
# no such word exists today): the reply lists every command and names
|
|
535
536
|
# `docs/github-run-control.md`. Skipped for a mention.
|
|
536
537
|
# Then THE BRANCH, and the exact form's arm (`control_run_verb`) or MENTION.
|
|
538
|
+
# `--needs-agent` answers only whether MENTION would start a session: the
|
|
539
|
+
# intake above, then refusals 1 to 3 (`control_gates`, shared with `control`),
|
|
540
|
+
# run alone, posting, dispatching and creating nothing; its one `gh` call is
|
|
541
|
+
# `authorise_actor`'s permission call, and the branch is never resolved. One
|
|
542
|
+
# stdout line each: exit 0 `needs-agent: yes, a mention by @<login> on #<n>`;
|
|
543
|
+
# exit 2 `needs-agent: no, <reason>` for an event other than `issue_comment`,
|
|
544
|
+
# an ignored comment (after its own line), the exact form (before any gate,
|
|
545
|
+
# no `gh` call), or a refusal, whose <reason> is that refusal's reply text;
|
|
546
|
+
# exit 3 `needs-agent: undecided, <AUTH_WHY>` when the permission call failed
|
|
547
|
+
# (`control` still refuses that with exit 2); exit 1 as `control`'s.
|
|
548
|
+
# `WORKFLOW_CONTROL_FILE` runs it before installing the agent; the act step,
|
|
549
|
+
# plain `control`, still re-checks everything.
|
|
537
550
|
# MENTION. `verb_control`'s first statements, for every event, copy `IN_OAUTH`
|
|
538
551
|
# / `IN_API` into the non-exported `MENTION_OAUTH` / `MENTION_API` and unset
|
|
539
552
|
# them, with `CLAUDE_CODE_OAUTH_TOKEN` / `ANTHROPIC_API_KEY`, so no `gh`, `git`
|
|
@@ -611,8 +624,8 @@
|
|
|
611
624
|
# is `branch-resume`'s confirmation. Exits: 0 answered or nothing to do; 2
|
|
612
625
|
# refused; 3 a state read, plugin, binary or agent failure, or a credential
|
|
613
626
|
# value in `text` or `answer`; a carried-out verb, its arm's. The workflow's
|
|
614
|
-
# interface: `IN_OAUTH` / `IN_API` and
|
|
615
|
-
# `
|
|
627
|
+
# interface: `IN_OAUTH` / `IN_API`, `HARNESS_MENTION_PLUGIN_DIR`, and
|
|
628
|
+
# `control --needs-agent`'s exit 2, on which it skips installing the agent.
|
|
616
629
|
# THE BRANCH. On a pull request (`.issue.pull_request.url` set), its head, by
|
|
617
630
|
# `pr view`: a fork's pull request is refused, because this event carries the
|
|
618
631
|
# repository's secrets, and nothing from its head is checked out or run; one
|
|
@@ -1540,6 +1553,13 @@
|
|
|
1540
1553
|
# harness-run.yml --ref feat_x -f action=pause -f branch=feat_x`,
|
|
1541
1554
|
# then one comment on 12 opening `Read from your mention as
|
|
1542
1555
|
# `@sdlc-harness pause`.`
|
|
1556
|
+
# needs-agent the mention's c.json, `control --needs-agent` -> 0, one line
|
|
1557
|
+
# `needs-agent: yes, a mention by @alice on #12`; "$s.log" holds
|
|
1558
|
+
# only the permission call
|
|
1559
|
+
# needs-agent c.json's body `@sdlc-harness pause` -> 2, one `needs-agent: no`
|
|
1560
|
+
# exact line naming the exact form; "$s.log" unchanged
|
|
1561
|
+
# needs-agent the mention with HARNESS_RUN_ACTORS=bob -> 2, one `needs-agent:
|
|
1562
|
+
# unlisted no` line naming HARNESS_RUN_ACTORS; nothing posted
|
|
1543
1563
|
|
|
1544
1564
|
set -u
|
|
1545
1565
|
|
|
@@ -1664,7 +1684,7 @@ usage() {
|
|
|
1664
1684
|
echo " remote-run.sh open <branch> [--repo <root>]" >&2
|
|
1665
1685
|
echo " remote-run.sh deliver <branch> <bundle_dir> [--repo <root>]" >&2
|
|
1666
1686
|
echo " remote-run.sh collect <branch> [--pr <n>] [--repo <root>]" >&2
|
|
1667
|
-
echo " remote-run.sh control [--repo <root>]" >&2
|
|
1687
|
+
echo " remote-run.sh control [--needs-agent] [--repo <root>]" >&2
|
|
1668
1688
|
[ "${verb-}" != save ] || exit "$EXIT_OK"
|
|
1669
1689
|
exit "$EXIT_USAGE"
|
|
1670
1690
|
}
|
|
@@ -1764,6 +1784,7 @@ reviewers_arg=""
|
|
|
1764
1784
|
allow_no_run=0
|
|
1765
1785
|
pr_arg=""
|
|
1766
1786
|
branch_gone=0
|
|
1787
|
+
needs_agent=0
|
|
1767
1788
|
|
|
1768
1789
|
while [ "$#" -gt 0 ]; do
|
|
1769
1790
|
case "$1" in
|
|
@@ -1824,6 +1845,9 @@ while [ "$#" -gt 0 ]; do
|
|
|
1824
1845
|
--branch-gone)
|
|
1825
1846
|
[ "$verb" = stop ] || usage "$1 is a stop option"
|
|
1826
1847
|
branch_gone=1; shift ;;
|
|
1848
|
+
--needs-agent)
|
|
1849
|
+
[ "$verb" = control ] || usage "$1 is a control option"
|
|
1850
|
+
needs_agent=1; shift ;;
|
|
1827
1851
|
-*)
|
|
1828
1852
|
usage "unknown option '$1'" ;;
|
|
1829
1853
|
*)
|
|
@@ -3541,7 +3565,7 @@ verb_poll() {
|
|
|
3541
3565
|
verb_pause_requested() {
|
|
3542
3566
|
local found
|
|
3543
3567
|
list_runs
|
|
3544
|
-
# An
|
|
3568
|
+
# An unparsable createdAt is no match rather than a failed read.
|
|
3545
3569
|
found=$(printf '%s' "$GH_OUT" | jq -r --arg t "harness pause $branch" --argjson since "$since_arg" '
|
|
3546
3570
|
[.[] | select(.displayTitle == $t)
|
|
3547
3571
|
| ((.createdAt // "") | try fromdateiso8601 catch null)
|
|
@@ -7423,9 +7447,75 @@ control_mention() {
|
|
|
7423
7447
|
exit "$EXIT_GH"
|
|
7424
7448
|
}
|
|
7425
7449
|
|
|
7450
|
+
# control_gates — refusals 1 to 3 of `control`, in its order, for both
|
|
7451
|
+
# `control` and `--needs-agent`. 0 when all pass; otherwise 1 with CG_EXIT,
|
|
7452
|
+
# CG_WHY and CG_WAY set as `control_refuse` takes them, and CG_AUTH the
|
|
7453
|
+
# `authorise_actor` status (0 when an earlier gate refused). Posts nothing.
|
|
7454
|
+
CG_EXIT=0
|
|
7455
|
+
CG_WHY=""
|
|
7456
|
+
CG_WAY=""
|
|
7457
|
+
CG_AUTH=0
|
|
7458
|
+
control_gates() {
|
|
7459
|
+
local forge target
|
|
7460
|
+
CG_EXIT="$EXIT_REFUSED"
|
|
7461
|
+
CG_WHY=""
|
|
7462
|
+
CG_WAY=""
|
|
7463
|
+
CG_AUTH=0
|
|
7464
|
+
if [ -n "${HARNESS_REMOTE_STOP-}" ]; then
|
|
7465
|
+
CG_WHY="the repository variable \`HARNESS_REMOTE_STOP\` is set, which stops every command"
|
|
7466
|
+
CG_WAY="Clear it under **Settings → Secrets and variables → Actions → Variables**, then comment again."
|
|
7467
|
+
return 1
|
|
7468
|
+
fi
|
|
7469
|
+
|
|
7470
|
+
forge=$(hr_forge "$root") || forge=""
|
|
7471
|
+
target=$(hr_execution_target "$root") || target=""
|
|
7472
|
+
if [ "$forge" != github ] || [ "$target" != github-actions ]; then
|
|
7473
|
+
CG_WHY="the default branch's \`harness.config.json\` does not turn run control on: it needs \`forge\` set to \`github\` (it is ${forge:-not set or unreadable}) and \`execution.target\` set to \`github-actions\` (it is ${target:-unreadable})"
|
|
7474
|
+
CG_WAY="Set both keys on the default branch, then comment again."
|
|
7475
|
+
return 1
|
|
7476
|
+
fi
|
|
7477
|
+
|
|
7478
|
+
if ! rerun_actor_listed; then
|
|
7479
|
+
CG_WHY="${RUN_ACTORS_WHY%.}"
|
|
7480
|
+
CG_WAY="Only a person the repository variable \`HARNESS_RUN_ACTORS\` admits may re-run this job; one of them can comment again."
|
|
7481
|
+
return 1
|
|
7482
|
+
fi
|
|
7483
|
+
|
|
7484
|
+
authorise_actor "$CONTROL_ACTOR" "$CONTROL_SENDER_TYPE" || CG_AUTH=$?
|
|
7485
|
+
if [ "$CG_AUTH" -ne 0 ]; then
|
|
7486
|
+
CG_WHY="${AUTH_WHY%.}"
|
|
7487
|
+
CG_WAY="Only a collaborator with write, maintain or admin access whom the repository variable \`HARNESS_RUN_ACTORS\` admits (when unset, the owner alone of a repository a personal account owns, and nobody in an organisation-owned one), or a bot listed in \`HARNESS_TRIGGER_ALLOWED_BOTS\`, commands a run."
|
|
7488
|
+
return 1
|
|
7489
|
+
fi
|
|
7490
|
+
return 0
|
|
7491
|
+
}
|
|
7492
|
+
|
|
7493
|
+
# control_needs_agent_no <reason> — the `--needs-agent` answer that no session
|
|
7494
|
+
# starts: one line, exit 2.
|
|
7495
|
+
control_needs_agent_no() {
|
|
7496
|
+
echo "remote-run.sh: control: needs-agent: no, $1"
|
|
7497
|
+
exit "$EXIT_REFUSED"
|
|
7498
|
+
}
|
|
7499
|
+
|
|
7500
|
+
# control_needs_agent — `--needs-agent` after the comment intake: the exact
|
|
7501
|
+
# form, then `control_gates`, answered as one line and an exit. Never returns.
|
|
7502
|
+
control_needs_agent() {
|
|
7503
|
+
[ -z "$CONTROL_VERB" ] \
|
|
7504
|
+
|| control_needs_agent_no "the comment is the exact form \`$COMMAND_HANDLE $CONTROL_VERB\`"
|
|
7505
|
+
if ! control_gates; then
|
|
7506
|
+
if [ "$CG_AUTH" -eq 4 ]; then
|
|
7507
|
+
echo "remote-run.sh: control: needs-agent: undecided, $AUTH_WHY"
|
|
7508
|
+
exit "$EXIT_GH"
|
|
7509
|
+
fi
|
|
7510
|
+
control_needs_agent_no "$CG_WHY"
|
|
7511
|
+
fi
|
|
7512
|
+
echo "remote-run.sh: control: needs-agent: yes, a mention by @$CONTROL_ACTOR on #$CONTROL_NUMBER"
|
|
7513
|
+
exit "$EXIT_OK"
|
|
7514
|
+
}
|
|
7515
|
+
|
|
7426
7516
|
verb_control() {
|
|
7427
7517
|
local LC_ALL=C
|
|
7428
|
-
local review=0 close=0
|
|
7518
|
+
local review=0 close=0
|
|
7429
7519
|
# Unset first so an inherited export of either name cannot keep it exported.
|
|
7430
7520
|
unset MENTION_OAUTH MENTION_API MENTION_JOB_TOKEN_B64
|
|
7431
7521
|
MENTION_OAUTH="${IN_OAUTH-}"
|
|
@@ -7447,9 +7537,17 @@ verb_control() {
|
|
|
7447
7537
|
fi
|
|
7448
7538
|
hr_have_jq || { echo "remote-run.sh: control needs jq" >&2; exit "$EXIT_USAGE"; }
|
|
7449
7539
|
|
|
7540
|
+
if [ "$needs_agent" -eq 1 ] && [ "$GITHUB_EVENT_NAME" != issue_comment ]; then
|
|
7541
|
+
control_needs_agent_no "a \`$GITHUB_EVENT_NAME\` event is not a comment"
|
|
7542
|
+
fi
|
|
7543
|
+
|
|
7450
7544
|
case "$GITHUB_EVENT_NAME" in
|
|
7451
7545
|
pull_request_review) control_review_intake || return 0 ;;
|
|
7452
|
-
issue_comment)
|
|
7546
|
+
issue_comment)
|
|
7547
|
+
if ! control_comment_intake; then
|
|
7548
|
+
[ "$needs_agent" -eq 0 ] || control_needs_agent_no "the comment is ignored"
|
|
7549
|
+
return 0
|
|
7550
|
+
fi ;;
|
|
7453
7551
|
issues) control_issues_intake || return 0 ;;
|
|
7454
7552
|
pull_request) control_pull_request_intake || return 0 ;;
|
|
7455
7553
|
delete) control_delete_intake || return 0 ;;
|
|
@@ -7463,6 +7561,8 @@ verb_control() {
|
|
|
7463
7561
|
esac
|
|
7464
7562
|
fi
|
|
7465
7563
|
|
|
7564
|
+
[ "$needs_agent" -eq 0 ] || control_needs_agent
|
|
7565
|
+
|
|
7466
7566
|
control_tmp="${RUNNER_TEMP-}"
|
|
7467
7567
|
if [ -z "$control_tmp" ] || [ ! -d "$control_tmp" ]; then
|
|
7468
7568
|
control_tmp=$(mktemp -d) || { echo "remote-run.sh: control: mktemp failed" >&2; exit "$EXIT_USAGE"; }
|
|
@@ -7473,29 +7573,7 @@ verb_control() {
|
|
|
7473
7573
|
# Decided by the event name, never CONTROL_VERB: a comment can name `close`.
|
|
7474
7574
|
[ "$close" -eq 0 ] || control_close
|
|
7475
7575
|
|
|
7476
|
-
|
|
7477
|
-
control_refuse "$EXIT_REFUSED" "the repository variable \`HARNESS_REMOTE_STOP\` is set, which stops every command" \
|
|
7478
|
-
"Clear it under **Settings → Secrets and variables → Actions → Variables**, then comment again."
|
|
7479
|
-
fi
|
|
7480
|
-
|
|
7481
|
-
forge=$(hr_forge "$root") || forge=""
|
|
7482
|
-
target=$(hr_execution_target "$root") || target=""
|
|
7483
|
-
if [ "$forge" != github ] || [ "$target" != github-actions ]; then
|
|
7484
|
-
control_refuse "$EXIT_REFUSED" "the default branch's \`harness.config.json\` does not turn run control on: it needs \`forge\` set to \`github\` (it is ${forge:-not set or unreadable}) and \`execution.target\` set to \`github-actions\` (it is ${target:-unreadable})" \
|
|
7485
|
-
"Set both keys on the default branch, then comment again."
|
|
7486
|
-
fi
|
|
7487
|
-
|
|
7488
|
-
if ! rerun_actor_listed; then
|
|
7489
|
-
control_refuse "$EXIT_REFUSED" "${RUN_ACTORS_WHY%.}" \
|
|
7490
|
-
"Only a person the repository variable \`HARNESS_RUN_ACTORS\` admits may re-run this job; one of them can comment again."
|
|
7491
|
-
fi
|
|
7492
|
-
|
|
7493
|
-
status=0
|
|
7494
|
-
authorise_actor "$CONTROL_ACTOR" "$CONTROL_SENDER_TYPE" || status=$?
|
|
7495
|
-
if [ "$status" -ne 0 ]; then
|
|
7496
|
-
control_refuse "$EXIT_REFUSED" "${AUTH_WHY%.}" \
|
|
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."
|
|
7498
|
-
fi
|
|
7576
|
+
control_gates || control_refuse "$CG_EXIT" "$CG_WHY" "$CG_WAY"
|
|
7499
7577
|
|
|
7500
7578
|
if [ "$review" -eq 0 ] && [ "$CONTROL_MENTION" != 1 ] \
|
|
7501
7579
|
&& { [ -z "$CONTROL_VERB" ] || ! control_verb_handled "$CONTROL_VERB"; }; then
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# autonomous_inbox/
|
|
2
2
|
|
|
3
|
-
The unattended run loop's drop point: one file per run request, and its **name** is the whole request — it selects the engine command, the working-copy strategy and where the dropped file lands, all three at once. `<branch>_task_prompt.md` starts a delivery run in a fresh working copy and lands the prompt in `<state_dir>/task_prompts/`; `<branch>_review[_<n>].md` starts a fix round for a hands-on review in that branch's existing working copy and lands the review in `<state_dir>/user_reviews/` with its round suffix intact; `<branch>_docs.md` starts a documentation run and lands the checklist in `<state_dir>/docs_catalog/`. The three patterns are anchored suffixes and mutually exclusive, so a branch whose own name contains `review` or `task_prompt` is not
|
|
3
|
+
The unattended run loop's drop point: one file per run request, and its **name** is the whole request — it selects the engine command, the working-copy strategy and where the dropped file lands, all three at once. `<branch>_task_prompt.md` starts a delivery run in a fresh working copy and lands the prompt in `<state_dir>/task_prompts/`; `<branch>_review[_<n>].md` starts a fix round for a hands-on review in that branch's existing working copy and lands the review in `<state_dir>/user_reviews/` with its round suffix intact; `<branch>_docs.md` starts a documentation run and lands the checklist in `<state_dir>/docs_catalog/`. The three patterns are anchored suffixes and mutually exclusive, so a branch whose own name contains `review` or `task_prompt` is not misrouted, and only the branch is read out of the name — never the round, which the engine resolves inside the working copy.
|
|
4
4
|
|
|
5
5
|
A drop is written by the harness's prompt command, by its hands-on-review command, or by hand for a documentation checklist, which no shipped command writes. The only reader is the run daemon, on its next poll pass: there is no queue server and no webhook in between. Once it has consumed a file the daemon moves it aside into `.processed/` under a timestamp; a file matching **none** of the three patterns is archived into the same place under a `rejected_` prefix, with one log line and no registry record. A drop the daemon defers instead — the kill switch is up, the run cap is full, the usage hold is on — is left exactly where it is, so a later pass picks it up with nothing to re-drop by hand.
|
|
6
6
|
|