autonomous-sdlc-harness 0.1.0 → 0.2.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 (86) hide show
  1. package/README.md +4 -3
  2. package/dist/commands/docs.js +219 -0
  3. package/dist/commands/docs.js.map +1 -0
  4. package/dist/commands/doctor.js +5 -5
  5. package/dist/commands/doctor.js.map +1 -1
  6. package/dist/commands/init.js +138 -44
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/registry.js +2 -0
  9. package/dist/commands/registry.js.map +1 -1
  10. package/dist/config/check.js +25 -5
  11. package/dist/config/check.js.map +1 -1
  12. package/dist/config/model.js +18 -1
  13. package/dist/config/model.js.map +1 -1
  14. package/dist/core/layerGapRemedy.js +3 -2
  15. package/dist/core/layerGapRemedy.js.map +1 -1
  16. package/dist/core/pluginIdentity.js +33 -0
  17. package/dist/core/pluginIdentity.js.map +1 -0
  18. package/dist/core/report.js +9 -0
  19. package/dist/core/report.js.map +1 -1
  20. package/dist/core/writer.js +22 -5
  21. package/dist/core/writer.js.map +1 -1
  22. package/dist/detect/presets.js +35 -26
  23. package/dist/detect/presets.js.map +1 -1
  24. package/dist/detect/signals.js +13 -9
  25. package/dist/detect/signals.js.map +1 -1
  26. package/dist/doctor/checks.js +141 -11
  27. package/dist/doctor/checks.js.map +1 -1
  28. package/dist/generators/claudeContext.js +4 -5
  29. package/dist/generators/claudeContext.js.map +1 -1
  30. package/dist/generators/harnessConfig.js +13 -5
  31. package/dist/generators/harnessConfig.js.map +1 -1
  32. package/dist/generators/outerLoopScripts.js +13 -0
  33. package/dist/generators/outerLoopScripts.js.map +1 -1
  34. package/dist/generators/permissionProfile.js +51 -9
  35. package/dist/generators/permissionProfile.js.map +1 -1
  36. package/dist/generators/projectSettings.js +5 -15
  37. package/dist/generators/projectSettings.js.map +1 -1
  38. package/dist/generators/repoRoot.js +130 -23
  39. package/dist/generators/repoRoot.js.map +1 -1
  40. package/dist/generators/scripts.js +4 -1
  41. package/dist/generators/scripts.js.map +1 -1
  42. package/dist/machine/paths.js +16 -4
  43. package/dist/machine/paths.js.map +1 -1
  44. package/dist/machine/plugins.js +2 -1
  45. package/dist/machine/plugins.js.map +1 -1
  46. package/dist/retrieval/chunk.js +158 -0
  47. package/dist/retrieval/chunk.js.map +1 -0
  48. package/dist/retrieval/corpus.js +75 -0
  49. package/dist/retrieval/corpus.js.map +1 -0
  50. package/dist/retrieval/models.js +175 -0
  51. package/dist/retrieval/models.js.map +1 -0
  52. package/dist/retrieval/queryLog.js +68 -0
  53. package/dist/retrieval/queryLog.js.map +1 -0
  54. package/dist/retrieval/refresh.js +55 -0
  55. package/dist/retrieval/refresh.js.map +1 -0
  56. package/dist/retrieval/runtime.js +171 -0
  57. package/dist/retrieval/runtime.js.map +1 -0
  58. package/dist/retrieval/search.js +118 -0
  59. package/dist/retrieval/search.js.map +1 -0
  60. package/dist/retrieval/server.js +197 -0
  61. package/dist/retrieval/server.js.map +1 -0
  62. package/dist/retrieval/session.js +41 -0
  63. package/dist/retrieval/session.js.map +1 -0
  64. package/dist/retrieval/setup.js +120 -0
  65. package/dist/retrieval/setup.js.map +1 -0
  66. package/dist/retrieval/store.js +170 -0
  67. package/dist/retrieval/store.js.map +1 -0
  68. package/package.json +22 -3
  69. package/templates/claude/CLAUDE.md +4 -4
  70. package/templates/claude/README.md +3 -1
  71. package/templates/claude/settings.autonomous.json +1 -1
  72. package/templates/claude/settings.autonomous.retrieval.json +9 -0
  73. package/templates/repo/README.md +2 -0
  74. package/templates/repo/gitignore +1 -0
  75. package/templates/repo/gitignore.retrieval +2 -0
  76. package/templates/repo/mcp.retrieval.json +11 -0
  77. package/templates/scripts/README.md +1 -1
  78. package/templates/scripts/autonomous-notify.sh +10 -4
  79. package/templates/scripts/autonomous-watcher.sh +336 -110
  80. package/templates/scripts/cleanup-merged-worktrees.sh +126 -8
  81. package/templates/scripts/docs-search-server.sh +64 -0
  82. package/templates/scripts/lib/harness-run-lib.sh +19 -3
  83. package/templates/scripts/restart-watcher.sh +4 -3
  84. package/templates/state-dir/business_parity_reviews/README.md +1 -1
  85. package/templates/state-dir/clarification_digests/README.md +1 -1
  86. package/templates/state-dir/clarifications/README.md +4 -4
