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
@@ -3,7 +3,9 @@
3
3
  # the repository it is operating on, reads that repository's
4
4
  # `harness.config.json` at run time, answers "is this branch protected?",
5
5
  # routes an inbox filename to its engine and branch (`hr_inbox_route_var`),
6
- # derives a branch name from a title (`hr_derive_branch`), places a dropped
6
+ # derives a branch name from a title (`hr_derive_branch`), judges whether a
7
+ # path lies strictly inside the state directory's scratch directory
8
+ # (`hr_scratch_path_var`), places a dropped
7
9
  # artifact in a working copy and commits and pushes it, and derives the
8
10
  # anchors (main checkout, work root, worktree directory, repo slug,
9
11
  # state-dir paths) the scripts would otherwise each re-derive slightly
@@ -594,9 +596,10 @@ hr_config_load() {
594
596
  # which is what lets an explicitly EMPTY list read as a configured set rather
595
597
  # than as an absent one.
596
598
  #
597
- # A `phases.*` value that is not a boolean is emitted as `invalid`, so the
598
- # string `"true"` — which `tostring` would otherwise make indistinguishable
599
- # from `true` — reaches `hr_phase_enabled` as a value it refuses (2).
599
+ # A `phases.*` or `docs.retrieval` value that is not a boolean is emitted as
600
+ # `invalid`, so the string `"true"` — which `tostring` would otherwise make
601
+ # indistinguishable from `true` — reaches `hr_phase_enabled` or
602
+ # `hr_docs_retrieval_applies` as a value it refuses (2).
600
603
  out=$(jq -n -r '
601
604
  def s($k; $v):
602
605
  if $v == null then empty
@@ -631,6 +634,8 @@ hr_config_load() {
631
634
  s("phases.qa"; try (.phases.qa | if type == "boolean" or . == null then . else "invalid" end) catch null),
632
635
  s("phases.docs"; try (.phases.docs | if type == "boolean" or . == null then . else "invalid" end) catch null),
633
636
  s("execution.target"; try .execution.target catch null),
637
+ s("docs.retrievalBackend"; try .docs.retrievalBackend catch null),
638
+ s("docs.retrieval"; try (.docs.retrieval | if type == "boolean" or . == null then . else "invalid" end) catch null),
634
639
  s("forge"; try .forge catch null),
635
640
  s("protectedBranches.present";
636
641
  try (if (.protectedBranches | type) == "array" then "1" else null end) catch null),
@@ -918,6 +923,53 @@ hr_forge() {
918
923
  return 2
919
924
  }
920
925
 
926
+ # Whether the docs phase's retrieval step runs: `phases.docs` and
927
+ # `docs.retrieval` both `true`. The shell mirror of `cli/src/config/model.ts` →
928
+ # `retrievalApplies`: change the predicate there and here together. PRINTS
929
+ # NOTHING: the answer is the status, as `hr_phase_enabled`'s is. 0 = both
930
+ # `true`; 1 = either `false` or unset; 2 = the configuration is unresolvable or
931
+ # either value is not a boolean — refused rather than guessed about.
932
+ hr_docs_retrieval_applies() {
933
+ local root="${1-}" status
934
+ hr_phase_enabled "$root" docs
935
+ status=$?
936
+ [ "$status" -eq 0 ] || return "$status"
937
+ hr_cfg_scalar_var "docs.retrieval" || return 1
938
+ case "$HR_CFG_VALUE" in
939
+ true) return 0 ;;
940
+ false) return 1 ;;
941
+ esac
942
+ return 2
943
+ }
944
+
945
+ # `docs.retrievalBackend` — which runtime the docs-retrieval index uses:
946
+ # `typescript` or `python`. THE ONE READER OF THE KEY IN THIS FAMILY, and the
947
+ # shell mirror of `cli/src/config/model.ts` → `RETRIEVAL_BACKENDS` /
948
+ # `DEFAULT_RETRIEVAL_BACKEND`: change the enum there and here together. Schema
949
+ # default `typescript`, so an absent key prints `typescript` and 1 is never
950
+ # returned; a `docs` parent of the wrong type (`"docs": "x"`) reads as absent
951
+ # too, because `hr_config_load`'s `try … catch` nulls that key alone — refusing
952
+ # that shape is the schema's job. 2 — printing nothing — when the configuration
953
+ # is unresolvable or the value is outside the enum, a refusal rather than a
954
+ # guess. It does NOT consult `phases.docs` / `docs.retrieval`: the key is read
955
+ # only inside the gate, so a caller asks `hr_docs_retrieval_applies` first and
956
+ # calls this only on its status 0.
957
+ hr_docs_retrieval_backend() {
958
+ local root="${1-}"
959
+ hr_config_load "$root" || return 2
960
+ if ! hr_cfg_scalar_var "docs.retrievalBackend"; then
961
+ printf 'typescript\n'
962
+ return 0
963
+ fi
964
+ case "$HR_CFG_VALUE" in
965
+ typescript|python)
966
+ printf '%s\n' "$HR_CFG_VALUE"
967
+ return 0
968
+ ;;
969
+ esac
970
+ return 2
971
+ }
972
+
921
973
  # ---------------------------------------------------------------------------
