autonomous-sdlc-harness 0.5.0 → 0.6.0

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 (35) hide show
  1. package/dist/commands/init.js +17 -8
  2. package/dist/commands/init.js.map +1 -1
  3. package/dist/config/check.js +27 -5
  4. package/dist/config/check.js.map +1 -1
  5. package/dist/config/model.js +54 -11
  6. package/dist/config/model.js.map +1 -1
  7. package/dist/core/pluginIdentity.js +2 -0
  8. package/dist/core/pluginIdentity.js.map +1 -1
  9. package/dist/core/writer.js +1 -0
  10. package/dist/core/writer.js.map +1 -1
  11. package/dist/doctor/checks.js +317 -40
  12. package/dist/doctor/checks.js.map +1 -1
  13. package/dist/generators/githubWorkflows.js +21 -11
  14. package/dist/generators/githubWorkflows.js.map +1 -1
  15. package/dist/remote/githubActions.js +63 -4
  16. package/dist/remote/githubActions.js.map +1 -1
  17. package/dist/retrieval/pythonBackend.js +114 -0
  18. package/dist/retrieval/pythonBackend.js.map +1 -0
  19. package/dist/retrieval/setup.js +8 -0
  20. package/dist/retrieval/setup.js.map +1 -1
  21. package/package.json +1 -1
  22. package/templates/README.md +1 -1
  23. package/templates/github/workflows/harness-control.yml +184 -0
  24. package/templates/github/workflows/harness-resume.yml +7 -0
  25. package/templates/github/workflows/harness-run.yml +100 -1
  26. package/templates/github/workflows/harness-trigger.yml +5 -4
  27. package/templates/scripts/README.md +1 -1
  28. package/templates/scripts/autonomous-watcher.sh +29 -2
  29. package/templates/scripts/docs-search-server.sh +88 -17
  30. package/templates/scripts/lib/harness-run-lib.sh +205 -26
  31. package/templates/scripts/remote-run.sh +2617 -150
  32. package/templates/scripts/scratch-run.sh +54 -73
  33. package/templates/state-dir/README-root.md +1 -1
  34. package/templates/state-dir/scratch/README.md +4 -2
  35. package/templates/state-dir/user_reviews/README.md +2 -2
@@ -41,6 +41,33 @@
41
41
  # cli/src/machine/paths.ts machineCacheDir, under which the retrieval
42
42
  # runtime sits in `retrieval/`
43
43
  # cli/src/retrieval/runtime.ts RETRIEVAL_CACHE_DIRNAME, that `retrieval/`
44
+ # remote-run.sh (the scriptsDir copy) RUN_JOB_NAME (`run`): the `run` job has no
45
+ # `name:`, so GitHub names it by its key,
46
+ # and the settledness test reads that
47
+ # job's status by that name
48
+ # harness-control.yml the concurrency group
49
+ # `harness-review-<branch>`, shared by its
50
+ # review jobs and the `collect` job here
51
+ #
52
+ # THE COLLECT JOB. When a run ends, `remote-run.sh collect` starts the next
53
+ # user-review round from every review requesting changes that `control`
54
+ # answered "collected" while the run was in flight, so a reviewer waits at most
55
+ # for that run and never resubmits. It is a job of its own, not a step of
56
+ # `run`, for two reasons: a step cannot hold a concurrency group, and this one
57
+ # must share `harness-review-<branch>` with harness-control.yml's review jobs so
58
+ # the two never place a round at once; and the settledness test reads "the
59
+ # `run` job is completed", which no step inside that job could see. Under
60
+ # `!cancelled()` a failed `run` job still reaches it, and the run's state bundle
61
+ # decides whether the run ended or only paused or parked; a cancelled or
62
+ # stopped run skips it. The group name mirrors harness-control.yml's, and
63
+ # RUN_JOB_NAME in remote-run.sh mirrors the `run` job's key (DECLARED MIRRORS
64
+ # above). It reads no secret: its own token pushes the round, dispatches it and
65
+ # comments. It checks out the default branch, as harness-control.yml's review
66
+ # jobs do, never the run's branch: `collect` fetches that branch itself and
67
+ # `review` cuts its own working copy of it, which create-worktree.sh refuses
68
+ # for a branch already checked out. Its step is `continue-on-error` for the
69
+ # report step's reason below: scripts older than this file carry no `collect`
70
+ # verb.
44
71
  #