@@ -37,10 +37,10 @@
37
37
  # checkout's own directory;
38
38
  # * a branch with an ACTIVE RUN — one whose record in the run registry
39
39
  # (`<state_dir>/autonomous_logs/registry.json`, shaped
40
- # `{"runs": {"<branch>": {"status": …}}}`) says `running`, `parked` or
41
- # `paused`. All three own a working copy the watcher will come back to: a
42
- # parked run is waiting for a clarification answer and a paused one for a
43
- # RESUME sentinel, and `worktree remove --force` would discard the pause
40
+ # `{"runs": {"<branch>": {"status": …}}}`) says `running`, `parked`,
41
+ # `park_loop` or `paused`. All four own a working copy the watcher will come
42
+ # back to: a parked run is waiting for a clarification answer, a `park_loop`
43
+ # one for an operator to clear it, and a paused one for a RESUME sentinel, and `worktree remove --force` would discard the pause
44
44
  # note the resumed engine is pointed at.
45
45
  #
46
46
  # CAVEAT: `: gone` + force-delete also catches an ABANDONED branch whose remote
@@ -89,6 +89,13 @@
89
89
  # Usage: cleanup-merged-worktrees.sh [--dry-run]
90
90
  # --dry-run report what would be removed; remove nothing
91
91
  #
92
+ # Environment:
93
+ # HARNESS_FETCH_TIMEOUT seconds to allow the `fetch --prune` before it is
94
+ # killed and the round skipped (default 60, clamped to
95
+ # 1..3600; anything else falls back to the default).
96
+ # The bound exists because this sweep runs inside the
97
+ # watcher's loop — see the paragraph on it below.
98
+ #
92
99
  # Exit map a caller can switch on:
93
100
  #
94
101
  # 0 the sweep ran, or refused and deleted nothing (the message says which)
@@ -120,7 +127,7 @@
120
127
  # printf '%s' '{"runs":{"feat_gone":{"status":"running"}}}' \
121
128
  # > "$d/sdlc-harness/autonomous_logs/registry.json"
122
129
  # -> the skip line for feat_gone; nothing deleted (same with
123
- # "parked" and "paused")
130
+ # "parked", "park_loop" and "paused")
124
131
  # unresolvable printf 'x' > "$d/harness.config.json"
125
132
  # -> one line, exit 0, nothing deleted
126
133
  # offline git -C "$d" remote set-url origin /nonexistent.git
@@ -206,17 +213,128 @@ if [ -e "$registry" ]; then
206
213
  # document without it is a registry this cannot enumerate, so `jq` fails and
207
214
  # the refusal below fires — which is the point: reading such a file as "no
208
215
  # active runs" would delete a branch a run is still working on.
