autonomous-sdlc-harness 0.1.0 → 0.4.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 (121) hide show
  1. package/README.md +4 -3
  2. package/dist/cli.js +0 -0
  3. package/dist/commands/docs.js +220 -0
  4. package/dist/commands/docs.js.map +1 -0
  5. package/dist/commands/doctor.js +32 -11
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/init.js +240 -64
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/registry.js +2 -0
  10. package/dist/commands/registry.js.map +1 -1
  11. package/dist/config/check.js +36 -9
  12. package/dist/config/check.js.map +1 -1
  13. package/dist/config/model.js +43 -1
  14. package/dist/config/model.js.map +1 -1
  15. package/dist/core/git.js +29 -0
  16. package/dist/core/git.js.map +1 -1
  17. package/dist/core/layerGapRemedy.js +3 -2
  18. package/dist/core/layerGapRemedy.js.map +1 -1
  19. package/dist/core/paths.js +22 -2
  20. package/dist/core/paths.js.map +1 -1
  21. package/dist/core/pluginIdentity.js +33 -0
  22. package/dist/core/pluginIdentity.js.map +1 -0
  23. package/dist/core/prompt.js +6 -2
  24. package/dist/core/prompt.js.map +1 -1
  25. package/dist/core/report.js +9 -0
  26. package/dist/core/report.js.map +1 -1
  27. package/dist/core/writer.js +24 -5
  28. package/dist/core/writer.js.map +1 -1
  29. package/dist/detect/presets.js +35 -26
  30. package/dist/detect/presets.js.map +1 -1
  31. package/dist/detect/signals.js +13 -9
  32. package/dist/detect/signals.js.map +1 -1
  33. package/dist/doctor/checks.js +483 -38
  34. package/dist/doctor/checks.js.map +1 -1
  35. package/dist/generators/claudeContext.js +4 -5
  36. package/dist/generators/claudeContext.js.map +1 -1
  37. package/dist/generators/githubWorkflows.js +66 -0
  38. package/dist/generators/githubWorkflows.js.map +1 -0
  39. package/dist/generators/harnessConfig.js +13 -5
  40. package/dist/generators/harnessConfig.js.map +1 -1
  41. package/dist/generators/notifications.js +105 -19
  42. package/dist/generators/notifications.js.map +1 -1
  43. package/dist/generators/outerLoopScripts.js +30 -3
  44. package/dist/generators/outerLoopScripts.js.map +1 -1
  45. package/dist/generators/permissionProfile.js +152 -21
  46. package/dist/generators/permissionProfile.js.map +1 -1
  47. package/dist/generators/projectSettings.js +5 -15
  48. package/dist/generators/projectSettings.js.map +1 -1
  49. package/dist/generators/repoRoot.js +148 -25
  50. package/dist/generators/repoRoot.js.map +1 -1
  51. package/dist/generators/scripts.js +4 -1
  52. package/dist/generators/scripts.js.map +1 -1
  53. package/dist/generators/stateDir.js +9 -3
  54. package/dist/generators/stateDir.js.map +1 -1
  55. package/dist/machine/paths.js +19 -4
  56. package/dist/machine/paths.js.map +1 -1
  57. package/dist/machine/plugins.js +2 -1
  58. package/dist/machine/plugins.js.map +1 -1
  59. package/dist/remote/githubActions.js +86 -0
  60. package/dist/remote/githubActions.js.map +1 -0
  61. package/dist/retrieval/chunk.js +158 -0
  62. package/dist/retrieval/chunk.js.map +1 -0
  63. package/dist/retrieval/corpus.js +75 -0
  64. package/dist/retrieval/corpus.js.map +1 -0
  65. package/dist/retrieval/models.js +175 -0
  66. package/dist/retrieval/models.js.map +1 -0
  67. package/dist/retrieval/queryLog.js +70 -0
  68. package/dist/retrieval/queryLog.js.map +1 -0
  69. package/dist/retrieval/refresh.js +55 -0
  70. package/dist/retrieval/refresh.js.map +1 -0
  71. package/dist/retrieval/runtime.js +161 -0
  72. package/dist/retrieval/runtime.js.map +1 -0
  73. package/dist/retrieval/search.js +129 -0
  74. package/dist/retrieval/search.js.map +1 -0
  75. package/dist/retrieval/server.js +197 -0
  76. package/dist/retrieval/server.js.map +1 -0
  77. package/dist/retrieval/session.js +41 -0
  78. package/dist/retrieval/session.js.map +1 -0
  79. package/dist/retrieval/setup.js +121 -0
  80. package/dist/retrieval/setup.js.map +1 -0
  81. package/dist/retrieval/store.js +170 -0
  82. package/dist/retrieval/store.js.map +1 -0
  83. package/package.json +22 -3
  84. package/templates/README.md +3 -2
  85. package/templates/claude/CLAUDE.md +4 -4
  86. package/templates/claude/README.md +3 -1
  87. package/templates/claude/context/conventions.md +1 -1
  88. package/templates/claude/context/layer.md +1 -1
  89. package/templates/claude/push-notify.env.example +7 -2
  90. package/templates/claude/settings.autonomous.json +1 -1
  91. package/templates/claude/settings.autonomous.retrieval.json +9 -0
  92. package/templates/github/workflows/harness-resume.yml +124 -0
  93. package/templates/github/workflows/harness-run.yml +446 -0
  94. package/templates/repo/README.md +2 -0
  95. package/templates/repo/gitignore +6 -0
  96. package/templates/repo/gitignore.retrieval +2 -0
  97. package/templates/repo/mcp.retrieval.json +11 -0
  98. package/templates/scripts/README.md +1 -1
  99. package/templates/scripts/autonomous-notify.sh +10 -4
  100. package/templates/scripts/autonomous-watcher.sh +1445 -221
  101. package/templates/scripts/cleanup-merged-worktrees.sh +126 -8
  102. package/templates/scripts/docs-search-server.sh +64 -0
  103. package/templates/scripts/flow-walker.sh +629 -0
  104. package/templates/scripts/flows/task_plan_writing.graph.json +192 -0
  105. package/templates/scripts/lib/flow-walker-gates.sh +165 -0
  106. package/templates/scripts/lib/harness-run-lib.sh +452 -21
  107. package/templates/scripts/remote-run.sh +1785 -0
  108. package/templates/scripts/restart-watcher.sh +24 -3
  109. package/templates/scripts/run-test-suite.sh +182 -0
  110. package/templates/scripts/scratch-run.sh +2 -1
  111. package/templates/state-dir/README-root.md +1 -1
  112. package/templates/state-dir/autonomous_logs/README.md +1 -1
  113. package/templates/state-dir/business_parity_reviews/README.md +1 -1
  114. package/templates/state-dir/clarification_digests/README.md +1 -1
  115. package/templates/state-dir/clarifications/README.md +4 -4
  116. package/templates/state-dir/improvement_observations/README.md +2 -0
  117. package/templates/state-dir/scratch/README.md +1 -1
  118. package/templates/state-dir/test_fix_plan_reviews/README.md +9 -0
  119. package/templates/state-dir/test_fix_plans/README.md +9 -0
  120. package/templates/state-dir/test_fix_point_reviews/README.md +9 -0
  121. package/templates/state-dir/test_run_logs/README.md +11 -0
