autonomous-sdlc-harness 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/commands/init.js +113 -16
  2. package/dist/commands/init.js.map +1 -1
  3. package/dist/config/check.js +28 -6
  4. package/dist/config/check.js.map +1 -1
  5. package/dist/config/model.js +64 -5
  6. package/dist/config/model.js.map +1 -1
  7. package/dist/core/pluginIdentity.js +2 -0
  8. package/dist/core/pluginIdentity.js.map +1 -1
  9. package/dist/core/writer.js +10 -5
  10. package/dist/core/writer.js.map +1 -1
  11. package/dist/core/yamlScalar.js +14 -0
  12. package/dist/core/yamlScalar.js.map +1 -0
  13. package/dist/doctor/checks.js +454 -23
  14. package/dist/doctor/checks.js.map +1 -1
  15. package/dist/generators/githubWorkflows.js +125 -20
  16. package/dist/generators/githubWorkflows.js.map +1 -1
  17. package/dist/generators/repoRoot.js +17 -8
  18. package/dist/generators/repoRoot.js.map +1 -1
  19. package/dist/remote/githubActions.js +110 -7
  20. package/dist/remote/githubActions.js.map +1 -1
  21. package/dist/retrieval/pythonBackend.js +114 -0
  22. package/dist/retrieval/pythonBackend.js.map +1 -0
  23. package/dist/retrieval/setup.js +8 -0
  24. package/dist/retrieval/setup.js.map +1 -1
  25. package/package.json +1 -1
  26. package/templates/README.md +1 -1
  27. package/templates/github/workflows/harness-control.yml +184 -0
  28. package/templates/github/workflows/harness-resume.yml +9 -0
  29. package/templates/github/workflows/harness-run.yml +127 -12
  30. package/templates/github/workflows/harness-trigger.yml +144 -0
  31. package/templates/repo/gitignore +5 -0
  32. package/templates/scripts/README.md +1 -1
  33. package/templates/scripts/autonomous-watcher.sh +121 -249
  34. package/templates/scripts/create-worktree.sh +52 -10
  35. package/templates/scripts/docs-search-server.sh +88 -17
  36. package/templates/scripts/lib/harness-run-lib.sh +614 -14
  37. package/templates/scripts/remote-run.sh +3684 -144
  38. package/templates/scripts/scratch-run.sh +54 -73
  39. package/templates/state-dir/README-root.md +1 -1
  40. package/templates/state-dir/scratch/README.md +4 -2
  41. package/templates/state-dir/user_reviews/README.md +2 -2
@@ -22,9 +22,10 @@
22
22
  * push-settings precedence is `generators/notifications.ts`'s {@link pushEnvCandidates}, the
23
23
  * repository registry's location, reader and staleness grading are `machine/registry.ts`'s {@link registryPath},
24
24
  * {@link readRegistry} and {@link inspect} — the same three `daemon list` enumerates through — the
25
- * remote-execution switch is `config/model.ts`'s {@link remoteExecutionApplies}, the workflow
26
- * paths and the binary run as `gh` are `remote/githubActions.ts`'s ({@link WORKFLOW_RUN_PATH},
27
- * {@link WORKFLOW_RESUME_PATH}, {@link GH_CLI_VARIABLE}, {@link ghCli}), whether a ref carries a
25
+ * remote-execution switch is `config/model.ts`'s {@link remoteExecutionApplies} and the
26
+ * issue-trigger switch its {@link forgeTriggerApplies}, the workflow paths and the binary run as
27
+ * `gh` are `remote/githubActions.ts`'s ({@link WORKFLOW_RUN_PATH}, {@link WORKFLOW_RESUME_PATH},
28
+ * {@link WORKFLOW_TRIGGER_PATH}, {@link WORKFLOW_CONTROL_PATH}, {@link GH_CLI_VARIABLE}, {@link ghCli}), whether a ref carries a
28
29
  * file is `core/git.ts`'s {@link pathAtRef}, and
29
30
  * the writability probe is the write engine's {@link probeWritable}. A
30
31
  * check that wanted a slightly different answer would be a second definition of the thing being
@@ -59,8 +60,8 @@
59
60
  * be fetched.
60
61
  *
61
62
  * **Remote setup keeps the same line: it is graded from local evidence by default and asks GitHub
62
- * only under `--check-github`.** {@link REMOTE_EXECUTION_CHECK} reads files, refs and `PATH`, and
63
- * spawns no `gh` subcommand; {@link REMOTE_GITHUB_CHECK} spawns them only under
63
+ * only under `--check-github`.** {@link REMOTE_EXECUTION_CHECK} and {@link FORGE_CHECK} read files,
64
+ * refs and `PATH`, and spawn no `gh` subcommand; {@link REMOTE_GITHUB_CHECK} spawns them only under
64
65
  * {@link CheckContext.probeGithub}, and each is a read.
65
66
  *
66
67
  * **The three docs-retrieval checks keep that line.** `retrieval-dependencies` and
@@ -69,13 +70,17 @@
69
70
  * nothing. `retrieval-index` spawns a child of this CLI running `docs index --in-memory`: a child
70
71
  * because {@link Check.run} is synchronous and the store is not, and because the child is the
71
72
  * installation whose optional peers resolve beside it. It builds in memory and exits, so it starts
72
- * no server and writes nothing.
73
+ * no server and writes nothing. The three `retrieval-python-*` checks share one child running the
74
+ * Python package's own `self-check` and grade its three lines; that child starts no server either.
73
75
  *
74
76
  * ## What this module deliberately does not do
75
77
  *
76
78
  * - **It writes nothing** beyond {@link probeWritable}'s temp file, which that function removes with
77
79
  * an in-process `fs.rm` on a single file — never a shelled-out recursive removal, which a