922
974
  # Inbox routing — the one owner of the drop filename patterns.
923
975
  # ---------------------------------------------------------------------------
@@ -1140,6 +1192,92 @@ hr_state_path() {
1140
1192
  fi
1141
1193
  }
1142
1194
 
1195
+ # Judge whether <path> resolves STRICTLY INSIDE `<root>/<state_dir>/scratch/`.
1196
+ # Usage: hr_scratch_path_var <root> <path> [<base>]
1197
+ #
1198
+ # Two callers, differing only in what they do with an accepted path:
1199
+ # `scratch-run.sh` executes a file there and `remote-run.sh discard` removes a
1200
+ # directory there. So this reports a keyword in `HR_SCRATCH_WHY` and leaves every
1201
+ # message and exit code to its caller.
1202
+ #
1203
+ # The rule, stated once:
1204
+ # - `..` is refused ANYWHERE in the argument, not only as a whole segment —
1205
+ # strictly stronger, and free, because nothing in that directory depends on
1206
+ # a file's name. Then any character outside `A-Za-z0-9._/-` is refused. Both
1207
+ # tests run on <path> as the caller received it, before anything resolves.
1208
+ # - A relative <path> resolves against <base> when given, else <root>; an
1209
+ # absolute one is taken as given.
1210
+ # - Both sides of the comparison are resolved PHYSICALLY (`cd … && pwd -P`), so
1211
+ # a symlinked directory planted inside the scratch tree cannot widen the
1212
+ # fence. The target's parent must be the scratch directory or beneath it, and
1213
+ # a basename of `.` (the scratch directory itself) is refused.
1214
+ # - A target that is itself a symlink is refused rather than followed: its
1215
+ # destination is outside this judgement. The target need not exist.
1216
+ #
1217
+ # Clears the other four, then sets: `HR_SCRATCH_SUBDIR` (`scratch`, mirroring the `scratch` row
1218
+ # of `STATE_DIR_ENTRIES` in `cli/src/generators/stateDir.ts`), `HR_SCRATCH_DIR`
1219
+ # (physical), `HR_SCRATCH_PARENT` (physical), `HR_SCRATCH_TARGET`
1220
+ # (`<parent>/<basename>`, the path a caller acts on) and `HR_SCRATCH_WHY`.
1221
+ # Status / `HR_SCRATCH_WHY`:
1222
+ # 0 accepted (WHY empty)
1223
+ # 1 refused — `dotdot`, `charset`, `outside`, `itself`, `symlink`
1224
+ # 2 the target's parent does not resolve — `no-parent`
1225
+ # 3 the scratch directory cannot be located — `no-config`, `no-scratch`
1226
+ # 4 <root> or <path> is empty — `usage`
1227
+ hr_scratch_path_var() {
1228
+ local root="${1-}" path="${2-}" base="${3-}" scratch candidate name
1229
+ HR_SCRATCH_DIR=""
1230
+ HR_SCRATCH_PARENT=""
1231
+ HR_SCRATCH_TARGET=""
1232
+ HR_SCRATCH_WHY=""
1233
+ HR_SCRATCH_SUBDIR='scratch'
1234
+ if [ -z "$root" ] || [ -z "$path" ]; then
1235
+ HR_SCRATCH_WHY="usage"
1236
+ return 4
1237
+ fi
1238
+ case "$path" in
1239
+ *..*) HR_SCRATCH_WHY="dotdot"; return 1 ;;
1240
+ esac
1241
+ case "$path" in
1242
+ *[!A-Za-z0-9._/-]*) HR_SCRATCH_WHY="charset"; return 1 ;;
1243
+ esac
1244
+ scratch=$(hr_state_path "$root" "$HR_SCRATCH_SUBDIR")
1245
+ if [ "$?" -ne 0 ] || [ -z "$scratch" ]; then
1246
+ HR_SCRATCH_WHY="no-config"
1247
+ return 3
1248
+ fi
1249
+ HR_SCRATCH_DIR=$(cd "$scratch" 2>/dev/null && pwd -P)
1250
+ if [ -z "$HR_SCRATCH_DIR" ]; then
1251
+ HR_SCRATCH_WHY="no-scratch"
1252
+ return 3
1253
+ fi
1254
+ [ -n "$base" ] || base="$root"
1255
+ case "$path" in
1256
+ /*) candidate="$path" ;;
1257
+ *) candidate="${base%/}/$path" ;;
1258
+ esac
1259
+ HR_SCRATCH_PARENT=$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P)
1260
+ if [ -z "$HR_SCRATCH_PARENT" ]; then
1261
+ HR_SCRATCH_WHY="no-parent"
1262
+ return 2
1263
+ fi
1264
+ case "$HR_SCRATCH_PARENT" in
1265
+ "$HR_SCRATCH_DIR" | "$HR_SCRATCH_DIR"/*) ;;
1266
+ *) HR_SCRATCH_WHY="outside"; return 1 ;;
1267
+ esac
1268
+ name=$(basename "$candidate")
1269
+ if [ "$name" = "." ]; then
1270
+ HR_SCRATCH_WHY="itself"
1271
+ return 1
1272
+ fi
1273
+ HR_SCRATCH_TARGET="$HR_SCRATCH_PARENT/$name"
1274
+ if [ -L "$HR_SCRATCH_TARGET" ]; then
1275
+ HR_SCRATCH_WHY="symlink"
1276
+ return 1
1277
+ fi
1278
+ return 0
1279
+ }
1280
+
1143
1281
  # The machine-local settings directory — one place an operator keeps values that
1144
1282
  # vary per machine rather than per repository, which is why it is not a config
1145
1283
  # key and not a repository file. Return 1 when there is no home to anchor it to.
@@ -1442,8 +1580,10 @@ hr_remote_record_init() {
1442
1580
  # limit and readable in the Actions run list. GitHub documents no ref limit.
1443
1581
  #
1444
1582
  # THIS SECTION ONLY READS. It fetches nothing and creates no branch or file; a
1445
- # caller that wants `origin` fresh fetches first. A registry is read only when
1446
- # it already exists, because `hr_registry_get` creates an absent one.
1583
+ # caller that wants `origin` fresh fetches first. Given a <gh_cli>, it also asks
1584
+ # GitHub one read per candidate — the run workflow's runs under that name — and
1585
+ # still writes nothing. A registry is read only when it already exists, because
1586
+ # `hr_registry_get` creates an absent one.
1447
1587
  # ---------------------------------------------------------------------------
1448
1588
 
1449
1589
  # The two limits the rule above names.
@@ -1525,11 +1665,37 @@ EOF
1525
1665
  return 0
1526
1666
  }
1527
1667
 
1528
- # Judge <name> against the listings `hr_branch_taken_lists_var` last read. The
1529
- # answers and `HR_TAKEN_WHY` are `hr_branch_name_taken`'s.
1668
+ # hr_branch_run_history <root> <gh_cli> <name> — 0 when the run workflow
1669
+ # (`HR_REMOTE_WORKFLOW_RUN_FILE`) lists a run under <name>, 1 when it lists
1670
+ # none, 2 when <gh_cli> cannot run, exits non-zero, or answers anything but a
1671
+ # JSON array. Sets no `HR_` variable: the names are assigned inside the
1672
+ # subshell that `cd`s to <root>.
1673
+ #
1674
+ # GitHub keeps a deleted branch's runs listed under its name, and a later run
1675
+ # of that name inherits them: Gate 12 round 5 found run `36569531374` of the
1676
+ # deleted `feat_invoices` (`docs/development.md` → Gate 12 → Round 5, finding
1677
+ # 1). One bounded read per candidate, not one listing of every branch, because
1678
+ # a bounded all-branch listing could miss an old name. Bash 3.2 and jq 1.5.
1679
+ hr_branch_run_history() {
1680
+ local root="${1-}" gh_cli="${2-}" name="${3-}" answer verdict
1681
+ [ -n "$root" ] && [ -n "$gh_cli" ] && [ -n "$name" ] || return 2
1682
+ answer=$(cd "$root" 2>/dev/null && hr_remote_names_var &&
1683
+ "$gh_cli" run list --workflow "$HR_REMOTE_WORKFLOW_RUN_FILE" --branch "$name" --limit 1 --json databaseId 2>/dev/null) || return 2
1684
+ verdict=$(printf '%s' "$answer" | jq -r 'if type == "array" then (if length > 0 then "history" else "none" end) else "other" end' 2>/dev/null) || return 2
1685
+ case "$verdict" in
1686
+ history) return 0 ;;
1687
+ none) return 1 ;;
1688
+ esac
1689
+ return 2
1690
+ }
1691
+
1692
+ # Judge <name> against the listings `hr_branch_taken_lists_var` last read, then,
1693
+ # given a <gh_cli>, against the run workflow's history — last, so the free local
1694
+ # checks answer first. The answers and `HR_TAKEN_WHY` are
1695
+ # `hr_branch_name_taken`'s.
1530
1696
  hr_branch_taken_judge() {
1531
1697
  local LC_ALL=C
1532
- local root="${1-}" name="${2-}" registry="${3-}" lower status nl
1698
+ local root="${1-}" name="${2-}" registry="${3-}" gh_cli="${4-}" lower status nl
1533
1699
  nl='
1534
1700
  '
1535
1701
  HR_TAKEN_WHY=""
@@ -1557,18 +1723,29 @@ hr_branch_taken_judge() {
1557
1723
  HR_TAKEN_WHY="a run registry record"
1558
1724
  return 0
1559
1725
  fi
1726
+ if [ -n "$gh_cli" ]; then
1727
+ status=0
1728
+ hr_branch_run_history "$root" "$gh_cli" "$name" || status=$?
1729
+ case "$status" in
1730
+ 0) HR_TAKEN_WHY="a run of the run workflow listed under that name"; return 0 ;;
1731
+ 1) ;;
1732
+ *) HR_TAKEN_WHY="the run history of $name could not be listed"; return 2 ;;
1733
+ esac
1734
+ fi
1560
1735
  return 1
1561
1736
  }
1562
1737
 
1563
- # hr_branch_name_taken <root> <name> [<registry>] — 0 taken, 1 free, 2 cannot
1564
- # tell. Sets `HR_TAKEN_WHY` to a short phrase naming the collision or the
1565
- # failure. Taken: a protected name; a branch on `origin`, compared
1566
- # case-insensitively; a local branch; under `<state_dir>` on
1567
- # `origin/<defaultBranch>`, a directory named <name> or a file
1568
- # `<name>_task_prompt.md`, `<name>_story_plan.md` or `<name>_docs.md` — which a
1569
- # merged and deleted branch still leaves; a record in an existing <registry>.
1738
+ # hr_branch_name_taken <root> <name> [<registry>] [<gh_cli>] — 0 taken, 1 free,
1739
+ # 2 cannot tell. Sets `HR_TAKEN_WHY` to a short phrase naming the collision or
1740
+ # the failure. A caller with no registry passes `""` before <gh_cli>. Taken: a
1741
+ # protected name; a branch on `origin`, compared case-insensitively; a local
1742
+ # branch; under `<state_dir>` on `origin/<defaultBranch>`, a directory named
1743
+ # <name> or a file `<name>_task_prompt.md`, `<name>_story_plan.md` or
1744
+ # `<name>_docs.md` — which a merged and deleted branch still leaves; a record in
1745
+ # an existing <registry>; given a <gh_cli>, any run of the run workflow listed
1746
+ # under <name> (`hr_branch_run_history`), whose listing failing is a 2.
1570
1747
  hr_branch_name_taken() {
1571
- local root="${1-}" name="${2-}" registry="${3-}"
1748
+ local root="${1-}" name="${2-}" registry="${3-}" gh_cli="${4-}"
1572
1749
  HR_TAKEN_WHY=""
1573
1750
  if [ -z "$root" ] || [ -z "$name" ]; then
1574
1751
  HR_TAKEN_WHY="no branch name to judge"
@@ -1576,20 +1753,21 @@ hr_branch_name_taken() {
1576
1753
  fi
1577
1754
  hr_config_load "$root" || :
1578
1755
  hr_branch_taken_lists_var "$root" || return 2
1579
- hr_branch_taken_judge "$root" "$name" "$registry"
1756
+ hr_branch_taken_judge "$root" "$name" "$registry" "$gh_cli"
1580
1757
  }
1581
1758
 
1582
- # hr_derive_branch <root> <text> <fallback> [<registry>] — print the derived name
1583
- # and return 0; return 2, printing nothing, when a `taken` judgement could not
1584
- # tell or no base routes back to itself; return 3, printing nothing, when every
1585
- # suffix through `HR_BRANCH_SUFFIX_MAX` is taken. Never a guessed name.
1759
+ # hr_derive_branch <root> <text> <fallback> [<registry>] [<gh_cli>] — print the
1760
+ # derived name and return 0; return 2, printing nothing, when a `taken`
1761
+ # judgement could not tell or no base routes back to itself; return 3, printing
1762
+ # nothing, when every suffix through `HR_BRANCH_SUFFIX_MAX` is taken. Never a
1763
+ # guessed name. <gh_cli> reaches every judgement as `hr_branch_name_taken`'s.
1586
1764
  #
1587
1765
  # THE BASE MUST ROUTE BACK TO ITSELF: `hr_inbox_route_var` on each drop filename
1588
1766
  # a run of that name produces has to give the base as its branch, so no derived
1589
1767
  # name makes the inbox patterns ambiguous. A base that does not is replaced by
1590
1768
  # <fallback> once.
1591
1769
  hr_derive_branch() {
1592
- local root="${1-}" text="${2-}" fallback="${3-}" registry="${4-}"
1770
+ local root="${1-}" text="${2-}" fallback="${3-}" registry="${4-}" gh_cli="${5-}"
1593
1771
  local base="" candidate suffix routes n status
1594
1772
  hr_branch_limits_var
1595
1773
  candidate=$(hr_branch_slug "$text") || candidate="$fallback"
@@ -1615,7 +1793,7 @@ hr_derive_branch() {
1615
1793
  n=1
1616
1794
  while :; do
1617
1795
  status=0
1618
- hr_branch_taken_judge "$root" "$candidate" "$registry" || status=$?
1796
+ hr_branch_taken_judge "$root" "$candidate" "$registry" "$gh_cli" || status=$?
1619
1797
  case "$status" in
1620
1798
  1) printf '%s\n' "$candidate"; return 0 ;;
1621
1799
  2) return 2 ;;
@@ -1704,8 +1882,9 @@ hr_push_landed() {
1704
1882
  # names. Every name below is a variable `hr_remote_names_var` assigns, and it
1705
1883
  # also assigns `HR_REMOTE_WORKFLOW_RUN_FILE`, the run workflow's file name, for
1706
1884
  # the GitHub-route producers `hr_github_answer_route` and
1707
- # `hr_github_resume_route`. Those two are mirrors of the header's table; no
1708
- # function spells either one, or any other name here.
1885
+ # `hr_github_resume_route` and for the run-history probe `hr_branch_run_history`.
1886
+ # Those two are mirrors of the header's table; no function spells either one,
1887
+ # or any other name here.
1709
1888
  #
1710
1889
  # <bundle>/status.json the job's record, fixed schema below
1711
1890
  # <bundle>/clarifications/<branch>/ the whole branch directory, answered/ included