@@ -7,7 +7,7 @@
7
7
  * reproducible: a second `init` over the same repository produces the same result, and an adopter can
8
8
  * re-derive the recorded preset from the detection table by hand. It is also why judgement-dependent
9
9
  * work — reading real code to refine a layer profile, filling a conventions stub — belongs to
10
- * `/harness-analyze` and is deliberately absent from this file. The offer to run it
10
+ * `/autonomous-sdlc-harness:harness-analyze` and is deliberately absent from this file. The offer to run it
11
11
  * ({@link resolveAnalyzeOffer}) adds no model call and no external process here: the answer's whole
12
12
  * material effect is which banner wording the generated always-loaded file carries and which closing
13
13
  * pointer this run prints.
@@ -91,7 +91,7 @@ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
91
91
  import { join, relative, resolve } from 'node:path';
92
92
  import { formatProblem } from '../config/check.js';
93
93
  import { configExists, loadConfig } from '../config/io.js';
94
- import { answersNone, asQaDriver, browserWiringApplies, CONFIG_FILENAME, DEFAULTS, isPlaceholder, qaDriverChoices, STATE_DIR_DOT_PATTERN, } from '../config/model.js';
94
+ import { answersNone, asQaDriver, browserWiringApplies, CONFIG_FILENAME, DEFAULTS, isPlaceholder, qaDriverChoices, retrievalApplies, STATE_DIR_DOT_PATTERN, } from '../config/model.js';
95
95
  import { EXIT, HarnessError } from '../core/errors.js';
96
96
  import { commitAll, hasCommits, initRepository, probeRepoRoot } from '../core/git.js';
97
97
  import { readJsonFile } from '../core/json.js';
@@ -99,7 +99,8 @@ import { layerCoverage } from '../core/layerCoverage.js';
99
99
  import { layerGapRemedy, recordedVerdictClause } from '../core/layerGapRemedy.js';
100
100
  import { nameList } from '../core/nameList.js';
101
101
  import { insideRepo, packageRoot } from '../core/paths.js';
102
- import { askLine, askYesNo, canPrompt } from '../core/prompt.js';
102
+ import { ANALYZE_COMMAND } from '../core/pluginIdentity.js';
103
+ import { askLine, askYesNo, canPrompt, REPROMPT_LIMIT } from '../core/prompt.js';
103
104
  import { normalizeRepoDir, normalizeRepoPathStrict } from '../core/repoPaths.js';
104
105
  import { WritePlan } from '../core/writer.js';
105
106
  import { findNestedApplicationDir } from '../detect/nestedApplication.js';