45
72
  # ACTION PINS.
46
73
  # actions/checkout@v5
@@ -62,7 +89,9 @@
62
89
  # Secrets: CLAUDE_CODE_OAUTH_TOKEN and/or ANTHROPIC_API_KEY (one is required;
63
90
  # billing follows the API key when both are set), HARNESS_GIT_TOKEN (optional:
64
91
  # a personal or App token, so the job's pushes trigger your own CI, which
65
- # GITHUB_TOKEN pushes never do), HARNESS_PUSH_URL (optional notifications).
92
+ # GITHUB_TOKEN pushes never do; it also opens the run's draft pull request,
93
+ # so your CI runs on it without an approval click), HARNESS_PUSH_URL
94
+ # (optional notifications).
66
95
  # Variables: HARNESS_RUNNER, HARNESS_REMOTE_STOP (any value stops every job
67
96
  # before it launches anything), HARNESS_STEP_TIMEOUT_MINUTES,
68
97
  # HARNESS_SELF_PAUSE_AFTER_MINUTES, HARNESS_MAX_CHAIN, and the watcher
@@ -71,6 +100,20 @@
71
100
  # harness.config.json, at run time: `scriptsDir` and whether docs retrieval
72
101
  # applies. No configured value is frozen into this file.
73
102
  #
103
+ # THE PERMISSIONS. A workflow-level `permissions:` block sets every permission
104
+ # it does not list to `none`, so each one the job uses is listed:
105
+ # contents to push the branch
106
+ # actions to dispatch, poll and enable the poller
107
+ # issues to comment and set labels on the issue and the pull request,
108
+ # which both use the issues API
109
+ # pull-requests to open the draft pull request and label it
110
+ #
111
+ # WHY THE REPORT STEP MAY FAIL. `Open the pull request and report` runs
112
+ # `remote-run.sh deliver` after the push, so the pull request's head carries the
113
+ # run's last commit. It is `continue-on-error`: a repository that re-rendered
114
+ # this file without `init --force` still runs scripts with no `deliver` verb,
115
+ # and a finished run must not be marked failed for its report.
116
+ #
74
117
  # THE SELF-PAUSE POINT, MEASURED. On a hosted runner the job drops its own PAUSE
75
118
  # REMOTE_SELF_PAUSE_AFTER_SECS after it started, the run yields at its next
76
119
  # dispatch boundary, and `remote-run.sh continue` chains a new job.
@@ -183,6 +226,8 @@ on:
183
226
  permissions:
184
227
  contents: write
185
228
  actions: write
229
+ issues: write
230
+ pull-requests: write
186
231
 
187
232
  defaults:
188
233
  run:
@@ -420,6 +465,13 @@ jobs:
420
465
  overwrite: true
421
466
  retention-days: 400
422
467
 
468
+ - name: Open the pull request and report
469
+ if: ${{ !cancelled() && env.SCRIPTS_DIR != '' }}
470
+ continue-on-error: true
471
+ env:
472
+ HARNESS_PR_TOKEN: ${{ secrets.HARNESS_GIT_TOKEN }}
473
+ run: bash "$SCRIPTS_DIR/remote-run.sh" deliver "$HARNESS_INPUT_BRANCH" "$RUNNER_TEMP/harness-state"
474
+
423
475
  - name: Continue, wait or stop
424
476
  if: ${{ !cancelled() && env.SCRIPTS_DIR != '' }}
425
477
  run: bash "$SCRIPTS_DIR/remote-run.sh" continue "$HARNESS_INPUT_BRANCH" "$RUNNER_TEMP/harness-state"
@@ -430,6 +482,53 @@ jobs:
430
482
  export HARNESS_REPO_SLUG="$HARNESS_REMOTE_SLUG"
431
483
  bash "$SCRIPTS_DIR/autonomous-notify.sh" failed "$HARNESS_INPUT_BRANCH" "" \
432
484
  "job cancelled: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" || true