78
- * user-level `permissions.deny` can silently block (`cli.ts`'s header).
80
+ * user-level `permissions.deny` can silently block (`cli.ts`'s header). One exception:
81
+ * `retrieval-python-index` refreshes the Python backend's index **in its database**, because
82
+ * `self-check`'s `index` question builds it there — the same write the server's first query makes.
83
+ * It writes nothing in the repository or the machine cache.
79
84
  * - **It repairs nothing.** Every failure names what to run — `init`, `init --force`, an edit to one
80
85
  * config key — and `doctor` stays a command that is safe to run against a repository at any time.
81
86
  */
@@ -84,14 +89,15 @@ import { accessSync, constants as fsConstants, existsSync, readFileSync, statSyn
84
89
  import { delimiter, join, posix, resolve as resolvePath } from 'node:path';
85
90
  import { formatProblem } from '../config/check.js';
86
91
  import { loadConfig } from '../config/io.js';
87
- import { answersNone, browserWiringApplies, COMMAND_NONE_SENTINEL, CONFIG_FILENAME, DEFAULTS, FALLBACK_PRESET, isPlaceholder, LAYER_CATCH_ALL_PATH, remoteExecutionApplies, retrievalApplies, STATE_DIR_DOT_PATTERN, STATE_DIR_PATTERN, } from '../config/model.js';
92
+ import { answersNone, browserWiringApplies, COMMAND_NONE_SENTINEL, CONFIG_FILENAME, DEFAULTS, FALLBACK_PRESET, FORGE_KINDS, forgeTriggerApplies, isPlaceholder, LAYER_CATCH_ALL_PATH, pythonRetrievalApplies, remoteExecutionApplies, retrievalApplies, STATE_DIR_DOT_PATTERN, STATE_DIR_PATTERN, } from '../config/model.js';
88
93
  import { branchResolves, checkedOutBranch, commitsAhead, configuredRemotes, hasCommits, mainWorktreeRoot, pathAtRef, pathIsIgnored, remoteTrackingBranchResolves, resolveRepoRoot, worktreeList, } from '../core/git.js';
89
94
  import { isJsonObject, readJsonFile } from '../core/json.js';
90
95
  import { defaultBranchPushCommand, defaultBranchPushReason, WORKFLOW_SCOPE_COMMAND, WORKFLOW_SCOPE_REASON, } from '../core/defaultBranchPush.js';
96
+ import { internal } from '../core/errors.js';
91
97
  import { layerCoverage } from '../core/layerCoverage.js';
92
98
  import { layerGapRemedy, recordedVerdictClause } from '../core/layerGapRemedy.js';
93
99
  import { nameList } from '../core/nameList.js';
94
- import { readTemplate, workRoot } from '../core/paths.js';
100
+ import { ownManifestString, readTemplate, workRoot } from '../core/paths.js';
95
101
  import { ANALYZE_COMMAND } from '../core/pluginIdentity.js';
96
102
  import { normalizeRepoPathStrict } from '../core/repoPaths.js';
97
103
  import { probeWritable } from '../core/writer.js';
@@ -102,6 +108,7 @@ import { FORCED_SIGNAL_ID } from '../detect/signals.js';
102
108
  import { CLAUDE_MD_PATH, SETUP_PENDING_CLOSE, SETUP_PENDING_OPEN, SKELETON_GUIDANCE_MARKER, TASK_OFFER_PATH, UNFILLED_STUB_MARKER, } from '../generators/claudeContext.js';
103
109
  import { caseLabelMatches, isGlobPattern, PRE_PUSH_HOOK, readProtectedCaseLabel, resolveGithooksDir, resolveProtectedBranches, } from '../generators/githooks.js';
104
110
  import { PUSH_CMD_KEY, PUSH_DESTINATION_PLACEHOLDER, PUSH_URL_KEY, pushEnvCandidates, } from '../generators/notifications.js';
111
+ import { IN_FLIGHT_RUNS_NOTE, pinnedCliCommand, upgradeWorkflowsCommand } from '../generators/githubWorkflows.js';
105
112
  import { DOCS_SEARCH_SERVER_SCRIPT_NAME, outerLoopScriptsDir } from '../generators/outerLoopScripts.js';
106
113
  import { bashScriptRule, entryWord, isUnderDirectory, namesBrowserTool, normalizedRoot, pluginRootDirectories, pluginRootEntries, pluginRootEntryTarget, pluginRootHelpers, PLUGIN_ROOT_ENTRIES_FLAG, PROFILE_PATH, renderProfile, TEMPLATE_PATH as PROFILE_TEMPLATE_PATH, } from '../generators/permissionProfile.js';
107
114
  import { ENABLED_PLUGINS_KEY, MARKETPLACE_ENTRY_SHAPE, MARKETPLACE_FLAG, MARKETPLACES_KEY, MARKETPLACE_NAME, marketplaceEntryDefect, PLUGIN_KEY, SETTINGS_PATH, SLUG_SHAPE, } from '../generators/projectSettings.js';
@@ -111,9 +118,11 @@ import { selectedStateDirs } from '../generators/stateDir.js';
111
118
  import { machineConfigDir } from '../machine/paths.js';
112
119
  import { installedPluginsPath, knownMarketplacesPath, pluginInstallRoot, pluginRuntimeRoot, pluginScriptsDir, } from '../machine/plugins.js';
113
120
  import { inspect, readRegistry, registryPath } from '../machine/registry.js';
114
- import { API_KEY_SECRET, DEFAULT_GH_CLI, GH_CLI_VARIABLE, ghCli, GIT_TOKEN_SECRET, OAUTH_TOKEN_SECRET, PUSH_URL_SECRET, REMOTE_STOP_VARIABLE, runGh, RUNNER_VARIABLE, WORKFLOW_RESUME_FILE, WORKFLOW_RESUME_PATH, WORKFLOW_RUN_FILE, WORKFLOW_RUN_PATH, } from '../remote/githubActions.js';
121
+ import { API_KEY_SECRET, CLI_VERSION_VARIABLE, DEFAULT_GH_CLI, COMMAND_HANDLE, COMMAND_VERBS, DEFAULT_TRIGGER_LABEL, GH_CLI_VARIABLE, ghCli, GIT_TOKEN_SECRET, LEGACY_TRIGGER_LABEL, OAUTH_TOKEN_SECRET, PR_CREATE_SETTING, PR_CREATE_SETTING_PATH, PUSH_URL_SECRET, REMOTE_STOP_VARIABLE, renderedCliVersions, runGh, RUNNER_VARIABLE, TRIGGER_ALLOWED_BOTS_VARIABLE, TRIGGER_LABEL_VARIABLE, triggerFallbackLabel, WORKFLOW_CONTROL_FILE, WORKFLOW_CONTROL_PATH, WORKFLOW_RESUME_FILE, WORKFLOW_RESUME_PATH, WORKFLOW_RUN_FILE, WORKFLOW_RUN_PATH, WORKFLOW_TRIGGER_FILE, WORKFLOW_TRIGGER_PATH, } from '../remote/githubActions.js';
115
122
  import { modelFilesPresent } from '../retrieval/models.js';
123
+ import { launcherSearchPath, parseSelfCheck, PYTHON_DATABASE_URL_VARIABLE, PYTHON_FETCH_MODELS_SUB_COMMAND, PYTHON_RETRIEVAL_COMMAND, PYTHON_SELF_CHECK_SUB_COMMAND, pythonDatabaseUrl, SELF_CHECK_INDEX_NOT_ATTEMPTED, } from '../retrieval/pythonBackend.js';
116
124
  import { retrievalCliEntry, retrievalModelCacheDir, retrievalRuntimeDir, retrievalRuntimeState, } from '../retrieval/runtime.js';
125
+ import { DOCS_SERVER_NAME } from '../retrieval/server.js';
117
126
  /**
118
127
  * How the CLI is typed, for every remedy that tells an operator what to run next. The `npx` prefix
119
128
  * is not decoration: the rule for which occurrences carry it is stated once in `commands/init.ts`,
@@ -147,6 +156,13 @@ const REMOTE_RUN_SCRIPT = 'remote-run.sh';
147
156
  * YAML file spells this endpoint.
148
157
  */
149
158
  const ARTIFACT_RETENTION_ENDPOINT = 'repos/{owner}/{repo}/actions/permissions/artifact-and-log-retention';
159
+ /**
160
+ * The `gh api` path {@link REMOTE_GITHUB_CHECK} reads the *Allow GitHub Actions to create and approve
161
+ * pull requests* setting from — `can_approve_pull_request_reviews`. Local for
162
+ * {@link ARTIFACT_RETENTION_ENDPOINT}'s reason. A workflow's own token cannot read it; a person's `gh`
163
+ * usually can.
164
+ */
165
+ const PR_SETTING_ENDPOINT = 'repos/{owner}/{repo}/actions/permissions/workflow';
150
166
  /** Below this many days of artifact retention {@link REMOTE_GITHUB_CHECK} warns; argued there. */
151
167
  const ARTIFACT_RETENTION_WARN_DAYS = 30;
152
168
  /** The permission lists a generated profile carries, in the order the template writes them. */
@@ -1849,8 +1865,16 @@ const DAEMON_PATH_CHECK = {
1849
1865
  *
1850
1866
  * **Every finding is reported, and the grade is the worst of them.** Two `fail`s: no
1851
1867
  * `harness-run.yml`, because no remote run can be dispatched; and no `gh`, because the watcher
1852
- * dispatches through it. Three `warn`s: no `harness-resume.yml`, because a usage-paused hosted run then
1853
- * waits for `/autonomous-sdlc-harness:branch-resume`; a `harness-run.yml` that
1868
+ * dispatches through it. Four `warn`s: a `harness-run.yml` pinned (`remote/githubActions.ts` →
1869
+ * {@link renderedCliVersions}) to a version other than this CLI's — never a `fail`, because the job
1870
+ * installs its pin and the adopter may stay on it deliberately; the remedy is
1871
+ * `generators/githubWorkflows.ts` → {@link upgradeWorkflowsCommand}, the alternative `doctor` at the
1872
+ * pin, both prefixed by that module's {@link pinnedCliCommand} so the two cannot name different
1873
+ * packages. The warning also prints that module's {@link IN_FLIGHT_RUNS_NOTE}; its move route names
1874
+ * no commit set of its own but the paths the upgrade's printed `git add` names; and its `push`
1875
+ * fragment is a complete sentence in both of its forms, so the join adds no punctuation of its own.
1876
+ * Under `--remote-job` the job runs `doctor` at its own pin, so this cannot arise there. A file with no pin, or unreadable, is a note. No `harness-resume.yml`, because a usage-paused
1877
+ * hosted run then waits for `/autonomous-sdlc-harness:branch-resume`; a `harness-run.yml` that
1854
1878
  * `origin/<defaultBranch>` does not carry, because GitHub dispatches only a workflow its default
1855
1879
  * branch has — the run starts once it is pushed, so nothing is broken here, and its remedy's push
1856
1880
  * skips the hook because the `pre-push` hook `init` wired refuses every push to the default branch
@@ -1905,9 +1929,32 @@ const REMOTE_EXECUTION_CHECK = {
1905
1929
  if (ctx.config.phases?.qa === true) {
1906
1930
  warnings.push("phases.qa is true, but a remote run skips the interactive-test phase — GitHub Actions jobs have no browser wiring, application dependencies or QA credentials for it (docs/remote-execution.md → §3, The interactive-test phase): run /autonomous-sdlc-harness:branch-qa-test <branch> locally before merging a remote run's branch");
1907
1931
  }
1932
+ const version = ownManifestString('version');
1933
+ let pinnedHere = false;
1908
1934
  if (runPresent) {
1909
1935
  const branch = ctx.config.defaultBranch;
1910
- if (typeof branch !== 'string' || branch.trim() === '') {
1936
+ const branchUsable = typeof branch === 'string' && branch.trim() !== '';
1937
+ let text;
1938
+ try {
1939
+ text = readFileSync(join(root, ...WORKFLOW_RUN_PATH.split('/')), 'utf8');
1940
+ }
1941
+ catch (error) {
1942
+ notes.push(`which version ${WORKFLOW_RUN_PATH} was rendered for is not graded, because it could not be read (${messageOf(error)})`);
1943
+ }
1944
+ const pins = text === undefined ? undefined : renderedCliVersions(text);
1945
+ if (pins !== undefined && pins.length === 0) {
1946
+ notes.push(`which version ${WORKFLOW_RUN_PATH} was rendered for could not be read, because it carries no ${CLI_VERSION_VARIABLE} line`);
1947
+ }
1948
+ else if (pins !== undefined && pins[0] !== undefined && pins.some((pin) => pin !== version)) {
1949
+ const push = branchUsable
1950
+ ? `run \`${WORKFLOW_SCOPE_COMMAND}\`, then \`${defaultBranchPushCommand(branch)}\`. ${WORKFLOW_SCOPE_REASON} ${defaultBranchPushReason(branch)}`
1951
+ : 'push them to the default branch.';
1952
+ warnings.push(`${WORKFLOW_RUN_PATH} was rendered for ${nameList(pins)} (${CLI_VERSION_VARIABLE}), and this CLI is ${version}; the job installs and runs the version it names, so nothing is broken, and moving is your choice. To move to ${version}: run \`${upgradeWorkflowsCommand(version)}\`, commit the paths its printed \`git add\` names, then ${push} ${IN_FLIGHT_RUNS_NOTE} To stay on ${nameList(pins)}: run doctor at that version instead, \`${pinnedCliCommand(pins[0])} doctor\``);
1953
+ }
1954
+ else if (pins !== undefined) {
1955
+ pinnedHere = true;
1956
+ }
1957
+ if (!branchUsable) {
1911
1958
  notes.push(`whether GitHub's default branch carries ${WORKFLOW_RUN_PATH} is not graded, because defaultBranch is not a branch name (see the config check)`);
1912
1959
  }
1913
1960
  else if (!remoteTrackingBranchResolves(root, branch)) {
@@ -1923,7 +1970,7 @@ const REMOTE_EXECUTION_CHECK = {
1923
1970
  return fail(`${on}: ${[...failures, ...warnings].join('; ')}${noted}`);
1924
1971
  if (warnings.length > 0)
1925
1972
  return warn(`${on}: ${warnings.join('; ')}${noted}`);
1926
- return pass(`${on}: ${WORKFLOW_RUN_PATH} and ${WORKFLOW_RESUME_PATH} are present and ${gh} resolves on PATH${noted}. What this cannot see lives on GitHub — a credential secret (${OAUTH_TOKEN_SECRET} or ${API_KEY_SECRET}), the ${PUSH_URL_SECRET} and ${GIT_TOKEN_SECRET} secrets, and the ${RUNNER_VARIABLE} and ${REMOTE_STOP_VARIABLE} variables; \`${CLI} doctor --check-github\` asks GitHub`);
1973
+ return pass(`${on}: ${WORKFLOW_RUN_PATH} and ${WORKFLOW_RESUME_PATH} are present${pinnedHere ? `, ${WORKFLOW_RUN_PATH} is rendered for this CLI's own version, ${version},` : ''} and ${gh} resolves on PATH${noted}. What this cannot see lives on GitHub — a credential secret (${OAUTH_TOKEN_SECRET} or ${API_KEY_SECRET}), the ${PUSH_URL_SECRET} and ${GIT_TOKEN_SECRET} secrets, and the ${RUNNER_VARIABLE} and ${REMOTE_STOP_VARIABLE} variables; \`${CLI} doctor --check-github\` asks GitHub`);
1927
1974
  },
1928
1975
  };
1929
1976
  /**
@@ -1979,6 +2026,20 @@ function retentionDaysOf(stdout) {
1979
2026
  const days = parsed.days;
1980
2027
  return typeof days === 'number' && Number.isInteger(days) && days > 0 ? days : undefined;
1981
2028
  }
2029
+ /** The boolean `can_approve_pull_request_reviews` of a workflow-permissions answer, or `undefined` for any other shape. */
2030
+ function prApprovalSettingOf(stdout) {
2031
+ let parsed;
2032
+ try {
2033
+ parsed = JSON.parse(stdout);
2034
+ }
2035
+ catch {
2036
+ return undefined;
2037
+ }
2038
+ if (!isJsonObject(parsed))
2039
+ return undefined;
2040
+ const allowed = parsed.can_approve_pull_request_reviews;
2041
+ return typeof allowed === 'boolean' ? allowed : undefined;
2042
+ }
1982
2043
  /**
1983
2044
  * What GitHub says about the remote setup — asked only under {@link CheckContext.probeGithub}.
1984
2045
  *
@@ -1990,12 +2051,30 @@ function retentionDaysOf(stdout) {
1990
2051
  * - `fail` — `gh` does not spawn or `gh auth status` refuses (nothing further is asked); GitHub does
1991
2052
  * not know `harness-run.yml`; neither credential secret is set.
1992
2053
  * - `warn` — `HARNESS_PUSH_URL` absent; `harness-resume.yml` unknown to GitHub; `HARNESS_REMOTE_STOP`
1993
- * set; artifact retention below {@link ARTIFACT_RETENTION_WARN_DAYS} days; and any call that timed
1994
- * out, could not reach GitHub, or answered in a shape not understood — *cannot tell* is not
1995
- * *missing*, so it never fails.
2054
+ * set; artifact retention below {@link ARTIFACT_RETENTION_WARN_DAYS} days; when
2055
+ * {@link forgeTriggerApplies}, `harness-trigger.yml` or `harness-control.yml` unknown to GitHub, no
2056
+ * label by the effective trigger name, or the pull-request setting off with no `HARNESS_GIT_TOKEN`
2057
+ * secret; and any call that timed out, could not reach GitHub, or answered in a shape not
2058
+ * understood — *cannot tell* is not *missing*, so it never fails.
2059
+ * - the pull-request setting off while the secret list was unreadable is a *cannot tell* warning,
2060
+ * never the missing-`HARNESS_GIT_TOKEN` one.
2061
+ * - the effective trigger label is `HARNESS_TRIGGER_LABEL`, else the fallback the committed
2062
+ * `harness-trigger.yml` carries ({@link triggerFallbackLabel}), else {@link DEFAULT_TRIGGER_LABEL};
2063
+ * a fallback of {@link LEGACY_TRIGGER_LABEL} is a note, since it still starts runs.
1996
2064
  * - both credential secrets present is a note, not a finding: billing follows `ANTHROPIC_API_KEY`.
2065
+ * - a non-empty `HARNESS_TRIGGER_ALLOWED_BOTS` is a note naming the bots, which start runs without a
2066
+ * permission check. When the trigger does not apply, no trigger, control or pull-request-setting
2067
+ * read is made.
1997
2068
  * - the retention read refused (typically HTTP 403: the endpoint needs admin access) is a note too —
1998
- * the read is best-effort, and a collaborator without admin can still run remotely.
2069
+ * the read is best-effort, and a collaborator without admin can still run remotely. A refused
2070
+ * pull-request-setting read is a note on the same terms.
2071
+ * - the pull-request setting off with `HARNESS_GIT_TOKEN` set is a note: `deliver` opens the pull
2072
+ * request with that token instead.
2073
+ * - all three trigger answers positive — both workflows known and the label present — is confirmed on
2074
+ * every outcome that reaches the trigger reads, `fail` and `warn` included, so an unrelated finding
2075
+ * never hides it; any answer not positive is already among the warnings. An outcome returned before
2076
+ * those reads — `gh` not runnable, no usable login, or no readable answer to the login probe — asks
2077
+ * GitHub nothing about the trigger.
1999
2078
  *
2000
2079
  * **Why 30 days.** A parked run waits on a human answer and a usage-paused one on a reset, and the
2001
2080
  * `harness-state` bundle is the only remote copy of either; once the repository's retention expires
@@ -2110,13 +2189,188 @@ const REMOTE_GITHUB_CHECK = {
2110
2189
  warnings.push(`the repository keeps artifacts for ${retentionDays} days, so a remote run parked or paused longer than that loses its state bundle: raise it under Settings → Actions → General → Artifact and log retention`);
2111
2190
  }
2112
2191
  }
2192
+ let triggerKnown = '';
2193
+ if (forgeTriggerApplies(ctx.config)) {
2194
+ const trigger = ask(['workflow', 'view', WORKFLOW_TRIGGER_FILE]);
2195
+ if (trigger.answer === undefined)
2196
+ return fail(noSpawn);
2197
+ if (trigger.answer.kind === 'unknown')
2198
+ warnings.push(cannotTell(trigger.call, trigger.answer.why, `whether GitHub knows ${WORKFLOW_TRIGGER_FILE}`));
2199
+ if (trigger.answer.kind === 'refused') {
2200
+ warnings.push(`GitHub does not know ${WORKFLOW_TRIGGER_FILE} (${trigger.call}: ${trigger.answer.why}), so labelling an issue starts nothing: push ${WORKFLOW_TRIGGER_PATH} to the repository's default branch`);
2201
+ }
2202
+ const control = ask(['workflow', 'view', WORKFLOW_CONTROL_FILE]);
2203
+ if (control.answer === undefined)
2204
+ return fail(noSpawn);
2205
+ if (control.answer.kind === 'unknown')
2206
+ warnings.push(cannotTell(control.call, control.answer.why, `whether GitHub knows ${WORKFLOW_CONTROL_FILE}`));
2207
+ if (control.answer.kind === 'refused') {
2208
+ warnings.push(`GitHub does not know ${WORKFLOW_CONTROL_FILE} (${control.call}: ${control.answer.why}), so comments and reviews start nothing: push ${WORKFLOW_CONTROL_PATH} to the repository's default branch`);
2209
+ }
2210
+ // The label's name is a repository variable, so an unread variable listing leaves nothing to compare.
2211
+ const configured = variableValues?.get(TRIGGER_LABEL_VARIABLE)?.trim() ?? '';
2212
+ let labelName = configured;
2213
+ if (configured === '') {
2214
+ // Unset, the committed workflow's own fallback starts a run; it is never re-rendered by an upgrade.
2215
+ let committed;
2216
+ try {
2217
+ committed = triggerFallbackLabel(readFileSync(join(root, ...WORKFLOW_TRIGGER_PATH.split('/')), 'utf8'));
2218
+ }
2219
+ catch {
2220
+ committed = undefined;
2221
+ }
2222
+ labelName = committed ?? DEFAULT_TRIGGER_LABEL;
2223
+ if (variableValues !== undefined && labelName === LEGACY_TRIGGER_LABEL) {
2224
+ notes.push(`${WORKFLOW_TRIGGER_PATH} was written by an earlier release and falls back to \`${LEGACY_TRIGGER_LABEL}\`, which keeps starting runs; \`${CLI} init --force\` re-renders it and the scripts to \`${DEFAULT_TRIGGER_LABEL}\`, and setting ${TRIGGER_LABEL_VARIABLE} keeps a name of your choosing under either`);
2225
+ }
2226
+ }
2227
+ let labelFound = false;
2228
+ if (variableValues === undefined) {
2229
+ warnings.push(`cannot tell whether the trigger label exists: its name is the ${TRIGGER_LABEL_VARIABLE} variable, and ${variables.call} gave no readable answer`);
2230
+ }
2231
+ else {
2232
+ const labels = ask(['label', 'list', '--json', 'name', '--limit', '1000']);
2233
+ if (labels.answer === undefined)
2234
+ return fail(noSpawn);
2235
+ const labelNames = labels.answer.kind === 'answered' ? ghJsonEntries(labels.answer.stdout) : undefined;
2236
+ if (labels.answer.kind !== 'answered') {
2237
+ warnings.push(cannotTell(labels.call, labels.answer.why, `whether the label \`${labelName}\` exists`));
2238
+ }
2239
+ else if (labelNames === undefined) {
2240
+ warnings.push(`cannot tell whether the label \`${labelName}\` exists: ${labels.call} answered in a shape this check does not read`);
2241
+ }
2242
+ else if (!labelNames.has(labelName)) {
2243
+ warnings.push(`no label \`${labelName}\` exists, so nobody can apply it: \`gh label create ${labelName}\``);
2244
+ }
2245
+ else {
2246
+ labelFound = true;
2247
+ }
2248
+ const bots = (variableValues.get(TRIGGER_ALLOWED_BOTS_VARIABLE) ?? '').split(',').map((bot) => bot.trim()).filter((bot) => bot !== '');
2249
+ if (bots.length > 0) {
2250
+ notes.push(`${TRIGGER_ALLOWED_BOTS_VARIABLE} admits ${nameList(bots)}, each of which can start a run without a permission check`);
2251
+ }
2252
+ }
2253
+ if (trigger.answer.kind === 'answered' && control.answer.kind === 'answered' && labelFound) {
2254
+ triggerKnown = `; GitHub knows ${WORKFLOW_TRIGGER_FILE} and ${WORKFLOW_CONTROL_FILE}, and the label \`${labelName}\` exists`;
2255
+ }
2256
+ const prSetting = ask(['api', PR_SETTING_ENDPOINT]);
2257
+ if (prSetting.answer === undefined)
2258
+ return fail(noSpawn);
2259
+ if (prSetting.answer.kind === 'unknown') {
2260
+ warnings.push(cannotTell(prSetting.call, prSetting.answer.why, `whether a run's own token may open its pull request (${PR_CREATE_SETTING})`));
2261
+ }
2262
+ else if (prSetting.answer.kind === 'refused') {
2263
+ notes.push(`the pull-request setting was not checked: ${prSetting.call} may need more access than this login has (${prSetting.answer.why})`);
2264
+ }
2265
+ else {
2266
+ const allowed = prApprovalSettingOf(prSetting.answer.stdout);
2267
+ if (allowed === undefined) {
2268
+ warnings.push(`cannot tell whether a run's own token may open its pull request: ${prSetting.call} answered in a shape this check does not read`);
2269
+ }
2270
+ else if (!allowed && secretNames === undefined) {
2271
+ warnings.push(`${PR_CREATE_SETTING} is off, and whether ${GIT_TOKEN_SECRET} is set could not be read, so a completed run may not be able to open its draft pull request: turn the setting on under ${PR_CREATE_SETTING_PATH}, or confirm ${GIT_TOKEN_SECRET} is a repository secret`);
2272
+ }
2273
+ else if (!allowed && secretNames?.has(GIT_TOKEN_SECRET) === true) {
2274
+ notes.push(`${PR_CREATE_SETTING} is off, so a completed run opens its draft pull request with ${GIT_TOKEN_SECRET}`);
2275
+ }
2276
+ else if (!allowed) {
2277
+ warnings.push(`${PR_CREATE_SETTING} is off and ${GIT_TOKEN_SECRET} is not a repository secret, so a completed run cannot open its draft pull request with the job's token: turn it on under ${PR_CREATE_SETTING_PATH}, or set ${GIT_TOKEN_SECRET} with \`gh secret set ${GIT_TOKEN_SECRET}\``);
2278
+ }
2279
+ }
2280
+ }
2113
2281
  const noted = notes.length > 0 ? `; ${notes.join('; ')}` : '';
2114
2282
  if (failures.length > 0)
2115
- return fail(`${[...failures, ...warnings].join('; ')}${noted}`);
2283
+ return fail(`${[...failures, ...warnings].join('; ')}${triggerKnown}${noted}`);
2116
2284
  if (warnings.length > 0)
2117
- return warn(`${warnings.join('; ')}${noted}`);
2285
+ return warn(`${warnings.join('; ')}${triggerKnown}${noted}`);
2118
2286
  const kept = retentionDays === undefined ? '' : `, and the repository keeps artifacts for ${retentionDays} days`;
2119
- return pass(`gh is authenticated, GitHub knows ${WORKFLOW_RUN_FILE} and ${WORKFLOW_RESUME_FILE}, a credential secret and ${PUSH_URL_SECRET} are set, and remote runs use ${runner}${kept}${noted}`);
2287
+ return pass(`gh is authenticated, GitHub knows ${WORKFLOW_RUN_FILE} and ${WORKFLOW_RESUME_FILE}, a credential secret and ${PUSH_URL_SECRET} are set, and remote runs use ${runner}${kept}${triggerKnown}${noted}`);
2288
+ },
2289
+ };
2290
+ /**
2291
+ * The configured `forge`, and what starts a run from it — the key's reporter.
2292
+ *
2293
+ * **Every state is named, the absent one included**, because a declared seam needs something that
2294
+ * observes it (`ARCHITECTURE.md` → `## 8. Declaring a seam before building it`), and the config
2295
+ * check speaks only about a value outside {@link FORGE_KINDS}. An absent key is a decision not yet
2296
+ * made, and this line is where an operator learns the decision exists.
2297
+ *
2298
+ * **Graded from local evidence only** (the module header's choice 3): the two forge workflows —
2299
+ * {@link WORKFLOW_TRIGGER_FILE} and {@link WORKFLOW_CONTROL_FILE}, graded the same way — one git ref
2300
+ * and the configuration. What GitHub says — whether the label exists, which workflows it knows, and
2301
+ * the pull-request setting — is left to {@link REMOTE_GITHUB_CHECK}: the `github` pass names
2302
+ * `--check-github` when it is absent, and under it names that check, which {@link CHECKS} runs first.
2303
+ *
2304
+ * **Its worst grade is `warn`**: no `forge` state stops a run, because the inbox path works whatever
2305
+ * the key says. The three `warn`s are all `github`: remote execution off, because a run started from
2306
+ * GitHub always executes through {@link WORKFLOW_RUN_FILE}; a forge workflow absent; and a forge
2307
+ * workflow not carried by `origin/<defaultBranch>`, because GitHub runs an `issues` or
2308
+ * `issue_comment` workflow only from its default branch — that last with
2309
+ * {@link REMOTE_EXECUTION_CHECK}'s push remedy and its two *not graded* notes, on the same reasoning.
2310
+ * Each warn names every file it is about, so two absent files are one line naming both.
2311
+ *
2312
+ * A value outside {@link FORGE_KINDS} is the config check's `fail`, and is not graded here.
2313
+ */
2314
+ const FORGE_CHECK = {
2315
+ id: 'forge',
2316
+ title: 'the configured forge, and what starts a run from it',
2317
+ run: (ctx) => {
2318
+ if (ctx.repoRoot === undefined)
2319
+ return unevaluated('the repository root did not resolve (see the git check)');
2320
+ if (ctx.config === undefined)
2321
+ return unevaluated(`${CONFIG_FILENAME} could not be read (see the config check)`);
2322
+ const root = ctx.repoRoot;
2323
+ const forge = ctx.config.forge;
2324
+ const forgeWorkflows = [WORKFLOW_TRIGGER_PATH, WORKFLOW_CONTROL_PATH];
2325
+ const presentWorkflows = forgeWorkflows.filter((path) => existsSync(join(root, ...path.split('/'))));
2326
+ const isAre = (paths) => (paths.length === 1 ? 'is' : 'are');
2327
+ if (forge === undefined) {
2328
+ return pass(`forge is not set, so the decision is not yet made and nothing starts a run from an issue. \`${CLI} config set forge github\`, with execution.target github-actions, turns the issue trigger on; \`${CLI} config set forge none\` records that this repository has no forge integration`);
2329
+ }
2330
+ if (!FORGE_KINDS.includes(forge)) {
2331
+ return pass('not graded, because forge holds a value this CLI does not know (see the config check)');
2332
+ }
2333
+ if (forge === 'none') {
2334
+ const left = presentWorkflows.length > 0
2335
+ ? `. ${nameList(presentWorkflows)} ${isAre(presentWorkflows)} present and unused: the job each starts refuses every event while forge is not github`
2336
+ : '';
2337
+ return pass(`forge is none: this repository has no forge integration, and nothing starts a run from an issue${left}`);
2338
+ }
2339
+ if (forge === 'gitlab') {
2340
+ return pass("forge is gitlab, and this release has no GitLab trigger: GitLab has no issue-label pipeline trigger, so starting a run from a GitLab issue needs a webhook relay calling GitHub's repository_dispatch or a GitLab pipeline trigger (docs/github-integration-research.md → T6). Nothing is written for it, and runs start from this machine as before");
2341
+ }
2342
+ if (!remoteExecutionApplies(ctx.config)) {
2343
+ return warn(`forge is github, but remote execution is off (\`execution.target\`), so no trigger workflow is written: a run started from GitHub always executes through ${WORKFLOW_RUN_FILE}, because GitHub cannot reach this machine. Run \`${CLI} config set execution.target github-actions\`, then \`${CLI} init\`; to run such runs on your own hardware, register a self-hosted runner and name its label in the ${RUNNER_VARIABLE} repository variable (docs/remote-execution.md → ## 8. Choosing a runner)`);
2344
+ }
2345
+ const on = 'forge is github and remote execution is on';
2346
+ const startsNothing = {
2347
+ [WORKFLOW_TRIGGER_PATH]: 'labelling an issue starts nothing',
2348
+ [WORKFLOW_CONTROL_PATH]: 'comments and reviews start nothing',
2349
+ };
2350
+ const consequence = (paths) => paths.map((path) => startsNothing[path]).join(', and ');
2351
+ const absent = forgeWorkflows.filter((path) => !presentWorkflows.includes(path));
2352
+ if (absent.length > 0) {
2353
+ return warn(`${on}, but ${nameList(absent)} ${isAre(absent)} absent, so ${consequence(absent)}: re-run \`${CLI} init\`, which writes ${absent.length === 1 ? 'it' : 'them'} create-if-absent`);
2354
+ }
2355
+ const branch = ctx.config.defaultBranch;
2356
+ let carried;
2357
+ if (typeof branch !== 'string' || branch.trim() === '') {
2358
+ carried = `; whether GitHub's default branch carries them is not graded, because defaultBranch is not a branch name (see the config check)`;
2359
+ }
2360
+ else if (!remoteTrackingBranchResolves(root, branch)) {
2361
+ carried = `; whether origin/${branch} carries them is not graded, because there is no origin/${branch} (see the remote check)`;
2362
+ }
2363
+ else {
2364
+ const uncarried = forgeWorkflows.filter((path) => !pathAtRef(root, `origin/${branch}`, path));
2365
+ if (uncarried.length > 0) {
2366
+ return warn(`${on}, but origin/${branch} does not carry ${nameList(uncarried)}, as this checkout last fetched it, and GitHub runs an issues or issue_comment workflow only from its default branch, so ${consequence(uncarried)} yet: commit ${uncarried.length === 1 ? 'it' : 'them'}, then run \`${WORKFLOW_SCOPE_COMMAND}\`, then \`${defaultBranchPushCommand(branch)}\`. ${WORKFLOW_SCOPE_REASON} ${defaultBranchPushReason(branch)}`);
2367
+ }
2368
+ carried = ` and origin/${branch} carries them`;
2369
+ }
2370
+ const asked = ctx.probeGithub
2371
+ ? `the ${REMOTE_GITHUB_CHECK.id} check above reports what GitHub says`
2372
+ : `\`${CLI} doctor --check-github\` asks GitHub`;
2373
+ return pass(`${on}: ${WORKFLOW_TRIGGER_PATH} and ${WORKFLOW_CONTROL_PATH} are present${carried}. Labelling an issue with the ${TRIGGER_LABEL_VARIABLE} label (default \`${DEFAULT_TRIGGER_LABEL}\`) starts a task run; a \`${COMMAND_HANDLE} <verb>\` comment (${nameList([...COMMAND_VERBS])}) steers it; a review requesting changes on the run's pull request starts a user-review round; and a completed run opens a draft pull request. What this cannot see lives on GitHub — the label, the workflows GitHub knows (${WORKFLOW_TRIGGER_FILE}, ${WORKFLOW_CONTROL_FILE}) and the pull-request setting; ${asked}`);
2120
2374
  },
2121
2375
  };
2122
2376
  /**
@@ -3794,6 +4048,10 @@ const BROWSER_WIRING_CHECK = {
3794
4048
  };
3795
4049
  /** The pass every retrieval check gives when {@link retrievalApplies} is false. */
3796
4050
  const RETRIEVAL_OFF = 'docs.retrieval is off (it needs phases.docs and docs.retrieval both true), so no RAG library is expected and none was resolved';
4051
+ /** The pass every `retrieval-python-*` check gives when retrieval is on under another backend. */
4052
+ const PYTHON_NOT_SELECTED = 'docs.retrieval is on with docs.retrievalBackend not python, so the launcher starts the TypeScript runtime and nothing of the Python backend is expected or checked';
4053
+ /** The pass the three TypeScript retrieval checks give when {@link pythonRetrievalApplies} is true. */
4054
+ const TYPESCRIPT_NOT_SELECTED = 'docs.retrievalBackend is python, so the launcher starts the Python backend rather than this runtime and it is not graded; init still installs it, so switching back costs nothing';
3797
4055
  /** How many missing model files {@link RETRIEVAL_MODEL_CACHE_CHECK} names before it counts the rest. */
3798
4056
  const MISSING_MODEL_FILES_NAMED = 5;
3799
4057
  /**
@@ -3821,6 +4079,8 @@ const RETRIEVAL_DEPENDENCIES_CHECK = {
3821
4079
  return unevaluated(`${CONFIG_FILENAME} could not be read (see the config check)`);
3822
4080
  if (!retrievalApplies(ctx.config))
3823
4081
  return pass(RETRIEVAL_OFF);
4082
+ if (pythonRetrievalApplies(ctx.config))
4083
+ return pass(TYPESCRIPT_NOT_SELECTED);
3824
4084
  const runtime = retrievalRuntimeDir();
3825
4085
  const state = retrievalRuntimeState();
3826
4086
  if (state.installed) {
@@ -3840,6 +4100,8 @@ const RETRIEVAL_MODEL_CACHE_CHECK = {
3840
4100
  return unevaluated(`${CONFIG_FILENAME} could not be read (see the config check)`);
3841
4101
  if (!retrievalApplies(ctx.config))
3842
4102
  return pass(RETRIEVAL_OFF);
4103
+ if (pythonRetrievalApplies(ctx.config))
4104
+ return pass(TYPESCRIPT_NOT_SELECTED);
3843
4105
  const dir = retrievalModelCacheDir();
3844
4106
  const { present, missing } = modelFilesPresent(dir);
3845
4107
  if (present)
@@ -3883,6 +4145,8 @@ const RETRIEVAL_INDEX_CHECK = {
3883
4145
  return unevaluated(`${CONFIG_FILENAME} could not be read (see the config check)`);
3884
4146
  if (!retrievalApplies(ctx.config))
3885
4147
  return pass(RETRIEVAL_OFF);
4148
+ if (pythonRetrievalApplies(ctx.config))
4149
+ return pass(TYPESCRIPT_NOT_SELECTED);
3886
4150
  const resolved = retrievalCliEntry();
3887
4151
  if (resolved === undefined)
3888
4152
  return fail('cannot build without the RAG libraries (see retrieval-dependencies)');
@@ -3907,6 +4171,166 @@ const RETRIEVAL_INDEX_CHECK = {
3907
4171
  }
3908
4172
  },
3909
4173
  };
4174
+ const pythonSelfCheckCache = new WeakMap();
4175
+ /** The `self-check` question each `retrieval-python-*` check id grades, for the index's cross-reference. */
4176
+ const PYTHON_CHECK_ID = Object.freeze({
4177
+ packages: 'retrieval-python-dependencies',
4178
+ weights: 'retrieval-python-model-cache',
4179
+ index: 'retrieval-python-index',
4180
+ });
4181
+ /**
4182
+ * Run the Python backend's own `self-check` once per {@link CheckContext}; the three
4183
+ * `retrieval-python-*` checks grade its lines and never re-derive them (choice 1).
4184
+ *
4185
+ * The command is resolved on {@link launcherSearchPath} — the `PATH` the launcher resolves it on —
4186
+ * not on this process's own, so a pass here is a pass on the server the launcher starts.
4187
+ *
4188
+ * **`spawnSync` rather than `execFileSync`:** `self-check` exits 1 whenever a line is `FAIL`, with its
4189
+ * whole answer on stdout, and `execFileSync` would turn that answer into a thrown error.
4190
+ *
4191
+ * **The env is the server's, not the shell's.** `process.env` is overlaid by the string entries of the
4192
+ * `.mcp.json` server's `env` object, which the agent runner passes to the server, and then
4193
+ * {@link PYTHON_DATABASE_URL_VARIABLE} is set to {@link pythonDatabaseUrl}'s answer from that object
4194
+ * alone: the runner never passes a shell export, so honouring one here would grade a database the
4195
+ * server does not use.
4196
+ */
4197
+ function pythonSelfCheck(ctx, repoRoot) {
4198
+ const cached = pythonSelfCheckCache.get(ctx);
4199
+ if (cached !== undefined)
4200
+ return cached;
4201
+ const answer = runPythonSelfCheck(repoRoot);
4202
+ pythonSelfCheckCache.set(ctx, answer);
4203
+ return answer;
4204
+ }
4205
+ function runPythonSelfCheck(repoRoot) {
4206
+ const searched = launcherSearchPath(process.env['PATH'] ?? '', process.env['HOME']);
4207
+ const directory = locateOnPath(PYTHON_RETRIEVAL_COMMAND, searched);
4208
+ if (directory === undefined)
4209
+ return { kind: 'unresolved', searched };
4210
+ const mcp = readJsonFile(join(repoRoot, MCP_PATH));
4211
+ const servers = isJsonObject(mcp) ? mcp[SERVERS_KEY] : undefined;
4212
+ const server = isJsonObject(servers) ? servers[DOCS_SERVER_NAME] : undefined;
4213
+ const serverEnv = isJsonObject(server) ? server['env'] : undefined;
4214
+ const overlay = {};
4215
+ if (isJsonObject(serverEnv)) {
4216
+ for (const [key, value] of Object.entries(serverEnv)) {
4217
+ if (typeof value === 'string')
4218
+ overlay[key] = value;
4219
+ }
4220
+ }
4221
+ const databaseUrl = pythonDatabaseUrl(serverEnv);
4222
+ const child = spawnSync(join(directory, PYTHON_RETRIEVAL_COMMAND), [PYTHON_SELF_CHECK_SUB_COMMAND, '--repo', repoRoot], {
4223
+ encoding: 'utf8',
4224
+ stdio: ['ignore', 'pipe', 'pipe'],
4225
+ timeout: RETRIEVAL_INDEX_TIMEOUT_MS,
4226
+ env: { ...process.env, ...overlay, [PYTHON_DATABASE_URL_VARIABLE]: databaseUrl },
4227
+ });
4228
+ if (child.error !== undefined)
4229
+ return { kind: 'unreadable', text: messageOf(child.error) };
4230
+ const lines = child.status === 0 || child.status === 1 ? parseSelfCheck(child.stdout ?? '') : undefined;
4231
+ if (lines !== undefined)
4232
+ return { kind: 'answered', lines, databaseUrl };
4233
+ const how = child.status === null ? `stopped by ${child.signal}` : `exit status ${child.status}`;
4234
+ const said = firstLine(child.stderr ?? '') || firstLine(child.stdout ?? '');
4235
+ return { kind: 'unreadable', text: said === '' ? how : `${how}: ${said}` };
4236
+ }
4237
+ /** One line of an answered `self-check`; `parseSelfCheck` guarantees all three are present. */
4238
+ function selfCheckLine(answer, question) {
4239
+ const line = answer.lines.get(question);
4240
+ if (line === undefined)
4241
+ throw internal(`self-check answered without its ${question} line, which parseSelfCheck guarantees`);
4242
+ return line;
4243
+ }
4244
+ /**
4245
+ * Host, port and database of a connection string, with the credentials dropped — a check's text is
4246
+ * printed and may be pasted, so the password never reaches it.
4247
+ */
4248
+ function databaseLocation(url) {
4249
+ try {
4250
+ const parsed = new URL(url);
4251
+ const port = parsed.port === '' ? '' : `:${parsed.port}`;
4252
+ return `${parsed.hostname}${port}${parsed.pathname}`;
4253
+ }
4254
+ catch {
4255
+ return 'a connection string this check cannot parse (not printed, as it may carry a password)';
4256
+ }
4257
+ }
4258
+ /** The install remedy every `retrieval-python-dependencies` failure names. */
4259
+ const PYTHON_INSTALL_REMEDY = "install the package with its `models` extra (docs/retrieval.md → `## Turning on the Python backend`)";
4260
+ /** Do the Python backend's console script, interpreter and packages resolve? */
4261
+ const RETRIEVAL_PYTHON_DEPENDENCIES_CHECK = {
4262
+ id: 'retrieval-python-dependencies',
4263
+ title: "RAG's Python backend resolves",
4264
+ run: (ctx) => {
4265
+ if (ctx.repoRoot === undefined)
4266
+ return unevaluated('the repository root did not resolve (see the git check)');
4267
+ if (ctx.config === undefined)
4268
+ return unevaluated(`${CONFIG_FILENAME} could not be read (see the config check)`);
4269
+ if (!retrievalApplies(ctx.config))
4270
+ return pass(RETRIEVAL_OFF);
4271
+ if (!pythonRetrievalApplies(ctx.config))
4272
+ return pass(PYTHON_NOT_SELECTED);
4273
+ const answer = pythonSelfCheck(ctx, ctx.repoRoot);
4274
+ if (answer.kind === 'unresolved') {
4275
+ return fail(`${PYTHON_RETRIEVAL_COMMAND} does not resolve on the launcher's PATH (${answer.searched}), so the search server never starts — ${PYTHON_INSTALL_REMEDY}`);
4276
+ }
4277
+ if (answer.kind === 'unreadable') {
4278
+ return fail(`${PYTHON_RETRIEVAL_COMMAND} ${PYTHON_SELF_CHECK_SUB_COMMAND} answered in a shape this CLI cannot grade (${answer.text}) — ${PYTHON_INSTALL_REMEDY}, from a clone at the release tag matching this CLI's version, then run doctor again`);
4279
+ }
4280
+ const line = selfCheckLine(answer, 'packages');
4281
+ return line.ok ? pass(line.detail) : fail(`${line.detail} — ${PYTHON_INSTALL_REMEDY}`);
4282
+ },
4283
+ };
4284
+ /** Are the Python backend's model weights in its cache? */
4285
+ const RETRIEVAL_PYTHON_MODEL_CACHE_CHECK = {
4286
+ id: 'retrieval-python-model-cache',
4287
+ title: "RAG's Python backend weights are cached",
4288
+ run: (ctx) => {
4289
+ if (ctx.repoRoot === undefined)
4290
+ return unevaluated('the repository root did not resolve (see the git check)');
4291
+ if (ctx.config === undefined)
4292
+ return unevaluated(`${CONFIG_FILENAME} could not be read (see the config check)`);
4293
+ if (!retrievalApplies(ctx.config))
4294
+ return pass(RETRIEVAL_OFF);
4295
+ if (!pythonRetrievalApplies(ctx.config))
4296
+ return pass(PYTHON_NOT_SELECTED);
4297
+ const answer = pythonSelfCheck(ctx, ctx.repoRoot);
4298
+ if (answer.kind !== 'answered') {
4299
+ return fail(`cannot be checked without the Python backend (see ${PYTHON_CHECK_ID.packages})`);
4300
+ }
4301
+ const line = selfCheckLine(answer, 'weights');
4302
+ if (line.ok)
4303
+ return pass(line.detail);
4304
+ return fail(`${line.detail} — run \`${PYTHON_RETRIEVAL_COMMAND} ${PYTHON_FETCH_MODELS_SUB_COMMAND}\` where an operator is present. An unattended run has no web access, so a weight missing now is never fetched later`);
4305
+ },
4306
+ };
4307
+ /** Does the Python backend build its index in its database? */
4308
+ const RETRIEVAL_PYTHON_INDEX_CHECK = {
4309
+ id: 'retrieval-python-index',
4310
+ title: "the RAG Python backend's index builds",
4311
+ run: (ctx) => {
4312
+ if (ctx.repoRoot === undefined)
4313
+ return unevaluated('the repository root did not resolve (see the git check)');
4314
+ if (ctx.config === undefined)
4315
+ return unevaluated(`${CONFIG_FILENAME} could not be read (see the config check)`);
4316
+ if (!retrievalApplies(ctx.config))
4317
+ return pass(RETRIEVAL_OFF);
4318
+ if (!pythonRetrievalApplies(ctx.config))
4319
+ return pass(PYTHON_NOT_SELECTED);
4320
+ const answer = pythonSelfCheck(ctx, ctx.repoRoot);
4321
+ if (answer.kind !== 'answered') {
4322
+ return fail(`cannot be checked without the Python backend (see ${PYTHON_CHECK_ID.packages})`);
4323
+ }
4324
+ const line = selfCheckLine(answer, 'index');
4325
+ if (line.ok)
4326
+ return pass(line.detail);
4327
+ const stoppedBy = SELF_CHECK_INDEX_NOT_ATTEMPTED.exec(line.detail)?.[1];
4328
+ if (stoppedBy !== undefined) {
4329
+ return fail(`cannot build without the Python backend's ${stoppedBy} (see ${PYTHON_CHECK_ID[stoppedBy]})`);
4330
+ }
4331
+ return fail(`${line.detail} — the backend's database is ${databaseLocation(answer.databaseUrl)} (${MCP_PATH}'s ${DOCS_SERVER_NAME} \`env\` ${PYTHON_DATABASE_URL_VARIABLE}, else the compose default): start the bundled one by running \`docker compose up -d --wait postgres\` in docs-retrieval-service/ of a clone of this CLI's repository at its release tag (docs/retrieval.md → \`## Turning on the Python backend\`, step 2), or point ${PYTHON_DATABASE_URL_VARIABLE} in that \`env\` object at yours`);
4332
+ },
4333
+ };
3910
4334
  /**
3911
4335
  * The checks, in the order they are evaluated and reported: the host and the tools first, then the
3912
4336
  * repository's own wiring, then the permission profile and the browser half that depends on it.
@@ -3985,7 +4409,8 @@ const RETRIEVAL_INDEX_CHECK = {
3985
4409
  * five above it are answered, and it says so rather than guessing.
3986
4410
  * `remote-execution` follows it because it asks the same "can this run unattended" question of a run
3987
4411
  * the watcher dispatches to GitHub rather than spawns here, and `remote-github` follows that because it
3988
- * asks GitHub the half of the same question local evidence cannot answer.
4412
+ * asks GitHub the half of the same question local evidence cannot answer. `forge` closes that block
4413
+ * because the trigger it grades starts a run through the same remote setup the two above it grade.
3989
4414
  *
3990
4415
  * `profile-tracked` sits under `profile-paths` because the two name the same file carried somewhere it
3991
4416
  * does not belong, and a committed profile is the usual reason a job's `profile-paths` fails.
@@ -3996,7 +4421,9 @@ const RETRIEVAL_INDEX_CHECK = {
3996
4421
  * profile lines all passed reads it as the last thing that can still be missing from that file.
3997
4422
  *
3998
4423
  * The three `retrieval-*` checks come last, under `browser-wiring`: the runtime, the models, then
3999
- * whether an index builds, which needs libraries and models both and so reads after them.
4424
+ * whether an index builds, which needs libraries and models both and so reads after them. The three
4425
+ * `retrieval-python-*` checks follow them in the same runtime → models → index order; whichever
4426
+ * backend `docs.retrievalBackend` does not select passes its three as not applicable.
4000
4427
  */
4001
4428
  export const CHECKS = Object.freeze([
4002
4429
  GIT_CHECK,
@@ -4016,6 +4443,7 @@ export const CHECKS = Object.freeze([
4016
4443
  DAEMON_PATH_CHECK,
4017
4444
  REMOTE_EXECUTION_CHECK,
4018
4445
  REMOTE_GITHUB_CHECK,
4446
+ FORGE_CHECK,
4019
4447
  CONFIG_CHECK,
4020
4448
  COMMAND_WRAPPERS_CHECK,
4021
4449
  COMMAND_PERMISSIONS_CHECK,
@@ -4038,6 +4466,9 @@ export const CHECKS = Object.freeze([
4038
4466
  RETRIEVAL_DEPENDENCIES_CHECK,
4039
4467
  RETRIEVAL_MODEL_CACHE_CHECK,
4040
4468
  RETRIEVAL_INDEX_CHECK,
4469
+ RETRIEVAL_PYTHON_DEPENDENCIES_CHECK,
4470
+ RETRIEVAL_PYTHON_MODEL_CACHE_CHECK,
4471
+ RETRIEVAL_PYTHON_INDEX_CHECK,
4041
4472
  ]);
4042
4473
  /**
4043
4474
  * Evaluate every check in order and return one result each.