@codyswann/lisa 2.325.1 → 2.325.3
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/core/upstream-evidence-manifest.js +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-detect-tooling/scripts/detect-tooling.mjs +80 -9
- package/plugins/lisa/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +80 -9
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +80 -9
- 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/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +80 -9
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +80 -9
- 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/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +80 -9
|
@@ -425,7 +425,7 @@ export const UPSTREAM_EVIDENCE_MANIFEST = Object.freeze({
|
|
|
425
425
|
"plugins/src/base/skills/lisa-debrief/SKILL.md": "47e4cda36b07994ff47ab15fb04a17dd6b0c310ad804f9cae9d69637edaaa72a",
|
|
426
426
|
"plugins/src/base/skills/lisa-delivery-effectiveness/SKILL.md": "21bc55fa0e86a9694bd22269fd089dbfae0c54c199262f46a4955447acea0f35",
|
|
427
427
|
"plugins/src/base/skills/lisa-detect-tooling/SKILL.md": "1778a009d06099b3bd2d9a33b37bec34836b295e21fad220ae926491d3557208",
|
|
428
|
-
"plugins/src/base/skills/lisa-detect-tooling/scripts/detect-tooling.mjs": "
|
|
428
|
+
"plugins/src/base/skills/lisa-detect-tooling/scripts/detect-tooling.mjs": "05e0c4808e64fbdc277419ad00b6a68da96766a9261a5b97f3a4961bbab107c4",
|
|
429
429
|
"plugins/src/base/skills/lisa-doctor/SKILL.md": "fb41a1e63c5332d47a46e715aff52551f3df867033b5e4f37dfaeee30019b891",
|
|
430
430
|
"plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md": "c78ae2ba32c830d0b2cd7d24de91adf99e2b3cef4c7cab1c417580d86f41351f",
|
|
431
431
|
"plugins/src/base/skills/lisa-epic-triage/SKILL.md": "d02760411249bddbd396f283191fe3e82bb7b95bf9393a19a7025dc5a57c3ab7",
|
package/package.json
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"brace-expansion": ">=5.0.9"
|
|
116
116
|
},
|
|
117
117
|
"name": "@codyswann/lisa",
|
|
118
|
-
"version": "2.325.
|
|
118
|
+
"version": "2.325.3",
|
|
119
119
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
120
120
|
"main": "dist/index.js",
|
|
121
121
|
"exports": {
|
|
@@ -51,8 +51,13 @@ const KNOWN_TOOLS = Object.freeze({
|
|
|
51
51
|
viaNpm: "@playwright/test",
|
|
52
52
|
},
|
|
53
53
|
linear: {
|
|
54
|
-
why: "Linear
|
|
54
|
+
why: "Linear reaches a container through lisa-linear-access with LINEAR_API_KEY; only the MCP path needs browser OAuth, and there is no official CLI to pin.",
|
|
55
55
|
mcpFallback: "linear-server",
|
|
56
|
+
// No official Linear CLI exists, so no proposal can name one. Kept even
|
|
57
|
+
// though the substrate allowlist normally removes Linear before this
|
|
58
|
+
// point: a second signal reaching here must not resurrect a binary that
|
|
59
|
+
// does not exist.
|
|
60
|
+
noCli: true,
|
|
56
61
|
},
|
|
57
62
|
});
|
|
58
63
|
|
|
@@ -107,18 +112,35 @@ export function toolsFromScripts(pkg) {
|
|
|
107
112
|
* Treating the MCP server as CLI evidence would push operators toward an
|
|
108
113
|
* arbitrary third-party executable instead of the access-layer contract.
|
|
109
114
|
*/
|
|
115
|
+
/** Prefix marking evidence that shows a path is missing, not what fills it. */
|
|
116
|
+
const MCP_EVIDENCE = "MCP";
|
|
117
|
+
|
|
118
|
+
/** What to ask when the only evidence is an MCP-only integration. */
|
|
119
|
+
const REMOTE_PATH_QUESTION =
|
|
120
|
+
"no manifest entry proposed — confirm how this reaches a container: a CLI, " +
|
|
121
|
+
"a key-authenticated API call, or deliberately local-only";
|
|
122
|
+
|
|
110
123
|
const MCP_ACCESS_LAYER_SUBSTRATES = Object.freeze({
|
|
111
124
|
"linear-server":
|
|
112
125
|
"lisa-linear-access uses LINEAR_API_KEY in headless sessions",
|
|
113
126
|
});
|
|
114
127
|
|
|
115
128
|
/**
|
|
116
|
-
*
|
|
129
|
+
* Integrations wired only as an MCP server — the weakest signal here, and the
|
|
130
|
+
* easiest to over-read.
|
|
131
|
+
*
|
|
132
|
+
* True: an MCP server that authenticates interactively cannot authenticate in a
|
|
133
|
+
* container, so THAT path does not survive the trip.
|
|
134
|
+
*
|
|
135
|
+
* Not implied: that the integration is unavailable, or that a CLI is the
|
|
136
|
+
* answer. Linear proves both halves — its MCP server needs browser OAuth, and
|
|
137
|
+
* its GraphQL API is key-authenticated, so `lisa-linear-access` already reaches
|
|
138
|
+
* it headlessly with LINEAR_API_KEY and there is no official CLI to pin at all.
|
|
139
|
+
* A server whose access layer owns headless auth is skipped entirely.
|
|
117
140
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* that survives the trip.
|
|
141
|
+
* For anything else, this raises a QUESTION rather than asserting a need: this
|
|
142
|
+
* integration has no remote path as configured, confirm it has one. The answer
|
|
143
|
+
* may be a CLI, may be a direct API call, and may be "local-only on purpose".
|
|
122
144
|
* @param {object|null} mcp Parsed .mcp.json.
|
|
123
145
|
* @returns {Map<string, string>} Tool name to the server that implies it.
|
|
124
146
|
*/
|
|
@@ -139,7 +161,8 @@ export function toolsFromMcp(mcp) {
|
|
|
139
161
|
if (server) {
|
|
140
162
|
found.set(
|
|
141
163
|
tool,
|
|
142
|
-
|
|
164
|
+
`${MCP_EVIDENCE} server "${server}" — an interactively authenticated ` +
|
|
165
|
+
`MCP server has no remote path; confirm this integration has one`
|
|
143
166
|
);
|
|
144
167
|
}
|
|
145
168
|
}
|
|
@@ -182,8 +205,16 @@ export function toolsFromSecretNotes(notes) {
|
|
|
182
205
|
*/
|
|
183
206
|
export function toolsFromQuality(config) {
|
|
184
207
|
const found = new Map();
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
// Every runner under e2eCoverage, not a hardcoded one. Naming `playwright`
|
|
209
|
+
// explicitly meant `quality.e2eCoverage.maestro` — configured in this very
|
|
210
|
+
// repository — produced no signal at all, so the one tool that genuinely
|
|
211
|
+
// needs a pinned binary and genuinely was not declared stayed invisible to
|
|
212
|
+
// the detector built to find it. A gate that reports "nothing outstanding"
|
|
213
|
+
// while the gap is right there is worse than no gate.
|
|
214
|
+
for (const runner of Object.keys(config?.quality?.e2eCoverage ?? {})) {
|
|
215
|
+
if (Object.hasOwn(KNOWN_TOOLS, runner)) {
|
|
216
|
+
found.set(runner, `quality.e2eCoverage.${runner} is configured`);
|
|
217
|
+
}
|
|
187
218
|
}
|
|
188
219
|
if (config?.quality?.sonar || config?.sonar) {
|
|
189
220
|
found.set("sonar-scanner", "Sonar analysis is configured");
|
|
@@ -215,6 +246,32 @@ export function declaredTools(config, surface = "remote") {
|
|
|
215
246
|
);
|
|
216
247
|
}
|
|
217
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Whether a tool already reaches this project as an npm dependency.
|
|
251
|
+
*
|
|
252
|
+
* A package that declares `@playwright/test` gets the `playwright` binary in
|
|
253
|
+
* `node_modules/.bin`, and its npm scripts resolve it from there. It never
|
|
254
|
+
* needs to be on PATH, so proposing a manifest entry for it is noise — and
|
|
255
|
+
* noise is how a detector teaches people to skim it, which is the one failure
|
|
256
|
+
* this skill's own documentation warns about.
|
|
257
|
+
*
|
|
258
|
+
* Only the CLI is covered by this. Anything else the tool needs at runtime —
|
|
259
|
+
* Playwright's browsers, for instance — is a separate concern that a package
|
|
260
|
+
* manager does not solve and this function does not claim to.
|
|
261
|
+
* @param {object|null} pkg Parsed package.json.
|
|
262
|
+
* @param {string} tool Tool name.
|
|
263
|
+
* @returns {boolean} Whether npm already provides the binary.
|
|
264
|
+
*/
|
|
265
|
+
export function satisfiedByNpm(pkg, tool) {
|
|
266
|
+
const viaNpm = KNOWN_TOOLS[tool]?.viaNpm;
|
|
267
|
+
if (!viaNpm) return false;
|
|
268
|
+
const declared = {
|
|
269
|
+
...(pkg?.dependencies ?? {}),
|
|
270
|
+
...(pkg?.devDependencies ?? {}),
|
|
271
|
+
};
|
|
272
|
+
return Object.hasOwn(declared, viaNpm);
|
|
273
|
+
}
|
|
274
|
+
|
|
218
275
|
/**
|
|
219
276
|
* Collect every signal and subtract what the manifest already covers.
|
|
220
277
|
* @param {string} [cwd] Project root.
|
|
@@ -238,6 +295,8 @@ export function detectTooling(cwd = process.cwd()) {
|
|
|
238
295
|
for (const signal of signals) {
|
|
239
296
|
for (const [tool, evidence] of signal) {
|
|
240
297
|
if (declared.has(tool)) continue;
|
|
298
|
+
// Already provided by the package manager, so there is nothing to pin.
|
|
299
|
+
if (satisfiedByNpm(pkg, tool)) continue;
|
|
241
300
|
const entry = merged.get(tool) ?? { evidence: [] };
|
|
242
301
|
entry.evidence.push(evidence);
|
|
243
302
|
merged.set(tool, entry);
|
|
@@ -263,6 +322,17 @@ export function detectTooling(cwd = process.cwd()) {
|
|
|
263
322
|
* @returns {{install: object[], require: object[]}} Manifest skeletons.
|
|
264
323
|
*/
|
|
265
324
|
export function proposedEntries(proposal) {
|
|
325
|
+
// Whether a binary can be proposed is a fact about the TOOL, not about how it
|
|
326
|
+
// was detected. Keying this on "the evidence was an MCP server" suppressed
|
|
327
|
+
// exactly the wrong case: the substrate allowlist already removes Linear
|
|
328
|
+
// before it becomes evidence, so the only tools reaching that branch were
|
|
329
|
+
// ones like Maestro that genuinely do have a CLI — the binary the Expo
|
|
330
|
+
// template invokes and nothing installs.
|
|
331
|
+
//
|
|
332
|
+
// So the question is asked only where there is no pinnable CLI to name.
|
|
333
|
+
if (KNOWN_TOOLS[proposal.name]?.noCli) {
|
|
334
|
+
return { install: [], require: [], question: REMOTE_PATH_QUESTION };
|
|
335
|
+
}
|
|
266
336
|
const viaNpm = KNOWN_TOOLS[proposal.name]?.viaNpm;
|
|
267
337
|
if (viaNpm) {
|
|
268
338
|
// npm resolves per platform, so one entry serves every surface.
|
|
@@ -317,6 +387,7 @@ if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
|
|
|
317
387
|
console.log(` evidence: ${evidence}`);
|
|
318
388
|
}
|
|
319
389
|
const entries = proposedEntries(proposal);
|
|
390
|
+
if (entries.question) console.log(` ${entries.question}`);
|
|
320
391
|
for (const entry of entries.install) {
|
|
321
392
|
console.log(` tools.install: ${JSON.stringify(entry)}`);
|
|
322
393
|
}
|
|
@@ -51,8 +51,13 @@ const KNOWN_TOOLS = Object.freeze({
|
|
|
51
51
|
viaNpm: "@playwright/test",
|
|
52
52
|
},
|
|
53
53
|
linear: {
|
|
54
|
-
why: "Linear
|
|
54
|
+
why: "Linear reaches a container through lisa-linear-access with LINEAR_API_KEY; only the MCP path needs browser OAuth, and there is no official CLI to pin.",
|
|
55
55
|
mcpFallback: "linear-server",
|
|
56
|
+
// No official Linear CLI exists, so no proposal can name one. Kept even
|
|
57
|
+
// though the substrate allowlist normally removes Linear before this
|
|
58
|
+
// point: a second signal reaching here must not resurrect a binary that
|
|
59
|
+
// does not exist.
|
|
60
|
+
noCli: true,
|
|
56
61
|
},
|
|
57
62
|
});
|
|
58
63
|
|
|
@@ -107,18 +112,35 @@ export function toolsFromScripts(pkg) {
|
|
|
107
112
|
* Treating the MCP server as CLI evidence would push operators toward an
|
|
108
113
|
* arbitrary third-party executable instead of the access-layer contract.
|
|
109
114
|
*/
|
|
115
|
+
/** Prefix marking evidence that shows a path is missing, not what fills it. */
|
|
116
|
+
const MCP_EVIDENCE = "MCP";
|
|
117
|
+
|
|
118
|
+
/** What to ask when the only evidence is an MCP-only integration. */
|
|
119
|
+
const REMOTE_PATH_QUESTION =
|
|
120
|
+
"no manifest entry proposed — confirm how this reaches a container: a CLI, " +
|
|
121
|
+
"a key-authenticated API call, or deliberately local-only";
|
|
122
|
+
|
|
110
123
|
const MCP_ACCESS_LAYER_SUBSTRATES = Object.freeze({
|
|
111
124
|
"linear-server":
|
|
112
125
|
"lisa-linear-access uses LINEAR_API_KEY in headless sessions",
|
|
113
126
|
});
|
|
114
127
|
|
|
115
128
|
/**
|
|
116
|
-
*
|
|
129
|
+
* Integrations wired only as an MCP server — the weakest signal here, and the
|
|
130
|
+
* easiest to over-read.
|
|
131
|
+
*
|
|
132
|
+
* True: an MCP server that authenticates interactively cannot authenticate in a
|
|
133
|
+
* container, so THAT path does not survive the trip.
|
|
134
|
+
*
|
|
135
|
+
* Not implied: that the integration is unavailable, or that a CLI is the
|
|
136
|
+
* answer. Linear proves both halves — its MCP server needs browser OAuth, and
|
|
137
|
+
* its GraphQL API is key-authenticated, so `lisa-linear-access` already reaches
|
|
138
|
+
* it headlessly with LINEAR_API_KEY and there is no official CLI to pin at all.
|
|
139
|
+
* A server whose access layer owns headless auth is skipped entirely.
|
|
117
140
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* that survives the trip.
|
|
141
|
+
* For anything else, this raises a QUESTION rather than asserting a need: this
|
|
142
|
+
* integration has no remote path as configured, confirm it has one. The answer
|
|
143
|
+
* may be a CLI, may be a direct API call, and may be "local-only on purpose".
|
|
122
144
|
* @param {object|null} mcp Parsed .mcp.json.
|
|
123
145
|
* @returns {Map<string, string>} Tool name to the server that implies it.
|
|
124
146
|
*/
|
|
@@ -139,7 +161,8 @@ export function toolsFromMcp(mcp) {
|
|
|
139
161
|
if (server) {
|
|
140
162
|
found.set(
|
|
141
163
|
tool,
|
|
142
|
-
|
|
164
|
+
`${MCP_EVIDENCE} server "${server}" — an interactively authenticated ` +
|
|
165
|
+
`MCP server has no remote path; confirm this integration has one`
|
|
143
166
|
);
|
|
144
167
|
}
|
|
145
168
|
}
|
|
@@ -182,8 +205,16 @@ export function toolsFromSecretNotes(notes) {
|
|
|
182
205
|
*/
|
|
183
206
|
export function toolsFromQuality(config) {
|
|
184
207
|
const found = new Map();
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
// Every runner under e2eCoverage, not a hardcoded one. Naming `playwright`
|
|
209
|
+
// explicitly meant `quality.e2eCoverage.maestro` — configured in this very
|
|
210
|
+
// repository — produced no signal at all, so the one tool that genuinely
|
|
211
|
+
// needs a pinned binary and genuinely was not declared stayed invisible to
|
|
212
|
+
// the detector built to find it. A gate that reports "nothing outstanding"
|
|
213
|
+
// while the gap is right there is worse than no gate.
|
|
214
|
+
for (const runner of Object.keys(config?.quality?.e2eCoverage ?? {})) {
|
|
215
|
+
if (Object.hasOwn(KNOWN_TOOLS, runner)) {
|
|
216
|
+
found.set(runner, `quality.e2eCoverage.${runner} is configured`);
|
|
217
|
+
}
|
|
187
218
|
}
|
|
188
219
|
if (config?.quality?.sonar || config?.sonar) {
|
|
189
220
|
found.set("sonar-scanner", "Sonar analysis is configured");
|
|
@@ -215,6 +246,32 @@ export function declaredTools(config, surface = "remote") {
|
|
|
215
246
|
);
|
|
216
247
|
}
|
|
217
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Whether a tool already reaches this project as an npm dependency.
|
|
251
|
+
*
|
|
252
|
+
* A package that declares `@playwright/test` gets the `playwright` binary in
|
|
253
|
+
* `node_modules/.bin`, and its npm scripts resolve it from there. It never
|
|
254
|
+
* needs to be on PATH, so proposing a manifest entry for it is noise — and
|
|
255
|
+
* noise is how a detector teaches people to skim it, which is the one failure
|
|
256
|
+
* this skill's own documentation warns about.
|
|
257
|
+
*
|
|
258
|
+
* Only the CLI is covered by this. Anything else the tool needs at runtime —
|
|
259
|
+
* Playwright's browsers, for instance — is a separate concern that a package
|
|
260
|
+
* manager does not solve and this function does not claim to.
|
|
261
|
+
* @param {object|null} pkg Parsed package.json.
|
|
262
|
+
* @param {string} tool Tool name.
|
|
263
|
+
* @returns {boolean} Whether npm already provides the binary.
|
|
264
|
+
*/
|
|
265
|
+
export function satisfiedByNpm(pkg, tool) {
|
|
266
|
+
const viaNpm = KNOWN_TOOLS[tool]?.viaNpm;
|
|
267
|
+
if (!viaNpm) return false;
|
|
268
|
+
const declared = {
|
|
269
|
+
...(pkg?.dependencies ?? {}),
|
|
270
|
+
...(pkg?.devDependencies ?? {}),
|
|
271
|
+
};
|
|
272
|
+
return Object.hasOwn(declared, viaNpm);
|
|
273
|
+
}
|
|
274
|
+
|
|
218
275
|
/**
|
|
219
276
|
* Collect every signal and subtract what the manifest already covers.
|
|
220
277
|
* @param {string} [cwd] Project root.
|
|
@@ -238,6 +295,8 @@ export function detectTooling(cwd = process.cwd()) {
|
|
|
238
295
|
for (const signal of signals) {
|
|
239
296
|
for (const [tool, evidence] of signal) {
|
|
240
297
|
if (declared.has(tool)) continue;
|
|
298
|
+
// Already provided by the package manager, so there is nothing to pin.
|
|
299
|
+
if (satisfiedByNpm(pkg, tool)) continue;
|
|
241
300
|
const entry = merged.get(tool) ?? { evidence: [] };
|
|
242
301
|
entry.evidence.push(evidence);
|
|
243
302
|
merged.set(tool, entry);
|
|
@@ -263,6 +322,17 @@ export function detectTooling(cwd = process.cwd()) {
|
|
|
263
322
|
* @returns {{install: object[], require: object[]}} Manifest skeletons.
|
|
264
323
|
*/
|
|
265
324
|
export function proposedEntries(proposal) {
|
|
325
|
+
// Whether a binary can be proposed is a fact about the TOOL, not about how it
|
|
326
|
+
// was detected. Keying this on "the evidence was an MCP server" suppressed
|
|
327
|
+
// exactly the wrong case: the substrate allowlist already removes Linear
|
|
328
|
+
// before it becomes evidence, so the only tools reaching that branch were
|
|
329
|
+
// ones like Maestro that genuinely do have a CLI — the binary the Expo
|
|
330
|
+
// template invokes and nothing installs.
|
|
331
|
+
//
|
|
332
|
+
// So the question is asked only where there is no pinnable CLI to name.
|
|
333
|
+
if (KNOWN_TOOLS[proposal.name]?.noCli) {
|
|
334
|
+
return { install: [], require: [], question: REMOTE_PATH_QUESTION };
|
|
335
|
+
}
|
|
266
336
|
const viaNpm = KNOWN_TOOLS[proposal.name]?.viaNpm;
|
|
267
337
|
if (viaNpm) {
|
|
268
338
|
// npm resolves per platform, so one entry serves every surface.
|
|
@@ -317,6 +387,7 @@ if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
|
|
|
317
387
|
console.log(` evidence: ${evidence}`);
|
|
318
388
|
}
|
|
319
389
|
const entries = proposedEntries(proposal);
|
|
390
|
+
if (entries.question) console.log(` ${entries.question}`);
|
|
320
391
|
for (const entry of entries.install) {
|
|
321
392
|
console.log(` tools.install: ${JSON.stringify(entry)}`);
|
|
322
393
|
}
|
|
@@ -51,8 +51,13 @@ const KNOWN_TOOLS = Object.freeze({
|
|
|
51
51
|
viaNpm: "@playwright/test",
|
|
52
52
|
},
|
|
53
53
|
linear: {
|
|
54
|
-
why: "Linear
|
|
54
|
+
why: "Linear reaches a container through lisa-linear-access with LINEAR_API_KEY; only the MCP path needs browser OAuth, and there is no official CLI to pin.",
|
|
55
55
|
mcpFallback: "linear-server",
|
|
56
|
+
// No official Linear CLI exists, so no proposal can name one. Kept even
|
|
57
|
+
// though the substrate allowlist normally removes Linear before this
|
|
58
|
+
// point: a second signal reaching here must not resurrect a binary that
|
|
59
|
+
// does not exist.
|
|
60
|
+
noCli: true,
|
|
56
61
|
},
|
|
57
62
|
});
|
|
58
63
|
|
|
@@ -107,18 +112,35 @@ export function toolsFromScripts(pkg) {
|
|
|
107
112
|
* Treating the MCP server as CLI evidence would push operators toward an
|
|
108
113
|
* arbitrary third-party executable instead of the access-layer contract.
|
|
109
114
|
*/
|
|
115
|
+
/** Prefix marking evidence that shows a path is missing, not what fills it. */
|
|
116
|
+
const MCP_EVIDENCE = "MCP";
|
|
117
|
+
|
|
118
|
+
/** What to ask when the only evidence is an MCP-only integration. */
|
|
119
|
+
const REMOTE_PATH_QUESTION =
|
|
120
|
+
"no manifest entry proposed — confirm how this reaches a container: a CLI, " +
|
|
121
|
+
"a key-authenticated API call, or deliberately local-only";
|
|
122
|
+
|
|
110
123
|
const MCP_ACCESS_LAYER_SUBSTRATES = Object.freeze({
|
|
111
124
|
"linear-server":
|
|
112
125
|
"lisa-linear-access uses LINEAR_API_KEY in headless sessions",
|
|
113
126
|
});
|
|
114
127
|
|
|
115
128
|
/**
|
|
116
|
-
*
|
|
129
|
+
* Integrations wired only as an MCP server — the weakest signal here, and the
|
|
130
|
+
* easiest to over-read.
|
|
131
|
+
*
|
|
132
|
+
* True: an MCP server that authenticates interactively cannot authenticate in a
|
|
133
|
+
* container, so THAT path does not survive the trip.
|
|
134
|
+
*
|
|
135
|
+
* Not implied: that the integration is unavailable, or that a CLI is the
|
|
136
|
+
* answer. Linear proves both halves — its MCP server needs browser OAuth, and
|
|
137
|
+
* its GraphQL API is key-authenticated, so `lisa-linear-access` already reaches
|
|
138
|
+
* it headlessly with LINEAR_API_KEY and there is no official CLI to pin at all.
|
|
139
|
+
* A server whose access layer owns headless auth is skipped entirely.
|
|
117
140
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* that survives the trip.
|
|
141
|
+
* For anything else, this raises a QUESTION rather than asserting a need: this
|
|
142
|
+
* integration has no remote path as configured, confirm it has one. The answer
|
|
143
|
+
* may be a CLI, may be a direct API call, and may be "local-only on purpose".
|
|
122
144
|
* @param {object|null} mcp Parsed .mcp.json.
|
|
123
145
|
* @returns {Map<string, string>} Tool name to the server that implies it.
|
|
124
146
|
*/
|
|
@@ -139,7 +161,8 @@ export function toolsFromMcp(mcp) {
|
|
|
139
161
|
if (server) {
|
|
140
162
|
found.set(
|
|
141
163
|
tool,
|
|
142
|
-
|
|
164
|
+
`${MCP_EVIDENCE} server "${server}" — an interactively authenticated ` +
|
|
165
|
+
`MCP server has no remote path; confirm this integration has one`
|
|
143
166
|
);
|
|
144
167
|
}
|
|
145
168
|
}
|
|
@@ -182,8 +205,16 @@ export function toolsFromSecretNotes(notes) {
|
|
|
182
205
|
*/
|
|
183
206
|
export function toolsFromQuality(config) {
|
|
184
207
|
const found = new Map();
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
// Every runner under e2eCoverage, not a hardcoded one. Naming `playwright`
|
|
209
|
+
// explicitly meant `quality.e2eCoverage.maestro` — configured in this very
|
|
210
|
+
// repository — produced no signal at all, so the one tool that genuinely
|
|
211
|
+
// needs a pinned binary and genuinely was not declared stayed invisible to
|
|
212
|
+
// the detector built to find it. A gate that reports "nothing outstanding"
|
|
213
|
+
// while the gap is right there is worse than no gate.
|
|
214
|
+
for (const runner of Object.keys(config?.quality?.e2eCoverage ?? {})) {
|
|
215
|
+
if (Object.hasOwn(KNOWN_TOOLS, runner)) {
|
|
216
|
+
found.set(runner, `quality.e2eCoverage.${runner} is configured`);
|
|
217
|
+
}
|
|
187
218
|
}
|
|
188
219
|
if (config?.quality?.sonar || config?.sonar) {
|
|
189
220
|
found.set("sonar-scanner", "Sonar analysis is configured");
|
|
@@ -215,6 +246,32 @@ export function declaredTools(config, surface = "remote") {
|
|
|
215
246
|
);
|
|
216
247
|
}
|
|
217
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Whether a tool already reaches this project as an npm dependency.
|
|
251
|
+
*
|
|
252
|
+
* A package that declares `@playwright/test` gets the `playwright` binary in
|
|
253
|
+
* `node_modules/.bin`, and its npm scripts resolve it from there. It never
|
|
254
|
+
* needs to be on PATH, so proposing a manifest entry for it is noise — and
|
|
255
|
+
* noise is how a detector teaches people to skim it, which is the one failure
|
|
256
|
+
* this skill's own documentation warns about.
|
|
257
|
+
*
|
|
258
|
+
* Only the CLI is covered by this. Anything else the tool needs at runtime —
|
|
259
|
+
* Playwright's browsers, for instance — is a separate concern that a package
|
|
260
|
+
* manager does not solve and this function does not claim to.
|
|
261
|
+
* @param {object|null} pkg Parsed package.json.
|
|
262
|
+
* @param {string} tool Tool name.
|
|
263
|
+
* @returns {boolean} Whether npm already provides the binary.
|
|
264
|
+
*/
|
|
265
|
+
export function satisfiedByNpm(pkg, tool) {
|
|
266
|
+
const viaNpm = KNOWN_TOOLS[tool]?.viaNpm;
|
|
267
|
+
if (!viaNpm) return false;
|
|
268
|
+
const declared = {
|
|
269
|
+
...(pkg?.dependencies ?? {}),
|
|
270
|
+
...(pkg?.devDependencies ?? {}),
|
|
271
|
+
};
|
|
272
|
+
return Object.hasOwn(declared, viaNpm);
|
|
273
|
+
}
|
|
274
|
+
|
|
218
275
|
/**
|
|
219
276
|
* Collect every signal and subtract what the manifest already covers.
|
|
220
277
|
* @param {string} [cwd] Project root.
|
|
@@ -238,6 +295,8 @@ export function detectTooling(cwd = process.cwd()) {
|
|
|
238
295
|
for (const signal of signals) {
|
|
239
296
|
for (const [tool, evidence] of signal) {
|
|
240
297
|
if (declared.has(tool)) continue;
|
|
298
|
+
// Already provided by the package manager, so there is nothing to pin.
|
|
299
|
+
if (satisfiedByNpm(pkg, tool)) continue;
|
|
241
300
|
const entry = merged.get(tool) ?? { evidence: [] };
|
|
242
301
|
entry.evidence.push(evidence);
|
|
243
302
|
merged.set(tool, entry);
|
|
@@ -263,6 +322,17 @@ export function detectTooling(cwd = process.cwd()) {
|
|
|
263
322
|
* @returns {{install: object[], require: object[]}} Manifest skeletons.
|
|
264
323
|
*/
|
|
265
324
|
export function proposedEntries(proposal) {
|
|
325
|
+
// Whether a binary can be proposed is a fact about the TOOL, not about how it
|
|
326
|
+
// was detected. Keying this on "the evidence was an MCP server" suppressed
|
|
327
|
+
// exactly the wrong case: the substrate allowlist already removes Linear
|
|
328
|
+
// before it becomes evidence, so the only tools reaching that branch were
|
|
329
|
+
// ones like Maestro that genuinely do have a CLI — the binary the Expo
|
|
330
|
+
// template invokes and nothing installs.
|
|
331
|
+
//
|
|
332
|
+
// So the question is asked only where there is no pinnable CLI to name.
|
|
333
|
+
if (KNOWN_TOOLS[proposal.name]?.noCli) {
|
|
334
|
+
return { install: [], require: [], question: REMOTE_PATH_QUESTION };
|
|
335
|
+
}
|
|
266
336
|
const viaNpm = KNOWN_TOOLS[proposal.name]?.viaNpm;
|
|
267
337
|
if (viaNpm) {
|
|
268
338
|
// npm resolves per platform, so one entry serves every surface.
|
|
@@ -317,6 +387,7 @@ if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
|
|
|
317
387
|
console.log(` evidence: ${evidence}`);
|
|
318
388
|
}
|
|
319
389
|
const entries = proposedEntries(proposal);
|
|
390
|
+
if (entries.question) console.log(` ${entries.question}`);
|
|
320
391
|
for (const entry of entries.install) {
|
|
321
392
|
console.log(` tools.install: ${JSON.stringify(entry)}`);
|
|
322
393
|
}
|