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.
- package/dist/commands/init.js +113 -16
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +28 -6
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +64 -5
- package/dist/config/model.js.map +1 -1
- package/dist/core/pluginIdentity.js +2 -0
- package/dist/core/pluginIdentity.js.map +1 -1
- package/dist/core/writer.js +10 -5
- package/dist/core/writer.js.map +1 -1
- package/dist/core/yamlScalar.js +14 -0
- package/dist/core/yamlScalar.js.map +1 -0
- package/dist/doctor/checks.js +454 -23
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +125 -20
- package/dist/generators/githubWorkflows.js.map +1 -1
- package/dist/generators/repoRoot.js +17 -8
- package/dist/generators/repoRoot.js.map +1 -1
- package/dist/remote/githubActions.js +110 -7
- package/dist/remote/githubActions.js.map +1 -1
- package/dist/retrieval/pythonBackend.js +114 -0
- package/dist/retrieval/pythonBackend.js.map +1 -0
- package/dist/retrieval/setup.js +8 -0
- package/dist/retrieval/setup.js.map +1 -1
- package/package.json +1 -1
- package/templates/README.md +1 -1
- package/templates/github/workflows/harness-control.yml +184 -0
- package/templates/github/workflows/harness-resume.yml +9 -0
- package/templates/github/workflows/harness-run.yml +127 -12
- package/templates/github/workflows/harness-trigger.yml +144 -0
- package/templates/repo/gitignore +5 -0
- package/templates/scripts/README.md +1 -1
- package/templates/scripts/autonomous-watcher.sh +121 -249
- package/templates/scripts/create-worktree.sh +52 -10
- package/templates/scripts/docs-search-server.sh +88 -17
- package/templates/scripts/lib/harness-run-lib.sh +614 -14
- package/templates/scripts/remote-run.sh +3684 -144
- package/templates/scripts/scratch-run.sh +54 -73
- package/templates/state-dir/README-root.md +1 -1
- package/templates/state-dir/scratch/README.md +4 -2
- package/templates/state-dir/user_reviews/README.md +2 -2
package/dist/doctor/checks.js
CHANGED
|
@@ -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}
|
|
26
|
-
* paths and the binary run as
|
|
27
|
-
*
|
|
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}
|
|
63
|
-
*
|
|
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.
|
|
1853
|
-
*
|
|
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
|
-
|
|
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;
|
|
1994
|
-
*
|
|
1995
|
-
*
|
|
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.
|