autonomous-sdlc-harness 0.1.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 (171) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +24 -0
  4. package/dist/cli.js +194 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/config.js +561 -0
  7. package/dist/commands/config.js.map +1 -0
  8. package/dist/commands/daemon.js +791 -0
  9. package/dist/commands/daemon.js.map +1 -0
  10. package/dist/commands/doctor.js +336 -0
  11. package/dist/commands/doctor.js.map +1 -0
  12. package/dist/commands/init.js +2023 -0
  13. package/dist/commands/init.js.map +1 -0
  14. package/dist/commands/registry.js +42 -0
  15. package/dist/commands/registry.js.map +1 -0
  16. package/dist/config/check.js +505 -0
  17. package/dist/config/check.js.map +1 -0
  18. package/dist/config/io.js +177 -0
  19. package/dist/config/io.js.map +1 -0
  20. package/dist/config/model.js +406 -0
  21. package/dist/config/model.js.map +1 -0
  22. package/dist/core/errors.js +71 -0
  23. package/dist/core/errors.js.map +1 -0
  24. package/dist/core/git.js +537 -0
  25. package/dist/core/git.js.map +1 -0
  26. package/dist/core/json.js +125 -0
  27. package/dist/core/json.js.map +1 -0
  28. package/dist/core/layerCoverage.js +141 -0
  29. package/dist/core/layerCoverage.js.map +1 -0
  30. package/dist/core/layerGapRemedy.js +62 -0
  31. package/dist/core/layerGapRemedy.js.map +1 -0
  32. package/dist/core/nameList.js +23 -0
  33. package/dist/core/nameList.js.map +1 -0
  34. package/dist/core/paths.js +153 -0
  35. package/dist/core/paths.js.map +1 -0
  36. package/dist/core/prompt.js +206 -0
  37. package/dist/core/prompt.js.map +1 -0
  38. package/dist/core/repoPaths.js +55 -0
  39. package/dist/core/repoPaths.js.map +1 -0
  40. package/dist/core/report.js +150 -0
  41. package/dist/core/report.js.map +1 -0
  42. package/dist/core/templating.js +88 -0
  43. package/dist/core/templating.js.map +1 -0
  44. package/dist/core/writer.js +479 -0
  45. package/dist/core/writer.js.map +1 -0
  46. package/dist/daemon/backend.js +180 -0
  47. package/dist/daemon/backend.js.map +1 -0
  48. package/dist/daemon/units.js +380 -0
  49. package/dist/daemon/units.js.map +1 -0
  50. package/dist/detect/nestedApplication.js +79 -0
  51. package/dist/detect/nestedApplication.js.map +1 -0
  52. package/dist/detect/presets.js +2033 -0
  53. package/dist/detect/presets.js.map +1 -0
  54. package/dist/detect/signals.js +1368 -0
  55. package/dist/detect/signals.js.map +1 -0
  56. package/dist/doctor/checks.js +3530 -0
  57. package/dist/doctor/checks.js.map +1 -0
  58. package/dist/generators/claudeContext.js +588 -0
  59. package/dist/generators/claudeContext.js.map +1 -0
  60. package/dist/generators/githooks.js +446 -0
  61. package/dist/generators/githooks.js.map +1 -0
  62. package/dist/generators/harnessConfig.js +632 -0
  63. package/dist/generators/harnessConfig.js.map +1 -0
  64. package/dist/generators/notifications.js +191 -0
  65. package/dist/generators/notifications.js.map +1 -0
  66. package/dist/generators/outerLoopScripts.js +165 -0
  67. package/dist/generators/outerLoopScripts.js.map +1 -0
  68. package/dist/generators/permissionProfile.js +1172 -0
  69. package/dist/generators/permissionProfile.js.map +1 -0
  70. package/dist/generators/projectSettings.js +322 -0
  71. package/dist/generators/projectSettings.js.map +1 -0
  72. package/dist/generators/repoRoot.js +417 -0
  73. package/dist/generators/repoRoot.js.map +1 -0
  74. package/dist/generators/scripts.js +557 -0
  75. package/dist/generators/scripts.js.map +1 -0
  76. package/dist/generators/stateDir.js +221 -0
  77. package/dist/generators/stateDir.js.map +1 -0
  78. package/dist/machine/paths.js +111 -0
  79. package/dist/machine/paths.js.map +1 -0
  80. package/dist/machine/plugins.js +224 -0
  81. package/dist/machine/plugins.js.map +1 -0
  82. package/dist/machine/registry.js +330 -0
  83. package/dist/machine/registry.js.map +1 -0
  84. package/package.json +23 -0
  85. package/scripts/README.md +13 -0
  86. package/scripts/daemon/launchd.plist.template +59 -0
  87. package/scripts/daemon/systemd.service.template +58 -0
  88. package/templates/README.md +15 -0
  89. package/templates/claude/CLAUDE.md +54 -0
  90. package/templates/claude/README.md +5 -0
  91. package/templates/claude/context/api.md +29 -0
  92. package/templates/claude/context/conventions.md +23 -0
  93. package/templates/claude/context/data-layer.md +28 -0
  94. package/templates/claude/context/data-storage.md +29 -0
  95. package/templates/claude/context/docs-catalog.md +29 -0
  96. package/templates/claude/context/domain.md +28 -0
  97. package/templates/claude/context/layer.md +20 -0
  98. package/templates/claude/context/module.md +30 -0
  99. package/templates/claude/context/package.md +29 -0
  100. package/templates/claude/context/presentation.md +32 -0
  101. package/templates/claude/context/state-slices.md +28 -0
  102. package/templates/claude/context/tests.md +28 -0
  103. package/templates/claude/harness-task-offer.md +58 -0
  104. package/templates/claude/push-notify.env.example +21 -0
  105. package/templates/claude/qa-accounts.env.example +38 -0
  106. package/templates/claude/qa_test_scenarios.md +110 -0
  107. package/templates/claude/settings.autonomous.json +93 -0
  108. package/templates/claude/settings.autonomous.qa.json +36 -0
  109. package/templates/githooks/README.md +3 -0
  110. package/templates/githooks/pre-push +72 -0
  111. package/templates/repo/README.md +3 -0
  112. package/templates/repo/gitattributes +16 -0
  113. package/templates/repo/gitignore +61 -0
  114. package/templates/repo/gitignore.qa +25 -0
  115. package/templates/repo/mcp.json +17 -0
  116. package/templates/scripts/README.md +5 -0
  117. package/templates/scripts/autonomous-format-stream.sh +95 -0
  118. package/templates/scripts/autonomous-notify.sh +337 -0
  119. package/templates/scripts/autonomous-watcher.sh +3087 -0
  120. package/templates/scripts/cleanup-merged-worktrees.sh +327 -0
  121. package/templates/scripts/commit-on-branch.sh +288 -0
  122. package/templates/scripts/create-worktree.sh +360 -0
  123. package/templates/scripts/deploy.sh +47 -0
  124. package/templates/scripts/lib/harness-run-lib.sh +1481 -0
  125. package/templates/scripts/push-branch.sh +140 -0
  126. package/templates/scripts/refresh-branch.sh +244 -0
  127. package/templates/scripts/restart-watcher.sh +401 -0
  128. package/templates/scripts/scratch-run.sh +302 -0
  129. package/templates/scripts/setup-worktree.sh +262 -0
  130. package/templates/scripts/start-dev-server.sh +99 -0
  131. package/templates/scripts/test.sh +50 -0
  132. package/templates/scripts/typecheck.sh +50 -0
  133. package/templates/state-dir/README-root.md +13 -0
  134. package/templates/state-dir/README.md +9 -0
  135. package/templates/state-dir/architecture_branch_review_point_reviews/README.md +9 -0
  136. package/templates/state-dir/architecture_branch_reviews/README.md +9 -0
  137. package/templates/state-dir/architecture_reviews/README.md +9 -0
  138. package/templates/state-dir/architecture_user_review_reviews/README.md +9 -0
  139. package/templates/state-dir/autonomous_inbox/README.md +9 -0
  140. package/templates/state-dir/autonomous_logs/README.md +9 -0
  141. package/templates/state-dir/branch_statistics/README.md +9 -0
  142. package/templates/state-dir/business_parity_branch_review_point_reviews/README.md +9 -0
  143. package/templates/state-dir/business_parity_branch_reviews/README.md +9 -0
  144. package/templates/state-dir/business_parity_reviews/README.md +9 -0
  145. package/templates/state-dir/business_parity_user_review_reviews/README.md +9 -0
  146. package/templates/state-dir/clarification_digests/README.md +9 -0
  147. package/templates/state-dir/clarifications/README.md +9 -0
  148. package/templates/state-dir/code_reviews/README.md +9 -0
  149. package/templates/state-dir/dispatch_additions/README.md +19 -0
  150. package/templates/state-dir/docs_catalog/README.md +9 -0
  151. package/templates/state-dir/flow_progress/README.md +9 -0
  152. package/templates/state-dir/improvement_observations/README.md +19 -0
  153. package/templates/state-dir/improvement_suggestions.md +29 -0
  154. package/templates/state-dir/lessons.md +23 -0
  155. package/templates/state-dir/qa_review_point_reviews/README.md +9 -0
  156. package/templates/state-dir/qa_reviews/README.md +9 -0
  157. package/templates/state-dir/review_plan_point_reviews/README.md +9 -0
  158. package/templates/state-dir/review_plan_reviews/README.md +9 -0
  159. package/templates/state-dir/scratch/README.md +11 -0
  160. package/templates/state-dir/skeptic_review_plan_reviews/README.md +9 -0
  161. package/templates/state-dir/skeptic_review_point_reviews/README.md +9 -0
  162. package/templates/state-dir/skeptic_reviews/README.md +9 -0
  163. package/templates/state-dir/story_plans/README.md +9 -0
  164. package/templates/state-dir/task_plan_point_reviews/README.md +9 -0
  165. package/templates/state-dir/task_plan_reviews/README.md +9 -0
  166. package/templates/state-dir/task_plans/README.md +9 -0
  167. package/templates/state-dir/task_prompts/README.md +9 -0
  168. package/templates/state-dir/ui_test_plan_reviews/README.md +9 -0
  169. package/templates/state-dir/ui_test_plans/README.md +9 -0
  170. package/templates/state-dir/user_review_fix_plan_point_reviews/README.md +9 -0
  171. package/templates/state-dir/user_reviews/README.md +9 -0
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bash
2
+ # push-branch.sh — push the current non-protected branch to its upstream, and
3
+ # never abort the run that called it.
4
+ #
5
+ # WHAT IT IS FOR. This is the single mechanism every unattended commit point
6
+ # calls right after a commit lands, so the remote branch reflects the local
7
+ # commit chain no matter where the run stops — the watcher after its task-prompt
8
+ # commit, the `committer` agent's opt-in push, and the task, user-review-fix and
9
+ # planning orchestrators. It is invoked as a SEPARATE statement from the commit
10
+ # (never `if commit-on-branch.sh …; then push-branch.sh; fi`: that compound is
11
+ # not covered by the guards and stalls an unattended run), which works because a
12
+ # push with nothing new to send is a harmless no-op — so callers may run it
13
+ # unconditionally, including after a commit wrapper that made no commit.
14
+ #
15
+ # WHY A WRAPPER EXISTS AT ALL. Same reason as its sibling: an invocation of a
16
+ # script path under the configured scripts directory is allow-listed literally
17
+ # by the generated permission profile and auto-allowed by the script-allowlist
18
+ # guard (when its basename is not on that guard's deny list, in a command
19
+ # carrying none of `$(…)`, a backtick, `|`, `<`, a braced form other than a bare
20
+ # `${IDENT}`, or a `>` that is neither a descriptor duplication (`2>&1`, `>&2`,
21
+ # `2>&-`) nor a redirection to the literal `/dev/null`), where a `git push`
22
+ # command line assembled with a shell expansion is matched statically by
23
+ # neither and draws a prompt an unattended run cannot answer.
24
+ #
25
+ # EVERY FAILURE PATH IS NON-FATAL (exit 0). A non-git directory, a detached
26
+ # HEAD, a protected branch, a configuration that cannot be resolved, or a failed
27
+ # push (a transient network, say) is made VISIBLE with a one-line message but
28
+ # must NEVER abort the calling run — a stale remote is recoverable and a halted
29
+ # run is not. That is why this script uses `set -uo pipefail` deliberately
30
+ # WITHOUT `set -e`, and why even the fail-closed arm below exits 0: it refuses
31
+ # to push, which is the closed outcome here, and says so.
32
+ #
33
+ # DEFENCE IN DEPTH, NOT THE SOLE GUARD. The caller-agnostic backstop is the
34
+ # committed pre-push hook `init` writes into the configured `githooksDir`, which
35
+ # git runs on every push GIT PERFORMS, however it was started. That backstop is
36
+ # caller-agnostic WITHIN GIT: a tool that implements push against the git backend
37
+ # itself, as `jj git push` does, performs no git push and runs no git hook. The
38
+ # plugin's protected-branch guard refuses an agent-submitted push only when it
39
+ # can PROVE the target is protected, and is silent on the rest — and it is the
40
+ # layer that sees a `jj git push` and refuses one that NAMES a protected target,
41
+ # so the two cover different callers. Neither covers an argument-less
42
+ # `jj git push`, which runs no git hook and gives that guard nothing to prove;
43
+ # only a forge-side ruleset does. This
44
+ # script refuses too, so it is safe when called from outside an unattended
45
+ # profile — and it takes its protected set from
46
+ # `<repo_root>/harness.config.json` at run time through
47
+ # `lib/harness-run-lib.sh`, which is where that set is kept in step across all
48
+ # of them. There is no second list to keep in sync with: a branch name written
49
+ # into this file would enforce the wrong set the moment `protectedBranches`
50
+ # changed, and nothing at all in a repository whose integration branch is named
51
+ # something else.
52
+ #
53
+ # WHAT IT NEVER DOES. It performs only a fast-forward push of already-committed
54
+ # work: no `--force`, no `--force-with-lease`, no history-rewriting flag, no
55
+ # commit of its own, and no non-zero exit.
56
+ #
57
+ # Usage: push-branch.sh [<repo-dir>]
58
+ # <repo-dir> worktree/repository to operate in (default: $PWD)
59
+ #
60
+ # REPRO — reproduce any decision by hand, against a throwaway fixture:
61
+ #
62
+ # b=$(mktemp -d)/origin.git; git init -q --bare "$b"
63
+ # d=$(mktemp -d); git -C "$d" init -q -b feat_x; git -C "$d" remote add origin "$b"
64
+ # printf '%s' '{"version":1,"defaultBranch":"trunk","protectedBranches":["trunk","release/*"],"stateDir":"sdlc-harness/","layers":[],"commands":{}}' > "$d/harness.config.json"
65
+ # git -C "$d" add -A && git -C "$d" commit -qm "seed"
66
+ #
67
+ # no upstream yet push-branch.sh "$d" -> exit 0; `git -C "$b" branch` lists
68
+ # feat_x and the branch now has an upstream
69
+ # nothing new push-branch.sh "$d" -> exit 0 ("everything up to date")
70
+ # protected branch git -C "$d" checkout -q -b trunk; push-branch.sh "$d"
71
+ # -> exit 0, refusal message, "$b" unchanged
72
+ # unresolvable printf 'x' > "$d/harness.config.json"; push-branch.sh "$d"
73
+ # -> exit 0, refusal message, nothing pushed
74
+ # push fails git -C "$d" remote set-url origin /nonexistent.git
75
+ # -> exit 0 with a visible failure line
76
+
77
+ set -uo pipefail
78
+
79
+ # The library is reached by a path computed from this script's own location — no
80
+ # session root, and no runtime-substituted token, is assumed. Not finding it is
81
+ # non-fatal like everything else here.
82
+ hr_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/harness-run-lib.sh"
83
+ if [ ! -r "$hr_lib" ]; then
84
+ echo "push-branch.sh: cannot read '$hr_lib' — refusing to push"
85
+ exit 0
86
+ fi
87
+ # shellcheck source=lib/harness-run-lib.sh
88
+ . "$hr_lib"
89
+
90
+ repo_dir="${1:-$PWD}"
91
+
92
+ # Resolve the repository top. Not a git repository => warn and skip (non-fatal).
93
+ top="$(hr_repo_root "$repo_dir")"
94
+ if [ -z "$top" ]; then
95
+ echo "push-branch.sh: '$repo_dir' is not a git repository — nothing to push"
96
+ exit 0
97
+ fi
98
+
99
+ # Current branch. Empty => detached HEAD => refuse (non-fatal skip).
100
+ branch="$(hr_current_branch "$top")"
101
+ if [ -z "$branch" ]; then
102
+ echo "push-branch.sh: detached HEAD — refusing to push"
103
+ exit 0
104
+ fi
105
+
106
+ # The protected-branch decision, from this repository's configuration. Called
107
+ # unsubstituted so the library's per-process cache is warmed here.
108
+ hr_branch_is_protected "$top" "$branch"
109
+ protected=$?
110
+ case "$protected" in
111
+ 0)
112
+ echo "push-branch.sh: branch '$branch' is protected — refusing to push"
113
+ exit 0
114
+ ;;
115
+ 2)
116
+ # Fail closed, and stay non-fatal: a configuration that cannot be read is a
117
+ # branch that cannot be judged, so the push does not happen — but the run
118
+ # that called this is not stopped over it.
119
+ echo "push-branch.sh: cannot resolve '$top/harness.config.json' — refusing to push"
120
+ exit 0
121
+ ;;
122
+ esac
123
+
124
+ # Push the branch. Use the existing upstream when one is configured; otherwise
125
+ # set it on the fly, so a branch with no upstream is still pushed. Fast-forward
126
+ # only.
127
+ if git -C "$top" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
128
+ git -C "$top" push
129
+ else
130
+ git -C "$top" push --set-upstream origin "$branch"
131
+ fi
132
+ push_status=$?
133
+
134
+ if [ "$push_status" -eq 0 ]; then
135
+ echo "push-branch.sh: pushed $branch to origin"
136
+ else
137
+ # Visible but non-blocking: a push problem must never abort the calling run.
138
+ echo "push-branch.sh: push failed for $branch (see output above)"
139
+ fi
140
+ exit 0
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env bash
2
+ # refresh-branch.sh — merge the configured default branch INTO the branch this
3
+ # checkout has out, so a run whose base went stale can catch up. One direction
4
+ # only: into the working branch, never into the integration line.
5
+ #
6
+ # WHY IT EXISTS. The plugin's protected-branch guard denies a `git merge` or
7
+ # `git rebase` whose target argument names a protected branch, and the generated
8
+ # permission profile puts `Bash(git merge:*)` on `ask` — which an unattended run
9
+ # cannot answer. Both fire on the SAFE direction too, because the safe direction
10
+ # still spells the protected branch as its argument. So a run cut from a stale
11
+ # base had no permitted way to refresh it, and the only route left was evasion.
12
+ # This wrapper is the sanctioned route: `bash <scripts_dir>/refresh-branch.sh` is
13
+ # allow-listed in three literal forms by the generated permission profile (its
14
+ # row in `cli/src/generators/outerLoopScripts.ts` carries `agentInvocable: true`)
15
+ # and auto-allowed by the script-allowlist guard, and it carries no `merge` token
16
+ # on the caller's command line for the protected-branch guard to judge.
17
+ #
18
+ # IT IS NOT A WAY AROUND THAT GUARD — IT RE-IMPLEMENTS ITS INTENT. The guard's
19
+ # dangerous case is a merge or rebase issued while HEAD is ON a protected branch,
20
+ # which would put unreviewed work on the integration line. This script makes that
21
+ # decision itself, from `<repo_root>/harness.config.json` at run time through
22
+ # `lib/harness-run-lib.sh` rather than from a branch name written into this file,
23
+ # and refuses (exit 2) before anything is fetched or merged. The library's
24
+ # unresolvable answer is a refusal here too, never a permit.
25
+ #
26
+ # WHAT IT NEVER DOES. It never pushes. It never checks out, resets, updates or
27
+ # deletes the base branch — the only ref it writes is the remote-tracking
28
+ # `refs/remotes/origin/<base>` a fetch updates. It never rebases and never
29
+ # rewrites history. It leaves a conflicted merge aborted rather than half-done,
30
+ # so a failed run's working copy is exactly what it was — with one exception it
31
+ # reports as its own status rather than hiding: exit 5 below, where the abort
32
+ # itself failed and the tree is left mid-merge.
33
+ #
34
+ # `--local` MERGES THE LOCAL `<base>` REF AND MAKES NO NETWORK CALL. Default mode
35
+ # fetches and merges `origin/<base>`. `--local` is for the state where the local
36
+ # base is AHEAD of the remote one — commits landed in the main checkout and were
37
+ # not pushed — which is exactly the state a worktree cut from `origin/<base>` is
38
+ # stale against while `origin/<base>` looks current.
39
+ #
40
+ # Usage: refresh-branch.sh [--local]
41
+ # --local merge the local `<base>` branch instead of `origin/<base>`, and
42
+ # skip the fetch
43
+ # No repository argument: the checkout is THIS FILE's own location, never $PWD,
44
+ # because the caller runs it from wherever it happens to be.
45
+ #
46
+ # Exit map a caller can switch on:
47
+ #
48
+ # 0 the current branch now contains the base (the merge landed)
49
+ # 1 usage error / not a git repository / detached HEAD / the configuration
50
+ # could not be read / the working tree has uncommitted tracked changes
51
+ # 2 protected-branch refusal — HEAD is ON a protected branch, so this would be
52
+ # a merge INTO the integration line; nothing was attempted
53
+ # 3 nothing to do — the current branch already contains the base
54
+ # 4 the merge conflicted and was aborted; the working copy is exactly as it was
55
+ # 5 the merge conflicted and `git merge --abort` ALSO failed, so the working
56
+ # copy is left mid-merge and is NOT what it was — the one row of this map on
57
+ # which this script's "leaves a conflicted merge aborted rather than
58
+ # half-done" guarantee does not hold, and the one that needs a human before
59
+ # anything else runs in this checkout
60
+ #
61
+ # Two conditions outside that list land on 1 as well, for the same reason every
62
+ # other row of it does — the script cannot proceed and changed nothing: the base
63
+ # ref named by the mode is not present, and a merge that failed for a reason
64
+ # other than a conflict.
65
+ #
66
+ # REPRO — reproduce every row by hand, against a throwaway fixture:
67
+ #
68
+ # b=$(mktemp -d)/origin.git; git init -q --bare "$b"
69
+ # d=$(mktemp -d); git -C "$d" init -q -b trunk; git -C "$d" remote add origin "$b"
70
+ # printf '%s' '{"version":1,"defaultBranch":"trunk","protectedBranches":["trunk","release/*"],"stateDir":"sdlc-harness/","layers":[],"commands":{}}' > "$d/harness.config.json"
71
+ # printf 'a\n' > "$d/f.txt"; git -C "$d" add -A; git -C "$d" commit -qm seed
72
+ # git -C "$d" push -q -u origin trunk
73
+ # git -C "$d" checkout -q -b feat_x
74
+ # mkdir -p "$d/scripts/lib" # copy this script + lib/ there
75
+ # # move trunk on and publish it, so feat_x is behind:
76
+ # git -C "$d" checkout -q trunk; printf 'b\n' >> "$d/f.txt"
77
+ # git -C "$d" commit -qam base; git -C "$d" push -q; git -C "$d" checkout -q feat_x
78
+ #
79
+ # behind the base bash "$d/scripts/refresh-branch.sh" -> exit 0, and
80
+ # `git -C "$d" log --oneline` shows the base commit
81
+ # already current the same call again -> exit 3
82
+ # protected HEAD git -C "$d" checkout -q trunk; same call -> exit 2, nothing
83
+ # merged; `git -C "$d" checkout -q feat_x` -> exit 0 again
84
+ # detached HEAD git -C "$d" checkout -q --detach; same call -> exit 1
85
+ # dirty tree printf 'x\n' >> "$d/f.txt"; same call -> exit 1, and
86
+ # `git -C "$d" status --porcelain` is byte-identical after
87
+ # conflict edit the same line on trunk and on feat_x, commit both
88
+ # -> exit 4, and `git -C "$d" status` shows no MERGING state
89
+ # failed abort (5) no fixture row: the state that makes `git merge --abort`
90
+ # fail has to arise between this script's own merge and its
91
+ # abort, and nothing outside the script runs in that window
92
+ # unresolvable printf 'x' > "$d/harness.config.json"; same call -> exit 1,
93
+ # nothing merged; restore -> exit 0
94
+ # --local commit on trunk WITHOUT pushing, then from feat_x:
95
+ # bash "$d/scripts/refresh-branch.sh" --local -> exit 0
96
+ # the same call without --local -> exit 3
97
+
98
+ set -uo pipefail
99
+
100
+ # The library is reached by a path computed from this script's own location, so a
101
+ # copy that was moved with its `lib/` still finds it, and no session root or
102
+ # runtime-substituted token is assumed.
103
+ hr_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/harness-run-lib.sh"
104
+ if [ ! -r "$hr_lib" ]; then
105
+ echo "refresh-branch.sh: cannot read '$hr_lib' — refusing to merge" >&2
106
+ exit 1
107
+ fi
108
+ # shellcheck source=lib/harness-run-lib.sh
109
+ . "$hr_lib"
110
+
111
+ use_local=0
112
+ while [ "$#" -gt 0 ]; do
113
+ case "$1" in
114
+ --local)
115
+ use_local=1
116
+ shift
117
+ ;;
118
+ *)
119
+ echo "refresh-branch.sh: unknown argument '$1'" >&2
120
+ echo " usage: refresh-branch.sh [--local]" >&2
121
+ exit 1
122
+ ;;
123
+ esac
124
+ done
125
+
126
+ # The checkout being refreshed is where THIS FILE lives — never $PWD, which
127
+ # belongs to whoever invoked it.
128
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
129
+ top="$(hr_repo_root "$script_dir")" || top=""
130
+ if [ -z "$top" ]; then
131
+ echo "refresh-branch.sh: '$script_dir' is not inside a git repository — refusing to merge" >&2
132
+ exit 1
133
+ fi
134
+
135
+ # Warm the library's cache once, unsubstituted, and fail closed here rather than
136
+ # letting each reader below fail separately: a configuration that cannot be read
137
+ # is a protected set that cannot be judged.
138
+ if ! hr_config_load "$top"; then
139
+ echo "refresh-branch.sh: cannot resolve '$top/harness.config.json' — refusing to merge" >&2
140
+ echo " (no configuration there, invalid JSON, more than one document, no defaultBranch, or jq missing/older than 1.5)" >&2
141
+ exit 1
142
+ fi
143
+
144
+ base="$(hr_default_branch "$top")" || base=""
145
+ if [ -z "$base" ]; then
146
+ echo "refresh-branch.sh: no defaultBranch in '$top/harness.config.json' — refusing to merge" >&2
147
+ exit 1
148
+ fi
149
+
150
+ branch="$(hr_current_branch "$top")"
151
+ if [ -z "$branch" ]; then
152
+ echo "refresh-branch.sh: detached HEAD — refusing to merge" >&2
153
+ exit 1
154
+ fi
155
+
156
+ # The direction check, and the reason this wrapper is safe to allow-list. It
157
+ # refuses on the branch the guard's dangerous case is about, and it takes the
158
+ # library's unresolvable answer as a refusal rather than as a permit.
159
+ hr_branch_is_protected "$top" "$branch"
160
+ protected=$?
161
+ case "$protected" in
162
+ 0)
163
+ patterns="$(hr_protected_patterns "$top")" || patterns=""
164
+ protected_set=""
165
+ while IFS= read -r pattern; do
166
+ [ -n "$pattern" ] || continue
167
+ if [ -z "$protected_set" ]; then
168
+ protected_set="$pattern"
169
+ else
170
+ protected_set="$protected_set $pattern"
171
+ fi
172
+ done <<EOF
173
+ $patterns
174
+ EOF
175
+ echo "refresh-branch.sh: HEAD is on protected branch '$branch' — refusing to merge into it" >&2
176
+ echo " protected set (from harness.config.json): $protected_set" >&2
177
+ echo " this wrapper only merges '$base' INTO a working branch; check one out and re-run" >&2
178
+ exit 2
179
+ ;;
180
+ 2)
181
+ echo "refresh-branch.sh: cannot resolve '$top/harness.config.json' — refusing to merge" >&2
182
+ exit 1
183
+ ;;
184
+ esac
185
+
186
+ # A merge writes the working tree, so an uncommitted tracked change is refused
187
+ # before anything is fetched: the caller's own edit must not be carried into a
188
+ # merge commit, and an aborted merge must be able to restore what was there.
189
+ if ! git -C "$top" diff --quiet || ! git -C "$top" diff --cached --quiet; then
190
+ echo "refresh-branch.sh: uncommitted tracked changes in '$top' — refusing to merge" >&2
191
+ echo " commit them (bash \"$script_dir/commit-on-branch.sh\" …) and re-run" >&2
192
+ exit 1
193
+ fi
194
+
195
+ if [ "$use_local" -eq 1 ]; then
196
+ base_ref="$base"
197
+ ref_path="refs/heads/$base"
198
+ else
199
+ base_ref="origin/$base"
200
+ ref_path="refs/remotes/origin/$base"
201
+ # Tolerate a failed fetch the way create-worktree.sh does — offline with the
202
+ # ref already present is still a refresh this can do — and judge the ref the
203
+ # next block names rather than the fetch. git's own stderr still reaches the
204
+ # caller's log whichever outcome follows.
205
+ git -C "$top" fetch origin "$base" || true
206
+ fi
207
+
208
+ if ! git -C "$top" show-ref --verify --quiet "$ref_path"; then
209
+ echo "refresh-branch.sh: no '$base_ref' in '$top' — nothing to merge from" >&2
210
+ if [ "$use_local" -eq 1 ]; then
211
+ echo " --local merges the local '$base' branch; drop the flag to merge origin/$base" >&2
212
+ else
213
+ echo " push the default branch (git push -u origin $base), or pass --local to merge the local one" >&2
214
+ fi
215
+ exit 1
216
+ fi
217
+
218
+ # Already contained => a distinct, dedicated status, so a caller can tell "the
219
+ # base is already here" apart from a refusal.
220
+ if git -C "$top" merge-base --is-ancestor "$base_ref" HEAD; then
221
+ echo "refresh-branch.sh: '$branch' already contains $base_ref — nothing to do"
222
+ exit 3
223
+ fi
224
+
225
+ if git -C "$top" merge --no-edit "$base_ref"; then
226
+ echo "refresh-branch.sh: merged $base_ref into $branch"
227
+ exit 0
228
+ fi
229
+
230
+ # A failed merge is one of two things, and they are distinguished by whether a
231
+ # merge is in progress: a conflict leaves MERGE_HEAD behind, anything else (a
232
+ # refused fast-forward, a hook, an unmergeable ref) never started one.
233
+ if git -C "$top" rev-parse --verify --quiet MERGE_HEAD >/dev/null; then
234
+ if git -C "$top" merge --abort; then
235
+ echo "refresh-branch.sh: $base_ref conflicts with $branch — merge aborted, working copy unchanged" >&2
236
+ exit 4
237
+ fi
238
+ echo "refresh-branch.sh: $base_ref conflicts with $branch and 'git merge --abort' failed — the working copy is LEFT MID-MERGE and is NOT what it was" >&2
239
+ echo " resolve or abandon it yourself (git status, git merge --abort), then re-run" >&2
240
+ exit 5
241
+ fi
242
+
243
+ echo "refresh-branch.sh: 'git merge $base_ref' failed (see output above) — nothing was merged" >&2
244
+ exit 1