209
- if ! active="$(jq -r '.runs | to_entries[] | select(.value.status == "running" or .value.status == "parked" or .value.status == "paused") | .key' "$registry" 2>/dev/null)"; then
216
+ if ! active="$(jq -r '.runs | to_entries[] | select(.value.status == "running" or .value.status == "parked" or .value.status == "park_loop" or .value.status == "paused") | .key' "$registry" 2>/dev/null)"; then
210
217
  echo "cleanup-merged-worktrees.sh: '$registry' could not be read as a run registry — refusing to delete anything"
211
218
  exit 0
212
219
  fi
213
220
  fi
214
221
 
222
+ # Run one command with a WALL-CLOCK CEILING, and report a timeout as 124 the way
223
+ # GNU `timeout` does. Written out here rather than taken from `timeout`, which is
224
+ # not on a stock macOS, nor from lib/harness-run-lib.sh, which an adopter's
225
+ # `scriptsDir` may be carrying at an older revision than this file — a sweep that
226
+ # dies on an unbound function is a worse failure than the hang it closes.
227
+ #
228
+ # The command runs in the BACKGROUND and is polled, because `wait` alone cannot be
229
+ # bounded in POSIX shell. On expiry the child gets a TERM, then a KILL two seconds
230
+ # later if it is still there, so it is asked before it is forced.
231
+ #
232
+ # THE SIGNAL GOES TO THE PROCESS GROUP, NOT THE CHILD — `kill -TERM -$pid`, with
233
+ # the negative pid — and `set -m` is what makes that possible, by giving the
234
+ # background job a process group of its own whose id is that pid. Signalling the
235
+ # child alone is not enough and was measured not to be: `git fetch` spawns the
236
+ # transport (`ssh`, `git-remote-https`) as its own child, and that grandchild
237
+ # INHERITS THIS SCRIPT'S STDOUT. Kill only git and the transport survives, holding
238
+ # the write end of the pipe the watcher is reading, so the watcher blocks on a
239
+ # sweep that already printed its verdict — the original hang, moved rather than
240
+ # closed. Against a fetch stalled for 3 seconds under a 3-second ceiling, the
241
+ # child-only version returned its message on time and did not let the script exit
242
+ # for 144 seconds; the group kill ends it in the 2 the escalation costs.
243
+ #
244
+ # Job control is restored to whatever it was, since a script that leaves `-m` on
245
+ # behind it changes how every later background command here reports.
246
+ #
247
+ # run_bounded <seconds> <command> [args…]
248
+ # NOT GROUPED WHEN A PERSON IS WATCHING. `set -m` puts the child in a process
249
+ # group that is not the terminal's foreground one, so a child that reads the
250
+ # CONTROLLING TERMINAL is stopped with SIGTTIN instead of being answered — and
251
+ # `ssh` reads a key passphrase from `/dev/tty` directly, which `GIT_TERMINAL_PROMPT=0`
252
+ # does not cover because it is not git asking. `kill -0` keeps succeeding on a
253
+ # stopped process, so a by-hand sweep on a passphrase-protected key would burn the
254
+ # whole ceiling and report a network failure that never happened. The header's
255
+ # "WHO RUNS IT" names a person by hand as a supported caller, so that path keeps
256
+ # the child in this script's own group and signals the child alone: the pipe a
257
+ # surviving transport could hold is a terminal the person is looking at, and they
258
+ # can end it themselves. The watcher is daemonised with no controlling terminal,
259
+ # so it takes the grouped path, which is the one the orphaned-transport hang
260
+ # needed.
261
+ run_bounded() {
262
+ local limit="$1" pid waited=0 job_control=0 grouped=1
263
+ shift
264
+ if { : > /dev/tty; } 2>/dev/null; then grouped=0; fi
265
+ case "$-" in *m*) job_control=1 ;; esac
266
+ [ "$grouped" -eq 1 ] && set -m
267
+ "$@" &
268
+ pid=$!
269
+ [ "$job_control" -eq 1 ] || set +m
270
+ while kill -0 "$pid" 2>/dev/null; do
271
+ if [ "$waited" -ge "$limit" ]; then
272
+ if [ "$grouped" -eq 1 ]; then
273
+ kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || :
274
+ sleep 2
275
+ # Only while the group is still there. Once it is gone the negative form
276
+ # fails, and retrying the bare pid would aim an unconditional SIGKILL at a
277
+ # number the kernel may already have handed to somebody else.
278
+ kill -0 "-$pid" 2>/dev/null && { kill -KILL "-$pid" 2>/dev/null || :; }
279
+ else
280
+ kill -TERM "$pid" 2>/dev/null || :
281
+ sleep 2
282
+ kill -0 "$pid" 2>/dev/null && { kill -KILL "$pid" 2>/dev/null || :; }
283
+ fi
284
+ wait "$pid" 2>/dev/null || :
285
+ return 124
286
+ fi
287
+ sleep 1
288
+ waited=$((waited + 1))
289
+ done
290
+ wait "$pid"
291
+ }
292
+
215
293
  # Refresh remote-tracking refs so a deleted upstream becomes `[gone]`. A failed
