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.
Files changed (78) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +0 -0
  3. package/dist/commands/docs.js +2 -1
  4. package/dist/commands/docs.js.map +1 -1
  5. package/dist/commands/doctor.js +56 -6
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/init.js +107 -20
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/config/check.js +11 -4
  10. package/dist/config/check.js.map +1 -1
  11. package/dist/config/model.js +25 -0
  12. package/dist/config/model.js.map +1 -1
  13. package/dist/core/defaultBranchPush.js +26 -0
  14. package/dist/core/defaultBranchPush.js.map +1 -0
  15. package/dist/core/git.js +61 -0
  16. package/dist/core/git.js.map +1 -1
  17. package/dist/core/paths.js +22 -2
  18. package/dist/core/paths.js.map +1 -1
  19. package/dist/core/prompt.js +6 -2
  20. package/dist/core/prompt.js.map +1 -1
  21. package/dist/core/writer.js +2 -0
  22. package/dist/core/writer.js.map +1 -1
  23. package/dist/doctor/checks.js +482 -73
  24. package/dist/doctor/checks.js.map +1 -1
  25. package/dist/generators/githubWorkflows.js +66 -0
  26. package/dist/generators/githubWorkflows.js.map +1 -0
  27. package/dist/generators/notifications.js +105 -19
  28. package/dist/generators/notifications.js.map +1 -1
  29. package/dist/generators/outerLoopScripts.js +17 -3
  30. package/dist/generators/outerLoopScripts.js.map +1 -1
  31. package/dist/generators/permissionProfile.js +141 -21
  32. package/dist/generators/permissionProfile.js.map +1 -1
  33. package/dist/generators/projectSettings.js +2 -1
  34. package/dist/generators/projectSettings.js.map +1 -1
  35. package/dist/generators/repoRoot.js +23 -4
  36. package/dist/generators/repoRoot.js.map +1 -1
  37. package/dist/generators/stateDir.js +9 -3
  38. package/dist/generators/stateDir.js.map +1 -1
  39. package/dist/machine/paths.js +4 -1
  40. package/dist/machine/paths.js.map +1 -1
  41. package/dist/remote/githubActions.js +86 -0
  42. package/dist/remote/githubActions.js.map +1 -0
  43. package/dist/retrieval/queryLog.js +6 -4
  44. package/dist/retrieval/queryLog.js.map +1 -1
  45. package/dist/retrieval/runtime.js +46 -33
  46. package/dist/retrieval/runtime.js.map +1 -1
  47. package/dist/retrieval/search.js +21 -10
  48. package/dist/retrieval/search.js.map +1 -1
  49. package/dist/retrieval/server.js +1 -1
  50. package/dist/retrieval/setup.js +2 -1
  51. package/dist/retrieval/setup.js.map +1 -1
  52. package/package.json +2 -2
  53. package/templates/README.md +3 -2
  54. package/templates/claude/context/conventions.md +1 -1
  55. package/templates/claude/context/layer.md +1 -1
  56. package/templates/claude/push-notify.env.example +7 -2
  57. package/templates/claude/settings.autonomous.json +1 -1
  58. package/templates/github/workflows/harness-resume.yml +124 -0
  59. package/templates/github/workflows/harness-run.yml +446 -0
  60. package/templates/repo/gitignore +11 -1
  61. package/templates/scripts/README.md +1 -1
  62. package/templates/scripts/autonomous-watcher.sh +1296 -169
  63. package/templates/scripts/flow-walker.sh +629 -0
  64. package/templates/scripts/flows/task_plan_writing.graph.json +192 -0
  65. package/templates/scripts/lib/flow-walker-gates.sh +165 -0
  66. package/templates/scripts/lib/harness-run-lib.sh +595 -18
  67. package/templates/scripts/remote-run.sh +1784 -0
  68. package/templates/scripts/restart-watcher.sh +21 -1
  69. package/templates/scripts/run-test-suite.sh +182 -0
  70. package/templates/scripts/scratch-run.sh +2 -1
  71. package/templates/state-dir/README-root.md +1 -1
  72. package/templates/state-dir/autonomous_logs/README.md +2 -2
  73. package/templates/state-dir/improvement_observations/README.md +2 -0
  74. package/templates/state-dir/scratch/README.md +1 -1
  75. package/templates/state-dir/test_fix_plan_reviews/README.md +9 -0
  76. package/templates/state-dir/test_fix_plans/README.md +9 -0
  77. package/templates/state-dir/test_fix_point_reviews/README.md +9 -0
  78. 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
@@ -9,8 +9,13 @@
9
9
  {{pushEnvPath}}
10
10
  {{qaCredentialsPath}}
11
11
  {{clientEnvPath}}
12
- # The per-machine settings file the agent runner writes beside the committed profiles.
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`.