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,632 @@
1
+ /**
2
+ * Generator: the adopting repository's `harness.config.json`.
3
+ *
4
+ * It assembles one object out of stack detection and the explicit `init` flags, refuses to write
5
+ * one it can already tell is invalid, and enqueues it under the `create-if-absent` contract so a
6
+ * second `init` never rewrites a config the adopter has edited unless it was asked to — which is
7
+ * the whole of what `--reset-config` is. It is the artifact the schema gate
8
+ * validates, so everything below is written to satisfy `schemas/harness.config.schema.json` rather
9
+ * than to look tidy.
10
+ *
11
+ * ## Three non-obvious choices, and where each comes from
12
+ *
13
+ * 1. **The generated file omits `$schema`.** The key is optional, and `docs/config.md` §5 fixes its
14
+ * frame: "its value is a URI or a path resolved relative to this file's own location, not a
15
+ * repo-relative path". An adopter's repository has no stable relative path to a schema shipped
16
+ * inside an installed package, so any value written here would be wrong somewhere.
17
+ * `examples/harness.config.json` keeps the key because that file sits beside the schema.
18
+ * 2. **An optional section is omitted entirely rather than emitted empty or null.**
19
+ * `additionalProperties: false` holds at every level, and an empty `qa: {}` or a
20
+ * `parity: { }` says nothing a reader or `doctor` can act on while looking like a decision that
21
+ * was made. Off means absent. The same reasoning omits `clientEnvPrefix`: its default is `null`,
22
+ * nothing in a repository states it, and a reader that wants its effective value reads
23
+ * `DEFAULTS`.
24
+ * 3. **`commands.typecheck` / `test` / `devServer` hold the wrapper *invocation*; `build` and
25
+ * `depInstall` hold the **raw** detected line** (the `commands` object of
26
+ * `examples/harness.config.json`).
27
+ * The invocation string is never formatted here — it comes from `generators/scripts.ts`'s
28
+ * {@link wrapperInvocation}, and the file name from its {@link WRAPPER_SCRIPTS} table — because
29
+ * the value written here, the wrapper file that generator writes, and the permission profile's
30
+ * repo-relative allow entry are three views of one string, and drift between them is invisible
31
+ * until an unattended run stalls on a command matching neither `allow` nor `deny`.
32
+ *
33
+ * **Every path written into this file is repo-relative** (`docs/config.md` §5), `commands.*`
34
+ * included: commands are executed from the repository root. The profile's absolute and
35
+ * worktree-glob twins of a wrapper path exist so that this repo-relative value can stay
36
+ * schema-conformant — writing an absolute path here to "match the profile" would break the rule the
37
+ * twins were added to preserve.
38
+ *
39
+ * ## What this generator deliberately does not decide
40
+ *
41
+ * - **Flag-shaped refusals.** A `--state-dir` naming a dot-directory is an adopter-fixable usage
42
+ * error that the `init` flag surface refuses before anything is assembled. The
43
+ * {@link EXIT.INTERNAL} throw below is the backstop for the other case — a config this generator
44
+ * itself assembled wrongly, which no adopter can fix.
45
+ * - **`deploy`.** It is an adopter-added section this release never generates; the wrapper-script
46
+ * and permission-profile generators both gate their deploy handling on its presence, so emitting
47
+ * an empty one here would turn a deliberate absence into a half-configured deploy.
48
+ */
49
+ import { checkConfigShape, formatProblem, hasErrors } from '../config/check.js';
50
+ import { configExists, loadConfig, saveConfig } from '../config/io.js';
51
+ import { asQaDriver, CONFIG_FILENAME, CONFIG_VERSION, DEFAULTS, isPlaceholder, QA_DRIVER_IMPLEMENTED, qaDriverChoices, } from '../config/model.js';
52
+ import { EXIT, HarnessError } from '../core/errors.js';
53
+ import { branchResolves, checkedOutBranch, configuredInitDefaultBranch, hasCommits, remoteHeadBranch, } from '../core/git.js';
54
+ import { defaultProjectName } from '../core/paths.js';
55
+ import { buildPreset } from '../detect/presets.js';
56
+ import { WRAPPER_SCRIPTS, wrapperInvocation } from './scripts.js';
57
+ /**
58
+ * Branch names a repository's integration line conventionally has — the trigger for the **second**
59
+ * arm of {@link warnAboutGuessedBranch}, and nothing else.
60
+ *
61
+ * It does not decide the value: {@link resolveDefaultBranch} detects that, and a repository whose
62
+ * integration line is called `develop` or `trunk` is wired to *that* name rather than overridden
63
+ * with a conventional one. Its role narrowed when the guard gained an arm that tests **resolution**:
64
+ * a guessed name resolving to no branch here is wrong whatever it is called, and that arm catches
65
+ * the case this list is blind to by construction — a conventional `main` guessed into a repository
66
+ * that has no `main`, which is the commonest wrong answer. What is left for the list is the weaker
67
+ * signal the resolution test cannot give: a name that *does* resolve but reads like the feature
68
+ * branch `init` happened to run from, wired in as the line work merges back into.
69
+ *
70
+ * The constant keeps its name and its two entries, so it stays greppable against the finding.
71
+ */
72
+ const DEFAULT_LOOKING_BRANCHES = ['main', 'master'];
73
+ /**
74
+ * How the CLI is typed, for the remedies the guard prints. The `npx` prefix rule is stated once in
75
+ * `commands/init.ts`, beside its own `CLI`.
76
+ */
77
+ const CLI = 'npx autonomous-sdlc-harness';
78
+ /**
79
+ * Seed for `qa.credentialsPath`: the gitignored file the interactive test phase reads accounts
80
+ * from. Exported because the generator that writes the `.example` companion has to put it beside
81
+ * the file an adopter is being told to create, and a second spelling here would separate them.
82
+ */
83
+ export const QA_CREDENTIALS_PATH = '.claude/qa-accounts.env';
84
+ /** Seed for `docs.root` when `--docs-root` is not given. */
85
+ const DEFAULT_DOCS_ROOT = 'docs';
86
+ /**
87
+ * Seed for `pushEnvPath`, written whatever the phases are: the *path* is repo-scoped configuration
88
+ * that is the same for everyone who clones, while the file it names is machine-local and is added
89
+ * to `.gitignore` by the repo-root generator. Exported for the same reason as
90
+ * {@link QA_CREDENTIALS_PATH}: its `.example` companion is written beside it.
91
+ */
92
+ export const PUSH_ENV_PATH = '.claude/push-notify.env';
93
+ /** The five command keys, in schema order, so the assembled object serializes in that order. */
94
+ const COMMAND_PLAN = [
95
+ { key: 'typecheck', wrapped: true },
96
+ { key: 'test', wrapped: true },
97
+ { key: 'build', wrapped: false },
98
+ { key: 'devServer', wrapped: true },
99
+ { key: 'depInstall', wrapped: false },
100
+ ];
101
+ /**
102
+ * The wrapper file one command key is invoked through, read from the wrapper table rather than
103
+ * spelled out, so no `.sh` name is written down twice in the CLI.
104
+ */
105
+ function wrapperFileFor(key) {
106
+ const script = WRAPPER_SCRIPTS.find((entry) => entry.key === key);
107
+ if (script === undefined) {
108
+ throw new HarnessError(`no wrapper script is declared for the ${key} command, so the value written into commands.${key} could not be derived from the wrapper table: the config generator and the wrapper generator disagree about the wrapper set, which is a fault in this CLI rather than in the repository it was run against`, EXIT.INTERNAL);
109
+ }
110
+ return script.file;
111
+ }
112
+ /**
113
+ * The `commands` object: the wrapper invocation for the three wrapped keys, the raw detected line
114
+ * for the two that are not, and an optional key detection did not resolve left absent.
115
+ *
116
+ * **A key still holding the placeholder is written as the placeholder**, not as an invocation: the
117
+ * config check's warning and `doctor` both look for that marker, and the wrapper generator writes
118
+ * no script for it — so pointing `commands.typecheck` at a wrapper that was never written would
119
+ * turn a visible "you still have to set this" into a command that fails on a missing file.
120
+ */
121
+ function buildCommands(scriptsDir, raw) {
122
+ const commands = {};
123
+ for (const entry of COMMAND_PLAN) {
124
+ const line = raw[entry.key];
125
+ if (line === undefined)
126
+ continue;
127
+ commands[entry.key] =
128
+ entry.wrapped && !isPlaceholder(line) ? wrapperInvocation(scriptsDir, wrapperFileFor(entry.key)) : line;
129
+ }
130
+ // `RawCommands` types both verifiers as required and the preset builder fills either one it
131
+ // could not detect with a placeholder, so the loop cannot leave them unset. Checked rather than
132
+ // asserted, because the cast below is exactly what would hide it if that ever changed.
133
+ if (commands.typecheck === undefined || commands.test === undefined) {
134
+ throw new HarnessError('the detected command set is missing typecheck or test, which the config schema requires: the two verification commands are what "this change is done" means', EXIT.INTERNAL);
135
+ }
136
+ return commands;
137
+ }
138
+ /**
139
+ * The `detection` object: stack detection's own result, copied into the config it produced, so an
140
+ * adopted repository states which layout was detected and on what evidence — which is what stops a
141
+ * `flat` fallback profile from being indistinguishable from a deliberately chosen one.
142
+ *
143
+ * Keys are set in the schema's order, as everywhere else in this generator. **`evidence` is set
144
+ * only when there is one** — absent rather than present-and-`undefined`, the discipline
145
+ * {@link buildPreset}'s `rawCommands` uses — so a `--preset` run lands `signal: "forced"`
146
+ * (`FORCED_SIGNAL_ID`, `detect/signals.ts`) with **no** `evidence` key at all, because no signal row
147
+ * was evaluated and there is nothing it matched on. That absence is what a reader of the adoption
148
+ * commit tells a forced preset from a detected one by.
149
+ *
150
+ * **`commandFamily` follows the same discipline** and for the same reason as recording it at all:
151
+ * the family that derived `commands.*` is selected independently of the preset, so it cannot be read
152
+ * back off `preset`, and a run with no family to name writes no key rather than a null.
153
+ *
154
+ * **What "no family to name" means is the caller's gate, not this function's.** A family answers off
155
+ * a *command set* (`buildPreset`'s loop, `detect/presets.ts`), which is not the two required keys: the
156
+ * Xcode arm can return an empty object, `composer`/`bundler` a `depInstall` alone, `npm` a `build`
157
+ * alone. Recording such a winner would put a family id beside a placeholder `commands.typecheck` and
158
+ * `commands.test` — the state every description of this key tells its reader cannot occur. So
159
+ * {@link buildConfig} passes the id only where {@link PresetProfile.resolvedRequired} is non-empty,
160
+ * on the same single-derivation rule the reporting lines in `detect/presets.ts` read it by, and the
161
+ * written key means **this family derived at least one of the two required commands**.
162
+ *
163
+ * **No `review` key is written here.** It is the `/harness-analyze` command's to record through
164
+ * `config set detection.review`, and an empty object would read as a review that happened.
165
+ *
166
+ * **Written on the run that generates the file, and never refreshed by a plain re-run.**
167
+ * `harness.config.json` is one of the artifacts `--force` does not upgrade (`docs/cli.md` §3), so a
168
+ * re-run keeps whatever record the file already holds and `--reset-config` is what rebuilds it —
169
+ * the same sentence {@link writeHarnessConfig}'s deferred start-over note already makes about every
170
+ * other re-derived value. The finding this implements said `init --force` refreshes it; that was
171
+ * corrected against `docs/cli.md` §3, and no refresh path is added here.
172
+ */
173
+ function buildDetection(detection, commandFamily) {
174
+ const record = { preset: detection.preset, signal: detection.matchedSignal };
175
+ if (detection.evidence !== undefined)
176
+ record.evidence = detection.evidence;
177
+ if (commandFamily !== undefined)
178
+ record.commandFamily = commandFamily;
179
+ return record;
180
+ }
181
+ /**
182
+ * The two incantations that actually change a `defaultBranch` this guard has just questioned, and
183
+ * the closing clause of both its arms.
184
+ *
185
+ * **A bare `--default-branch` is deliberately not among them.** This guard's messages reach an
186
+ * adopter only through {@link BuildConfigOptions.warnIfWritten}, which publishes them exactly on the
187
+ * runs where this config *landed* — so by the time one is read the repository has a
188
+ * {@link CONFIG_FILENAME}, and the next `init` keeps that file and discards the flag while printing
189
+ * a note that reads as if it had been applied. Naming the flag alone is what sent an adopter to the
190
+ * one route that could not work.
191
+ *
192
+ * `config set` is named first because it changes the single key and re-derives nothing else.
193
+ * `--reset-config`'s cost is quoted from that flag's own `INIT_OPTIONS` summary (`commands/init.ts`)
194
+ * rather than paraphrased, so the flag is not described two ways in one tool.
195
+ */
196
+ const GUESSED_BRANCH_REMEDY = `run \`git branch -a\` to see what this repository has, then name its integration line with \`${CLI} config set defaultBranch <branch>\`, which changes that one key — or \`${CLI} init --reset-config --default-branch <branch>\`, which rebuilds ${CONFIG_FILENAME} from detection and the flags, after a .bak`;
197
+ /**
198
+ * The warning the three *guessing* rungs carry, and neither of the two that state rather than guess.
199
+ *
200
+ * Two ordered arms, and the order is the point:
201
+ *
202
+ * 1. **The name resolves to nothing here.** That is wrong whatever the name is, because both readers
203
+ * of the value are then reading a ref that is not there. The consequence clause is worded from
204
+ * `doctor`'s `DEFAULT_BRANCH_CHECK` failure sentence (`doctor/checks.ts`), which asks this same
205
+ * question of this same value later, so `init` and `doctor` do not spell one fact two ways.
206
+ * 2. **The name resolves, but is not one an integration line conventionally has**
207
+ * ({@link DEFAULT_LOOKING_BRANCHES}) — the weaker signal, and the arm that was the whole guard
208
+ * before: `init` run from a feature branch, with an `origin/HEAD` that names nothing to outrank
209
+ * it.
210
+ *
211
+ * **{@link hasCommits} gates arm 1, and that gate is the arm's own precondition rather than a
212
+ * nicety.** In a repository with no commit *nothing* resolves, so an ungated arm 1 would fire on
213
+ * every `--git-init` adoption and on every repository wired between `git init` and its first commit
214
+ * — which is exactly the noise this warning exists not to be. The same split is why `doctor` grades
215
+ * that state a `warn` rather than a `fail`.
216
+ *
217
+ * The value is kept rather than replaced — replacing it is what the removed heuristic did, and it
218
+ * turned "your integration line is called `develop`" into a config nobody was told was wrong. So the
219
+ * detected name is written, and the remedies that correct it are named in the same line
220
+ * ({@link GUESSED_BRANCH_REMEDY}).
221
+ *
222
+ * **Deferred**, by {@link buildConfig}'s rule: both sentences assert what `defaultBranch` and
223
+ * `protectedBranches` *were written as*, which is false of a re-run that kept a file naming something
224
+ * else.
225
+ */
226
+ function warnAboutGuessedBranch(repoRoot, branch, source, defer) {
227
+ if (hasCommits(repoRoot) && branchResolves(repoRoot, branch) === 'none') {
228
+ defer(`defaultBranch and protectedBranches were taken from ${source} as ${JSON.stringify(branch)}, and this repository has neither a local branch nor an origin/ remote-tracking ref by that name: a branch review takes its diff base from this value and the protected-branch guards decide what an unattended run may push by matching against it, so neither works — the diff is against a ref git cannot find, and the guard matches no branch and therefore stops nothing. To fix it, ${GUESSED_BRANCH_REMEDY}`);
229
+ return;
230
+ }
231
+ if (DEFAULT_LOOKING_BRANCHES.includes(branch))
232
+ return;
233
+ defer(`defaultBranch and protectedBranches were taken from ${source} as ${JSON.stringify(branch)}, with no origin/HEAD to confirm it, and that is not one of the names an integration line conventionally has (${DEFAULT_LOOKING_BRANCHES.join(', ')}): if init ran from a feature branch it has just wired that branch in as the line work merges back into, so ${GUESSED_BRANCH_REMEDY}`);
234
+ }
235
+ /**
236
+ * The clause rungs 3 and 4 open with: what the rungs above them failed to answer, worded from the
237
+ * fact this checkout actually presented.
238
+ *
239
+ * A detached `HEAD` and a repository with no branch at all both demote the detection a rung, but
240
+ * only one of them is a repository with nothing to offer. Saying *no checked-out branch answered*
241
+ * over a detached `HEAD` reads as the second while describing the first — and on a colocated `jj`
242
+ * repository, where a detached `HEAD` is the steady state after every `jj new`, that is the wording
243
+ * every adoption run would land. So the detached case names the fact it met, inside a note that
244
+ * already closes by naming `--default-branch`; every other answer keeps today's sentence exactly.
245
+ */
246
+ function unansweredAbove(head, alsoInitDefaultBranch) {
247
+ if (head.kind === 'detached') {
248
+ const detached = 'no origin/HEAD is known here and HEAD is detached, so the checked-out branch could not answer — a detached HEAD is the ordinary working state of a colocated jj repository, and it is what demoted this detection a rung';
249
+ return alsoInitDefaultBranch ? `${detached}, and no init.defaultBranch answered either` : detached;
250
+ }
251
+ return alsoInitDefaultBranch
252
+ ? 'no origin/HEAD, no checked-out branch and no init.defaultBranch answered'
253
+ : 'no origin/HEAD and no checked-out branch answered';
254
+ }
255
+ /**
256
+ * `defaultBranch`, by **detection rather than assumption**, in one settled order:
257
+ *
258
+ * 0. `--default-branch`, which wins outright and is reported as the source it is;
259
+ * 1. `origin/HEAD` — the repository's own statement of its integration line, and the one rung never
260
+ * second-guessed;
261
+ * 2. the branch `HEAD` names — **including one no commit has landed on yet**, which is the state of
262
+ * every repository `init` wires that has no commit, whether `--git-init` created it or the
263
+ * adopter did and never committed into it, and the branch `init`'s own first commit is about to
264
+ * create ({@link checkedOutBranch} answers it there). That probe has **three** answers, not two,
265
+ * and the rung falls through on either of the other two — a detached `HEAD` and a repository with
266
+ * no branch to name — carrying which of them it met into the notes rungs 3 and 4 write
267
+ * ({@link unansweredAbove});
268
+ * 3. git's configured `init.defaultBranch`;
269
+ * 4. the schema default.
270
+ *
271
+ * The order is the whole of it. `master` repositories are common, and a `main` assumed over one is
272
+ * silent in every direction it is wrong: it is a valid config, so nothing refuses it, while every
273
+ * branch diff, every review's diff base and the protected-branch guard are all taken from it. That
274
+ * is why each rung reports **which one answered** through {@link BuildConfigOptions.noteIfWritten}
275
+ * rather than only reporting the value, and why every rung that is a *guess* — 2, 3 and 4 —
276
+ * additionally runs {@link warnAboutGuessedBranch} over what it produced. **Rung 4 included**:
277
+ * writing the schema default into a repository that has no branch by that name is the guess that is
278
+ * wrong most often, and it is the one rung that used to say nothing at all.
279
+ *
280
+ * Rungs 0 and 1 stay silent, each for its own reason: `--default-branch` is the adopter's own
281
+ * statement of the value, which `doctor`'s `default-branch` check grades standing rather than at
282
+ * write time; and `origin/HEAD` is the repository's own answer, the rung this header already calls
283
+ * never second-guessed.
284
+ *
285
+ * `doctor`'s `default-branch` check is the other half: it re-reads the configured name and reports
286
+ * whether the repository can still resolve it.
287
+ *
288
+ * **Every one of the five rung notes is deferred** ({@link BuildConfigOptions.noteIfWritten}), by
289
+ * {@link buildConfig}'s rule: each says what `defaultBranch` and `protectedBranches` were set from,
290
+ * which is false of a re-run that kept a file naming something else. The `note` parameter is that
291
+ * deferred sink.
292
+ */
293
+ function resolveDefaultBranch(repoRoot, flag, defer, note) {
294
+ if (flag !== undefined) {
295
+ note(`defaultBranch and protectedBranches were set from --default-branch as ${JSON.stringify(flag)}, so nothing about the repository was consulted`);
296
+ return flag;
297
+ }
298
+ const fromRemoteHead = remoteHeadBranch(repoRoot);
299
+ if (fromRemoteHead !== undefined) {
300
+ note(`defaultBranch and protectedBranches were detected from origin/HEAD as ${JSON.stringify(fromRemoteHead)}, which is the repository's own statement of the line work merges back into; --default-branch overrides it`);
301
+ return fromRemoteHead;
302
+ }
303
+ const head = checkedOutBranch(repoRoot);
304
+ if (head.kind === 'branch') {
305
+ note(`no origin/HEAD is known here, so defaultBranch and protectedBranches were taken from the checked-out branch ${JSON.stringify(head.name)}; --default-branch overrides it`);
306
+ warnAboutGuessedBranch(repoRoot, head.name, 'the checked-out branch', defer);
307
+ return head.name;
308
+ }
309
+ const configured = configuredInitDefaultBranch(repoRoot);
310
+ if (configured !== undefined) {
311
+ note(`${unansweredAbove(head, false)}, so defaultBranch and protectedBranches were taken from git's init.defaultBranch as ${JSON.stringify(configured)}; --default-branch overrides it`);
312
+ warnAboutGuessedBranch(repoRoot, configured, "git's init.defaultBranch setting", defer);
313
+ return configured;
314
+ }
315
+ const fallback = DEFAULTS.defaultBranch;
316
+ note(`${unansweredAbove(head, true)}, so defaultBranch and protectedBranches were written as the schema default ${JSON.stringify(fallback)}; --default-branch overrides it`);
317
+ warnAboutGuessedBranch(repoRoot, fallback, 'the schema default', defer);
318
+ return fallback;
319
+ }
320
+ /**
321
+ * The rung-independent half of {@link resolveQaDriver}: return the value that rung settled on, and
322
+ * record once — on **any** rung — the statement owed when it is a driver this release declares but
323
+ * does not implement ({@link QA_DRIVER_IMPLEMENTED}, the one owner of that state). Every rung returns
324
+ * through here, so the statement cannot be attached to some rungs and not others.
325
+ *
326
+ * **Recorded, not emitted.** Whether the statement is *true* is not known here: `resolveQaDriver`
327
+ * runs on the generation path alone, and {@link writeHarnessConfig} discards the whole generated
328
+ * config where a file already exists and `--reset-config` was not given. "Every interactive-test
329
+ * dispatch returns a blocker" is false of a repository whose own file carries a different driver, so
330
+ * only that caller — the one place that knows whether this config lands — decides to publish it. The
331
+ * standing reporter for what is *in* a config is `doctor`'s `browser-wiring` check, which names the
332
+ * driver and its status on every run; this line is about the choice being made here.
333
+ *
334
+ * **Published as a `warn`, not a `note`, deliberately.** The cost lands hours later at the first
335
+ * interactive-test dispatch, and `warn` is the one stream that survives `--quiet` (`core/report.ts`)
336
+ * — which is the unattended run that most needs to have been told. Nothing about the exit status
337
+ * changes: `init` does not fail on a warning, and a legal, schema-valid, deliberately-supported
338
+ * configuration stays writable — the two mobile values exist so a mobile project can record what it
339
+ * is.
340
+ */
341
+ function settleQaDriver(driver, defer) {
342
+ if (QA_DRIVER_IMPLEMENTED[driver])
343
+ return driver;
344
+ defer(`this run resolved qa.driver to ${JSON.stringify(driver)}, and that variant of the interactive test agent ships declared but not implemented in this release: every interactive-test dispatch returns a blocker instead of running a test. The configuration is otherwise correct and nothing else is withheld by it — if this application is in fact reachable by a browser, \`config set qa.driver ${DEFAULTS.qa.driver}\` in ${CONFIG_FILENAME} is the whole change`);
345
+ return driver;
346
+ }
347
+ /**
348
+ * `qa.driver`, in one settled order: `--qa-driver`, then the answer to the question `init` puts on a
349
+ * terminal, then the schema default.
350
+ *
351
+ * **Why it is asked at all.** The default drives a browser, and it is the one generated value that
352
+ * can be wrong in a way nothing downstream catches: every other key of a mobile project's config is
353
+ * correct, the file validates, `doctor` passes — and the interactive test phase runs an agent that
354
+ * cannot reach the application. Writing it silently made that a choice nobody was asked to make.
355
+ *
356
+ * The rungs are reported apart rather than together, because the useful thing to say differs by
357
+ * rung: a run that was asked is told what it chose, and only a run that could not be asked is
358
+ * pointed at `config set qa.driver`. Telling an adopter to go and set a key they had just answered
359
+ * is what the note this replaces did.
360
+ *
361
+ * An answer that is not one of the drivers falls back to the default **with a deferred warning**
362
+ * rather than being written — deferred by {@link buildConfig}'s rule, since the sentence asserts what
363
+ * `qa.driver` was written as: the prompt must not be able to produce a config the schema rejects, since
364
+ * {@link writeHarnessConfig}'s guard would then exit {@link EXIT.INTERNAL} and blame this CLI for
365
+ * the adopter's typo. Its list of the legal values comes from {@link qaDriverChoices}, the same
366
+ * renderer `init`'s question and its `--qa-driver` refusal print, so a value's release status is
367
+ * stated identically wherever one is offered or rejected.
368
+ *
369
+ * The per-rung note says *what answered*; {@link settleQaDriver} records a second line saying *what
370
+ * that value costs*, on whichever rung answered, for the caller to publish only where this config is
371
+ * the one that lands. They are kept apart for the same reason the rungs are.
372
+ *
373
+ * **All three rung notes are deferred too** ({@link BuildConfigOptions.noteIfWritten}), by the same
374
+ * rule: each says what `qa.driver` was written as, which a kept re-run's own file may contradict.
375
+ * The `note` parameter is that deferred sink.
376
+ */
377
+ function resolveQaDriver(flag, ask, note, defer) {
378
+ const fallback = DEFAULTS.qa.driver;
379
+ if (flag !== undefined) {
380
+ note(`the interactive test phase is on, so qa.driver was set from --qa-driver as ${JSON.stringify(flag)} and nothing was asked`);
381
+ return settleQaDriver(flag, defer);
382
+ }
383
+ const answer = ask?.();
384
+ if (answer !== undefined) {
385
+ const chosen = asQaDriver(answer);
386
+ if (chosen !== undefined) {
387
+ note(`the interactive test phase is on, so qa.driver was written as ${JSON.stringify(chosen)} — the answer given when init asked which driver reaches this application; --qa-driver answers it without being asked`);
388
+ return settleQaDriver(chosen, defer);
389
+ }
390
+ defer(`${JSON.stringify(answer)} is not one of the drivers the interactive test phase can run (${qaDriverChoices()}), so qa.driver was written as the default ${JSON.stringify(fallback)} instead: re-run with --qa-driver, or set the key in ${CONFIG_FILENAME} with \`config set qa.driver <value>\``);
391
+ return settleQaDriver(fallback, defer);
392
+ }
393
+ note(`the interactive test phase is on and this run could not be asked which driver it should run — no terminal, or --non-interactive — so qa.driver was written as the documented default ${JSON.stringify(fallback)}, which reaches the application by driving a browser: pass --qa-driver to choose one without being asked, and a project whose application is not a browser application sets qa.driver in ${CONFIG_FILENAME} with \`config set qa.driver <value>\`, which names the values it accepts, before the phase runs`);
394
+ return settleQaDriver(fallback, defer);
395
+ }
396
+ /**
397
+ * Assemble the config from detection plus the flags.
398
+ *
399
+ * Keys are set in the schema's own order, because the write engine serializes the object as it
400
+ * stands and JSON keeps insertion order: assembling in schema order is what makes the generated
401
+ * file read like `examples/harness.config.json` and what makes an unchanged re-write byte-identical.
402
+ * Nothing here touches the filesystem beyond the read-only git probes behind
403
+ * {@link resolveDefaultBranch}.
404
+ *
405
+ * ## Which sink a line goes to — one rule, so a later line is not routed by feel
406
+ *
407
+ * **A line whose sentence asserts what the configuration now holds goes to the deferred sink of its
408
+ * stream — {@link BuildConfigOptions.warnIfWritten} for a warning,
409
+ * {@link BuildConfigOptions.noteIfWritten} for a note; a line about the flags this invocation
410
+ * carried stays on the immediate sink, {@link BuildConfigOptions.warn}.**
411
+ * {@link writeHarnessConfig} discards this whole config on a kept re-run, so the first kind is false
412
+ * there and the second is true either way. The note stream has no immediate sink because no line
413
+ * here needs one: a note about what this invocation carried adds a `note` option beside `warn`,
414
+ * binds it here and wires it in {@link writeHarnessConfig}, rather than being routed by feel into a
415
+ * deferred sink whose sentences it does not fit. By that rule {@link warnAboutGuessedBranch} (what
416
+ * `defaultBranch` and `protectedBranches` were written as), {@link resolveQaDriver}'s unknown-answer
417
+ * arm (what `qa.driver` was written as) and {@link settleQaDriver} (what the written driver costs)
418
+ * are deferred warnings, and {@link resolveDefaultBranch}'s five rung notes together with
419
+ * {@link resolveQaDriver}'s three are deferred notes — every note this assembler has. The
420
+ * `--qa-driver` without `--qa` and `--parity` without `--reference-impl` arms below are not
421
+ * deferred: each is about a flag the adopter typed on this command line.
422
+ */
423
+ export function buildConfig({ repoRoot, detection, preset, flags, warn, noteIfWritten, warnIfWritten, askDriver, }) {
424
+ const report = warn ?? (() => { });
425
+ const informIfWritten = noteIfWritten ?? (() => { });
426
+ const defer = warnIfWritten ?? (() => { });
427
+ const profile = preset ?? buildPreset(detection);
428
+ const scriptsDir = DEFAULTS.scriptsDir;
429
+ const defaultBranch = resolveDefaultBranch(repoRoot, flags.defaultBranch, defer, informIfWritten);
430
+ const phases = { qa: flags.qa ?? false, docs: flags.docs ?? false, parity: flags.parity ?? false };
431
+ const config = {
432
+ // `$schema` is deliberately absent — see choice 1 in the module header.
433
+ version: CONFIG_VERSION,
434
+ projectName: flags.projectName ?? defaultProjectName(repoRoot),
435
+ defaultBranch,
436
+ // The branch work merges back into is the one an automated run must never push to directly.
437
+ protectedBranches: [defaultBranch],
438
+ stateDir: flags.stateDir ?? DEFAULTS.stateDir,
439
+ appDir: detection.context.appDir,
440
+ scriptsDir,
441
+ githooksDir: DEFAULTS.githooksDir,
442
+ agentModel: DEFAULTS.agentModel,
443
+ pushEnvPath: PUSH_ENV_PATH,
444
+ // `clientEnvPrefix` is deliberately absent — see choice 2 in the module header.
445
+ // Sits directly above the `layers` profile it explains, which is the schema's own order.
446
+ // The family id is recorded only where that family derived one of the two required commands —
447
+ // see {@link buildDetection}'s `commandFamily` paragraph for why answering is not enough.
448
+ detection: buildDetection(detection, profile.resolvedRequired.length === 0 ? undefined : profile.commandFamily),
449
+ // Copied entry by entry so the written config owns its layer objects and a later edit to it
450
+ // cannot reach back into the preset the detection result still holds.
451
+ layers: profile.layers.map((layer) => ({ ...layer })),
452
+ commands: buildCommands(scriptsDir, profile.rawCommands),
453
+ phases,
454
+ };
455
+ if (phases.qa) {
456
+ // `driver` first, in the schema's own key order, and resolved rather than defaulted: which
457
+ // variant of the interactive test agent the phase runs has to be visible in the config an
458
+ // adopter commits, and {@link resolveQaDriver} is what keeps it from being a choice nobody was
459
+ // asked to make. Every rung reports itself, so the run says which one answered.
460
+ const driver = resolveQaDriver(flags.qaDriver, askDriver, informIfWritten, defer);
461
+ config.qa = { driver, portSeed: DEFAULTS.qa.portSeed, credentialsPath: QA_CREDENTIALS_PATH };
462
+ }
463
+ else if (flags.qaDriver !== undefined) {
464
+ // Warned rather than refused, and deliberately not written: no `qa` section exists while the
465
+ // phase is off (choice 2 in the module header), so the value would have nowhere to go and
466
+ // nothing to read it. The flag is only ever given on purpose, so silence would be worse — it
467
+ // reads as a run that turned the phase on.
468
+ report('--qa-driver was given without --qa, so it was not written anywhere: qa.driver is read only while the interactive test phase is on, and no qa section is written while it is off — re-run with --qa to turn the phase on and set the driver in the same run');
469
+ }
470
+ if (phases.docs) {
471
+ config.docs = { root: flags.docsRoot ?? DEFAULT_DOCS_ROOT };
472
+ }
473
+ if (phases.parity) {
474
+ // Omitted rather than written empty when there is nothing to put in it: the phase toggle is
475
+ // still on and the config check warns about the missing path, which is the actionable half.
476
+ if (flags.referenceImpl === undefined) {
477
+ report('--parity was given without --reference-impl, so no parity section was written: set parity.referenceImplPath in harness.config.json before the phase runs, or it has nothing to compare a change against');
478
+ }
479
+ else {
480
+ config.parity = { referenceImplPath: flags.referenceImpl };
481
+ }
482
+ }
483
+ return config;
484
+ }
485
+ /**
486
+ * The kept-config note's `qa.driver` clause: the driver that file puts in force, or nothing at all.
487
+ *
488
+ * Conditioned on `phases.qa` rather than on the section's presence, because the key is read only
489
+ * while the interactive test phase is on (choice 2 in the module header) — naming a driver for a
490
+ * repository that runs no interactive test would report a value nothing consults. A phase that is on
491
+ * over a file missing the key is left unnamed for the same reason: this note states what it read,
492
+ * and what a config *holds* is `doctor`'s `browser-wiring` check to grade.
493
+ */
494
+ function keptQaDriverClause(config) {
495
+ const driver = config.phases?.qa === true ? config.qa?.driver : undefined;
496
+ return driver === undefined ? '' : `, and runs the interactive test phase with qa.driver ${JSON.stringify(driver)}`;
497
+ }
498
+ /**
499
+ * Build the config and enqueue it as `create-if-absent`, returning the config **in effect**.
500
+ *
501
+ * The order matters and is the point of the function:
502
+ *
503
+ * 1. assemble, collecting anything the adopter should know — including the lines of **both** streams
504
+ * that are only true if this config is the one that lands, held apart until step 3 has decided
505
+ * ({@link BuildConfigOptions.warnIfWritten}, {@link BuildConfigOptions.noteIfWritten});
506
+ * 2. check the assembled object **before** enqueuing anything — `init` must never produce a config
507
+ * it can already tell is invalid, and an invalid one here is a fault in this CLI, so it exits
508
+ * {@link EXIT.INTERNAL} rather than blaming the repository;
509
+ * 3. decide which config the rest of `init` writes against. An existing file is **always** kept and
510
+ * is that config; only `--reset-config` rebuilds it, in which case the write engine copies it to
511
+ * `<path>.bak` and the fresh one takes effect. `--force` does not enter into it — it upgrades
512
+ * the other `create-if-absent` artifacts and stops at this one, which is exactly what makes
513
+ * *edit the config, re-run `init --force`* regenerate the hook, the profile and the stubs from
514
+ * the edit rather than from a file rebuilt over it. `--dry-run` does not enter into it either:
515
+ * the outcome is computed the same way in both modes, which is what makes a dry run a faithful
516
+ * preview — it reaches this generator only for the **tense** of the start-over note, which would
517
+ * otherwise claim a rebuild and a `.bak` that a preview never made ({@link
518
+ * HarnessConfigOptions.dryRun});
519
+ * 4. enqueue. Always — including when the file is kept, so the run's action log records that it was
520
+ * seen and left alone rather than silently skipped.
521
+ *
522
+ * The write itself, the `.bak` and the `--dry-run` suppression all belong to the write engine; this
523
+ * generator has no filesystem-mutating call.
524
+ */
525
+ export function writeHarnessConfig({ repoRoot, detection, preset, flags, plan, resetConfig = false, dryRun = false, appDirSource = 'default', askDriver, }) {
526
+ const warnings = [];
527
+ const notes = [];
528
+ // Both held back until `kept` is known, and published or dropped there — one list per stream
529
+ // ({@link BuildConfigOptions.warnIfWritten}, {@link BuildConfigOptions.noteIfWritten}).
530
+ const ifWritten = [];
531
+ const notesIfWritten = [];
532
+ const generated = buildConfig({
533
+ repoRoot,
534
+ detection,
535
+ ...(preset === undefined ? {} : { preset }),
536
+ ...(askDriver === undefined ? {} : { askDriver }),
537
+ flags,
538
+ warn: (message) => warnings.push(message),
539
+ warnIfWritten: (message) => ifWritten.push(message),
540
+ noteIfWritten: (message) => notesIfWritten.push(message),
541
+ });
542
+ // Errors only. A warning is expected on a generated config — an undetected command's placeholder
543
+ // is one — and the preset builder has already reported the ones an adopter can act on, so
544
+ // repeating them here would print each twice.
545
+ const problems = checkConfigShape(generated);
546
+ if (hasErrors(problems)) {
547
+ const detail = problems
548
+ .filter((problem) => problem.severity === 'error')
549
+ .map(formatProblem)
550
+ .join('; ');
551
+ throw new HarnessError(`init assembled a ${CONFIG_FILENAME} that is already invalid, so nothing was written: ${detail}. This is a fault in this CLI rather than in the repository it was run against`, EXIT.INTERNAL);
552
+ }
553
+ const kept = !resetConfig && configExists(repoRoot);
554
+ let effective = generated;
555
+ if (kept) {
556
+ const loaded = loadConfig(repoRoot);
557
+ if (loaded.config === undefined || hasErrors(loaded.problems)) {
558
+ const detail = loaded.problems.map(formatProblem).join('; ');
559
+ throw new HarnessError(`${CONFIG_FILENAME} is already present at ${loaded.path} but cannot be used, and init will not overwrite it: ${detail}. Fix it, or re-run with --reset-config to copy it to ${CONFIG_FILENAME}.bak and regenerate`);
560
+ }
561
+ effective = loaded.config;
562
+ notes.push(`${CONFIG_FILENAME} already exists and was left exactly as it is, so the rest of init used the values in it; re-run with --reset-config to regenerate it, which copies the current file to ${CONFIG_FILENAME}.bak first`);
563
+ // What replaces the discarded config's **notes**, and the reason those are dropped rather than
564
+ // simply gated: exactly one of `resolveDefaultBranch`'s rungs fires on any run, so dropping it
565
+ // silently would leave the notes block unable to answer *what branch is this repository wired
566
+ // to* at all. This says it from `effective` — the file that is there — instead of from the
567
+ // detection this run threw away, which is the sentence the finding measured as a false report.
568
+ // It names the file rather than saying "that file", so it does not depend on standing next to
569
+ // the note above it in a block whose order is the caller's.
570
+ //
571
+ // `protectedBranches` falls back to the one branch that is protected whatever the key lists
572
+ // (`config/model.ts`), so an absent key is reported as what the guards actually match rather
573
+ // than as the schema's own default, which need not be this file's `defaultBranch`.
574
+ notes.push(`${CONFIG_FILENAME} wires this repository to defaultBranch ${JSON.stringify(effective.defaultBranch)} and protects ${JSON.stringify(effective.protectedBranches ?? [effective.defaultBranch])}${keptQaDriverClause(effective)}, whatever this run's flags and detection resolved: change one of them with \`${CLI} config set <key> <value>\`, which changes that one key — or \`${CLI} init --reset-config\`, which rebuilds ${CONFIG_FILENAME} from detection and the flags, after a .bak`);
575
+ // What replaces the discarded config's warnings rather than leaving the kept path silent: the
576
+ // problems of the file that is actually there, from the same checker `doctor`'s `config` check
577
+ // reports them with. Errors are already the refusal above, so this is the warning severity only
578
+ // — a kept key still holding the placeholder is named as a placeholder *in that file*, which is
579
+ // the sentence the dropped generated ones only looked like.
580
+ warnings.push(...loaded.problems.filter((problem) => problem.severity === 'warning').map(formatProblem));
581
+ }
582
+ // Published only where the generated config is the one that lands. On a kept-config re-run the
583
+ // value this run resolved is not the value the repository carries, so the sentence's own claim —
584
+ // that every interactive-test dispatch returns a blocker — would be false of the repository in
585
+ // front of the adopter, three lines below a note saying the file was left exactly as it is. What
586
+ // is *in* a kept config is `doctor`'s `browser-wiring` check to report, on every run.
587
+ if (!kept)
588
+ warnings.push(...ifWritten);
589
+ // The note stream's half of the same gate, published at the same point and for the same reason:
590
+ // every one of these asserts what a key was written as, and a kept re-run wrote no key. The kept
591
+ // path's replacement is pushed above, so the block says what is in force rather than nothing.
592
+ if (!kept)
593
+ notes.push(...notesIfWritten);
594
+ // A **note**, not a warning: a start-over that was asked for is not something needing attention.
595
+ // It is pushed only when there was a file to rebuild — on a first `init` the flag changes nothing
596
+ // and there is nothing to say. What it has to say is what the rebuild did *not* do: it did not
597
+ // restore the previous file, and it did not regenerate everything else in the repository.
598
+ //
599
+ // Two texts, because three of its claims — the rebuild, the `.bak` the previous file "is at", and
600
+ // the artifacts written from the rebuilt config — are things `--dry-run` does not do, and a note
601
+ // that states them anyway reports a success a real run would not have had (`docs/cli.md` §1).
602
+ // Only the **tense** splits: the `if` above and the `kept` it mirrors stay mode-independent, which
603
+ // is what step 3's JSDoc promises and what makes the preview faithful.
604
+ if (resetConfig && configExists(repoRoot)) {
605
+ // The `appDir` half is **reported, not asserted**: it is re-read from the file being rebuilt
606
+ // only where that file answered ({@link HarnessConfigOptions.appDirSource}). On the other two
607
+ // rungs the sentence names the directory the layer scopes were actually derived under, which is
608
+ // the fact an adopter acts on; *why* rung 2 did not answer is the resolver's warning to give,
609
+ // and it gives one whenever there was a file to answer from (`commands/init.ts`).
610
+ const appDirClause = appDirSource === 'config'
611
+ ? `appDir is re-read from the file being rebuilt, so every layer scope is re-derived under the same application directory`
612
+ : `appDir is ${JSON.stringify(detection.context.appDir)} — ${appDirSource === 'flag' ? 'the directory --app-dir named on this command line' : 'the repository root, because the file being rebuilt supplied no appDir this run could use'} — so every layer scope is re-derived under that directory`;
613
+ const consequence = `It is re-derived rather than restored: ${appDirClause}, but any value that lived only in the edited file — a phase toggle, an added protectedBranches entry, a corrected command line — becomes whatever detection and the flags say, so anything you meant to keep has to be given as a flag on this run or set again afterwards with \`config set\`.`;
614
+ // The closing clause carries one exception, and it is stated here because this note is the
615
+ // enumeration of what a rebuild costs. A guard still enforcing the pre-rebuild set is a state
616
+ // the rebuild itself creates, so the run that creates it is the run that clears it
617
+ // (`generators/githooks.ts`); which branch sets those were is that generator's own note to give,
618
+ // and is not restated here.
619
+ notes.push(dryRun
620
+ ? `${CONFIG_FILENAME} would be rebuilt from stack detection and this command line because --reset-config was given, and the file that is there would be copied to ${CONFIG_FILENAME}.bak first. ${consequence} Everything generated after it would be written from the rebuilt config, while the create-if-absent artifacts already on disk would be kept unless --force was given too — with one exception, the pre-push guard: where its case label no longer matched the set the rebuilt config resolves it would be re-rendered from that config, after being copied to a .bak beside it, and where the label could not be read at all it would be left exactly as it is. This was a dry run, so nothing was written and nothing was backed up`
621
+ : `${CONFIG_FILENAME} was rebuilt from stack detection and this command line because --reset-config was given, and the file that was there is at ${CONFIG_FILENAME}.bak. ${consequence} Everything generated after it in this run was written from the rebuilt config, while the create-if-absent artifacts already on disk were kept unless --force was given too — with one exception, the pre-push guard: where its case label no longer matched the set the rebuilt config resolves it was re-rendered from that config, after being copied to a .bak beside it, and where the label could not be read at all it was left exactly as it is`);
622
+ }
623
+ // The request answers the overwrite question itself in **both** directions (`config/io.ts`), which
624
+ // is what takes this one artifact out of the run's `--force` entirely: `'never'` keeps the file a
625
+ // `--force` run would otherwise have replaced — and replaced *before* every artifact that reads it
626
+ // was regenerated from the replacement — while `'always'` lets `--reset-config` rebuild it after a
627
+ // `.bak` on a run with no `--force` at all. Neither arm consults the flag, so the contract is
628
+ // structural rather than a condition a later edit could re-introduce.
629
+ const path = saveConfig(repoRoot, generated, plan, { forceOverride: kept ? 'never' : 'always' });
630
+ return { config: effective, path, kept, warnings, notes };
631
+ }
632
+ //# sourceMappingURL=harnessConfig.js.map