216
294
  # fetch (offline, or an unreachable remote) skips this round rather than sweeping
217
295
  # on stale tracking information, which would report every branch as gone.
218
- if ! git -C "$main_repo" fetch --prune --quiet 2>/dev/null; then
219
- echo "cleanup-merged-worktrees.sh: fetch --prune failed (offline?); skipping this round"
296
+ #
297
+ # BOUNDED, BECAUSE THIS SWEEP RUNS INSIDE THE WATCHER'S LOOP.
298
+ # `autonomous-watcher.sh` calls this script synchronously, so a fetch that HANGS
299
+ # rather than fails stalls the whole loop — no inbox pass, no drop picked up,
300
+ # nothing written to `watcher.log`, and the watcher still alive and healthy-looking
301
+ # in `ps`. The paragraph above anticipated a fetch that FAILS and skips the round;
302
+ # a stale TCP connection left behind by a network change or a laptop sleep never
303
+ # fails, and an unbounded `git fetch` sits in it indefinitely. The ceiling turns
304
+ # that second mode into the first, which this script already handles.
305
+ #
306
+ # `GIT_TERMINAL_PROMPT=0` closes the other hang of the same shape: a credential
307
+ # prompt, on a remote whose authentication has lapsed, blocks on a terminal that
308
+ # no unattended run is watching.
309
+ #
310
+ # SIXTY SECONDS, AND OVERRIDABLE. The default is generous next to a healthy fetch
311
+ # of a repository this size and short next to `CLEANUP_INTERVAL_SECS` (300), the
312
+ # interval the watcher runs this sweep on, so a hung remote costs a fraction of
313
+ # that round rather than the loop. It is NOT short next to `POLL_INTERVAL_SECS`
314
+ # (15): a sweep that spends the whole ceiling does hold the loop for it, which is
315
+ # the price of bounding the fetch at all and the reason the ceiling is not larger.
316
+ # `HARNESS_FETCH_TIMEOUT` raises it for an adopter on a link where 60 s is not
317
+ # generous, and lowers it for the test that drives this ceiling.
318
+ #
319
+ # VALIDATED BY VALUE, NOT BY SHAPE. An earlier form of this clause tested the
320
+ # string for non-digits, which let through two values the sentence below calls
321
+ # impossible. A 19-digit number is all digits, and `[ "$waited" -ge <huge> ]` does
322
+ # not compare false — it ERRORS, and a non-zero status reads as false, so the
323
+ # timeout branch never fires and the poll loop runs forever: unbounded, which is
324
+ # the one thing this knob must not express. `00` is all digits and is not the
325
+ # string `0`, and `[ 0 -ge 00 ]` is true on the first iteration, so every sweep
326
+ # kills its own fetch before it starts, blames the network, and never cleans up
327
+ # another branch. Comparing the VALUE closes both: `[ ]` returns non-zero for an
328
+ # unparseable or out-of-range string as readily as for a number that fails the
329
+ # test, and the default is what survives either way. The upper clamp is what keeps
330
+ # a fat-fingered value from expressing "wait forever" in seconds.
331
+ fetch_timeout="${HARNESS_FETCH_TIMEOUT:-60}"
332
+ if ! { [ "$fetch_timeout" -ge 1 ] && [ "$fetch_timeout" -le 3600 ]; } 2>/dev/null; then
333
+ fetch_timeout=60
334
+ fi
335
+
336
+ if ! GIT_TERMINAL_PROMPT=0 run_bounded "$fetch_timeout" git -C "$main_repo" fetch --prune --quiet 2>/dev/null; then
337
+ echo "cleanup-merged-worktrees.sh: fetch --prune failed (offline, unreachable or timed out); skipping this round"
220
338
  exit 0