485
+ bash "$SCRIPTS_DIR/remote-run.sh" report failed "$HARNESS_INPUT_BRANCH" --note "the job was cancelled: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" || true
486
+
487
+ collect:
488
+ needs: run
489
+ if: ${{ inputs.action == 'run' && !cancelled() }}
490
+ runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
491
+ concurrency:
492
+ group: harness-review-${{ inputs.branch }}
493
+ cancel-in-progress: false
494
+ env:
495
+ GH_TOKEN: ${{ github.token }}
496
+ HARNESS_REMOTE_SLUG: ${{ github.repository }}
497
+ HARNESS_INPUT_BRANCH: ${{ inputs.branch }}
498
+ HARNESS_REMOTE_STOP: ${{ vars.HARNESS_REMOTE_STOP }}
499
+ HARNESS_TRIGGER_ALLOWED_BOTS: ${{ vars.HARNESS_TRIGGER_ALLOWED_BOTS }}
500
+ steps:
501
+ - name: Check out the default branch for collecting
502
+ uses: actions/checkout@v5
503
+ with:
504
+ ref: ${{ github.event.repository.default_branch }}
505
+ token: ${{ github.token }}
506
+ fetch-depth: 0
507
+
508
+ - name: Check for jq and gh before collecting
509
+ run: |
510
+ missing=""
511
+ for tool in jq gh; do
512
+ command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool"
513
+ done
514
+ if [ -n "$missing" ]; then
515
+ echo "::error::this runner lacks:$missing. Install them on the self-hosted runner, or use a GitHub-hosted one."
516
+ exit 1
517
+ fi
518
+
519
+ - name: Read the configuration for collecting
520
+ run: |
521
+ scripts_dir=$(jq -r '.scriptsDir // "scripts"' harness.config.json)
522
+ echo "SCRIPTS_DIR=${scripts_dir%/}" >> "$GITHUB_ENV"
523
+
524
+ - name: Set the git identity for collecting
525
+ run: |
526
+ git config user.name "github-actions[bot]"
527
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
528
+
529
+ - name: Start the next round from collected reviews
530
+ continue-on-error: true
531
+ run: bash "$SCRIPTS_DIR/remote-run.sh" collect "$HARNESS_INPUT_BRANCH"
433
532
 
434
533
  warm:
435
534
  if: inputs.action == 'warm'
@@ -20,7 +20,8 @@
20
20
  # issues, type `labeled` only, never `opened`: labelling an issue at creation
21
21
  # raises `labeled` too, so listening to both would start twice
22
22
  # (docs/github-integration-research.md, T1). The job runs only for the
23
- # label the HARNESS_TRIGGER_LABEL variable names (`harness` when unset);
23
+ # label the HARNESS_TRIGGER_LABEL variable names (`sdlc-harness` when
24
+ # unset), and passes the job that same label, never an empty one;
24
25
  # every other label yields a skipped job, which runs no runner and bills
25
26
  # nothing (docs/remote-execution.md, `### Verified in Gate 12 round 2`).
26
27
  # repository_dispatch, type `harness-task`: the entry for any other adapter
@@ -66,7 +67,7 @@
66
67
  # DECLARED MIRRORS — a rename on either side is an edit to both:
67
68
  # cli/src/remote/githubActions.ts WORKFLOW_TRIGGER_FILE, HARNESS_RUNNER,
68
69
  # HARNESS_REMOTE_STOP, HARNESS_TRIGGER_LABEL,
69
- # DEFAULT_TRIGGER_LABEL (`harness`),
70
+ # DEFAULT_TRIGGER_LABEL (`sdlc-harness`),
70
71
  # HARNESS_TRIGGER_ALLOWED_BOTS,
71
72
  # TRIGGER_DISPATCH_EVENT_TYPE (`harness-task`)
72
73
  # cli/src/config/model.ts DEFAULTS.scriptsDir
@@ -103,13 +104,13 @@ defaults:
103
104
 
104
105
  jobs:
105
106
  trigger:
106
- if: github.event_name == 'repository_dispatch' || github.event.label.name == (vars.HARNESS_TRIGGER_LABEL || 'harness')
107
+ if: github.event_name == 'repository_dispatch' || github.event.label.name == (vars.HARNESS_TRIGGER_LABEL || 'sdlc-harness')
107
108
  runs-on: ${{ vars.HARNESS_RUNNER || 'ubuntu-latest' }}
108
109
  env:
109
110
  GH_TOKEN: ${{ github.token }}
110
111
  HARNESS_REMOTE_STOP: ${{ vars.HARNESS_REMOTE_STOP }}
111
112
  HARNESS_REMOTE_SLUG: ${{ github.repository }}
