@codyswann/lisa 3.33.8 → 3.35.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/all/copy-overwrite/scripts/lisa-lint-staged-preflight.mjs +399 -0
- package/dist/cli/doctor-traceability-gate.d.ts +39 -0
- package/dist/cli/doctor-traceability-gate.d.ts.map +1 -0
- package/dist/cli/doctor-traceability-gate.js +164 -0
- package/dist/cli/doctor-traceability-gate.js.map +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +7 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +3 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +32 -9
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +53 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-intake/SKILL.md +17 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +53 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +48 -7
- package/plugins/lisa/.codex-plugin/skills/lisa-tracker-build-intake/SKILL.md +24 -0
- package/plugins/lisa/rules/eager/automation-runbook-contract.md +2 -0
- package/plugins/lisa/rules/reference/automation-runbook-contract.md +19 -0
- package/plugins/lisa/scripts/automation-run-record.mjs +146 -33
- package/plugins/lisa/scripts/intake-blocker-reprobe.mjs +190 -0
- package/plugins/lisa/scripts/intake-prework-denominator.mjs +249 -0
- package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +53 -3
- package/plugins/lisa/skills/lisa-intake/SKILL.md +17 -1
- package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +53 -5
- package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +48 -7
- package/plugins/lisa/skills/lisa-tracker-build-intake/SKILL.md +24 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/automation-run-record.mjs +146 -33
- package/plugins/lisa-agy/scripts/intake-blocker-reprobe.mjs +190 -0
- package/plugins/lisa-agy/scripts/intake-prework-denominator.mjs +249 -0
- package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +53 -3
- package/plugins/lisa-agy/skills/lisa-intake/SKILL.md +17 -1
- package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +53 -5
- package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +48 -7
- package/plugins/lisa-agy/skills/lisa-tracker-build-intake/SKILL.md +24 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/rules/eager/automation-runbook-contract.md +2 -0
- package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +19 -0
- package/plugins/lisa-copilot/scripts/automation-run-record.mjs +146 -33
- package/plugins/lisa-copilot/scripts/intake-blocker-reprobe.mjs +190 -0
- package/plugins/lisa-copilot/scripts/intake-prework-denominator.mjs +249 -0
- package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +53 -3
- package/plugins/lisa-copilot/skills/lisa-intake/SKILL.md +17 -1
- package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +53 -5
- package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +48 -7
- package/plugins/lisa-copilot/skills/lisa-tracker-build-intake/SKILL.md +24 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +19 -0
- package/plugins/lisa-cursor/rules/automation-runbook-contract.mdc +2 -0
- package/plugins/lisa-cursor/scripts/automation-run-record.mjs +146 -33
- package/plugins/lisa-cursor/scripts/intake-blocker-reprobe.mjs +190 -0
- package/plugins/lisa-cursor/scripts/intake-prework-denominator.mjs +249 -0
- package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +53 -3
- package/plugins/lisa-cursor/skills/lisa-intake/SKILL.md +17 -1
- package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +53 -5
- package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +48 -7
- package/plugins/lisa-cursor/skills/lisa-tracker-build-intake/SKILL.md +24 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/rules/eager/automation-runbook-contract.md +2 -0
- package/plugins/src/base/rules/reference/automation-runbook-contract.md +19 -0
- package/plugins/src/base/scripts/automation-run-record.mjs +146 -33
- package/plugins/src/base/scripts/intake-blocker-reprobe.mjs +190 -0
- package/plugins/src/base/scripts/intake-prework-denominator.mjs +249 -0
- package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +53 -3
- package/plugins/src/base/skills/lisa-intake/SKILL.md +17 -1
- package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +53 -5
- package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +48 -7
- package/plugins/src/base/skills/lisa-tracker-build-intake/SKILL.md +24 -0
- package/typescript/copy-contents/.husky/pre-commit +64 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
3
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* lisa-lint-staged-preflight — prove every tool lint-staged is about to run can
|
|
7
|
+
* actually be started, before lint-staged gets the chance to swallow the answer.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/lisa-lint-staged-preflight.mjs --config .lintstagedrc.json
|
|
11
|
+
* node scripts/lisa-lint-staged-preflight.mjs --config=.lintstagedrc.json
|
|
12
|
+
*
|
|
13
|
+
* ## Why this exists
|
|
14
|
+
*
|
|
15
|
+
* `lint-staged` reports **exit 0** when a task's executable cannot be spawned
|
|
16
|
+
* with `ENOEXEC`, while printing `[FAILED] spawn ENOEXEC` to the terminal. The
|
|
17
|
+
* asymmetry is the whole problem, and it is what defeats the intuition that a
|
|
18
|
+
* broken tool would get noticed. Measured with one harness, three tasks,
|
|
19
|
+
* everything else held constant:
|
|
20
|
+
*
|
|
21
|
+
* | task | 16.2.7 | 16.4.0 | 17.3.0 |
|
|
22
|
+
* | ----------------------------------------------- | ------ | ------ | ------ |
|
|
23
|
+
* | script exits 1 (ordinary failure) | 1 | 1 | 1 |
|
|
24
|
+
* | executable absent entirely (`ENOENT`) | 1 | 1 | 1 |
|
|
25
|
+
* | present, `chmod +x`, not executable (`ENOEXEC`) | 1 | **0** | **0** |
|
|
26
|
+
*
|
|
27
|
+
* A *missing* tool is caught. A tool that is present but unrunnable is not.
|
|
28
|
+
*
|
|
29
|
+
* That is precisely the state some published tools leave behind. A package
|
|
30
|
+
* whose real binary is materialized during `postinstall` installs a
|
|
31
|
+
* shebang-less placeholder shim first; the shim is written to fail loudly and
|
|
32
|
+
* explain itself, and it would, because it `exit 1`s. lint-staged spawns
|
|
33
|
+
* without a shell, so no shebang plus no shell means the kernel returns
|
|
34
|
+
* `ENOEXEC` before the shim's own `exit 1` can ever run. Skip `postinstall` —
|
|
35
|
+
* which is a route agents take routinely — and the task runs, reports nothing,
|
|
36
|
+
* and blocks nothing, while the commit proceeds.
|
|
37
|
+
*
|
|
38
|
+
* ## Why hardening the hook could not fix it
|
|
39
|
+
*
|
|
40
|
+
* Worth stating, because it is the obvious first response. The hook already
|
|
41
|
+
* captures lint-staged's status and exits on it. No exit-code check can catch
|
|
42
|
+
* a process that reports `0` for work it did not do. The answer has to arrive
|
|
43
|
+
* *before* the handoff, from something that spawns the tools itself.
|
|
44
|
+
*
|
|
45
|
+
* ## What "invocable" means here, and why it is measured rather than inferred
|
|
46
|
+
*
|
|
47
|
+
* This guard does not read the file's first bytes looking for a shebang or an
|
|
48
|
+
* object-file magic number and reason about what the kernel would do with them.
|
|
49
|
+
* It spawns each executable, shell-less, exactly as lint-staged will, and
|
|
50
|
+
* watches for the `spawn` event. `ENOEXEC`, `ENOENT` and `EACCES` all arrive as
|
|
51
|
+
* an `error` event instead, which is the signal. The probe passes when — and
|
|
52
|
+
* only when — the operating system really started the process.
|
|
53
|
+
*
|
|
54
|
+
* `--version` is the probe argument because every tool Lisa ships in
|
|
55
|
+
* `.lintstagedrc.json` answers it instantly. The exit code is deliberately
|
|
56
|
+
* ignored: a tool that rejects `--version` still *started*, which is the entire
|
|
57
|
+
* question. A tool that hangs is also a pass for the same reason — it started —
|
|
58
|
+
* so the probe kills it and moves on.
|
|
59
|
+
*
|
|
60
|
+
* ## Silence is not the success signal
|
|
61
|
+
*
|
|
62
|
+
* The defect being fixed is a control reporting success for work it no longer
|
|
63
|
+
* performs, so this file refuses to be a second instance of it:
|
|
64
|
+
*
|
|
65
|
+
* - It prints one line per executable it probed and a count. A run that proved
|
|
66
|
+
* nothing cannot look like a run that proved everything.
|
|
67
|
+
* - A config it cannot read, cannot parse, or whose shape it does not
|
|
68
|
+
* understand is a FAILURE, never a quiet pass.
|
|
69
|
+
* - A config that declares no tasks is a FAILURE: lint-staged would prove
|
|
70
|
+
* nothing, and "nothing to check" is exactly the report this guard exists to
|
|
71
|
+
* stop trusting.
|
|
72
|
+
* - Extracting zero executables from a config that has tasks is a FAILURE.
|
|
73
|
+
* - Executable-name extraction that disagrees with lint-staged's own
|
|
74
|
+
* tokenization can only produce a name that fails to spawn, which this guard
|
|
75
|
+
* reports loudly. It has no path to a silent pass.
|
|
76
|
+
* @module scripts/lisa-lint-staged-preflight
|
|
77
|
+
*/
|
|
78
|
+
import { spawn } from "node:child_process";
|
|
79
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
80
|
+
import path from "node:path";
|
|
81
|
+
import process from "node:process";
|
|
82
|
+
import { fileURLToPath } from "node:url";
|
|
83
|
+
|
|
84
|
+
/** Default config path, matching what the shipped pre-commit hook passes. */
|
|
85
|
+
const DEFAULT_CONFIG = ".lintstagedrc.json";
|
|
86
|
+
|
|
87
|
+
/** Argument handed to each probed executable. */
|
|
88
|
+
const PROBE_ARGS = Object.freeze(["--version"]);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* How long a probe waits before concluding the process really did start.
|
|
92
|
+
*
|
|
93
|
+
* Only reached by a tool that spawns and then neither exits nor is killed.
|
|
94
|
+
* Since spawning is the whole question, a timeout is a PASS.
|
|
95
|
+
*/
|
|
96
|
+
const PROBE_TIMEOUT_MS = 15_000;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The config path named on the command line.
|
|
100
|
+
*
|
|
101
|
+
* Accepts `--config X` and `--config=X` because the hook and a human invoking
|
|
102
|
+
* this by hand write it both ways.
|
|
103
|
+
* @param {string[]} argv - Arguments after the script name.
|
|
104
|
+
* @returns {string} Config path, relative or absolute.
|
|
105
|
+
*/
|
|
106
|
+
export const configPathFrom = argv => {
|
|
107
|
+
const inline = argv.find(arg => arg.startsWith("--config="));
|
|
108
|
+
if (inline !== undefined) return inline.slice("--config=".length);
|
|
109
|
+
const flagAt = argv.indexOf("--config");
|
|
110
|
+
if (flagAt >= 0 && argv[flagAt + 1] !== undefined) return argv[flagAt + 1];
|
|
111
|
+
return DEFAULT_CONFIG;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The executable a lint-staged task string spawns.
|
|
116
|
+
*
|
|
117
|
+
* lint-staged splits the task with `string-argv` and spawns element zero, so
|
|
118
|
+
* this reproduces that for element zero only: quoted runs are unwrapped, and
|
|
119
|
+
* the token ends at the first unquoted whitespace. A disagreement with
|
|
120
|
+
* `string-argv` on an exotic task string yields a name that will not spawn,
|
|
121
|
+
* which this guard reports — never a name it silently skips.
|
|
122
|
+
* @param {string} command - A lint-staged task string.
|
|
123
|
+
* @returns {string} The executable name, or "" when the task is blank.
|
|
124
|
+
*/
|
|
125
|
+
export const executableOf = command => {
|
|
126
|
+
let token = "";
|
|
127
|
+
let quote = "";
|
|
128
|
+
for (const character of command) {
|
|
129
|
+
if (quote !== "") {
|
|
130
|
+
if (character === quote) quote = "";
|
|
131
|
+
else token += character;
|
|
132
|
+
} else if (character === '"' || character === "'") {
|
|
133
|
+
quote = character;
|
|
134
|
+
} else if (/\s/u.test(character)) {
|
|
135
|
+
if (token !== "") break;
|
|
136
|
+
} else {
|
|
137
|
+
token += character;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return token;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Every task string a lint-staged JSON config declares.
|
|
145
|
+
* @param {unknown} config - Parsed config contents.
|
|
146
|
+
* @returns {{ tasks: string[] } | { problem: string }} Tasks, or why not.
|
|
147
|
+
*/
|
|
148
|
+
export const tasksOf = config => {
|
|
149
|
+
if (config === null || typeof config !== "object" || Array.isArray(config)) {
|
|
150
|
+
return { problem: "the config is not a JSON object of glob → task(s)" };
|
|
151
|
+
}
|
|
152
|
+
const tasks = [];
|
|
153
|
+
for (const [glob, value] of Object.entries(config)) {
|
|
154
|
+
if (typeof value === "string") {
|
|
155
|
+
tasks.push(value);
|
|
156
|
+
} else if (
|
|
157
|
+
Array.isArray(value) &&
|
|
158
|
+
value.every(v => typeof v === "string")
|
|
159
|
+
) {
|
|
160
|
+
tasks.push(...value);
|
|
161
|
+
} else {
|
|
162
|
+
return {
|
|
163
|
+
problem: `the entry for "${glob}" is neither a task string nor an array of them`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { tasks };
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The environment a probe runs in: PATH augmented the way lint-staged's own
|
|
172
|
+
* spawner augments it, so a locally installed tool resolves identically.
|
|
173
|
+
*
|
|
174
|
+
* `node_modules/.bin` is prepended for the working directory and every ancestor
|
|
175
|
+
* of it, then the directory holding the running Node binary.
|
|
176
|
+
* @param {string} cwd - Directory the probe runs in.
|
|
177
|
+
* @returns {NodeJS.ProcessEnv} Environment for the probe.
|
|
178
|
+
*/
|
|
179
|
+
export const probeEnv = cwd => {
|
|
180
|
+
const binDirectories = [];
|
|
181
|
+
let current = cwd;
|
|
182
|
+
let previous = "";
|
|
183
|
+
while (current !== previous) {
|
|
184
|
+
binDirectories.push(path.resolve(current, "node_modules", ".bin"));
|
|
185
|
+
previous = current;
|
|
186
|
+
current = path.dirname(current);
|
|
187
|
+
}
|
|
188
|
+
binDirectories.push(path.dirname(process.execPath));
|
|
189
|
+
|
|
190
|
+
const key =
|
|
191
|
+
Object.keys(process.env).find(name => /^path$/iu.test(name)) ?? "PATH";
|
|
192
|
+
const existing = process.env[key] ?? "";
|
|
193
|
+
return {
|
|
194
|
+
...process.env,
|
|
195
|
+
[key]: [
|
|
196
|
+
...binDirectories,
|
|
197
|
+
...(existing === "" ? [] : existing.split(path.delimiter)),
|
|
198
|
+
].join(path.delimiter),
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Start one executable and report whether the operating system started it.
|
|
204
|
+
*
|
|
205
|
+
* Shell-less on POSIX, matching how lint-staged spawns and therefore matching
|
|
206
|
+
* the `ENOEXEC` this guard exists to catch. On win32 the shell is used because
|
|
207
|
+
* that is how `.cmd` shims are invoked there; `ENOEXEC` in the sense meant here
|
|
208
|
+
* is a POSIX `execve` outcome and has no win32 counterpart.
|
|
209
|
+
* @param {string} executable - Name or path to start.
|
|
210
|
+
* @param {string} cwd - Directory to start it in.
|
|
211
|
+
* @param {NodeJS.ProcessEnv} env - Environment to start it with.
|
|
212
|
+
* @returns {Promise<{ executable: string, started: boolean, code?: string, detail?: string }>} Probe outcome.
|
|
213
|
+
*/
|
|
214
|
+
export const probe = (executable, cwd, env) =>
|
|
215
|
+
new Promise(resolve => {
|
|
216
|
+
let settled = false;
|
|
217
|
+
/**
|
|
218
|
+
* Resolve once, ignoring any later event from the same child.
|
|
219
|
+
* @param {{ executable: string, started: boolean, code?: string, detail?: string }} outcome - What happened.
|
|
220
|
+
* @returns {void}
|
|
221
|
+
*/
|
|
222
|
+
const settle = outcome => {
|
|
223
|
+
if (settled) return;
|
|
224
|
+
settled = true;
|
|
225
|
+
resolve(outcome);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
let child;
|
|
229
|
+
try {
|
|
230
|
+
child = spawn(executable, [...PROBE_ARGS], {
|
|
231
|
+
cwd,
|
|
232
|
+
env,
|
|
233
|
+
stdio: "ignore",
|
|
234
|
+
shell: process.platform === "win32",
|
|
235
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
settle({
|
|
239
|
+
executable,
|
|
240
|
+
started: false,
|
|
241
|
+
code: /** @type {NodeJS.ErrnoException} */ (error).code ?? "FAILED",
|
|
242
|
+
detail: /** @type {Error} */ (error).message,
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
child.on("error", error => {
|
|
248
|
+
settle({
|
|
249
|
+
executable,
|
|
250
|
+
started: false,
|
|
251
|
+
code: /** @type {NodeJS.ErrnoException} */ (error).code ?? "FAILED",
|
|
252
|
+
detail: error.message,
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
child.on("spawn", () => {
|
|
256
|
+
child.kill();
|
|
257
|
+
settle({ executable, started: true });
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Refuse to continue, saying what could not be established.
|
|
263
|
+
* @param {string} reason - Operator-readable reason.
|
|
264
|
+
* @param {string[]} [remedy] - Lines telling the reader what to do.
|
|
265
|
+
* @returns {number} Exit code 1.
|
|
266
|
+
*/
|
|
267
|
+
const refuse = (reason, remedy = []) => {
|
|
268
|
+
process.stderr.write(`\n❌ lint-staged preflight: ${reason}\n`);
|
|
269
|
+
for (const line of remedy) process.stderr.write(` ${line}\n`);
|
|
270
|
+
process.stderr.write(
|
|
271
|
+
"\n Nothing was proved about the tools lint-staged would run, so the\n" +
|
|
272
|
+
" commit is blocked rather than allowed to look checked.\n\n"
|
|
273
|
+
);
|
|
274
|
+
return 1;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Run the preflight.
|
|
279
|
+
* @param {string[]} argv - Arguments after the script name.
|
|
280
|
+
* @param {string} cwd - Directory the probes run in.
|
|
281
|
+
* @returns {Promise<number>} Process exit code.
|
|
282
|
+
*/
|
|
283
|
+
export const run = async (argv, cwd) => {
|
|
284
|
+
const configPath = configPathFrom(argv);
|
|
285
|
+
const absolute = path.resolve(cwd, configPath);
|
|
286
|
+
if (!existsSync(absolute)) {
|
|
287
|
+
return refuse(`no config at ${configPath}`, [
|
|
288
|
+
"lint-staged is about to be handed this path and will fail too.",
|
|
289
|
+
"Restore the file, or point the pre-commit hook at the right one.",
|
|
290
|
+
]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let parsed;
|
|
294
|
+
try {
|
|
295
|
+
parsed = JSON.parse(readFileSync(absolute, "utf8"));
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return refuse(`${configPath} is not valid JSON`, [
|
|
298
|
+
/** @type {Error} */ (error).message,
|
|
299
|
+
]);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const extracted = tasksOf(parsed);
|
|
303
|
+
if ("problem" in extracted) {
|
|
304
|
+
return refuse(`${configPath} has a shape this guard cannot read`, [
|
|
305
|
+
extracted.problem,
|
|
306
|
+
"This guard reads JSON configs only, which is what Lisa ships.",
|
|
307
|
+
]);
|
|
308
|
+
}
|
|
309
|
+
if (extracted.tasks.length === 0) {
|
|
310
|
+
return refuse(`${configPath} declares no tasks`, [
|
|
311
|
+
"lint-staged would run nothing and report success.",
|
|
312
|
+
"Add the tasks back, or stop running lint-staged from the hook.",
|
|
313
|
+
]);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const executables = [...new Set(extracted.tasks.map(executableOf))].filter(
|
|
317
|
+
name => name !== ""
|
|
318
|
+
);
|
|
319
|
+
if (executables.length === 0) {
|
|
320
|
+
return refuse(
|
|
321
|
+
`no executable name could be read from the ${extracted.tasks.length} task(s) in ${configPath}`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
process.stdout.write(
|
|
326
|
+
`🔎 lint-staged preflight: probing ${executables.length} tool(s) named by ${configPath}\n`
|
|
327
|
+
);
|
|
328
|
+
const env = probeEnv(cwd);
|
|
329
|
+
const outcomes = await Promise.all(
|
|
330
|
+
executables.map(executable => probe(executable, cwd, env))
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
for (const outcome of outcomes) {
|
|
334
|
+
process.stdout.write(
|
|
335
|
+
outcome.started
|
|
336
|
+
? ` ✅ ${outcome.executable}\n`
|
|
337
|
+
: ` ❌ ${outcome.executable} — could not be started (${outcome.code})\n`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const broken = outcomes.filter(outcome => !outcome.started);
|
|
342
|
+
if (broken.length > 0) {
|
|
343
|
+
return refuse(
|
|
344
|
+
`${broken.length} of ${outcomes.length} tool(s) cannot be started`,
|
|
345
|
+
[
|
|
346
|
+
...broken.map(
|
|
347
|
+
outcome => `${outcome.executable}: ${outcome.detail ?? outcome.code}`
|
|
348
|
+
),
|
|
349
|
+
"",
|
|
350
|
+
"lint-staged spawns these without a shell. A tool that is present but",
|
|
351
|
+
"not executable makes it print [FAILED] and still exit 0, so the scan",
|
|
352
|
+
"would run, report nothing, and block nothing.",
|
|
353
|
+
"",
|
|
354
|
+
"Most often this is an install that skipped postinstall scripts, which",
|
|
355
|
+
"leaves a placeholder where the real binary belongs. Reinstall with",
|
|
356
|
+
"scripts enabled (for example `bun install --force`) and try again.",
|
|
357
|
+
]
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
process.stdout.write(
|
|
362
|
+
` ${outcomes.length} tool(s) verified runnable; handing off to lint-staged.\n`
|
|
363
|
+
);
|
|
364
|
+
return 0;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* True when `moduleUrl` names the module node was asked to run.
|
|
369
|
+
*
|
|
370
|
+
* Both sides are realpath'd: `import.meta.url` is the real path while `argv[1]`
|
|
371
|
+
* is whatever the caller typed, so through a symlinked checkout, a git
|
|
372
|
+
* worktree, or a `/tmp` path on macOS a raw comparison is false and the body
|
|
373
|
+
* never runs — for a guard, exiting 0 having proved nothing.
|
|
374
|
+
* @param {string} moduleUrl - The caller's own `import.meta.url`.
|
|
375
|
+
* @param {string} [argv1] - Entry path; defaults to `process.argv[1]`.
|
|
376
|
+
* @returns {boolean} Whether the CLI body should run.
|
|
377
|
+
*/
|
|
378
|
+
export const invokedAsScript = (moduleUrl, argv1 = process.argv[1]) => {
|
|
379
|
+
if (argv1 === undefined || argv1 === "") return false;
|
|
380
|
+
try {
|
|
381
|
+
return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
|
|
382
|
+
} catch {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
if (invokedAsScript(import.meta.url)) {
|
|
388
|
+
run(process.argv.slice(2), process.cwd())
|
|
389
|
+
.then(code => {
|
|
390
|
+
process.exitCode = code;
|
|
391
|
+
})
|
|
392
|
+
.catch(error => {
|
|
393
|
+
process.stderr.write(
|
|
394
|
+
`\n❌ lint-staged preflight crashed: ${/** @type {Error} */ (error).message}\n` +
|
|
395
|
+
" Nothing was proved. The commit is blocked.\n\n"
|
|
396
|
+
);
|
|
397
|
+
process.exitCode = 1;
|
|
398
|
+
});
|
|
399
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DoctorCheck } from "./doctor.js";
|
|
2
|
+
/** The gate id as the registry declares it. */
|
|
3
|
+
export declare const GATE_ID = "traceability";
|
|
4
|
+
/** The moment the registry permits for this gate. */
|
|
5
|
+
export declare const GATE_MOMENT = "pull-request";
|
|
6
|
+
/** What an undeclared project is repaired to. */
|
|
7
|
+
export declare const GATE_LEVEL = "required";
|
|
8
|
+
/** The subset of `.lisa.config.json` this check reads. */
|
|
9
|
+
interface GateConfig {
|
|
10
|
+
readonly gates?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Whether a config declares the traceability gate at all.
|
|
14
|
+
*
|
|
15
|
+
* Declared-as-`off` counts as declared. A repair that overwrote an explicit
|
|
16
|
+
* opt-out would make the gate un-declinable, which is worse than the hole it
|
|
17
|
+
* closes: the project could never express a considered exemption, and the next
|
|
18
|
+
* `lisa doctor` would silently undo a deliberate decision.
|
|
19
|
+
* @param config - Parsed `.lisa.config.json`
|
|
20
|
+
* @returns True when the project has already made a decision about this gate.
|
|
21
|
+
*/
|
|
22
|
+
export declare function declaresTraceability(config: GateConfig): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Add the gate at its registry-legal moment, without touching anything else.
|
|
25
|
+
*
|
|
26
|
+
* Returns a new object rather than mutating, so a caller that decides not to
|
|
27
|
+
* write still holds the original.
|
|
28
|
+
* @param config - Parsed `.lisa.config.json`
|
|
29
|
+
* @returns A copy declaring the gate
|
|
30
|
+
*/
|
|
31
|
+
export declare function withTraceabilityGate<T extends GateConfig>(config: T): T;
|
|
32
|
+
/**
|
|
33
|
+
* Report — and repair — an undeclared traceability gate.
|
|
34
|
+
* @param targetPath - Project path to inspect and repair
|
|
35
|
+
* @returns The doctor check result
|
|
36
|
+
*/
|
|
37
|
+
export declare function checkTraceabilityGate(targetPath: string): Promise<DoctorCheck>;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=doctor-traceability-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-traceability-gate.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-traceability-gate.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAsB/C,+CAA+C;AAC/C,eAAO,MAAM,OAAO,iBAAiB,CAAC;AAEtC,qDAAqD;AACrD,eAAO,MAAM,WAAW,iBAAiB,CAAC;AAE1C,iDAAiD;AACjD,eAAO,MAAM,UAAU,aAAa,CAAC;AAErC,0DAA0D;AAC1D,UAAU,UAAU;IAClB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAEhE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAQvE;AA8BD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CAoDtB"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure a project actually declares the Work-Item Traceability gate.
|
|
3
|
+
*
|
|
4
|
+
* Every upstream layer already sanctions it — the gate registry declares it at
|
|
5
|
+
* `pull-request`, the TypeScript ruleset template names the context as
|
|
6
|
+
* required, and the promotion ledger grandfathers it. Lisa's own repository
|
|
7
|
+
* enforces it. Measured 2026-08-18, **no consumer did**: `traceability` was
|
|
8
|
+
* absent from all five projects' `.lisa.config.json`.
|
|
9
|
+
*
|
|
10
|
+
* WHAT AN UNDECLARED GATE COSTS, RESTATED AFTER #2680. An earlier draft of this
|
|
11
|
+
* check said an undeclared gate meant "the job never ran at all". That was true
|
|
12
|
+
* when the job's `if:` keyed on `skip_jobs` and ignored the config. Since #2680
|
|
13
|
+
* it is false: the job runs on every pull request, and with the gate undeclared
|
|
14
|
+
* (`configured=false`) the built-in validation step still runs and still reds.
|
|
15
|
+
*
|
|
16
|
+
* The real cost is narrower and easier to miss. `contextsFor` filters on
|
|
17
|
+
* `gate.level === "required"`, so an undeclared gate emits no required status
|
|
18
|
+
* context — the job reds and **merges anyway**. Declaring it is what converts an
|
|
19
|
+
* ignorable red into a blocking one. Do not restore the stronger claim without
|
|
20
|
+
* re-measuring: it was wrong once already, and a repair that overstates what it
|
|
21
|
+
* fixes is the same defect class as a check that reports work it never did.
|
|
22
|
+
*
|
|
23
|
+
* Two independent layers decide whether a missing trailer blocks, and
|
|
24
|
+
* conflating them is why this went unseen:
|
|
25
|
+
*
|
|
26
|
+
* 1. the gate's level in `.lisa.config.json` — does the JOB run and fail?
|
|
27
|
+
* 2. branch-protection required contexts — does a failing job BLOCK a merge?
|
|
28
|
+
*
|
|
29
|
+
* This check repairs layer 1, which is the one that produces any signal at all,
|
|
30
|
+
* and only REPORTS layer 2. Promoting a context is governed by its own control
|
|
31
|
+
* (`check-required-check-promotions.mjs`), and mutating live branch protection
|
|
32
|
+
* belongs to `scripts/lisa-github-rulesets.sh`.
|
|
33
|
+
* @module cli/doctor-traceability-gate
|
|
34
|
+
*/
|
|
35
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
36
|
+
import * as path from "node:path";
|
|
37
|
+
const CHECK_NAME = "Work-Item Traceability declared?";
|
|
38
|
+
/**
|
|
39
|
+
* What this check can and cannot see about layer 2.
|
|
40
|
+
*
|
|
41
|
+
* `lisa doctor` runs offline, so it never observes live branch protection. A
|
|
42
|
+
* declared gate therefore proves the job will FAIL, never that the failure will
|
|
43
|
+
* BLOCK — `contextsFor` only emits the context, and the ruleset has to require
|
|
44
|
+
* it. Saying nothing here would let a green doctor read as "traceability is
|
|
45
|
+
* enforced" when the context may not be required at all, which is the exact
|
|
46
|
+
* conflation #2677 was filed about. So both branches say it, and both name
|
|
47
|
+
* where layer 2 IS observed rather than leaving the reader to find it.
|
|
48
|
+
*/
|
|
49
|
+
const LAYER_TWO_NOTE = `Whether a failure BLOCKS a merge is a separate layer that \`lisa doctor\` ` +
|
|
50
|
+
`cannot see: it runs offline and never reads live branch protection. Check ` +
|
|
51
|
+
`it with \`lisa health\` (the \`github.rulesets\` finding names any context ` +
|
|
52
|
+
`that "runs without blocking"), and apply the ruleset with ` +
|
|
53
|
+
`scripts/lisa-github-rulesets.sh if the context is missing.`;
|
|
54
|
+
/** The gate id as the registry declares it. */
|
|
55
|
+
export const GATE_ID = "traceability";
|
|
56
|
+
/** The moment the registry permits for this gate. */
|
|
57
|
+
export const GATE_MOMENT = "pull-request";
|
|
58
|
+
/** What an undeclared project is repaired to. */
|
|
59
|
+
export const GATE_LEVEL = "required";
|
|
60
|
+
/**
|
|
61
|
+
* Whether a config declares the traceability gate at all.
|
|
62
|
+
*
|
|
63
|
+
* Declared-as-`off` counts as declared. A repair that overwrote an explicit
|
|
64
|
+
* opt-out would make the gate un-declinable, which is worse than the hole it
|
|
65
|
+
* closes: the project could never express a considered exemption, and the next
|
|
66
|
+
* `lisa doctor` would silently undo a deliberate decision.
|
|
67
|
+
* @param config - Parsed `.lisa.config.json`
|
|
68
|
+
* @returns True when the project has already made a decision about this gate.
|
|
69
|
+
*/
|
|
70
|
+
export function declaresTraceability(config) {
|
|
71
|
+
return Object.hasOwn(config.gates ?? {}, GATE_ID);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Add the gate at its registry-legal moment, without touching anything else.
|
|
75
|
+
*
|
|
76
|
+
* Returns a new object rather than mutating, so a caller that decides not to
|
|
77
|
+
* write still holds the original.
|
|
78
|
+
* @param config - Parsed `.lisa.config.json`
|
|
79
|
+
* @returns A copy declaring the gate
|
|
80
|
+
*/
|
|
81
|
+
export function withTraceabilityGate(config) {
|
|
82
|
+
return {
|
|
83
|
+
...config,
|
|
84
|
+
gates: {
|
|
85
|
+
...(config.gates ?? {}),
|
|
86
|
+
[GATE_ID]: { [GATE_MOMENT]: GATE_LEVEL },
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Read the config file, or `null` when there is none.
|
|
92
|
+
* @param configPath - Absolute path to `.lisa.config.json`
|
|
93
|
+
* @returns File contents, or `null` when absent or unreadable
|
|
94
|
+
*/
|
|
95
|
+
async function readConfigText(configPath) {
|
|
96
|
+
try {
|
|
97
|
+
return await readFile(configPath, "utf-8");
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse the config, reporting the failure rather than throwing.
|
|
105
|
+
* @param raw - File contents
|
|
106
|
+
* @returns The parsed config, or the parse error's message
|
|
107
|
+
*/
|
|
108
|
+
function parseConfig(raw) {
|
|
109
|
+
try {
|
|
110
|
+
return { config: JSON.parse(raw) };
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Report — and repair — an undeclared traceability gate.
|
|
118
|
+
* @param targetPath - Project path to inspect and repair
|
|
119
|
+
* @returns The doctor check result
|
|
120
|
+
*/
|
|
121
|
+
export async function checkTraceabilityGate(targetPath) {
|
|
122
|
+
const configPath = path.join(targetPath, ".lisa.config.json");
|
|
123
|
+
const raw = await readConfigText(configPath);
|
|
124
|
+
if (raw === null) {
|
|
125
|
+
// Not a Lisa project, or not one yet. Nothing to declare and nothing to
|
|
126
|
+
// repair — reporting a gap here would fire on every unrelated directory.
|
|
127
|
+
return {
|
|
128
|
+
name: CHECK_NAME,
|
|
129
|
+
status: "ok",
|
|
130
|
+
detail: "no .lisa.config.json — nothing to declare",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const parsed = parseConfig(raw);
|
|
134
|
+
if ("error" in parsed) {
|
|
135
|
+
return {
|
|
136
|
+
name: CHECK_NAME,
|
|
137
|
+
status: "warn",
|
|
138
|
+
detail: `.lisa.config.json is not valid JSON, so the gate could not be ` +
|
|
139
|
+
`checked: ${parsed.error}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const config = parsed.config;
|
|
143
|
+
if (declaresTraceability(config)) {
|
|
144
|
+
return {
|
|
145
|
+
name: CHECK_NAME,
|
|
146
|
+
status: "ok",
|
|
147
|
+
detail: `gates.${GATE_ID} is declared — left as the project set it. ${LAYER_TWO_NOTE}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// Two-space indent and a trailing newline: the convention every Lisa-written
|
|
151
|
+
// JSON in a host project uses, so the repair does not show up as a whole-file
|
|
152
|
+
// reformat in the diff that carries it.
|
|
153
|
+
await writeFile(configPath, `${JSON.stringify(withTraceabilityGate(config), null, 2)}\n`, "utf-8");
|
|
154
|
+
return {
|
|
155
|
+
name: CHECK_NAME,
|
|
156
|
+
status: "warn",
|
|
157
|
+
detail: `gates.${GATE_ID} was undeclared, so a missing Work-Item trailer reddened ` +
|
|
158
|
+
`the check and merged anyway — an undeclared gate emits no required ` +
|
|
159
|
+
`status context. ADDED as {"${GATE_MOMENT}": "${GATE_LEVEL}"} — commit ` +
|
|
160
|
+
`it. Declare it as "off" instead if this project is deliberately opting ` +
|
|
161
|
+
`out; that decision is preserved. ${LAYER_TWO_NOTE}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=doctor-traceability-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-traceability-gate.js","sourceRoot":"","sources":["../../src/cli/doctor-traceability-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAIlC,MAAM,UAAU,GAAG,kCAAkC,CAAC;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,cAAc,GAClB,4EAA4E;IAC5E,4EAA4E;IAC5E,6EAA6E;IAC7E,4DAA4D;IAC5D,4DAA4D,CAAC;AAE/D,+CAA+C;AAC/C,MAAM,CAAC,MAAM,OAAO,GAAG,cAAc,CAAC;AAEtC,qDAAqD;AACrD,MAAM,CAAC,MAAM,WAAW,GAAG,cAAc,CAAC;AAE1C,iDAAiD;AACjD,MAAM,CAAC,MAAM,UAAU,GAAG,UAAU,CAAC;AAOrC;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAkB;IACrD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAuB,MAAS;IAClE,OAAO;QACL,GAAG,MAAM;QACT,KAAK,EAAE;YACL,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;YACvB,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,EAAE,UAAU,EAAE;SACzC;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,cAAc,CAAC,UAAkB;IAC9C,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,WAAW,CAClB,GAAW;IAEX,IAAI,CAAC;QACH,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAe,EAAE,CAAC;IACnD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IAC3E,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,UAAkB;IAElB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,mBAAmB,CAAC,CAAC;IAC9D,MAAM,GAAG,GAAG,MAAM,cAAc,CAAC,UAAU,CAAC,CAAC;IAC7C,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,wEAAwE;QACxE,yEAAyE;QACzE,OAAO;YACL,IAAI,EAAE,UAAU;YAChB,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,2CAA2C;SACpD,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,OAAO,IAAI,MAAM,EAAE,CAAC;QACtB,OAAO;YACL,IAAI,EAAE,UAAU;YAChB,MAAM,EAAE,MAAM;YACd,MAAM,EACJ,gEAAgE;gBAChE,YAAY,MAAM,CAAC,KAAK,EAAE;SAC7B,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAE7B,IAAI,oBAAoB,CAAC,MAAM,CAAC,EAAE,CAAC;QACjC,OAAO;YACL,IAAI,EAAE,UAAU;YAChB,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,SAAS,OAAO,8CAA8C,cAAc,EAAE;SACvF,CAAC;IACJ,CAAC;IAED,6EAA6E;IAC7E,8EAA8E;IAC9E,wCAAwC;IACxC,MAAM,SAAS,CACb,UAAU,EACV,GAAG,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAC5D,OAAO,CACR,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,MAAM,EAAE,MAAM;QACd,MAAM,EACJ,SAAS,OAAO,2DAA2D;YAC3E,qEAAqE;YACrE,8BAA8B,WAAW,OAAO,UAAU,cAAc;YACxE,yEAAyE;YACzE,oCAAoC,cAAc,EAAE;KACvD,CAAC;AACJ,CAAC"}
|
package/dist/cli/doctor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAqBnE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,mDAAmD;AACnD,KAAK,YAAY,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAO3C,oCAAoC;AACpC,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,sCAAsC;AACtC,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,wCAAwC;AACxC,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wCAAwC;AACxC,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,OAAO,KAAK,CAAC;IACxB,cAAc,EAAE,OAAO,cAAc,CAAC;IACtC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;CACjD;AAaD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,IAAI,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,EAChD,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,WAAW,CAAC,CAyBtB;AA4MD;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,EAAE,aAAa,EACtB,YAAY,GAAE,OAAO,CAAC,kBAAkB,CAAM,GAC7C,OAAO,CAAC,YAAY,CAAC,CA4EvB"}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -19,6 +19,7 @@ import { checkRepositoryReadiness } from "./doctor-readiness.js";
|
|
|
19
19
|
import { checkReusableWorkflowRefs } from "./doctor-reusable-workflow-refs.js";
|
|
20
20
|
import { checkWorkerEpoch } from "./doctor-worker-epoch.js";
|
|
21
21
|
import { checkSerializeLegsContract } from "./doctor-serialize-legs-contract.js";
|
|
22
|
+
import { checkTraceabilityGate } from "./doctor-traceability-gate.js";
|
|
22
23
|
import { checkWorktreeHygiene } from "./doctor-worktree-hygiene.js";
|
|
23
24
|
import { checkWorktreeWorkAtRisk } from "./doctor-worktree-work-at-risk.js";
|
|
24
25
|
import { STARTERS } from "./starters.js";
|
|
@@ -262,6 +263,12 @@ export async function runDoctor(targetPath, options, dependencies = {}) {
|
|
|
262
263
|
await checkApplyFreshness(resolvedTarget),
|
|
263
264
|
checkYamlRuntime(),
|
|
264
265
|
await checkProjectConfig(resolvedTarget),
|
|
266
|
+
// Immediately after the config check, because it repairs the same file and
|
|
267
|
+
// an operator reading the output wants both config findings together. It
|
|
268
|
+
// is mutating: an undeclared gate is ADDED rather than merely reported,
|
|
269
|
+
// since an undeclared gate means the job never runs and there is no signal
|
|
270
|
+
// to notice (CodySwannGT/lisa#2677).
|
|
271
|
+
await checkTraceabilityGate(resolvedTarget),
|
|
265
272
|
await checkKaneProvider(resolvedTarget, deps),
|
|
266
273
|
await checkSonarProvider(resolvedTarget, deps),
|
|
267
274
|
await checkLegacyMonitorThresholds(resolvedTarget),
|