221
339
  fi
222
340
 
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
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`.
5
+ #
6
+ # WHO RUNS IT. The agent runner, from the `harness-docs` entry `init` writes into
7
+ # `.mcp.json` when `docs.retrieval` is on — never a dispatched agent's Bash call,
8
+ # so its row in `cli/src/generators/outerLoopScripts.ts` is
9
+ # `agentInvocable: false` and the generated permission profile carries no entry
10
+ # for it.
11
+ #
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.
15
+ #
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.
22
+ #
23
+ # MIRRORS — a change to any owner below is an edit here too:
24
+ # `retrieval/runtime` and `node_modules/autonomous-sdlc-harness/dist/cli.js`
25
+ # mirror `RETRIEVAL_CACHE_DIRNAME`, `RETRIEVAL_RUNTIME_DIRNAME` and
26
+ # `RUNTIME_CLI_RELATIVE` in `cli/src/retrieval/runtime.ts`;
27
+ # `hr_cache_dir` mirrors `machineCacheDir()` in `cli/src/machine/paths.ts`.
28
+ #
29
+ # 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
35
+
36
+ set -euo pipefail
37
+
38
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
39
+ # shellcheck source=lib/harness-run-lib.sh
40
+ . "$script_dir/lib/harness-run-lib.sh"
41
+
42
+ # The agent runner starts this script, and a runner launched from a desktop session carries a
43
+ # minimal PATH. The library appends the usual locations without promoting any of them, which is the
44
+ # same bootstrap autonomous-watcher.sh runs for the same reason.
45
+ PATH="$(hr_path_with_fallbacks)"
46
+ export PATH
47
+
48
+ if ! root="$(hr_repo_root "$script_dir")"; then
49
+ echo "docs-search-server: $script_dir is not inside a git repository, so there is no checkout to serve" >&2
50
+ exit 1
51
+ fi
52
+
53
+ if ! cache_dir="$(hr_cache_dir)"; then
54
+ echo "docs-search-server: neither XDG_CACHE_HOME nor HOME is set, so the retrieval runtime cannot be located" >&2
55
+ exit 1
56
+ fi
57
+ entry="$cache_dir/retrieval/runtime/node_modules/autonomous-sdlc-harness/dist/cli.js"
58
+
59
+ if [ ! -f "$entry" ]; then
60
+ echo "docs-search-server: no retrieval runtime at $entry; run \`npx autonomous-sdlc-harness init\` in a repository with docs.retrieval on" >&2
61
+ exit 1
62
+ fi
63
+
64
+ exec node "$entry" docs serve --cwd "$root"
@@ -52,8 +52,8 @@
52
52
  # an `hr_lane_*` function still gets a library that only reads. The lane's
53
53
  # ceilings are the only environment values here that carry policy, because the
54
54
  # lane is machine-scoped and has no configuration key to carry them; each is
55
- # named where it is used. `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `HOME` and `PWD`
56
- # are also read, as location anchors only, and `PATH` is read by
55
+ # named where it is used. `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`,
56
+ # `HOME` and `PWD` are also read, as location anchors only, and `PATH` is read by
57
57
  # `hr_path_with_fallbacks` alone — as that function's input, which it prints back
58
58
  # transformed and never assigns.
59
59
  #
@@ -196,6 +196,8 @@
196
196
  # anchors hr_main_repo "$root"; hr_work_root "$root"