112
- HARNESS_TRIGGER_LABEL: ${{ vars.HARNESS_TRIGGER_LABEL }}
113
+ HARNESS_TRIGGER_LABEL: ${{ vars.HARNESS_TRIGGER_LABEL || 'sdlc-harness' }}
113
114
  HARNESS_TRIGGER_ALLOWED_BOTS: ${{ vars.HARNESS_TRIGGER_ALLOWED_BOTS }}
114
115
  steps:
115
116
  - name: Check out the default branch
@@ -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 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`.
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 starts the backend `docs.retrievalBackend` selects: it `exec`s the machine-shared retrieval runtime's `docs serve` by default, or runs the Python package's `harness-docs-retrieval serve-mcp` when the key is `python` (its header carries the full exit contract).
@@ -376,7 +376,12 @@
376
376
  # and the next job no `resumed` for it: a chained continuation is not an
377
377
  # event the user acts on. classify_run_exit's `paused` arm notifies only a
378
378
  # `user` pause; job mode sends the others once it has decided. Their titles
379
- # carry HARNESS_REMOTE_SLUG when set.
379
+ # carry HARNESS_REMOTE_SLUG when set. Each job-mode event except `failed` is
380
+ # also passed to `remote-run.sh report` as it happens, and `failed` once,
381
+ # when the job ends `failed`; `report` comments on the run's pull request or
382
+ # issue and moves its state label when `forge` is `github`; `autonomous-notify.sh`
383
+ # is unchanged, and `completed` is reported by the workflow's `deliver` step
384
+ # after the push.
380
385
  # * THE INTERACTIVE-TEST PHASE IS SKIPPED, not run: a runner has no browser
381
386
  # wiring, application dependencies or QA credentials for it. With
382
387
  # `phases.qa` true, the task and user_review launch prompts gain one clause
@@ -1150,12 +1155,33 @@ unset USAGE_LANE_ENABLED_RETIRED
1150
1155
  # Every lifecycle event goes out through here, so a notifier that is missing or
1151
1156
  # not executable costs one log line instead of ending a pass. Best-effort by
1152
1157
  # contract: the notifier itself never fails its caller.
1158
+ # JOB MODE ONLY: the event is also passed to `remote-run.sh report` via
1159
+ # job_report. `failed` is not reported here: run_job reports it once its
1160
+ # automatic resumes are ruled out.
1153
1161
  notify() {
1154
1162
  if [ ! -x "$NOTIFY" ]; then
1155
1163
  log "notify: '$NOTIFY' is not executable — '${1:-?}' event for '${2:-?}' not delivered"
1164
+ else
1165
+ "$NOTIFY" "$@" || true
1166
+ fi
1167
+ [ "$JOB_MODE" = 1 ] || return 0
1168
+ [ "${1:-}" != "failed" ] || return 0
1169
+ job_report "$1" "$2" "${3:-}"
1170
+ }
1171
+
1172
+ # job_report <event> <branch> [<log>] — the only route to GitHub. The detail is
1173
+ # withheld: it names local slash commands, and `report` words its comment from
1174
+ # the registry record itself.
1175
+ job_report() {
1176
+ if [ ! -r "$REMOTE_RUN" ]; then
1177
+ log "notify: '$REMOTE_RUN' is not readable — '${1:-?}' event for '${2:-?}' not reported on GitHub"
1156
1178
  return 0
1157
1179
  fi
1158
- "$NOTIFY" "$@" || true
1180
+ if [ -n "${3:-}" ]; then
1181
+ bash "$REMOTE_RUN" report "$1" "$2" --repo "$MAIN_REPO" >>"$3" 2>&1 || true
1182
+ else
1183
+ bash "$REMOTE_RUN" report "$1" "$2" --repo "$MAIN_REPO" >/dev/null 2>&1 || true
1184
+ fi
1159
1185
  }
1160
1186
 
1161
1187
  # -----------------------------------------------------------------------------
@@ -4222,6 +4248,7 @@ run_job() {
4222
4248
  # A reason recorded for a pause the run finished before honouring.
4223
4249
  [ "$final" = "paused" ] || registry_set "$branch" pause_reason ""
4224
4250
  [ -n "$detail" ] || detail="the run ended $final in this job"
4251
+ [ "$final" != "failed" ] || job_report failed "$branch" "$log_path"
4225
4252
  job_write_status "$branch" "$remote_status" "$decision" "$detail"
4226
4253
  echo "job: $final $decision"
4227
4254
  exit 0
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # docs-search-server.sh — start the docs-retrieval MCP server for the checkout
3
- # this script sits in, by `exec`ing the machine-shared retrieval runtime's
4
- # `docs serve`.
3
+ # this script sits in, on the backend `docs.retrievalBackend` selects.
5
4
  #
6
5
  # WHO RUNS IT. The agent runner, from the `harness-docs` entry `init` writes into
7
6
  # `.mcp.json` when `docs.retrieval` is on — never a dispatched agent's Bash call,
@@ -9,29 +8,52 @@
9
8
  # `agentInvocable: false` and the generated permission profile carries no entry
10
9
  # for it.
11
10
  #
12
- # STDOUT BELONGS TO THE MCP TRANSPORT. Nothing here may print to stdout before
13
- # the `exec`: a stray byte there is a malformed frame for the client. Every
14
- # diagnostic goes to stderr.
11
+ # STDOUT BELONGS TO THE MCP TRANSPORT. Nothing here may print to stdout: a stray
12
+ # byte there is a malformed frame for the client. Every diagnostic goes to
13
+ # stderr.
15
14
  #
16
- # THE RUNTIME IS THE ONLY THING THIS RUNS, with no fallback to any other
17
- # installation: a committed `.mcp.json` cannot know where an adopter's own CLI
18
- # lives. `doctor`'s `retrieval-dependencies` check and `init`'s install both key
19
- # on the same entry file this script tests. `PATH` is settled through the
20
- # library's fallback list before the `exec`, because the starter is the agent
21
- # runner rather than a login shell.
15
+ # WHICH SERVER. Two backends, chosen by `docs.retrievalBackend` read at run
16
+ # time, with no fallback between them and no fallback to any other
17
+ # installation. The key is read ONLY WHEN `phases.docs` and `docs.retrieval` are
18
+ # both `true` (`hr_docs_retrieval_applies`, the shell mirror of
19
+ # `retrievalApplies`): `init` merges `.mcp.json` and removes nothing, so this
20
+ # entry outlives turning retrieval off, and with the gate closed the TypeScript
21
+ # runtime is `exec`ed whatever the key holds. `typescript`, or the key absent,
22
+ # `exec`s the machine-shared runtime's `docs serve`; `doctor`'s
23
+ # `retrieval-dependencies` check and `init`'s install both key on the same entry
24
+ # file this script tests. `python` starts the Python package's stdio server.
25
+ # This script checks no database and runs no `self-check`: grading the backend's
26
+ # prerequisites is `doctor`'s. `PATH` is settled through the library's fallback
27
+ # list first, because the starter is the agent runner rather than a login shell.
28
+ #
29
+ # WHY THE PYTHON BRANCH IS NOT `exec`ED. Its failure must surface as exit 3,
30
+ # which an `exec` could not report. It runs as a child with stdin passed
31
+ # explicitly, and a `TERM` or an `INT` the launcher receives is passed on to it
32
+ # as `TERM`, the signal the server stops on: bash starts a background child with
33
+ # `SIGINT` ignored when job control is off, and Python installs no handler over
34
+ # an ignored `SIGINT`, so an `INT` passed on as itself would never arrive.
22
35
  #
23
36
  # MIRRORS — a change to any owner below is an edit here too:
24
37
  # `retrieval/runtime` and `node_modules/autonomous-sdlc-harness/dist/cli.js`
25
38
  # mirror `RETRIEVAL_CACHE_DIRNAME`, `RETRIEVAL_RUNTIME_DIRNAME` and
26
39
  # `RUNTIME_CLI_RELATIVE` in `cli/src/retrieval/runtime.ts`;
27
- # `hr_cache_dir` mirrors `machineCacheDir()` in `cli/src/machine/paths.ts`.
40
+ # `hr_cache_dir` mirrors `machineCacheDir()` in `cli/src/machine/paths.ts`;
41
+ # `harness-docs-retrieval`, `serve-mcp`, `HARNESS_DOCS_RETRIEVAL_DATABASE_URL`,
42
+ # the default connection string and exit 3 mirror `PYTHON_RETRIEVAL_COMMAND`,
43
+ # `PYTHON_SERVE_SUB_COMMAND`, `PYTHON_DATABASE_URL_VARIABLE`,
44
+ # `PYTHON_DEFAULT_DATABASE_URL` and `PYTHON_BACKEND_UNAVAILABLE_EXIT` in
45
+ # `cli/src/retrieval/pythonBackend.ts`.
28
46
  #
29
47
  # Exit contract:
30
- # 1 no runtime entry at the resolved path, no cache directory to resolve
31
- # it under, or no repository at this script's location; stderr names
32
- # which
33
- # N otherwise `docs serve`'s own status, through `exec`; a failure to
34
- # source the library exits non-zero before that, on stderr only
48
+ # 0 the Python backend exited 0
49
+ # 1 no repository at this script's location; `docs.retrievalBackend`
50
+ # outside the enum; or, on the TypeScript backend, no runtime entry at
51
+ # the resolved path or no cache directory to resolve it under; stderr
52
+ # names which
53
+ # 3 the Python backend is selected but cannot serve: its command does not
54
+ # resolve on `PATH`, or it exited non-zero; stderr names which
55
+ # N on the TypeScript backend, `docs serve`'s own status, through `exec`; a
56
+ # failure to source the library exits non-zero before that, on stderr only
35
57
 
36
58
  set -euo pipefail
37
59
 
@@ -50,6 +72,55 @@ if ! root="$(hr_repo_root "$script_dir")"; then
50
72
  exit 1
51
73
  fi
52
74
 
75
+ backend=typescript
76
+ config_status=0
77
+ hr_config_load "$root" || config_status=$?
78
+ if [ "$config_status" -eq 2 ]; then
79
+ echo "docs-search-server: $root/harness.config.json could not be read, so the default backend (typescript) was taken" >&2
80
+ elif [ "$config_status" -eq 0 ] && hr_docs_retrieval_applies "$root"; then
81
+ backend_status=0
82
+ backend="$(hr_docs_retrieval_backend "$root")" || backend_status=$?
83
+ if [ "$backend_status" -ne 0 ]; then
84
+ echo "docs-search-server: docs.retrievalBackend in $root/harness.config.json is neither \`typescript\` nor \`python\`; set one of them, then run \`npx autonomous-sdlc-harness doctor\`" >&2
85
+ exit 1
86
+ fi
87
+ fi
88
+
89
+ if [ "$backend" = python ]; then
90
+ if ! command -v harness-docs-retrieval >/dev/null 2>&1; then
91
+ echo "docs-search-server: the Python backend is selected but \`harness-docs-retrieval\` does not resolve on PATH (interpreter or package missing); see docs/retrieval.md and run \`npx autonomous-sdlc-harness doctor\`" >&2
92
+ exit 3
93
+ fi
94
+ if [ -z "${HARNESS_DOCS_RETRIEVAL_DATABASE_URL-}" ]; then
95
+ HARNESS_DOCS_RETRIEVAL_DATABASE_URL='postgresql://harness:harness@127.0.0.1:5432/docs_retrieval'
96
+ fi
97
+ export HARNESS_DOCS_RETRIEVAL_DATABASE_URL
98
+
99
+ # A background job's stdin is /dev/null unless it is passed explicitly.
100
+ harness-docs-retrieval serve-mcp --repo "$root" <&0 &
101
+ child=$!
102
+ trapped=0
103
+ trap 'trapped=1; kill -TERM "$child" 2>/dev/null || true' TERM
104
+ trap 'trapped=1; kill -TERM "$child" 2>/dev/null || true' INT
105
+
106
+ # A trapped signal interrupts `wait` before the child's status is collected, so wait again. 127
107
+ # means the first `wait` had already collected it.
108
+ child_status=0
109
+ wait "$child" || child_status=$?
110
+ while [ "$trapped" -eq 1 ]; do
111
+ trapped=0
112
+ again=0
113
+ wait "$child" 2>/dev/null || again=$?
114
+ [ "$again" -eq 127 ] || child_status=$again
115
+ done
116
+
117
+ if [ "$child_status" -eq 0 ]; then
118
+ exit 0
119
+ fi
120
+ echo "docs-search-server: the Python backend exited with status $child_status before or while serving; the line above (\`harness-docs-retrieval: …\`) names why; run \`npx autonomous-sdlc-harness doctor\`" >&2
121
+ exit 3
122
+ fi
123
+
53
124
  if ! cache_dir="$(hr_cache_dir)"; then
54
125
  echo "docs-search-server: neither XDG_CACHE_HOME nor HOME is set, so the retrieval runtime cannot be located" >&2
55
126
  exit 1