@@ -107,38 +108,21 @@ import { buildPreset, commandSourceClaim, searchedRoots, SHARED_CONVENTIONS_PATH
107
108
  import { detectPreset, parsePresetName, FLAT_FALLBACK_SIGNAL_ID, FORCED_SIGNAL_ID, } from '../detect/signals.js';
108
109
  import { writeClaudeContext, CLAUDE_MD_PATH, RESERVED_ANALYZE_TARGETS, SETUP_PENDING_OPEN, isUntouchedSkeletonText, } from '../generators/claudeContext.js';
109
110
  import { pointHooksPath, writeGitHooks } from '../generators/githooks.js';
111
+ import { writeGithubWorkflows } from '../generators/githubWorkflows.js';
110
112
  import { writeHarnessConfig } from '../generators/harnessConfig.js';
111
- import { writeNotifications, GUIDED_ENDPOINT_EXAMPLE } from '../generators/notifications.js';
113
+ import { writeNotifications, PUSH_DESTINATION_FORMS, PUSH_DESTINATION_PLACEHOLDER, resolvePushDestination, UNRECOGNISED_DESTINATION_NOTE, } from '../generators/notifications.js';
112
114
  import { writeOuterLoopScripts } from '../generators/outerLoopScripts.js';
113
- import { writePermissionProfile } from '../generators/permissionProfile.js';
114
- import { writeProjectSettings, MARKETPLACE_FLAG, MARKETPLACE_NAME, MARKETPLACES_KEY, PLUGIN_NAME, SETTINGS_PATH, SLUG_SHAPE, } from '../generators/projectSettings.js';
115
+ import { PLUGIN_ROOT_ENTRIES_FLAG, pluginRootEntriesNote, writePermissionProfile, } from '../generators/permissionProfile.js';
116
+ import { writeProjectSettings, MARKETPLACE_FLAG, MARKETPLACE_NAME, MARKETPLACES_KEY, SETTINGS_PATH, SLUG_SHAPE, } from '../generators/projectSettings.js';
115
117
  import { writeRepoRootFiles, MCP_PATH } from '../generators/repoRoot.js';
116
118
  import { configKeyPath, configuredCommand, wrapperCommandLine, wrapperPath, writeWrapperScripts, WRAPPER_SCRIPTS, } from '../generators/scripts.js';
117
119
  import { writeStateDir } from '../generators/stateDir.js';
120
+ import { API_KEY_SECRET, GIT_TOKEN_SECRET, OAUTH_TOKEN_SECRET, PUSH_URL_SECRET, RUNNER_VARIABLE, } from '../remote/githubActions.js';
121
+ import { setUpRetrieval } from '../retrieval/setup.js';
118
122
  /** The command's one-line summary, in the usage block and at the head of its own `--help`. */
119
123
  const SUMMARY = 'Wire a repository: config, scripts, state tree, permission profile, project settings, conventions stubs';
120
124
  /** The roadmap item this command belongs to, as the registry reports it. */
121
125
  const ROADMAP_ITEM = 13;
122
- /** The analyze command's own name, without the leading `/` and without the plugin prefix. */
123
- const ANALYZE_COMMAND_NAME = 'harness-analyze';
124
- /** The analyze command as a line addressing the adopter names it — step C of the install story. */
125
- const ANALYZE_COMMAND = `/${ANALYZE_COMMAND_NAME}`;
126
- /**
127
- * The same command, spelled as something **executed** rather than read.
128
- *
129
- * A session's `/` picker lists every command of this plugin under the plugin's own prefix and
130
- * fuzzy-matches a bare name onto it, so {@link ANALYZE_COMMAND} reaches the command wherever a
131
- * person types it into a session. A command passed as a session's **first message** — which is
132
- * what {@link ANALYZE_INVOCATION} does — meets no picker and is matched exactly, so the bare
133
- * form fails there with `Unknown command`. That the **prefixed** form succeeds where the bare
134
- * one fails is not established: the only measurement in this tree is headless and negative on
135
- * both spellings (root `README.md`, `### Measured while building that evidence, and not fixed
136
- * here`), and the interactive first-message form waits on the hand-run gate — if that comes back
137
- * negative the printed line is dropped rather than respelled (`docs/analyze.md` §9). The prefix
138
- * is taken from {@link PLUGIN_NAME}, which mirrors the plugin manifest, rather than written out
139
- * here.
140
- */
141
- const ANALYZE_COMMAND_QUALIFIED = `/${PLUGIN_NAME}:${ANALYZE_COMMAND_NAME}`;
142
126
  /**
143
127
  * How this CLI is typed, for every line that tells an adopter to run something — stated here once
144
128
  * for the whole command surface.
@@ -181,6 +165,8 @@ const DAEMON_INSTALL_COMMAND = `${CLI} daemon install`;
181
165
  * `browserWiringApplies` holds (`doctor/checks.ts`'s browser-wiring check, `commands/doctor.ts`).
182
166
  */
183
167
  const DOCTOR_CHECK_REGISTRY_COMMAND = `${DOCTOR_COMMAND} --check-registry`;
168
+ /** The online check of GitHub-side setup, named last in the remote-execution block ({@link reportGithubSteps}). */
169
+ const DOCTOR_CHECK_GITHUB_COMMAND = `${DOCTOR_COMMAND} --check-github`;
184
170
  /**
185
171
  * How `.mcp.json` launches those servers, as the sentence names it.
186
172
  *
@@ -199,7 +185,27 @@ const MCP_LAUNCHER = 'npx -y';
199
185
  */
200
186
  const AGENT_CLI = 'claude';
201
187
  const MARKETPLACE_ADD_COMMAND = `${AGENT_CLI} plugin marketplace add ${SLUG_SHAPE}`;
202
- const ANALYZE_INVOCATION = `${AGENT_CLI} "${ANALYZE_COMMAND_QUALIFIED}"`;
188
+ /**
189
+ * Step C of the install story as a first message: {@link ANALYZE_COMMAND} is the one spelling both
190
+ * for the name a person types into an open session and for this. The interactive first-message form
191
+ * waits on the hand-run gate — if that comes back negative the printed line is dropped rather than
192
+ * respelled (`docs/analyze.md` §9).
193
+ *
194
+ * Headless first-message leg, re-measured: Claude Code 2.1.274, from this checkout with the plugin
195
+ * enabled; the `system`/`init` event of both runs listed `autonomous-sdlc-harness:harness-analyze`
196
+ * among its slash commands and did not list `harness-analyze`.
197
+ * - `claude -p "/autonomous-sdlc-harness:harness-analyze" --permission-mode plan --max-turns 2 --output-format stream-json --verbose`:
198
+ * exit 1; no `Skill` call, the first tool call was the command's own `Bash` read of the
199
+ * unfilled-stub markers; final `result` event `is_error: true`, `result: null`, verbatim message
200
+ * `Reached maximum number of turns (2)`.
201
+ * - `claude -p "/harness-analyze" --permission-mode plan --max-turns 2 --output-format stream-json --verbose`:
202
+ * exit 1; the first tool call was `Skill` naming `autonomous-sdlc-harness:harness-analyze`, which
203
+ * loaded the command; final `result` event `is_error: true`, `result: null`, verbatim message
204
+ * `Reached maximum number of turns (2)`.
205
+ * Both exits are the two-turn cap, not a resolution failure: the prefixed spelling ran the command
206
+ * directly, and the bare one reached it only through the model choosing the `Skill` tool.
207
+ */
208
+ const ANALYZE_INVOCATION = `${AGENT_CLI} "${ANALYZE_COMMAND}"`;
203
209
  /** The verb that applies a layer-profile revision — the one writer of `layers[]` (`docs/analyze.md` §3). */
204
210
  const CONFIG_SET_LAYERS_COMMAND = `${CLI} config set layers`;
205
211
  /** The one-key form of the same verb, named in step 3 for the two run settings (`commands/config.ts`). */
@@ -214,6 +220,22 @@ const CONFIG_SET_COMMAND = `${CLI} config set <key> <value>`;
214
220
  */
215
221
  const MARKETPLACE_MANIFEST = join('.claude-plugin', 'marketplace.json');
216
222
  const CLI_MANIFEST = join('cli', 'package.json');
223
+ /**
224
+ * The environment variable that lets a run wire the harness's own repository on purpose.
225
+ *
226
+ * **Self-adoption** — running the harness's own flows against the harness while developing it — is a
227
+ * contributor workflow, so the escape is an environment variable rather than a flag. A flag would
228
+ * appear in `--help` and owe a row in `docs/cli.md`'s table, where the only reader it could reach is
229
+ * an adopter, for whom it can never mean anything but a mistake. It is documented in
230
+ * `docs/development.md` instead, beside the gates that are the rest of that audience's contract.
231
+ *
232
+ * **A variable rather than a one-off source edit**, which is the other way to get one run through,
233
+ * because `init` is not one-shot: `--force` regenerates a generated file when `cli/templates/`
234
+ * changes and `--reset-config` rebuilds the configuration, so the run this exists for recurs every
235
+ * time a template moves. An edit-build-revert-build cycle per template change is a standing invitation
236
+ * to ship the CLI with its guard removed, and the test that would catch it only runs if it is run.
237
+ */
238
+ const SELF_ADOPT_ENV = 'HARNESS_SELF_ADOPT';
217
239
  /** The flag whose value the dot-directory refusal is raised against. */
218
240
  const STATE_DIR_FLAG = '--state-dir';
219
241
  /** The flag that answers the git gate — named in the prompt, in the refusal, and in the note. */
@@ -225,6 +247,18 @@ const GIT_INIT_FLAG = '--git-init';
225
247
  const APP_DIR_FLAG = '--app-dir';
226
248
  /** The flag that answers the QA-driver question — named in the prompt, the refusal and the note. */
227
249
  const QA_DRIVER_FLAG = '--qa-driver';
250
+ /** The flag that answers the docs-retrieval question, read by its option row, its refusal and its prompt. */
251
+ const DOCS_RETRIEVAL_FLAG = '--docs-retrieval';
252
+ /**
253
+ * The second accepted spelling of that flag, in the word an adopter arrives with. It sets the same
254
+ * key; {@link DOCS_RETRIEVAL_FLAG} stays the canonical one the prompt and the documentation name.
255
+ *
256
+ * A constant rather than a literal for the reason every flag above is one: the `--help` row, the
257
+ * parser's lookup and the messages that name it to a reader are one spelling read from one owner,
258
+ * and it reaches all three through {@link InitOption.aliases} rather than through a second accepted-
259
+ * token list beside {@link INIT_OPTIONS}.
260
+ */
261
+ const DOCS_RETRIEVAL_ALIAS_FLAG = '--rag';
228
262
  /**
229
263
  * The two spellings that answer the analyze offer — one accept, one decline, and neither given is
230
264
  * the state {@link resolveAnalyzeOffer} answers with the documented default.
@@ -266,8 +300,9 @@ function initOptions(rows, ..._rowsMissingFor) {
266
300
  * then the two that decide what is detected, then the values written into the config, then the three
267
301
  * phase toggles with their own inputs beside them, then the onboarding slug, then the pair that
268
302
  * answers the offer to analyze this repository — which decides the wording the generated
269
- * always-loaded file carries — and last the pair that decides whether this account gets told when an
270
- * unattended run finishes, which is the one pair that writes nothing into the repository at all.
303
+ * always-loaded file carries — then the pair that decides whether this account gets told when an
304
+ * unattended run finishes, which is the one pair that writes nothing into the repository at all, and
305
+ * last the switch that adds this machine's plugin-root entries to a freshly generated profile.
271
306
  *
272
307
  * The first two sit together, and ahead of everything else, because they are the rows whose subject
273
308
  * is the **shape of the run** rather than a value in the generated file: one settles what `init` is
@@ -361,6 +396,14 @@ const INIT_OPTIONS = initOptions([
361
396
  summary: 'Documentation root the docs phase keeps current (with --docs)',
362
397
  configValue: 'docs.root',
363
398
  },
399
+ {
400
+ key: 'docsRetrieval',
401
+ flag: DOCS_RETRIEVAL_FLAG,
402
+ aliases: [DOCS_RETRIEVAL_ALIAS_FLAG],
403
+ kind: 'switch',
404
+ summary: 'Turn RAG (docs retrieval) on: a local search tool over the docs and conventions (with --docs)',
405
+ configValue: 'docs.retrieval',
406
+ },
364
407
  {
365
408
  key: 'parity',
366
409
  flag: '--parity',
@@ -414,10 +457,24 @@ const INIT_OPTIONS = initOptions([
414
457
  key: 'pushUrl',
415
458
  flag: PUSH_URL_FLAG,
416
459
  kind: 'value',
417
- placeholder: '<url>',
418
- summary: 'Endpoint unattended-run notifications are posted to (with --notifications)',
460
+ placeholder: PUSH_DESTINATION_PLACEHOLDER,
461
+ summary: 'Where notifications are posted: an ntfy topic name, or the full http:// or https:// URL of any endpoint that accepts a POST (with --notifications)',
462
+ },
463
+ {
464
+ key: 'pluginRootEntries',
465
+ flag: PLUGIN_ROOT_ENTRIES_FLAG,
466
+ kind: 'switch',
467
+ summary: "Include this machine's plugin-root permission entries when the profile is generated (for a remote job)",
419
468
  },
420
469
  ]);
470
+ /**
471
+ * Every token a row accepts, canonical first: its flag and any {@link InitOption.aliases}. The one
472
+ * reader of that field, so the parser, the `--help` block and the discarded-flag warning cannot
473
+ * disagree about which spellings exist.
474
+ */
475
+ function optionSpellings(option) {
476
+ return [option.flag, ...(option.aliases ?? [])];
477
+ }
421
478
  /**
422
479
  * A flag value as a reader can retype it: quoted only where a shell would otherwise split or expand
423
480
  * it, so the remedy below stays copy-pasteable for an ordinary branch name and stays correct for a
@@ -457,16 +514,19 @@ function discardedConfigFlagsWarning(flags) {
457
514
  if (configKey === undefined)
458
515
  continue;
459
516
  const steersDetection = option.steersDetection === true;
517
+ // The naming carries every accepted spelling, so a reader who typed an alias recognises the line
518
+ // as theirs; the re-invocation stays on the canonical one, because that half is pasted back.
519
+ const named = option.aliases === undefined ? option.flag : `${option.flag} (or ${option.aliases.join(', ')})`;
460
520
  if (option.kind === 'switch') {
461
521
  if (flags[option.key] === true) {
462
- dropped.push({ flag: option.flag, configKey, invocation: option.flag, steersDetection });
522
+ dropped.push({ flag: named, configKey, invocation: option.flag, steersDetection });
463
523
  }
464
524
  continue;
465
525
  }
466
526
  const value = flags[option.key];
467
527
  if (value === undefined)
468
528
  continue;
469
- dropped.push({ flag: option.flag, configKey, invocation: `${option.flag} ${retypable(value)}`, steersDetection });
529
+ dropped.push({ flag: named, configKey, invocation: `${option.flag} ${retypable(value)}`, steersDetection });
470
530
  }
471
531
  if (dropped.length === 0)
472
532
  return undefined;
@@ -478,9 +538,12 @@ function discardedConfigFlagsWarning(flags) {
478
538
  : ` ${steering.join(', ')} also steer${steering.length === 1 ? 's' : ''} stack detection, and still did on this run: what \`--force\` then re-renders from that detection is the wrapper scripts, not this file.`;
479
539
  return `${dropped.map((entry) => entry.flag).join(', ')} ${one ? 'was' : 'were'} given, and ${CONFIG_FILENAME} was read rather than written on this run: ${one ? 'that flag supplies a value' : 'each of those flags supplies a value'} for a key of that file, so no such value was written and the keys in the file are the ones every generator after it read.${steeringClause} Apply ${one ? 'it' : 'them'} by rebuilding the file from detection and this command line with \`${INIT_VERB} --reset-config ${dropped.map((entry) => entry.invocation).join(' ')}\`, which copies the file that is there to a .bak first and is the only route that also re-derives what a value implies — or change ${one ? 'the key' : 'the keys'} alone with ${keyRemedies}`;
480
540
  }
481
- /** The command's own `Options:` rows, invocation-aligned, derived from {@link INIT_OPTIONS}. */
541
+ /**
542
+ * The command's own `Options:` rows, invocation-aligned, derived from {@link INIT_OPTIONS}. A row
543
+ * with aliases prints every spelling it accepts, so no accepted token is absent from `--help`.
544
+ */
482
545
  function initOptionLines() {
483
- const invocations = INIT_OPTIONS.map((option) => `${option.flag}${option.kind === 'switch' ? '' : ` ${option.placeholder}`}`);
546
+ const invocations = INIT_OPTIONS.map((option) => `${optionSpellings(option).join(', ')}${option.kind === 'switch' ? '' : ` ${option.placeholder}`}`);
484
547
  const width = Math.max(...invocations.map((invocation) => invocation.length));
485
548
  return INIT_OPTIONS.map((option, index) => ` ${invocations[index].padEnd(width)} ${option.summary}`);
486
549
  }
@@ -523,13 +586,15 @@ function parseQaDriver(value) {
523
586
  }
524
587
  function parseInitFlags(argv) {
525
588
  const values = new Map();
526
- const switches = new Set();
589
+ // The spelling each switch was typed as, not merely that it was given: a row accepting an alias
590
+ // ({@link InitOption.aliases}) must be named back to the reader in the words they used.
591
+ const switches = new Map();
527
592
  for (let index = 0; index < argv.length; index += 1) {
528
593
  const token = argv[index];
529
594
  const separator = token.startsWith('--') ? token.indexOf('=') : -1;
530
595
  const name = separator > 0 ? token.slice(0, separator) : token;
531
596
  const inlineValue = separator > 0 ? token.slice(separator + 1) : undefined;
532
- const option = INIT_OPTIONS.find((candidate) => candidate.flag === name);
597
+ const option = INIT_OPTIONS.find((candidate) => optionSpellings(candidate).includes(name));
533
598
  if (option === undefined) {
534
599
  throw new HarnessError(token.startsWith('-')
535
600
  ? `init: unknown option ${JSON.stringify(name)} — run \`${INIT_VERB} --help\` for the options it takes`
@@ -538,7 +603,7 @@ function parseInitFlags(argv) {
538
603
  if (option.kind === 'switch') {
539
604
  if (inlineValue !== undefined)
540
605
  throw new HarnessError(`init: ${name} does not take a value`);
541
- switches.add(option.key);
606
+ switches.set(option.key, name);
542
607
  continue;
543
608
  }
544
609
  let value = inlineValue;
@@ -568,6 +633,22 @@ function parseInitFlags(argv) {
568
633
  if (switches.has('analyze') && switches.has('noAnalyze')) {
569
634
  throw new HarnessError(`init: ${ANALYZE_FLAG} and ${NO_ANALYZE_FLAG} answer the same question opposite ways and both were given: pass one, or neither — with neither, the documented default accepts the offer`);
570
635
  }
636
+ // Refused rather than warned, unlike `--qa-driver` without `--qa`: `docs.retrieval: true` without
637
+ // `phases.docs` is a config-check error, so the generated config would fail the write guard.
638
+ if (switches.has('docsRetrieval') && !switches.has('docs')) {
639
+ // Named as it was typed: a run refused for passing the alias is not told about a flag it never
640
+ // used. The fallback is unreachable while the key is set only from a token, and is here so the
641
+ // message has a spelling rather than `undefined` if that ever stops holding.
642
+ const typed = switches.get('docsRetrieval') ?? DOCS_RETRIEVAL_FLAG;
643
+ throw new HarnessError(`init: ${typed} needs --docs: retrieval searches the documentation corpus the docs phase maintains, so it is legal only with that phase on`);
644
+ }
645
+ // Checked here for the reason `qaDriver` is, whether or not --notifications was given: a bad value
646
+ // must not cost an adopter a repository this run created. The value is withheld from the message,
647
+ // unlike {@link parseQaDriver}'s, because a push destination is a credential and stderr is logged.
648
+ const pushUrl = values.get('pushUrl');
649
+ if (pushUrl !== undefined && resolvePushDestination(pushUrl).kind === 'unrecognised') {
650
+ throw new HarnessError(`init: ${PUSH_URL_FLAG} takes ${PUSH_DESTINATION_FORMS}; the value given is neither, so nothing was written. It is not repeated here, because a push destination is a credential — check it and pass it again`);
651
+ }
571
652
  return {
572
653
  ...Object.fromEntries([...values]),
573
654
  ...(qaDriver === undefined ? {} : { qaDriver: parseQaDriver(qaDriver) }),
@@ -578,7 +659,9 @@ function parseInitFlags(argv) {
578
659
  notifications: switches.has('notifications'),
579
660
  qa: switches.has('qa'),
580
661
  docs: switches.has('docs'),
662
+ docsRetrieval: switches.has('docsRetrieval'),
581
663
  parity: switches.has('parity'),
664
+ pluginRootEntries: switches.has('pluginRootEntries'),
582
665
  };
583
666
  }
584
667
  /** The `name` of a JSON manifest, or `undefined` when there is no readable one with a name. */
@@ -614,13 +697,22 @@ function ownPackageName() {
614
697
  * `docs/development.md` §5's gate 2 documents running the built CLI from this root, which was a
615
698
  * refusal check for as long as `init` refused everything. Once it works, that invocation would
616
699
  * generate a config, a state tree and a permission profile into the harness's own tree.
700
+ *
701
+ * It is a refusal and not a prohibition: {@link SELF_ADOPT_ENV} set to `1` is how a contributor asks
702
+ * for that wiring deliberately, and nothing else reaches it.
617
703
  */
618
704
  function assertNotHarnessOwnRepository(repoRoot) {
619
705
  if (!existsSync(join(repoRoot, MARKETPLACE_MANIFEST)))
620
706
  return;
621
707
  if (packageNameAt(join(repoRoot, CLI_MANIFEST)) !== ownPackageName())
622
708
  return;
623
- throw new HarnessError(`refusing to wire the harness's own repository; run \`${INIT_VERB}\` in the repository you want to adopt it. ${repoRoot} carries ${MARKETPLACE_MANIFEST} and a ${CLI_MANIFEST} naming this package, so it is the harness itself rather than an adopting project: wiring it would generate a ${CONFIG_FILENAME}, a run-artifact tree and a permission profile into the tree that ships them. Run ${INIT_VERB} from the adopting repository, or point it at one with --cwd <path>`);
709
+ // Checked **after** both identity tests rather than before them, which is what keeps the variable
710
+ // inert everywhere it is not wanted: every root that is not this one has already returned above,
711
+ // so a value left in a shell's environment changes the behaviour of no adopter's run. Only the
712
+ // exact `1` opens it — a variable that is merely *set* is too easy to inherit by accident.
713
+ if (process.env[SELF_ADOPT_ENV] === '1')
714
+ return;
715
+ throw new HarnessError(`refusing to wire the harness's own repository; run \`${INIT_VERB}\` in the repository you want to adopt it. ${repoRoot} carries ${MARKETPLACE_MANIFEST} and a ${CLI_MANIFEST} naming this package, so it is the harness itself rather than an adopting project: wiring it would generate a ${CONFIG_FILENAME}, a run-artifact tree and a permission profile into the tree that ships them. Run ${INIT_VERB} from the adopting repository, or point it at one with --cwd <path>. To wire this repository on purpose — the harness's own self-adoption, a contributor workflow \`docs/development.md\` documents — set ${SELF_ADOPT_ENV}=1`);
624
716
  }
625
717
  /**
626
718
  * Refuse a `--state-dir` that names, or reaches through, a dot-directory — **before** anything is
@@ -874,6 +966,21 @@ function askQaDriver(ctx) {
874
966
  defaultValue: DEFAULTS.qa.driver,
875
967
  }, promptCtx);
876
968
  }
969
+ /**
970
+ * Ask whether to turn docs retrieval on — or answer `undefined` on every run that cannot be asked, for
971
+ * {@link askQaDriver}'s reason: the generator notes an unasked run, and must not note a declined one.
972
+ */
973
+ function askRetrieval(ctx) {
974
+ const promptCtx = { flags: ctx.flags, report: ctx.report };
975
+ if (!canPrompt(promptCtx))
976
+ return undefined;
977
+ return askYesNo({
978
+ question: 'Turn on RAG (docs retrieval)? It adds a local search tool over the docs and conventions for the plan writer and reviewers. Setup installs about 300 MB of local runtime and downloads two small models into a cache shared by every checkout on this machine. Off by default.',
979
+ defaultAnswer: false,
980
+ flag: DOCS_RETRIEVAL_FLAG,
981
+ flagHint: 'to turn it on without being asked',
982
+ }, promptCtx);
983
+ }
877
984
  /**
878
985
  * The conventions documents this repository has: every distinct `layers[].conventions` value, plus
879
986
  * the shared cross-layer document whether or not a layer points at it.
@@ -1026,14 +1133,15 @@ function resolveAnalyzeOffer(ctx, flags, repoRoot, config) {
1026
1133
  *
1027
1134
  * The two questions are asked in order and the second only inside the first's yes, because an
1028
1135
  * endpoint is meaningless without the opt-in and the opt-in writes nothing without an endpoint. The
1029
- * endpoint question carries no default: `askLine` answers `undefined` with none, and `undefined` is
1030
- * what routes the run to the generator's guided-setup note rather than to a file — writing an empty
1031
- * machine-local file would shadow a repository-side one that already has values
1032
- * (`generators/notifications.ts`, choice 1).
1136
+ * endpoint question carries no default, and {@link askPushDestination} ends in one of three outcomes:
1137
+ * `undefined`, which routes the run to the generator's guided-setup note rather than to a file —
1138
+ * writing an empty machine-local file would shadow a repository-side one that already has values
1139
+ * (`generators/notifications.ts`, choice 1); a recognised destination, passed on as typed; or, after
1140
+ * the re-asks run out, the last unrecognised answer, on which the generator writes nothing and warns.
1033
1141
  *
1034
1142
  * A `--push-url` passed **without** the opt-in is left in place rather than dropped here: the
1035
1143
  * generator owns what that means and warns about it, as it owns every other line about the artifact
1036
- * it writes.
1144
+ * it writes. The flag always wins over asking.
1037
1145
  */
1038
1146
  function resolveNotifications(ctx, flags) {
1039
1147
  const promptCtx = { flags: ctx.flags, report: ctx.report };
@@ -1044,15 +1152,27 @@ function resolveNotifications(ctx, flags) {
1044
1152
  flag: NOTIFICATIONS_FLAG,
1045
1153
  flagHint: 'to set them up without being asked',
1046
1154
  }, promptCtx);
1047
- const pushUrl = flags.pushUrl ??
1048
- (enabled
1049
- ? askLine({
1050
- question: `Where should notifications be posted? (any endpoint that accepts a POST, e.g. ${GUIDED_ENDPOINT_EXAMPLE})`,
1051
- flag: PUSH_URL_FLAG,
1052
- }, promptCtx)
1053
- : undefined);
1155
+ const pushUrl = flags.pushUrl ?? (enabled ? askPushDestination(promptCtx) : undefined);
1054
1156
  return pushUrl === undefined ? { enabled } : { enabled, pushUrl };
1055
1157
  }
1158
+ /**
1159
+ * Ask for the push destination, re-asking an unrecognised answer up to `REPROMPT_LIMIT` times. Loops
1160
+ * over `askLine` rather than reading the terminal, so `core/prompt.ts` stays the one way to ask.
1161
+ * Returns `undefined` for no answer, a recognised answer as typed, or the last unrecognised one.
1162
+ */
1163
+ function askPushDestination(promptCtx) {
1164
+ let answer;
1165
+ for (let attempt = 0; attempt <= REPROMPT_LIMIT; attempt += 1) {
1166
+ answer = askLine({
1167
+ question: `Where should notifications be posted? Type ${PUSH_DESTINATION_FORMS}.`,
1168
+ flag: PUSH_URL_FLAG,
1169
+ }, promptCtx);
1170
+ if (answer === undefined || resolvePushDestination(answer).kind !== 'unrecognised')
1171
+ return answer;
1172
+ promptCtx.report.info(UNRECOGNISED_DESTINATION_NOTE);
1173
+ }
1174
+ return answer;
1175
+ }
1056
1176
  /** What detection concluded, as one line the summary can carry. */
1057
1177
  function detectionLine(detection) {
1058
1178
  if (detection.matchedSignal === FORCED_SIGNAL_ID) {
@@ -1568,6 +1688,8 @@ async function run(ctx) {
1568
1688
  // back-references it ({@link askQaDriver}) — "the detected preset" names nothing an adopter who
1569
1689
  // has not read that line can resolve.
1570
1690
  askDriver: () => askQaDriver(ctx),
1691
+ // Lazy for the same reason: asked only while the docs phase is on and `--docs-retrieval` was absent.
1692
+ askRetrieval: () => askRetrieval(ctx),
1571
1693
  });
1572
1694
  // `kept` is known only now, so this is where the four held-back lists are published or dropped —
1573
1695
  // ahead of `config.warnings`, which is the order they printed in before they were gated. A kept
@@ -1632,6 +1754,9 @@ async function run(ctx) {
1632
1754
  // exist (`generators/outerLoopScripts.ts`).
1633
1755
  const outerLoop = writeOuterLoopScripts({ repoRoot, config: effective, plan });
1634
1756
  notes.push(...outerLoop.notes);
1757
+ // After the scripts, which the workflows run, and before the permission profile. Enqueues nothing
1758
+ // unless `execution.target` is `github-actions` (`generators/githubWorkflows.ts`).
1759
+ const workflows = writeGithubWorkflows({ repoRoot, config: effective, plan });
1635
1760
  const state = writeStateDir({ repoRoot, config: effective, plan });
1636
1761
  notes.push(...state.notes);
1637
1762
  // The third of the four questions this run puts, and settled here because its answer is an
@@ -1676,6 +1801,7 @@ async function run(ctx) {
1676
1801
  // The run's `--dry-run`, which changes that report's tense and nothing else — the same contract
1677
1802
  // the project-file generator above keeps.
1678
1803
  dryRun: ctx.flags.dryRun,
1804
+ pluginRootEntries: flags.pluginRootEntries === true,
1679
1805
  });
1680
1806
  warnings.push(...permissions.warnings);
1681
1807
  notes.push(...permissions.notes);
@@ -1711,12 +1837,24 @@ async function run(ctx) {
1711
1837
  warnings.push(...hooks.warnings);
1712
1838
  notes.push(...hooks.notes);
1713
1839
  ctx.report.step(ctx.flags.dryRun ? 'files (dry run — nothing is written)' : 'files');
1714
- plan.apply({ repoRoot, report: ctx.report, dryRun: ctx.flags.dryRun, force: ctx.flags.force });
1840
+ const applied = plan.apply({ repoRoot, report: ctx.report, dryRun: ctx.flags.dryRun, force: ctx.flags.force });
1841
+ const profileWrite = applied.find((result) => result.path === permissions.path);
1842
+ const pluginRootNote = profileWrite === undefined ? undefined : pluginRootEntriesNote(permissions.pluginRootEntries, profileWrite.effect);
1843
+ if (pluginRootNote !== undefined)
1844
+ notes.push(pluginRootNote);
1715
1845
  // After the plan, deliberately: this is the one git-configuration write, and pointing
1716
1846
  // `core.hooksPath` at a directory whose hook has not landed yet would enable nothing.
1717
1847
  const hooksPath = pointHooksPath({ repoRoot, githooksDir: hooks.githooksDir, dryRun: ctx.flags.dryRun });
1718
1848
  warnings.push(...hooksPath.warnings);
1719
1849
  notes.push(...hooksPath.notes);
1850
+ // After the plan: this step writes nothing into the repository, and a real install takes minutes, so
1851
+ // it must not delay the plan's own report. Before the commit, which a failure here must not stop.
1852
+ if (retrievalApplies(effective)) {
1853
+ ctx.report.step('docs retrieval setup');
1854
+ const retrieval = setUpRetrieval({ dryRun: ctx.flags.dryRun });
1855
+ warnings.push(...retrieval.warnings);
1856
+ notes.push(...retrieval.notes);
1857
+ }
1720
1858
  // The second post-plan step, and after the plan for a reason of its own: the managed `.gitignore`
1721
1859
  // block that decides what `git add -A` may stage arrived with the plan. Before the summary, so the
1722
1860
  // commit is reported inside the run's action log rather than after its closing pointer.
@@ -1737,8 +1875,50 @@ async function run(ctx) {
1737
1875
  // browser wiring at all. Taken from the one predicate the two generators that write that wiring
1738
1876
  // read, never re-spelled here (`config/model.ts`).
1739
1877
  browserWiringApplies(effective));
1878
+ // Only the workflows this run created or replaced: a kept one is the adopter's already, and telling
1879
+ // them to commit it again is false on every unforced re-run and in every remote job's `init`.
1880
+ const freshWorkflows = workflows.workflows
1881
+ .filter(({ absolute }) => {
1882
+ const result = applied.find((r) => r.path === absolute);
1883
+ return result !== undefined && result.effect !== 'kept';
1884
+ })
1885
+ .map(({ repoPath }) => repoPath);
1886
+ if (freshWorkflows.length > 0)
1887
+ reportGithubSteps(ctx, effective.defaultBranch, ctx.flags.dryRun, freshWorkflows);
1740
1888
  return EXIT.OK;
1741
1889
  }
1890
+ /**
1891
+ * The GitHub-side steps only the adopter can take, printed when this run created or replaced at least
1892
+ * one of the two workflows; `workflowPaths` names those, repo-relative.
1893
+ *
1894
+ * Commands stand on their own lines so each can be pasted. The push comes first because GitHub
1895
+ * dispatches a `workflow_dispatch` workflow only once it exists on the default branch.
1896
+ */
1897
+ function reportGithubSteps(ctx, defaultBranch, dryRun, workflowPaths) {
1898
+ const wrote = dryRun ? 'would write' : 'wrote';
1899
+ const command = (line) => ctx.report.info(` ${line}`);
1900
+ ctx.report.step('remote execution');
1901
+ ctx.report.info(`1. This run ${wrote} ${workflowPaths.join(' and ')}. Commit and push ${workflowPaths.length === 1 ? 'it' : 'both'} to GitHub's default branch (assumed \`${defaultBranch}\` below) — a workflow_dispatch workflow can be dispatched only once it exists there:`);
1902
+ command(`git add ${workflowPaths.join(' ')}`);
1903
+ command('git commit -m "Add the harness workflows"');
1904
+ command(`git push origin ${defaultBranch}`);
1905
+ ctx.report.info('');
1906
+ ctx.report.info(`2. Set one credential secret: ${OAUTH_TOKEN_SECRET} for subscription billing, or ${API_KEY_SECRET} for API billing. When both are set, billing follows ${API_KEY_SECRET}:`);
1907
+ command(`gh secret set ${OAUTH_TOKEN_SECRET}`);
1908
+ command(`gh secret set ${API_KEY_SECRET}`);
1909
+ ctx.report.info('');
1910
+ ctx.report.info(`3. Optionally set ${PUSH_URL_SECRET} to receive push notifications from the job, and ${GIT_TOKEN_SECRET} — a personal or App token — so the job's pushes trigger your own CI, which pushes made with the job's built-in token never do:`);
1911
+ command(`gh secret set ${PUSH_URL_SECRET}`);
1912
+ command(`gh secret set ${GIT_TOKEN_SECRET}`);
1913
+ ctx.report.info('');
1914
+ ctx.report.info(`4. Optionally set the repository variable ${RUNNER_VARIABLE} to run on a self-hosted runner label instead of ubuntu-latest:`);
1915
+ command(`gh variable set ${RUNNER_VARIABLE} --body <runner-label>`);
1916
+ ctx.report.info('');
1917
+ ctx.report.info('5. Then verify the GitHub side:');
1918
+ command(DOCTOR_CHECK_GITHUB_COMMAND);
1919
+ ctx.report.info('');
1920
+ ctx.report.info('Runner choices, costs, billing and security: the harness documentation, docs/remote-execution.md — Remote execution on GitHub Actions.');
1921
+ }
1742
1922
  /**
1743
1923
  * The caveats, after the action log: the informational lines first, then the ones needing attention.
1744
1924
  *
@@ -1898,12 +2078,9 @@ function analyzeRecordSentence(analyze, force, dryRun, backupWouldCarryContent,
1898
2078
  * rather than a fifth thing to do. It names {@link AGENT_CLI} directly where the generated watcher
1899
2079
  * reaches the same binary through `${HARNESS_AGENT_CLI:-claude}`
1900
2080
  * (`templates/scripts/autonomous-watcher.sh`) — an override that belongs to a script's own
1901
- * environment and would mean nothing on a line printed for a person to paste. It names
1902
- * {@link ANALYZE_COMMAND_QUALIFIED} where step 2 above it names {@link ANALYZE_COMMAND}, and the
1903
- * difference is the two paths rather than an inconsistency: step 2 is a name to type into a session,
1904
- * whose picker resolves the bare form, while this line is a first message, which is matched exactly —
1905
- * the introducing sentence says so, because a report that showed both spellings and explained neither
1906
- * would read as a typo. It is withheld wherever
2081
+ * environment and would mean nothing on a line printed for a person to paste. It names the same
2082
+ * {@link ANALYZE_COMMAND} spelling step 2 above it names, so the introducing sentence gives no
2083
+ * spelling reason. It is withheld wherever
1907
2084
  * it would contradict the report around it: on the declined arm, whose answer the same step just
1908
2085
  * quoted; on the skip branch, where there is nothing to fill; and under `--dry-run`, which wired
1909
2086
  * nothing to run it against. On the unresolved-slug arm it is **printed but qualified** rather than
@@ -1911,7 +2088,7 @@ function analyzeRecordSentence(analyze, force, dryRun, backupWouldCarryContent,
1911
2088
  * published — because step 1 has just said the session resolves the plugin only once one of the two
1912
2089
  * routes it names is taken, and handing the line over without that precondition would contradict it.
1913
2090
  * **The outcome is stated as intended on both arms**: whether the prefixed first-message form
1914
- * succeeds is not established ({@link ANALYZE_COMMAND_QUALIFIED}), so the line an adopter acts on
2091
+ * succeeds is not established ({@link ANALYZE_INVOCATION}), so the line an adopter acts on
1915
2092
  * immediately names the typed route as the measured one instead of asserting its own.
1916
2093
  *
1917
2094
  * **One blank line between the numbered steps, and none before that paste line.** Each step is a
@@ -1974,13 +2151,12 @@ function reportNextSteps(ctx, layerNames, analyze, force, dryRun, backupWouldCar
1974
2151
  // The sentence introducing the line carries the precondition step 1 just stated. On the unresolved
1975
2152
  // arm, handing the line over without that precondition would contradict the step above it, which
1976
2153
  // has said the session resolves the plugin only once one of the two routes it names is taken.
1977
- const pasteReason = " It is spelled with the plugin prefix because a first message is matched exactly, where a session's command picker matches the name above.";
1978
- // Whether the prefixed first message succeeds is unmeasured, so the outcome is named as intended
1979
- // and the measured route is pointed at ({@link ANALYZE_COMMAND_QUALIFIED}, `docs/analyze.md` §9).
2154
+ // The name typed into an open session is the measured route; the interactive first message is not,
2155
+ // so the outcome is named as intended ({@link ANALYZE_INVOCATION}, `docs/analyze.md` §9).
1980
2156
  const pasteUnconfirmed = ' That first-message form is not confirmed on the version measured here; the name above, typed into an open session, is the route that is.';
1981
2157
  const pasteIntro = wiring.marketplaceSlug === undefined
1982
- ? ` Once the plugin resolves — step 1 names both routes — paste the line below at this repository's root: it is meant to start a session with that command already running.${pasteReason}${pasteUnconfirmed}`
1983
- : ` Paste the line below at this repository's root: it is meant to start a session with that command already running.${pasteReason}${pasteUnconfirmed}`;
2158
+ ? ` Once the plugin resolves — step 1 names both routes — paste the line below at this repository's root: it is meant to start a session with that command already running.${pasteUnconfirmed}`
2159
+ : ` Paste the line below at this repository's root: it is meant to start a session with that command already running.${pasteUnconfirmed}`;
1984
2160
  ctx.report.step('next');
1985
2161
  ctx.report.info(`1. ${wired}`);
1986
2162
  ctx.report.info('');