197
197
  # hr_worktree_dir "$root" feat/x; hr_repo_slug "$root"
198
198
  # hr_state_path "$root" autonomous_logs/registry.json
199
+ # machine dirs ( XDG_CACHE_HOME= hr_cache_dir ) -> $HOME/.cache/autonomous-sdlc-harness
200
+ # ( XDG_CACHE_HOME=/x/ hr_cache_dir ) -> /x/autonomous-sdlc-harness
199
201
  # the PATH policy run each in a SUBSHELL, so your own PATH is untouched:
200
202
  # ( PATH="$HOME/.rbenv/shims:/usr/bin:/bin"
201
203
  # hr_path_with_fallbacks )
@@ -966,6 +968,20 @@ hr_machine_config_dir() {
966
968
  printf '%s/autonomous-sdlc-harness\n' "${base%/}"
967
969
  }
968
970
 
971
+ # The machine-local cache directory, holding the shared docs-retrieval runtime.
972
+ # Mirrors `machineCacheDir()` in `cli/src/machine/paths.ts`: the variable when
973
+ # set and non-empty, else `$HOME/.cache`, one trailing slash stripped. Return 1
974
+ # when there is no home to anchor it to.
975
+ hr_cache_dir() {
976
+ local base="${XDG_CACHE_HOME-}"
977
+ [ -n "$base" ] || base="${HOME-}/.cache"
978
+ case "$base" in
979
+ /.cache) return 1 ;;
980
+ esac
981
+ [ -n "$base" ] || return 1
982
+ printf '%s/autonomous-sdlc-harness\n' "${base%/}"
983
+ }
984
+
969
985
  # The push-notification credential files, in RESOLUTION ORDER, one per line and
970
986
  # whether or not each exists — the caller sources the first that does:
971
987
  #
