autonomous-sdlc-harness 0.2.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.js +0 -0
- package/dist/commands/docs.js +2 -1
- package/dist/commands/docs.js.map +1 -1
- package/dist/commands/doctor.js +56 -6
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +107 -20
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +11 -4
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +25 -0
- package/dist/config/model.js.map +1 -1
- package/dist/core/defaultBranchPush.js +26 -0
- package/dist/core/defaultBranchPush.js.map +1 -0
- package/dist/core/git.js +61 -0
- package/dist/core/git.js.map +1 -1
- package/dist/core/paths.js +22 -2
- package/dist/core/paths.js.map +1 -1
- package/dist/core/prompt.js +6 -2
- package/dist/core/prompt.js.map +1 -1
- package/dist/core/writer.js +2 -0
- package/dist/core/writer.js.map +1 -1
- package/dist/doctor/checks.js +482 -73
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +66 -0
- package/dist/generators/githubWorkflows.js.map +1 -0
- package/dist/generators/notifications.js +105 -19
- package/dist/generators/notifications.js.map +1 -1
- package/dist/generators/outerLoopScripts.js +17 -3
- package/dist/generators/outerLoopScripts.js.map +1 -1
- package/dist/generators/permissionProfile.js +141 -21
- package/dist/generators/permissionProfile.js.map +1 -1
- package/dist/generators/projectSettings.js +2 -1
- package/dist/generators/projectSettings.js.map +1 -1
- package/dist/generators/repoRoot.js +23 -4
- package/dist/generators/repoRoot.js.map +1 -1
- package/dist/generators/stateDir.js +9 -3
- package/dist/generators/stateDir.js.map +1 -1
- package/dist/machine/paths.js +4 -1
- package/dist/machine/paths.js.map +1 -1
- package/dist/remote/githubActions.js +86 -0
- package/dist/remote/githubActions.js.map +1 -0
- package/dist/retrieval/queryLog.js +6 -4
- package/dist/retrieval/queryLog.js.map +1 -1
- package/dist/retrieval/runtime.js +46 -33
- package/dist/retrieval/runtime.js.map +1 -1
- package/dist/retrieval/search.js +21 -10
- package/dist/retrieval/search.js.map +1 -1
- package/dist/retrieval/server.js +1 -1
- package/dist/retrieval/setup.js +2 -1
- package/dist/retrieval/setup.js.map +1 -1
- package/package.json +2 -2
- package/templates/README.md +3 -2
- package/templates/claude/context/conventions.md +1 -1
- package/templates/claude/context/layer.md +1 -1
- package/templates/claude/push-notify.env.example +7 -2
- package/templates/claude/settings.autonomous.json +1 -1
- package/templates/github/workflows/harness-resume.yml +124 -0
- package/templates/github/workflows/harness-run.yml +446 -0
- package/templates/repo/gitignore +11 -1
- package/templates/scripts/README.md +1 -1
- package/templates/scripts/autonomous-watcher.sh +1296 -169
- package/templates/scripts/flow-walker.sh +629 -0
- package/templates/scripts/flows/task_plan_writing.graph.json +192 -0
- package/templates/scripts/lib/flow-walker-gates.sh +165 -0
- package/templates/scripts/lib/harness-run-lib.sh +595 -18
- package/templates/scripts/remote-run.sh +1784 -0
- package/templates/scripts/restart-watcher.sh +21 -1
- package/templates/scripts/run-test-suite.sh +182 -0
- package/templates/scripts/scratch-run.sh +2 -1
- package/templates/state-dir/README-root.md +1 -1
- package/templates/state-dir/autonomous_logs/README.md +2 -2
- package/templates/state-dir/improvement_observations/README.md +2 -0
- package/templates/state-dir/scratch/README.md +1 -1
- package/templates/state-dir/test_fix_plan_reviews/README.md +9 -0
- package/templates/state-dir/test_fix_plans/README.md +9 -0
- package/templates/state-dir/test_fix_point_reviews/README.md +9 -0
- package/templates/state-dir/test_run_logs/README.md +11 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# harness-run.yml — runs one autonomous-sdlc-harness run in a GitHub Actions job,
|
|
2
|
+
# on a GitHub-hosted or a self-hosted runner chosen by the HARNESS_RUNNER variable.
|
|
3
|
+
#
|
|
4
|
+
# WHO WRITES IT. `autonomous-sdlc-harness init`, only when harness.config.json's
|
|
5
|
+
# `execution.target` is `github-actions`. Written create-if-absent: from then on
|
|
6
|
+
# it is yours to tune, and a re-run of `init` keeps your copy.
|
|
7
|
+
#
|
|
8
|
+
# THE INPUT CONTRACT. Composed on the shell side by `remote-run.sh` alone:
|
|
9
|
+
# action run | pause | warm | stop. Only `run` and `warm` start a
|
|
10
|
+
# job; `pause` and `stop` are jobless, titled marker runs
|
|
11
|
+
# branch the run's branch (for warm: GitHub's default branch)
|
|
12
|
+
# engine task | user_review | docs (action=run)
|
|
13
|
+
# resume none | answer | pause (action=run)
|
|
14
|
+
# answers JSON object {"<n>": "<answer text>"} (resume=answer)
|
|
15
|
+
# park_loop_clear true clears the restored park-loop count
|
|
16
|
+
# chain automatic dispatches since the last user action; 0 from a user
|
|
17
|
+
# `run-name` is `harness <action> <branch>`: the job's pause poll and
|
|
18
|
+
# `remote-run.sh continue` / `poll` match runs by that title, so it is a wire.
|
|
19
|
+
#
|
|
20
|
+
# TWO RULES EVERY EDIT KEEPS.
|
|
21
|
+
# * Every input, variable and secret reaches a shell line through `env:`, never
|
|
22
|
+
# through a GitHub expression inside `run:` — an interpolated input is shell
|
|
23
|
+
# source (script injection).
|
|
24
|
+
# * Every GitHub expression has a space after its opening braces. `init`
|
|
25
|
+
# renders this file with the CLI's double-brace token renderer, which reads
|
|
26
|
+
# a letter straight after the braces as a token; the only token here is the
|
|
27
|
+
# CLI version, HARNESS_CLI_VERSION in each job's `env:`.
|
|
28
|
+
#
|
|
29
|
+
# DECLARED MIRRORS — a rename on either side is an edit to both:
|
|
30
|
+
# cli/src/remote/githubActions.ts the file names, HARNESS_RUNNER,
|
|
31
|
+
# HARNESS_REMOTE_STOP, CLAUDE_CODE_OAUTH_TOKEN,
|
|
32
|
+
# ANTHROPIC_API_KEY, HARNESS_PUSH_URL,
|
|
33
|
+
# HARNESS_GIT_TOKEN, the artifact harness-state
|
|
34
|
+
# cli/src/generators/projectSettings.ts the marketplace and plugin key
|
|
35
|
+
# autonomous-sdlc-harness@autonomous-sdlc-harness
|
|
36
|
+
# cli/src/config/model.ts DEFAULTS.scriptsDir and retrievalApplies
|
|
37
|
+
# cli/src/machine/paths.ts machineCacheDir, under which the retrieval
|
|
38
|
+
# runtime sits in `retrieval/`
|
|
39
|
+
# cli/src/retrieval/runtime.ts RETRIEVAL_CACHE_DIRNAME, that `retrieval/`
|
|
40
|
+
#
|
|
41
|
+
# WHAT IT READS.
|
|
42
|
+
# Secrets: CLAUDE_CODE_OAUTH_TOKEN and/or ANTHROPIC_API_KEY (one is required;
|
|
43
|
+
# billing follows the API key when both are set), HARNESS_GIT_TOKEN (optional:
|
|
44
|
+
# a personal or App token, so the job's pushes trigger your own CI, which
|
|
45
|
+
# GITHUB_TOKEN pushes never do), HARNESS_PUSH_URL (optional notifications).
|
|
46
|
+
# Variables: HARNESS_RUNNER, HARNESS_REMOTE_STOP (any value stops every job
|
|
47
|
+
# before it launches anything), HARNESS_STEP_TIMEOUT_MINUTES,
|
|
48
|
+
# HARNESS_SELF_PAUSE_AFTER_MINUTES, HARNESS_MAX_CHAIN, and the watcher
|
|
49
|
+
# tunables listed in the `run` job's `env:` under their own names. An unset
|
|
50
|
+
# variable arrives empty and the script reading it applies its own default.
|
|
51
|
+
# harness.config.json, at run time: `scriptsDir` and whether docs retrieval
|
|
52
|
+
# applies. No configured value is frozen into this file.
|
|
53
|
+
#
|
|
54
|
+
# THE SELF-PAUSE POINT, MEASURED. On a hosted runner the job drops its own PAUSE
|
|
55
|
+
# REMOTE_SELF_PAUSE_AFTER_SECS after it started, the run yields at its next
|
|
56
|
+
# dispatch boundary, and `remote-run.sh continue` chains a new job.
|
|
57
|
+
# Command, over this harness's own repository's main-checkout logs, 2026-09-25
|
|
58
|
+
# (17 stream logs; a tool_use event with no timestamp would inherit the nearest
|
|
59
|
+
# preceding one — none lacked one):
|
|
60
|
+
# jq -n 'def e: .[0:19]+"Z"|fromdateiso8601; reduce inputs as $v ({f:null,t:null,s:{},d:[]}; (if .f!=input_filename then .f=input_filename|.t=null|.s={} else . end) | .t=($v.timestamp//.t) | .t as $t | if $v.type=="assistant" then reduce ($v.message.content[]? | objects | select(.type=="tool_use" and (.name=="Agent" or .name=="Task"))) as $u (.; .s[$u.id]=$t) elif $v.type=="user" then reduce ($v.message.content[]? | objects | select(.type=="tool_result")) as $r (.; if .s[$r.tool_use_id] then .d+=[($t|e)-(.s[$r.tool_use_id]|e)] | del(.s[$r.tool_use_id]) else . end) else . end) | .d|sort|length as $n|{dispatches:$n,longest_min:(.[-1]/60),p95_min:(.[(($n*95+99)/100|floor)-1]/60)}' harness-runs/autonomous_logs/*.stream.jsonl
|
|
61
|
+
# Figures: 957 sub-agent dispatches; longest 73.05 min; 95th percentile 15.5 min.
|
|
62
|
+
# Margin: 73.05 rounded up to the next 15 (75) plus 15 for the pause
|
|
63
|
+
# bookkeeping and the post-steps = 90 min (HARNESS_SELF_PAUSE_MARGIN_MINUTES).
|
|
64
|
+
# Hosted step timeout: the 360-minute hosted job limit minus a 30-minute
|
|
65
|
+
# allowance for the steps around it = 330. Default self-pause: 330 - 90 = 240
|
|
66
|
+
# minutes, floored at 60 when a smaller step timeout is configured. A dispatch
|
|
67
|
+
# longer than the window left after the self-pause costs its in-flight unit:
|
|
68
|
+
# the step timeout kills it, and the continuation resumes from the pushed ledger.
|
|
69
|
+
# On a self-hosted runner the self-pause is left UNSET (disabled, not merely
|
|
70
|
+
# unreached) and the step timeout defaults to the 5-day self-hosted job limit
|
|
71
|
+
# minus the same allowance. HARNESS_JOB_DEADLINE_EPOCH is the job's start plus
|
|
72
|
+
# the step timeout. The job-level timeout-minutes is the self-hosted limit; a
|
|
73
|
+
# hosted job is stopped at its own limit whatever that says.
|
|
74
|
+
#
|
|
75
|
+
# WHY THE TIMEOUT IS COMPUTED INTO GITHUB_ENV. Whether a step's timeout-minutes
|
|
76
|
+
# accepts an expression over `runner.environment` could not be checked against
|
|
77
|
+
# GitHub's workflow-syntax documentation when this file was written (no network
|
|
78
|
+
# access), so the first step computes every budget value from
|
|
79
|
+
# `runner.environment` into GITHUB_ENV, and the harness step reads a single
|
|
80
|
+
# `env` value. Gate 12 records the real behaviour.
|
|
81
|
+
#
|
|
82
|
+
# THE PLUGIN PIN. `claude plugin marketplace add --help` and
|
|
83
|
+
# `claude plugin install --help` (Claude Code 2.1.282) offer no ref or version:
|
|
84
|
+
# "Usage: claude plugin marketplace add [options] <source>" (options: --claudeai,
|
|
85
|
+
# --scope, --sparse); "Usage: claude plugin install|i [options] <plugin>".
|
|
86
|
+
# So the job installs the plugin, then refuses to run, naming both versions,
|
|
87
|
+
# when `claude plugin list --json` reports a version other than this file's
|
|
88
|
+
# rendered CLI version.
|
|
89
|
+
#
|
|
90
|
+
# WHY retention-days IS SET. The `harness-state` bundle is the only remote copy
|
|
91
|
+
# of a run's clarifications and carried counts; a run parked or paused longer
|
|
92
|
+
# than its retention cannot be answered and loses those counts. 400 is the
|
|
93
|
+
# largest retention any repository can configure, and `actions/upload-artifact`
|
|
94
|
+
# caps a larger `retention-days` at the repository's own maximum, so the value
|
|
95
|
+
# means "as long as this repository allows" — the same bound as leaving it
|
|
96
|
+
# unset. It is explicit so that lowering it is a visible edit. The real bound is
|
|
97
|
+
# the repository's Artifact and log retention setting, which
|
|
98
|
+
# `autonomous-sdlc-harness doctor --check-github` reads. The cap (rather than a
|
|
99
|
+
# rejection) is carried from the action's `@actions/artifact` retention code
|
|
100
|
+
# and could not be re-checked against its v4 README when this file was written
|
|
101
|
+
# (no network access); Gate 12 records the real behaviour.
|
|
102
|
+
|
|
103
|
+
name: harness-run
|
|
104
|
+
run-name: harness ${{ inputs.action }} ${{ inputs.branch }}
|
|
105
|
+
|
|
106
|
+
on:
|
|
107
|
+
workflow_dispatch:
|
|
108
|
+
inputs:
|
|
109
|
+
action:
|
|
110
|
+
description: What to do
|
|
111
|
+
required: true
|
|
112
|
+
type: choice
|
|
113
|
+
options:
|
|
114
|
+
- run
|
|
115
|
+
- pause
|
|
116
|
+
- warm
|
|
117
|
+
- stop
|
|
118
|
+
branch:
|
|
119
|
+
description: The run's branch
|
|
120
|
+
required: true
|
|
121
|
+
type: string
|
|
122
|
+
engine:
|
|
123
|
+
description: Which flow runs
|
|
124
|
+
required: false
|
|
125
|
+
type: choice
|
|
126
|
+
default: task
|
|
127
|
+
options:
|
|
128
|
+
- task
|
|
129
|
+
- user_review
|
|
130
|
+
- docs
|
|
131
|
+
resume:
|
|
132
|
+
description: How the run resumes
|
|
133
|
+
required: false
|
|
134
|
+
type: choice
|
|
135
|
+
default: none
|
|
136
|
+
options:
|
|
137
|
+
- none
|
|
138
|
+
- answer
|
|
139
|
+
- pause
|
|
140
|
+
answers:
|
|
141
|
+
description: Clarification answers, a JSON object
|
|
142
|
+
required: false
|
|
143
|
+
type: string
|
|
144
|
+
default: '{}'
|
|
145
|
+
park_loop_clear:
|
|
146
|
+
description: Clear the park-loop count
|
|
147
|
+
required: false
|
|
148
|
+
type: boolean
|
|
149
|
+
default: false
|
|
150
|
+
chain:
|
|
151
|
+
description: Automatic dispatches since the last user action
|
|
152
|
+
required: false
|
|
153
|
+
type: number
|
|
154
|
+
default: 0
|
|
155
|
+
|
|
156
|
+
permissions:
|
|
157
|
+
contents: write
|
|
158
|
+
actions: write
|
|
159
|
+
|
|
160
|
+
defaults:
|
|
161
|
+
run:
|
|
162
|
+
shell: bash
|
|
163
|
+
|
|
164
|
+
jobs:
|
|
165
|
+
run:
|
|
166
|
+
if: inputs.action == 'run'
|
|
167
|
+
runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
|
|
168
|
+
# The self-hosted job limit, 5 days; a hosted job stops at its own limit.
|
|
169
|
+
timeout-minutes: 7200
|
|
170
|
+
concurrency:
|
|
171
|
+
group: harness-run-${{ inputs.branch }}
|
|
172
|
+
cancel-in-progress: false
|
|
173
|
+
env:
|
|
174
|
+
HARNESS_CLI_VERSION: '{{cliVersion}}'
|
|
175
|
+
HARNESS_JOB_MODE: '1'
|
|
176
|
+
HARNESS_REMOTE_SLUG: ${{ github.repository }}
|
|
177
|
+
HARNESS_INPUT_BRANCH: ${{ inputs.branch }}
|
|
178
|
+
HARNESS_INPUT_ENGINE: ${{ inputs.engine }}
|
|
179
|
+
HARNESS_INPUT_RESUME: ${{ inputs.resume }}
|
|
180
|
+
HARNESS_INPUT_ANSWERS: ${{ inputs.answers }}
|
|
181
|
+
HARNESS_INPUT_PARK_LOOP_CLEAR: ${{ inputs.park_loop_clear }}
|
|
182
|
+
HARNESS_INPUT_CHAIN: ${{ inputs.chain }}
|
|
183
|
+
HARNESS_REMOTE_STOP: ${{ vars.HARNESS_REMOTE_STOP }}
|
|
184
|
+
HARNESS_MAX_CHAIN: ${{ vars.HARNESS_MAX_CHAIN }}
|
|
185
|
+
STALL_WARN_SECS: ${{ vars.STALL_WARN_SECS }}
|
|
186
|
+
STALL_KILL_SECS: ${{ vars.STALL_KILL_SECS }}
|
|
187
|
+
STALL_MAX_RESTARTS: ${{ vars.STALL_MAX_RESTARTS }}
|
|
188
|
+
USAGE_PAUSE_TRIGGER: ${{ vars.USAGE_PAUSE_TRIGGER }}
|
|
189
|
+
USAGE_WARNING_DEBOUNCE: ${{ vars.USAGE_WARNING_DEBOUNCE }}
|
|
190
|
+
USAGE_RESUME_MARGIN_SECS: ${{ vars.USAGE_RESUME_MARGIN_SECS }}
|
|
191
|
+
USAGE_SEVEN_DAY_PAUSE_PCT: ${{ vars.USAGE_SEVEN_DAY_PAUSE_PCT }}
|
|
192
|
+
PARK_LOOP_MAX_CYCLES: ${{ vars.PARK_LOOP_MAX_CYCLES }}
|
|
193
|
+
PARK_LOOP_WINDOW_SECS: ${{ vars.PARK_LOOP_WINDOW_SECS }}
|
|
194
|
+
REMOTE_WAIT_MAX_SECS: ${{ vars.REMOTE_WAIT_MAX_SECS }}
|
|
195
|
+
REMOTE_AUTO_RESUME_MAX: ${{ vars.REMOTE_AUTO_RESUME_MAX }}
|
|
196
|
+
REMOTE_AUTO_RESUME_DELAY_SECS: ${{ vars.REMOTE_AUTO_RESUME_DELAY_SECS }}
|
|
197
|
+
REMOTE_CONTROL_POLL_SECS: ${{ vars.REMOTE_CONTROL_POLL_SECS }}
|
|
198
|
+
HARNESS_PUSH_URL: ${{ secrets.HARNESS_PUSH_URL }}
|
|
199
|
+
GH_TOKEN: ${{ github.token }}
|
|
200
|
+
HARNESS_HOSTED_JOB_LIMIT_MINUTES: '360'
|
|
201
|
+
HARNESS_SELF_HOSTED_JOB_LIMIT_MINUTES: '7200'
|
|
202
|
+
HARNESS_STEP_ALLOWANCE_MINUTES: '30'
|
|
203
|
+
HARNESS_SELF_PAUSE_MARGIN_MINUTES: '90'
|
|
204
|
+
steps:
|
|
205
|
+
- name: Compute the time budget
|
|
206
|
+
env:
|
|
207
|
+
IN_RUNNER_ENVIRONMENT: ${{ runner.environment }}
|
|
208
|
+
IN_STEP_TIMEOUT: ${{ vars.HARNESS_STEP_TIMEOUT_MINUTES }}
|
|
209
|
+
IN_SELF_PAUSE: ${{ vars.HARNESS_SELF_PAUSE_AFTER_MINUTES }}
|
|
210
|
+
run: |
|
|
211
|
+
started=$(date +%s)
|
|
212
|
+
kind="${IN_RUNNER_ENVIRONMENT:-${RUNNER_ENVIRONMENT:-github-hosted}}"
|
|
213
|
+
positive() { case "$1" in ''|*[!0-9]*) return 1 ;; esac; [ "$1" -gt 0 ]; }
|
|
214
|
+
if [ "$kind" = self-hosted ]; then
|
|
215
|
+
limit="$HARNESS_SELF_HOSTED_JOB_LIMIT_MINUTES"
|
|
216
|
+
else
|
|
217
|
+
limit="$HARNESS_HOSTED_JOB_LIMIT_MINUTES"
|
|
218
|
+
fi
|
|
219
|
+
step="${IN_STEP_TIMEOUT:-$((limit - HARNESS_STEP_ALLOWANCE_MINUTES))}"
|
|
220
|
+
if ! positive "$step"; then
|
|
221
|
+
echo "::error::HARNESS_STEP_TIMEOUT_MINUTES must be a positive integer, got '$step'"
|
|
222
|
+
exit 1
|
|
223
|
+
fi
|
|
224
|
+
{
|
|
225
|
+
echo "HARNESS_JOB_STARTED_EPOCH=$started"
|
|
226
|
+
echo "HARNESS_STEP_TIMEOUT_MINUTES=$step"
|
|
227
|
+
echo "HARNESS_JOB_DEADLINE_EPOCH=$((started + step * 60))"
|
|
228
|
+
} >> "$GITHUB_ENV"
|
|
229
|
+
if [ "$kind" != self-hosted ]; then
|
|
230
|
+
pause="${IN_SELF_PAUSE:-$((step - HARNESS_SELF_PAUSE_MARGIN_MINUTES))}"
|
|
231
|
+
if [ -z "$IN_SELF_PAUSE" ] && [ "$pause" -lt 60 ]; then pause=60; fi
|
|
232
|
+
if ! positive "$pause"; then
|
|
233
|
+
echo "::error::HARNESS_SELF_PAUSE_AFTER_MINUTES must be a positive integer, got '$pause'"
|
|
234
|
+
exit 1
|
|
235
|
+
fi
|
|
236
|
+
echo "REMOTE_SELF_PAUSE_AFTER_SECS=$((pause * 60))" >> "$GITHUB_ENV"
|
|
237
|
+
echo "runner $kind: step timeout ${step} min, self-pause after ${pause} min"
|
|
238
|
+
else
|
|
239
|
+
echo "runner $kind: step timeout ${step} min, self-pause disabled"
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
- name: Stop when HARNESS_REMOTE_STOP is set
|
|
243
|
+
run: |
|
|
244
|
+
if [ -n "$HARNESS_REMOTE_STOP" ]; then
|
|
245
|
+
echo "HARNESS_STOPPED=1" >> "$GITHUB_ENV"
|
|
246
|
+
echo "::notice::HARNESS_REMOTE_STOP is set: nothing is launched."
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
- name: Check out the run's branch
|
|
250
|
+
if: env.HARNESS_STOPPED != '1'
|
|
251
|
+
uses: actions/checkout@v4
|
|
252
|
+
with:
|
|
253
|
+
ref: ${{ inputs.branch }}
|
|
254
|
+
fetch-depth: 0
|
|
255
|
+
token: ${{ secrets.HARNESS_GIT_TOKEN || github.token }}
|
|
256
|
+
|
|
257
|
+
- name: Check for jq and gh
|
|
258
|
+
if: env.HARNESS_STOPPED != '1'
|
|
259
|
+
run: |
|
|
260
|
+
missing=""
|
|
261
|
+
for tool in jq gh; do
|
|
262
|
+
command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool"
|
|
263
|
+
done
|
|
264
|
+
if [ -n "$missing" ]; then
|
|
265
|
+
echo "::error::this runner lacks:$missing. Install them on the self-hosted runner, or use a GitHub-hosted one."
|
|
266
|
+
exit 1
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
- name: Read the configuration
|
|
270
|
+
id: config
|
|
271
|
+
if: env.HARNESS_STOPPED != '1'
|
|
272
|
+
run: |
|
|
273
|
+
scripts_dir=$(jq -r '.scriptsDir // "scripts"' harness.config.json)
|
|
274
|
+
retrieval=$(jq -r '(.phases.docs == true and .docs.retrieval == true)' harness.config.json)
|
|
275
|
+
{
|
|
276
|
+
echo "SCRIPTS_DIR=${scripts_dir%/}"
|
|
277
|
+
echo "HARNESS_RETRIEVAL_CACHE=${XDG_CACHE_HOME:-$HOME/.cache}/autonomous-sdlc-harness/retrieval"
|
|
278
|
+
} >> "$GITHUB_ENV"
|
|
279
|
+
echo "retrieval=$retrieval" >> "$GITHUB_OUTPUT"
|
|
280
|
+
|
|
281
|
+
- name: Set up Node
|
|
282
|
+
if: env.HARNESS_STOPPED != '1'
|
|
283
|
+
uses: actions/setup-node@v4
|
|
284
|
+
with:
|
|
285
|
+
node-version: '22'
|
|
286
|
+
|
|
287
|
+
- name: Install the claude CLI when absent
|
|
288
|
+
if: env.HARNESS_STOPPED != '1'
|
|
289
|
+
run: |
|
|
290
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
291
|
+
npm install -g @anthropic-ai/claude-code
|
|
292
|
+
fi
|
|
293
|
+
claude --version
|
|
294
|
+
|
|
295
|
+
- name: Check the credentials
|
|
296
|
+
if: env.HARNESS_STOPPED != '1'
|
|
297
|
+
env:
|
|
298
|
+
IN_OAUTH: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
299
|
+
IN_API: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
300
|
+
run: |
|
|
301
|
+
if [ -z "$IN_OAUTH" ] && [ -z "$IN_API" ]; then
|
|
302
|
+
echo "::error::neither the CLAUDE_CODE_OAUTH_TOKEN nor the ANTHROPIC_API_KEY repository secret is set; set one."
|
|
303
|
+
exit 1
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
- name: Set the git identity
|
|
307
|
+
if: env.HARNESS_STOPPED != '1'
|
|
308
|
+
run: |
|
|
309
|
+
git config user.name "github-actions[bot]"
|
|
310
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
311
|
+
|
|
312
|
+
- name: Install the pinned plugin
|
|
313
|
+
if: env.HARNESS_STOPPED != '1'
|
|
314
|
+
run: |
|
|
315
|
+
source_repo=$(jq -r '.extraKnownMarketplaces["autonomous-sdlc-harness"].source.repo // empty' .claude/settings.json 2>/dev/null || true)
|
|
316
|
+
if [ -z "$source_repo" ]; then
|
|
317
|
+
echo "::error::.claude/settings.json names no marketplace source for autonomous-sdlc-harness. Run init locally with --marketplace <owner>/<repo> and commit the file."
|
|
318
|
+
exit 1
|
|
319
|
+
fi
|
|
320
|
+
claude plugin marketplace add "$source_repo"
|
|
321
|
+
claude plugin install autonomous-sdlc-harness@autonomous-sdlc-harness
|
|
322
|
+
installed=$(claude plugin list --json | jq -r '[.[] | select(.id == "autonomous-sdlc-harness@autonomous-sdlc-harness" and .scope == "user")][0].version // empty')
|
|
323
|
+
if [ "$installed" != "$HARNESS_CLI_VERSION" ]; then
|
|
324
|
+
echo "::error::the installed plugin is version '${installed:-none}', but this workflow was rendered for $HARNESS_CLI_VERSION. Publish the matching plugin, or re-run init with the installed version and commit the workflow."
|
|
325
|
+
exit 1
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
- name: Restore the docs-retrieval cache
|
|
329
|
+
if: env.HARNESS_STOPPED != '1' && steps.config.outputs.retrieval == 'true'
|
|
330
|
+
uses: actions/cache/restore@v4
|
|
331
|
+
with:
|
|
332
|
+
path: ${{ env.HARNESS_RETRIEVAL_CACHE }}
|
|
333
|
+
key: harness-retrieval-${{ runner.os }}-${{ env.HARNESS_CLI_VERSION }}
|
|
334
|
+
|
|
335
|
+
- name: Generate the job's permission profile
|
|
336
|
+
if: env.HARNESS_STOPPED != '1'
|
|
337
|
+
run: |
|
|
338
|
+
npx --yes "autonomous-sdlc-harness@$HARNESS_CLI_VERSION" init --plugin-root-entries
|
|
339
|
+
changed=$(git status --porcelain --untracked-files=no)
|
|
340
|
+
if [ -n "$changed" ]; then
|
|
341
|
+
printf '%s\n' "$changed"
|
|
342
|
+
echo "::error::init $HARNESS_CLI_VERSION changed tracked files. Run 'npx autonomous-sdlc-harness@$HARNESS_CLI_VERSION init' locally and commit the result."
|
|
343
|
+
exit 1
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
- name: Preflight with doctor
|
|
347
|
+
if: env.HARNESS_STOPPED != '1'
|
|
348
|
+
run: npx --yes "autonomous-sdlc-harness@$HARNESS_CLI_VERSION" doctor --remote-job
|
|
349
|
+
|
|
350
|
+
- name: Bootstrap the checkout
|
|
351
|
+
if: env.HARNESS_STOPPED != '1'
|
|
352
|
+
run: bash "$SCRIPTS_DIR/setup-worktree.sh"
|
|
353
|
+
|
|
354
|
+
- name: Restore the previous job's state
|
|
355
|
+
if: env.HARNESS_STOPPED != '1'
|
|
356
|
+
run: bash "$SCRIPTS_DIR/remote-run.sh" restore "$HARNESS_INPUT_BRANCH" --resume "$HARNESS_INPUT_RESUME"
|
|
357
|
+
|
|
358
|
+
- name: Run the harness
|
|
359
|
+
if: env.HARNESS_STOPPED != '1'
|
|
360
|
+
timeout-minutes: ${{ fromJSON(env.HARNESS_STEP_TIMEOUT_MINUTES || env.HARNESS_HOSTED_JOB_LIMIT_MINUTES) }}
|
|
361
|
+
env:
|
|
362
|
+
IN_OAUTH: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
363
|
+
IN_API: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
364
|
+
run: |
|
|
365
|
+
if [ -n "$IN_OAUTH" ]; then export CLAUDE_CODE_OAUTH_TOKEN="$IN_OAUTH"; fi
|
|
366
|
+
if [ -n "$IN_API" ]; then export ANTHROPIC_API_KEY="$IN_API"; fi
|
|
367
|
+
unset IN_OAUTH IN_API
|
|
368
|
+
bash "$SCRIPTS_DIR/autonomous-watcher.sh" job "$HARNESS_INPUT_BRANCH" "$HARNESS_INPUT_ENGINE" "$HARNESS_INPUT_RESUME"
|
|
369
|
+
|
|
370
|
+
- name: Push the branch
|
|
371
|
+
if: always() && env.SCRIPTS_DIR != ''
|
|
372
|
+
run: bash "$SCRIPTS_DIR/push-branch.sh"
|
|
373
|
+
|
|
374
|
+
- name: Save the state bundle
|
|
375
|
+
if: always() && env.SCRIPTS_DIR != ''
|
|
376
|
+
run: bash "$SCRIPTS_DIR/remote-run.sh" save "$HARNESS_INPUT_BRANCH" "$RUNNER_TEMP/harness-state"
|
|
377
|
+
|
|
378
|
+
- name: Upload the state bundle
|
|
379
|
+
if: always() && env.SCRIPTS_DIR != ''
|
|
380
|
+
uses: actions/upload-artifact@v4
|
|
381
|
+
with:
|
|
382
|
+
name: harness-state
|
|
383
|
+
path: ${{ runner.temp }}/harness-state
|
|
384
|
+
overwrite: true
|
|
385
|
+
retention-days: 400
|
|
386
|
+
|
|
387
|
+
- name: Continue, wait or stop
|
|
388
|
+
if: ${{ !cancelled() && env.SCRIPTS_DIR != '' }}
|
|
389
|
+
run: bash "$SCRIPTS_DIR/remote-run.sh" continue "$HARNESS_INPUT_BRANCH" "$RUNNER_TEMP/harness-state"
|
|
390
|
+
|
|
391
|
+
- name: Notify a cancelled job
|
|
392
|
+
if: cancelled() && env.SCRIPTS_DIR != ''
|
|
393
|
+
run: |
|
|
394
|
+
export HARNESS_REPO_SLUG="$HARNESS_REMOTE_SLUG"
|
|
395
|
+
bash "$SCRIPTS_DIR/autonomous-notify.sh" failed "$HARNESS_INPUT_BRANCH" "" \
|
|
396
|
+
"job cancelled: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" || true
|
|
397
|
+
|
|
398
|
+
warm:
|
|
399
|
+
if: inputs.action == 'warm'
|
|
400
|
+
runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
|
|
401
|
+
env:
|
|
402
|
+
HARNESS_CLI_VERSION: '{{cliVersion}}'
|
|
403
|
+
HARNESS_REMOTE_STOP: ${{ vars.HARNESS_REMOTE_STOP }}
|
|
404
|
+
steps:
|
|
405
|
+
- name: Stop when HARNESS_REMOTE_STOP is set
|
|
406
|
+
run: |
|
|
407
|
+
if [ -n "$HARNESS_REMOTE_STOP" ]; then
|
|
408
|
+
echo "HARNESS_STOPPED=1" >> "$GITHUB_ENV"
|
|
409
|
+
echo "::notice::HARNESS_REMOTE_STOP is set: nothing is launched."
|
|
410
|
+
fi
|
|
411
|
+
|
|
412
|
+
- name: Check out the dispatched ref
|
|
413
|
+
if: env.HARNESS_STOPPED != '1'
|
|
414
|
+
uses: actions/checkout@v4
|
|
415
|
+
|
|
416
|
+
- name: Read the configuration
|
|
417
|
+
id: config
|
|
418
|
+
if: env.HARNESS_STOPPED != '1'
|
|
419
|
+
run: |
|
|
420
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
421
|
+
echo "::error::this runner lacks: jq."
|
|
422
|
+
exit 1
|
|
423
|
+
fi
|
|
424
|
+
retrieval=$(jq -r '(.phases.docs == true and .docs.retrieval == true)' harness.config.json)
|
|
425
|
+
echo "HARNESS_RETRIEVAL_CACHE=${XDG_CACHE_HOME:-$HOME/.cache}/autonomous-sdlc-harness/retrieval" >> "$GITHUB_ENV"
|
|
426
|
+
echo "retrieval=$retrieval" >> "$GITHUB_OUTPUT"
|
|
427
|
+
if [ "$retrieval" != true ]; then
|
|
428
|
+
echo "::notice::docs retrieval does not apply to this configuration: there is no cache to warm."
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
- name: Set up Node
|
|
432
|
+
if: env.HARNESS_STOPPED != '1' && steps.config.outputs.retrieval == 'true'
|
|
433
|
+
uses: actions/setup-node@v4
|
|
434
|
+
with:
|
|
435
|
+
node-version: '22'
|
|
436
|
+
|
|
437
|
+
- name: Restore and save the docs-retrieval cache
|
|
438
|
+
if: env.HARNESS_STOPPED != '1' && steps.config.outputs.retrieval == 'true'
|
|
439
|
+
uses: actions/cache@v4
|
|
440
|
+
with:
|
|
441
|
+
path: ${{ env.HARNESS_RETRIEVAL_CACHE }}
|
|
442
|
+
key: harness-retrieval-${{ runner.os }}-${{ env.HARNESS_CLI_VERSION }}
|
|
443
|
+
|
|
444
|
+
- name: Install what is missing
|
|
445
|
+
if: env.HARNESS_STOPPED != '1' && steps.config.outputs.retrieval == 'true'
|
|
446
|
+
run: npx --yes "autonomous-sdlc-harness@$HARNESS_CLI_VERSION" init
|
package/templates/repo/gitignore
CHANGED
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
{{pushEnvPath}}
|
|
10
10
|
{{qaCredentialsPath}}
|
|
11
11
|
{{clientEnvPath}}
|
|
12
|
-
# The per-machine settings file the agent runner writes
|
|
12
|
+
# The per-machine settings file the agent runner writes.
|
|
13
13
|
.claude/settings.local.json
|
|
14
|
+
# The unattended run's permission profile. It names this checkout's absolute paths, so a copy is
|
|
15
|
+
# wrong on every other clone; the watcher and `doctor` read it from the main checkout only - a linked
|
|
16
|
+
# worktree carries none - and a remote job's own `init` generates its own. A copy an earlier release
|
|
17
|
+
# committed stays tracked despite this rule until it is untracked by hand - `doctor` reports that state.
|
|
18
|
+
{{permissionProfilePath}}
|
|
14
19
|
# The presence-only marker one user's "don't ask again" answer writes to silence the change-request
|
|
15
20
|
# offer. It lives at the main worktree's root and is checked there from every worktree of this
|
|
16
21
|
# checkout, so one answer covers them all - its existence is the whole signal, so it has no
|
|
@@ -53,6 +58,11 @@
|
|
|
53
58
|
# log directory above.
|
|
54
59
|
{{scratchGlob}}
|
|
55
60
|
{{scratchReadmeException}}
|
|
61
|
+
# The full output of each gate run, one log per round. It is machine-local because it carries
|
|
62
|
+
# machine paths, while the directory's README is the committed contract for it - so the contents are
|
|
63
|
+
# ignored and that one file is excepted, exactly as for the log directory above.
|
|
64
|
+
{{testRunLogsGlob}}
|
|
65
|
+
{{testRunLogsReadmeException}}
|
|
56
66
|
# The backup `config set` copies the configuration to before every write, and the one .bak an
|
|
57
67
|
# adopter gets without asking for it: applying a layer split is the documented path, so without this
|
|
58
68
|
# rule a wired repository carries an untracked file from its first `config set` onward. The leading
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
The project-command wrapper scripts `init` writes into the configured `scriptsDir`: type-check, test and dev-server, configured through `commands.*` in `harness.config.json`, and deploy, configured through `deploy.command` on the separate `deploy` object — so no build tool and no hosting provider is hardcoded anywhere in the harness. They exist as scripts rather than as raw command lines because an unattended run's permission profile can allow-list a literal script path far more safely than an arbitrary command, which is why `commands.<key>` holds the **wrapper invocation** `bash <scriptsDir>/<name>.sh` — the literal the profile allow-lists — while the **raw** command line lives inside the script. Which raw line that is follows one precedence, and it is what keeps the pair from being circular: a raw command line already sitting in `commands.<key>` wins, because an adopter who edited it there meant it — and that state is **reported** rather than silently blessed, since the raw line is not the value the permission profile allow-lists: `init` names it as it writes the wrapper from it, `config set` names it as the value is stored, and `doctor`'s `command-wrappers` check grades it on every later run; otherwise, when that key holds this wrapper's own invocation — the normal state after a first `init` — the body is the line stack detection produced; and when neither resolves, the body names what to fix — the key to set, or for `deploy`, which detection never supplies, this script itself — and exits non-zero rather than appearing to have run a check it never ran. Two states get no wrapper at all, and therefore no allow entry: a key still holding the placeholder `init` writes for a command it could not detect, and `commands.typecheck` holding the `<none>` sentinel — the first is unfinished and is the one that asks the adopter to do something about it, the second is the answer that this repository has no such command and asks for nothing. **Each script prints its own verdict line**, so a caller never appends an exit-code probe to it: that compound form is exactly what stalls an unattended run on a permission prompt — except `start-dev-server.sh`, which has no verdict to give because it starts a long-running process. Every wrapper anchors itself to its own checkout before it runs and forwards whatever arguments it was given to the **last** command of its raw line — whether that command accepts a bare path or name filter is a property of the command, not of the wrapper, so a line ending in a sub-command's own flags takes none; `docs/cli.md` §5 is the full contract.
|
|
4
4
|
|
|
5
|
-
**The other family in this directory is not generated.** The run watcher, its restart wrapper, the notifier and its stream formatter, the commit / push / branch-refresh / worktree / cleanup wrappers, the scratch runner, the docs-retrieval server launcher and the shared library at `lib/harness-run-lib.sh` are copied byte for byte into the same `scriptsDir`, because each reads `harness.config.json` *at run time* rather than carrying a value frozen in when `init` ran — a guard whose protected-branch set was baked into a file at generation time enforces the wrong set the moment that list changes, and does it silently. They therefore carry no `{{token}}` at all. `cli/src/generators/outerLoopScripts.ts` declares that set and marks which of its rows an agent may invoke; `cli/scripts/README.md` records why they live under `scriptsDir` rather than in the installed package. **One of them runs a file it is given:** `scratch-run.sh` runs an agent's language probe or mutation check in the interpreter that file's extension names, and refuses any argument that does not resolve inside `<state_dir>/scratch/`. **One of them is started by neither the watcher nor an agent:** `docs-search-server.sh` is started by the agent runner from `.mcp.json` when `docs.retrieval` is on, and `exec`s the machine-shared retrieval runtime's `docs serve`.
|
|
5
|
+
**The other family in this directory is not generated.** The run watcher, its restart wrapper, the notifier and its stream formatter, the commit / push / branch-refresh / worktree / cleanup wrappers, the scratch runner, the flow walker `flow-walker.sh` with its gate library `lib/flow-walker-gates.sh` and the planning flow's graph `flows/task_plan_writing.graph.json`, the test-suite runner `run-test-suite.sh`, the docs-retrieval server launcher, `remote-run.sh` (sends a remote run's dispatch, pause, warm-up and stop to GitHub; run by the watcher, the remote job and a person) and the shared library at `lib/harness-run-lib.sh` are copied byte for byte into the same `scriptsDir`, because each reads `harness.config.json` *at run time* rather than carrying a value frozen in when `init` ran — a guard whose protected-branch set was baked into a file at generation time enforces the wrong set the moment that list changes, and does it silently. They therefore carry no `{{token}}` at all. `cli/src/generators/outerLoopScripts.ts` declares that set and marks which of its rows an agent may invoke; `cli/scripts/README.md` records why they live under `scriptsDir` rather than in the installed package. **One of them runs a file it is given:** `scratch-run.sh` runs an agent's language probe or mutation check in the interpreter that file's extension names, and refuses any argument that does not resolve inside `<state_dir>/scratch/`. **Two of them are run by the orchestrating session:** `flow-walker.sh` prints the next routing step of the planning flow and keeps machine-local state at `<state_dir>/.flow_walker_state`; `run-test-suite.sh` is run once per Run gates phase, runs the configured test command, writes its output to a per-round log under `<state_dir>/test_run_logs/` and prints only `pass` or `fail <log path>`. **One of them is started by neither the watcher nor an agent:** `docs-search-server.sh` is started by the agent runner from `.mcp.json` when `docs.retrieval` is on, and `exec`s the machine-shared retrieval runtime's `docs serve`.
|