@@ -1068,7 +1084,7 @@ hr_push_env_files() {
1068
1084
  # THE THREE CEILINGS ARE THE ONLY ENVIRONMENT VALUES THAT CARRY POLICY HERE. The
1069
1085
  # file's other environment reads are location anchors, not policy:
1070
1086
  # `XDG_STATE_HOME` and `HOME` in `hr_lane_dir`, `XDG_CONFIG_HOME` and `HOME` in
1071
- # `hr_machine_config_dir`, `PWD` in `hr_repo_root` and `hr_main_repo`. The
1087
+ # `hr_machine_config_dir`, `XDG_CACHE_HOME` and `HOME` in `hr_cache_dir`, `PWD` in `hr_repo_root` and `hr_main_repo`. The
1072
1088
  # ceilings are machine-scoped policy with no configuration key:
1073
1089
  #
1074
1090
  # HR_LANE_STATE_MAX_AGE_SECS 21600 when a PUBLISHED RECORD THAT NAMED NO
@@ -52,7 +52,7 @@
52
52
  #
53
53
  # * the run registry (`<state_dir>/autonomous_logs/registry.json`, shaped
54
54
  # `{"runs": {"<branch>": {"status": …}}}`) is the SOURCE OF TRUTH: a record
55
- # whose status is `running` or `parked` is a run in flight;
55
+ # whose status is `running`, `parked` or `park_loop` is a run in flight;
56
56
  # * a process probe for the agent binary — `${HARNESS_AGENT_CLI:-claude}`, the
57
57
  # same variable the watcher launches through — is a BACKSTOP, for a run OF
58
58
  # THIS PROJECT whose record has not been written yet or was written by a
@@ -119,7 +119,8 @@
119
119
  #
120
120
  # in flight printf '%s' '{"runs":{"feat_x":{"status":"running"}}}' > "$reg"
121
121
  # run -> the in-flight report, exit 2, and no
122
- # "$w/calls" at all (same for "parked")
122
+ # "$w/calls" at all (same for "parked"
123
+ # and "park_loop")
123
124
  # forced run --force -> the same report plus the restarting line,
124
125
  # and "$w/calls" holds `daemon stop` then
125
126
  # `daemon install`, in that order
@@ -257,7 +258,7 @@ else
257
258
  # document that has no such wrapper is a registry this cannot enumerate, so
258
259
  # `jq` fails and the refusal below fires. Reading such a file as "no active
259
260
  # runs" is the one misreading that costs a run.
260
- elif ! active="$(jq -r '.runs | to_entries[] | select(.value.status == "running" or .value.status == "parked") | "\(.value.status) \(.key)"' "$registry" 2>/dev/null)"; then
261
+ elif ! active="$(jq -r '.runs | to_entries[] | select(.value.status == "running" or .value.status == "parked" or .value.status == "park_loop") | "\(.value.status) \(.key)"' "$registry" 2>/dev/null)"; then
261
262
  active=""
262
263
  unknown="'$registry' could not be read as a run registry (invalid JSON, or no .runs wrapper)"
263
264
  fi
@@ -6,4 +6,4 @@ The reviewer reads the story index's `## Context` plus every per-task file and j
6
6
 
7
7
  The directory exists only while `phases.parity` is `true` in `harness.config.json`: a project with no reference implementation to compare against has no parity gate and will not have it, and turning the phase on and re-running `init` creates it — an absent directory here is a configuration answer, not a missing artifact. While the phase is on, files appear one per failed gate and stop at convergence or at the fifth iteration, where the gate escalates with the latest path. Nothing supersedes an earlier file, and the whole directory is committed with the branch, since no ignore rule reaches it.
8
8
 
9
- The mistake worth naming is expecting a plan to pass here on plausible prose. This gate is graded on **citations**: every constant, call name, wire field and boundary comparison the plan describes has to trace to a source line in the reference implementation, and a behaviour the reference has that no per-task file covers is a Must Fix of the same grade as a wrong field. A plan that describes the right behaviour without saying where it came from is the ordinary way a round is lost here.
9
+ The mistake worth naming is expecting a plan to pass here on plausible prose. This gate is graded on **citations**: every constant, call name, wire field and boundary comparison the plan describes has to trace to an anchored source in the reference implementation (its path and symbol, never a line number in their place), and a behaviour the reference has that no per-task file covers is a Must Fix of the same grade as a wrong field. A plan that describes the right behaviour without saying where it came from is the ordinary way a round is lost here.
@@ -1,6 +1,6 @@
1
1
  # clarification_digests/
2
2
 
3
- One `<state_dir>/clarification_digests/<branch>.md` per branch, holding one block per clarification the runs on that branch parked on and had answered: what was asked, the operator's ruling in the words it was given, and whatever that ruling settles beyond the question that prompted it. One file per **branch**, appended to and never rewritten — each block is headed by the `question_<n>` index it was derived from, and a later round or a resumed session adds only the blocks whose key is not in it already.
3
+ One `<state_dir>/clarification_digests/<branch>.md` per branch, holding one block per park the runs on that branch stopped on and had answered — one per question file, however many questions it raised: what was asked, the operator's ruling in the words it was given, and whatever that ruling settles beyond the questions that prompted it. One file per **branch**, appended to and never rewritten — each block is headed by the `question_<n>` index of the question file it was derived from, and a later round or a resumed session adds only the blocks whose key is not in it already. Blocks written before a park held several questions — one per one-question file — are left exactly as written, and stay uniquely keyed because that index is never reused.
4
4
 
5
5
  Each block is written by the run's own orchestrator at the end of the run, from the question and answer files in `<state_dir>/clarifications/<branch>/` and its `answered/` archive. Those files are machine-local and go with the working copy — a working copy removed once its branch has merged takes every question and every answer with it, and **this file is the copy that survives**. Its readers are whoever comes to the branch afterwards: a reviewer asking why a decision was taken the way it was, and the human folding a ruling that outlives the branch into the rules the flow reads.
6
6
 
@@ -1,9 +1,9 @@
1
1
  # clarifications/
2
2
 
3
- The park-and-ask channel: one question per file at `<state_dir>/clarifications/<branch>/question_<n>.md`, with the answer it waits for beside it as `answer_<n>.md`. Pairing is by the index `<n>`, which is 1-based and unique across the branch's directory **including its `answered/` archive** — a second question takes the next index above the highest `question_<n>.md` in either location, never merely the next free name at the top level and never overwriting the first, so an archived pair does not free its index for reuse and `answer_3.md` answers `question_3.md` and nothing else. One `<branch>` subdirectory holds one branch's exchange and is created with that branch's first question.
3
+ The park-and-ask channel: one question file per park at `<state_dir>/clarifications/<branch>/question_<n>.md`, holding every question that park raises as its own `## Q<k> — <decision needed>` section with `k` counted from 1, and one answer file for the whole park beside it as `answer_<n>.md`. Pairing is by the index `<n>`, which is 1-based and unique across the branch's directory **including its `answered/` archive** — a second park takes the next index above the highest `question_<n>.md` in either location, never merely the next free name at the top level and never overwriting the first, so an archived pair does not free its index for reuse and `answer_3.md` answers `question_3.md` and nothing else. One `<branch>` subdirectory holds one branch's exchange and is created with that branch's first park.
4
4
 
5
- A question is written by the agent that reached a decision it must not take alone: writing the file and ending the session is what parks the run. The answer is written by the harness's answer command on the operator's behalf, verbatim and beside the question. The run daemon reads the pair and re-launches the same engine in the same working copy for the lowest answered index, and the re-entering run is what consumes the answer — the daemon never answers a question itself.
5
+ A question file is written by the agent that reached decisions it must not take alone: writing the file and ending the session is what parks the run. The answer is written by the harness's answer command on the operator's behalf, verbatim and beside the question file; it addresses each question by its `Q<k>` label, and a question it leaves unaddressed is asked again by the resumed run, in a new park file. The run daemon resumes the same engine in the same working copy only once every question file at the top level of the branch's directory has its answer, and the re-entering run is what consumes the answers — the daemon never answers a question itself. A park written before this layout, as several one-question files, is resumed by the same rule.
6
6
 
7
- The pair stays at the top level of the branch's directory while that resumed session runs, and moves into `<branch>/answered/` only once it has exited, so the answer is still where the resumed run looks for it and an already-consumed one is never read a second time. Everything the runs write here is machine-local and gitignored; this README is the only committed file in **this** directory. What survives the working copy is the record: at the end of each run the branch's resolved questions are digested into `<state_dir>/clarification_digests/<branch>.md`, one committed block per question, keyed by that same index — what was asked and the operator's ruling in the words it was given. Reusing an index would make two parks indistinguishable there, which is the other reason it is never reused. That sibling directory's own README states the format. The harness's watcher documentation, which ships with it, states the same protocol from the daemon's side.
7
+ The answered pairs stay at the top level of the branch's directory while that resumed session runs, and once it has exited exactly the pairs that resume consumed move into `<branch>/answered/`, so an answer is still where the resumed run looks for it and an already-consumed one is never read a second time. A pair written while the resumed run is going was not consumed by it and stays at the top level for the next resume. Everything the runs write here is machine-local and gitignored; this README is the only committed file in **this** directory. What survives the working copy is the record: at the end of each run the branch's resolved parks are digested into `<state_dir>/clarification_digests/<branch>.md`, one committed block per park, keyed by that same index — what was asked and the operator's ruling in the words it was given. Reusing an index would make two parks indistinguishable there, which is the other reason it is never reused. That sibling directory's own README states the format. The harness's watcher documentation, which ships with it, states the same protocol from the daemon's side.
8
8
 
9
- The mistake worth naming is tidying up by hand. Deleting a question, or archiving it into `answered/` before the run that parked on it has been resumed, does not unpark that run — an unanswered question is the sole signal that classifies a run as parked, so removing it strands the run instead, waiting on an answer nothing will now deliver.
9
+ The mistake worth naming is tidying up by hand. Deleting a question file, or archiving it into `answered/` before the run that parked on it has been resumed, does not unpark that run — a question file left at the top level is the sole signal that classifies a run as parked, so removing it strands the run instead, waiting on an answer nothing will now deliver.