@codyswann/lisa 3.51.2 → 3.51.4
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-hooks/block-direct-issue-create.sh +271 -12
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +2 -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 +9 -7
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +162 -9
- 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-sonarcloud-access/SKILL.md +33 -15
- package/plugins/lisa/hooks/block-direct-issue-create.sh +271 -12
- package/plugins/lisa/rules/eager/ready-role-filing.md +2 -0
- package/plugins/lisa/rules/reference/credential-substrate-precedence.md +11 -5
- package/plugins/lisa/rules/reference/integration-access-layer.md +12 -9
- package/plugins/lisa/rules/reference/ready-role-filing.md +27 -0
- package/plugins/lisa/skills/lisa-sonarcloud-access/SKILL.md +34 -16
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +271 -12
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-sonarcloud-access/SKILL.md +34 -16
- 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/hooks/block-direct-issue-create.sh +271 -12
- package/plugins/lisa-copilot/rules/eager/ready-role-filing.md +2 -0
- package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +11 -5
- package/plugins/lisa-copilot/rules/reference/integration-access-layer.md +12 -9
- package/plugins/lisa-copilot/rules/reference/ready-role-filing.md +27 -0
- package/plugins/lisa-copilot/skills/lisa-sonarcloud-access/SKILL.md +34 -16
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +271 -12
- package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +11 -5
- package/plugins/lisa-cursor/rules/integration-access-layer-reference.mdc +12 -9
- package/plugins/lisa-cursor/rules/ready-role-filing-reference.mdc +27 -0
- package/plugins/lisa-cursor/rules/ready-role-filing.mdc +2 -0
- package/plugins/lisa-cursor/skills/lisa-sonarcloud-access/SKILL.md +34 -16
- 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/hooks/block-direct-issue-create.sh +271 -12
- package/plugins/src/base/rules/eager/ready-role-filing.md +2 -0
- package/plugins/src/base/rules/reference/credential-substrate-precedence.md +11 -5
- package/plugins/src/base/rules/reference/integration-access-layer.md +12 -9
- package/plugins/src/base/rules/reference/ready-role-filing.md +27 -0
- package/plugins/src/base/skills/lisa-sonarcloud-access/SKILL.md +34 -16
|
@@ -43,6 +43,16 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
43
43
|
*/
|
|
44
44
|
const LABEL_FLAG =
|
|
45
45
|
/--(?:label|labels|add-label|status|state)(?:=|\s+)(['"]?)([^'"\s]+)\1/g;
|
|
46
|
+
/**
|
|
47
|
+
* The repository a creation is ADDRESSED at, which decides whose ready role
|
|
48
|
+
* answers for it. `-R` is honored here even though short flags are refused
|
|
49
|
+
* for labels above: `--repo`/`-R` is one flag on one CLI with one meaning,
|
|
50
|
+
* where the label short forms collide across trackers.
|
|
51
|
+
*/
|
|
52
|
+
const REPO_FLAG = /(?:^|\s)(?:--repo|-R)(?:=|\s+)(['"]?)([^'"\s]+)\1/;
|
|
53
|
+
const ISSUES_ENDPOINT = /repos\/([^/\s]+)\/([^/\s]+)\/issues\b/;
|
|
54
|
+
const DEFAULT_READY_ROLE = "status:ready";
|
|
55
|
+
const DEFAULT_UPSTREAM_REPO = "CodySwannGT/lisa";
|
|
46
56
|
const CREATION_SIGNATURES: readonly {
|
|
47
57
|
readonly re: RegExp;
|
|
48
58
|
readonly name: string;
|
|
@@ -85,9 +95,35 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
85
95
|
|
|
86
96
|
interface LisaConfig {
|
|
87
97
|
tracker?: string;
|
|
88
|
-
github?: {
|
|
98
|
+
github?: {
|
|
99
|
+
org?: string;
|
|
100
|
+
repo?: string;
|
|
101
|
+
labels?: { build?: { ready?: string } };
|
|
102
|
+
};
|
|
89
103
|
jira?: { workflow?: { ready?: string } };
|
|
90
104
|
linear?: { workflow?: { ready?: string } };
|
|
105
|
+
hardening?: { upstreamRepo?: string; upstreamReadyRole?: string };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Everything the guard needs to decide whose ready role answers. */
|
|
109
|
+
interface FilingPolicy {
|
|
110
|
+
/** The calling project's own build-ready role. */
|
|
111
|
+
readonly readyRole: string;
|
|
112
|
+
/** The calling project's own repository, when it declares one. */
|
|
113
|
+
readonly ownRepo: string | undefined;
|
|
114
|
+
/** The repository upstream defects are filed at. */
|
|
115
|
+
readonly upstreamRepo: string;
|
|
116
|
+
/** The ready role that repository runs its build queue off. */
|
|
117
|
+
readonly upstreamReadyRole: string;
|
|
118
|
+
/** Whether the caller's ready role is a GitHub label at all. */
|
|
119
|
+
readonly callerIsGithub: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Which roles satisfy a filing, and the target a refusal should name. */
|
|
123
|
+
interface Verdict {
|
|
124
|
+
readonly roles: readonly string[];
|
|
125
|
+
/** Set only when the filing is provably addressed at another repository. */
|
|
126
|
+
readonly named: string | undefined;
|
|
91
127
|
}
|
|
92
128
|
|
|
93
129
|
/**
|
|
@@ -104,7 +140,7 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
104
140
|
};
|
|
105
141
|
|
|
106
142
|
/**
|
|
107
|
-
* The
|
|
143
|
+
* The filing policy, or undefined when no tracker is set.
|
|
108
144
|
*
|
|
109
145
|
* Keyed off the resolved `tracker` rather than provider precedence: reading
|
|
110
146
|
* whichever provider block happened to appear first could hand a GitHub label
|
|
@@ -117,9 +153,9 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
117
153
|
* Resolved once per session at plugin init. That is a deliberate snapshot: a
|
|
118
154
|
* config edit mid-session needs a session restart to take effect, which is
|
|
119
155
|
* the same lifetime as the rest of this plugin's state.
|
|
120
|
-
* @returns The
|
|
156
|
+
* @returns The filing policy, or undefined.
|
|
121
157
|
*/
|
|
122
|
-
const
|
|
158
|
+
const resolvePolicy = async (): Promise<FilingPolicy | undefined> => {
|
|
123
159
|
const base = await readConfig(".lisa.config.json");
|
|
124
160
|
const local = await readConfig(".lisa.config.local.json");
|
|
125
161
|
const tracker = local.tracker ?? base.tracker;
|
|
@@ -132,10 +168,81 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
132
168
|
if (tracker === "linear") return config.linear?.workflow?.ready;
|
|
133
169
|
return undefined;
|
|
134
170
|
};
|
|
135
|
-
|
|
171
|
+
const org = local.github?.org ?? base.github?.org;
|
|
172
|
+
const name = local.github?.repo ?? base.github?.repo;
|
|
173
|
+
const hardening = { ...base.hardening, ...local.hardening };
|
|
174
|
+
return {
|
|
175
|
+
readyRole: pick(local) ?? pick(base) ?? DEFAULT_READY_ROLE,
|
|
176
|
+
ownRepo: org && name ? `${org}/${name}`.toLowerCase() : undefined,
|
|
177
|
+
upstreamRepo: (
|
|
178
|
+
hardening.upstreamRepo ?? DEFAULT_UPSTREAM_REPO
|
|
179
|
+
).toLowerCase(),
|
|
180
|
+
upstreamReadyRole: hardening.upstreamReadyRole ?? DEFAULT_READY_ROLE,
|
|
181
|
+
callerIsGithub: tracker === "github",
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const policy = await resolvePolicy();
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A repository token reduced to a comparable `owner/name`.
|
|
189
|
+
*
|
|
190
|
+
* gh accepts `OWNER/REPO`, `HOST/OWNER/REPO`, and a full browser URL, and
|
|
191
|
+
* GitHub is case-insensitive about both halves — so comparing raw tokens
|
|
192
|
+
* would call one repository two different places depending on how it was
|
|
193
|
+
* typed.
|
|
194
|
+
* @param value The raw token.
|
|
195
|
+
* @returns A lowercased `owner/name`, or undefined when it names no repo.
|
|
196
|
+
*/
|
|
197
|
+
const normaliseRepo = (value: string): string | undefined => {
|
|
198
|
+
const text = value.replace(/\.git$/, "");
|
|
199
|
+
const parts = text.split("/").filter(part => part && !part.endsWith(":"));
|
|
200
|
+
if (parts.length < 2) return undefined;
|
|
201
|
+
return `${parts.at(-2)}/${parts.at(-1)}`.toLowerCase();
|
|
136
202
|
};
|
|
137
203
|
|
|
138
|
-
|
|
204
|
+
/**
|
|
205
|
+
* The repository this creation is addressed at, when it names one.
|
|
206
|
+
* @param declarable The command text up to a bare `--`.
|
|
207
|
+
* @returns A lowercased `owner/name`, or undefined when the calling project
|
|
208
|
+
* is the target — the common case, and today's behaviour.
|
|
209
|
+
*/
|
|
210
|
+
const targetRepository = (declarable: string): string | undefined => {
|
|
211
|
+
const flag = REPO_FLAG.exec(declarable);
|
|
212
|
+
if (flag?.[2]) return normaliseRepo(flag[2]);
|
|
213
|
+
const endpoint = ISSUES_ENDPOINT.exec(declarable);
|
|
214
|
+
if (endpoint) return normaliseRepo(`${endpoint[1]}/${endpoint[2]}`);
|
|
215
|
+
return undefined;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Which ready-role tokens satisfy a creation addressed at `target`.
|
|
220
|
+
*
|
|
221
|
+
* A declaration is demanded either way; this decides only WHOSE vocabulary
|
|
222
|
+
* it is written in. The last branch is the indeterminate case — a
|
|
223
|
+
* GitHub-tracked project declaring no `github.org`/`github.repo` cannot be
|
|
224
|
+
* compared against a target, so both roles are accepted rather than
|
|
225
|
+
* inventing a refusal, and no cross-repo target is reported: the cross-repo
|
|
226
|
+
* message would claim this project's role does not answer, which is false in
|
|
227
|
+
* exactly that branch.
|
|
228
|
+
* @param resolved The filing policy.
|
|
229
|
+
* @param target The addressed repository, or undefined.
|
|
230
|
+
* @returns The acceptable role tokens, and the target to name in a refusal.
|
|
231
|
+
*/
|
|
232
|
+
const rolesFor = (
|
|
233
|
+
resolved: FilingPolicy,
|
|
234
|
+
target: string | undefined
|
|
235
|
+
): Verdict => {
|
|
236
|
+
if (target === undefined || target === resolved.ownRepo)
|
|
237
|
+
return { roles: [resolved.readyRole], named: undefined };
|
|
238
|
+
const role =
|
|
239
|
+
target === resolved.upstreamRepo
|
|
240
|
+
? resolved.upstreamReadyRole
|
|
241
|
+
: DEFAULT_READY_ROLE;
|
|
242
|
+
if (resolved.ownRepo !== undefined || !resolved.callerIsGithub)
|
|
243
|
+
return { roles: [role], named: target };
|
|
244
|
+
return { roles: [resolved.readyRole, role], named: undefined };
|
|
245
|
+
};
|
|
139
246
|
|
|
140
247
|
return {
|
|
141
248
|
"tool.execute.before": async (
|
|
@@ -143,7 +250,7 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
143
250
|
output: { args?: { command?: string } }
|
|
144
251
|
) => {
|
|
145
252
|
if (input.tool !== "bash") return;
|
|
146
|
-
if (
|
|
253
|
+
if (policy === undefined) return;
|
|
147
254
|
const command = String(output.args?.command ?? "");
|
|
148
255
|
if (!command) return;
|
|
149
256
|
if (/--help\b|\s-h(\s|$)/.test(command)) return;
|
|
@@ -168,13 +275,59 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
168
275
|
// post-`--` flags outright; acli parses straight past them and creates
|
|
169
276
|
// the item with the flag silently unapplied (verified). Fails closed.
|
|
170
277
|
const declarable = command.split(/(?:^|\s)--(?:\s|$)/)[0] ?? command;
|
|
278
|
+
// WHICH repository's vocabulary answers is decided by where the create
|
|
279
|
+
// is addressed, not by whose config file is nearest. A filing aimed at
|
|
280
|
+
// another repository used to be judged against this project's role,
|
|
281
|
+
// which that repository does not carry — and on a JIRA or Linear caller
|
|
282
|
+
// the demanded token was a workflow STATE, so there was no satisfiable
|
|
283
|
+
// answer at all. The property is unchanged: a declaration is still
|
|
284
|
+
// required, wherever the item lands.
|
|
285
|
+
const { roles, named } = rolesFor(policy, targetRepository(declarable));
|
|
171
286
|
const declaresRole = [...declarable.matchAll(LABEL_FLAG)].some(match =>
|
|
172
287
|
(match[2] ?? "")
|
|
173
288
|
.split(",")
|
|
174
289
|
.map(part => part.trim())
|
|
175
|
-
.includes(
|
|
290
|
+
.some(candidate => roles.includes(candidate))
|
|
176
291
|
);
|
|
177
292
|
if (declaresRole || declarable.includes(HUMAN_GATE_MARKER)) return;
|
|
293
|
+
if (named !== undefined)
|
|
294
|
+
throw new Error(
|
|
295
|
+
[
|
|
296
|
+
`block-direct-issue-create: refusing ${signature.name} — this filing declares no readiness.`,
|
|
297
|
+
"",
|
|
298
|
+
"WHY: a work item filed without the build-ready role is an incomplete",
|
|
299
|
+
"handoff. Build-intake scans the ready lane and nothing else, so nothing",
|
|
300
|
+
"will ever pick it up: the write succeeds and the work still dies.",
|
|
301
|
+
"",
|
|
302
|
+
`THIS FILING IS ADDRESSED AT ANOTHER REPOSITORY: ${named}.`,
|
|
303
|
+
"That repository runs its own build queue off its own ready role, so this",
|
|
304
|
+
"project's role does not answer for it — and this project's filing flow",
|
|
305
|
+
"writes to this project's tracker, so it cannot reach the target at all.",
|
|
306
|
+
"",
|
|
307
|
+
"FILE IT THE SANCTIONED WAY:",
|
|
308
|
+
"",
|
|
309
|
+
"1. An upstream defect or hardening report. Use the upstream filing path,",
|
|
310
|
+
" which composes a redacted, public-safe body through an allowlist",
|
|
311
|
+
" projection instead of free-form prose:",
|
|
312
|
+
"",
|
|
313
|
+
" bunx @codyswann/lisa file-upstream --input <filing-event>.json",
|
|
314
|
+
"",
|
|
315
|
+
" lisa-persist-learning step 6 runs exactly this, headless, on a cron.",
|
|
316
|
+
"2. If you must run the CLI directly, the command has to carry the TARGET",
|
|
317
|
+
` repository's build-ready role — ${roles.join(", ")} — as the value of a`,
|
|
318
|
+
" --label flag. Configure it as hardening.upstreamReadyRole when the",
|
|
319
|
+
" target renamed its lane.",
|
|
320
|
+
"",
|
|
321
|
+
`DO NOT reach for ${HUMAN_GATE_MARKER} to get past this one. It still`,
|
|
322
|
+
"satisfies the guard, but on an upstream defect report it is a false",
|
|
323
|
+
"declaration: the target's build queue scans the ready role and nothing",
|
|
324
|
+
"else, so the report is filed and never picked up.",
|
|
325
|
+
"",
|
|
326
|
+
"OPERATOR ESCAPE: a human can export LISA_ALLOW_DIRECT_ISSUE_CREATE=1 in",
|
|
327
|
+
"the environment before starting the session. Setting it inline on this",
|
|
328
|
+
"command is deliberately refused.",
|
|
329
|
+
].join("\n")
|
|
330
|
+
);
|
|
178
331
|
throw new Error(
|
|
179
332
|
[
|
|
180
333
|
`block-direct-issue-create: refusing ${signature.name} — this filing declares no readiness.`,
|
|
@@ -197,7 +350,7 @@ const LisaBlockDirectIssueCreate = async () => {
|
|
|
197
350
|
"the ready-role-filing rule for the full contract.",
|
|
198
351
|
"",
|
|
199
352
|
"If you must run the CLI directly, the command has to carry one of the",
|
|
200
|
-
`two declarations itself: the configured build-ready role "${readyRole}",`,
|
|
353
|
+
`two declarations itself: the configured build-ready role "${policy.readyRole}",`,
|
|
201
354
|
`or a ${HUMAN_GATE_MARKER} marker in the body it submits.`,
|
|
202
355
|
"",
|
|
203
356
|
"OPERATOR ESCAPE: a human can export LISA_ALLOW_DIRECT_ISSUE_CREATE=1 in",
|
package/package.json
CHANGED
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
"zod-validation-error": "^4.0.0"
|
|
142
142
|
},
|
|
143
143
|
"name": "@codyswann/lisa",
|
|
144
|
-
"version": "3.51.
|
|
144
|
+
"version": "3.51.4",
|
|
145
145
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
146
146
|
"main": "dist/index.js",
|
|
147
147
|
"exports": {
|
|
@@ -12,16 +12,26 @@ this skill owns the tool selection.
|
|
|
12
12
|
|
|
13
13
|
## Substrate
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The **preferred** substrate is the **official SonarQube MCP server**, provided by
|
|
16
16
|
the `sonarqube` plugin and launched by the `sonar` CLI (`sonar run mcp`). It
|
|
17
17
|
authenticates headlessly from environment variables — no browser, no OS keychain —
|
|
18
18
|
so it is the same substrate on a developer machine and in a headless cloud routine.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
Prefer it because it owns the tool selection this skill exists to centralise.
|
|
20
|
+
|
|
21
|
+
It is not the only sanctioned substrate. The **Sonar Web API**, authenticated with
|
|
22
|
+
the same token, is a sanctioned fallback for read-only operations when the MCP is
|
|
23
|
+
not wired on this surface. An earlier revision of this file said otherwise — that
|
|
24
|
+
the MCP was "the only sanctioned substrate" and a missing MCP was "not a reason to
|
|
25
|
+
curl the Web API" — and that was wrong on its face: `.github/workflows/quality.yml`
|
|
26
|
+
already reads `api/ce/task` with a token to turn an opaque failed scan into an
|
|
27
|
+
operator-readable one. A rule the shipping repository contradicts is documentation,
|
|
28
|
+
not a constraint.
|
|
29
|
+
|
|
30
|
+
The distinction that does hold: a token-authenticated MCP **is** the
|
|
31
|
+
configured-provider substrate under `credential-substrate-precedence` (it is not
|
|
32
|
+
browser-OAuth), so there is no interactive tier to demote here. That is a statement
|
|
33
|
+
about tier ORDER, not about exclusivity. Identity-match against the configured org
|
|
34
|
+
remains mandatory on either substrate.
|
|
25
35
|
|
|
26
36
|
- `SONARQUBE_CLI_TOKEN` — required (Sonar user/analysis token).
|
|
27
37
|
- `SONARQUBE_CLI_ORG` — required for SonarQube Cloud.
|
|
@@ -89,16 +99,21 @@ being on PATH is not access. Probe by searching projects (the `projects` toolset
|
|
|
89
99
|
always enabled):
|
|
90
100
|
|
|
91
101
|
- If a `mcp__sonarqube__*` project-search tool returns, access is proven.
|
|
92
|
-
- If no `mcp__sonarqube__*` tool is present,
|
|
93
|
-
|
|
102
|
+
- If no `mcp__sonarqube__*` tool is present on this surface, that is a **demotion,
|
|
103
|
+
not a blocker**. Fall back to the Sonar Web API with the same resolved token for
|
|
104
|
+
the read-only operation at hand.
|
|
105
|
+
- Only when neither substrate can prove access — no MCP tool *and* no usable token
|
|
106
|
+
— is this a terminal tool-access-gate failure. Say so, and do not improvise a
|
|
107
|
+
third route:
|
|
94
108
|
|
|
95
109
|
```text
|
|
96
110
|
Error: no SonarQube access. Run /lisa:setup:sonar (or `sonar integrate <agent>`), and set SONARQUBE_CLI_TOKEN (+ SONARQUBE_CLI_ORG for Cloud / SONARQUBE_CLI_SERVER for Server).
|
|
97
111
|
```
|
|
98
112
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
The distinction matters because the two cases have different remedies. An absent
|
|
114
|
+
MCP on a surface that holds a valid credential is a wiring gap the agent can work
|
|
115
|
+
around; treating it as terminal manufactures a tool-access failure on a surface
|
|
116
|
+
that has access.
|
|
102
117
|
|
|
103
118
|
## Operation → toolset map
|
|
104
119
|
|
|
@@ -127,14 +142,17 @@ tool accepts them.
|
|
|
127
142
|
Every operation in the map above is **read-only** — quality, coverage, and
|
|
128
143
|
security data. So the `credential-substrate-precedence` guarded fallback for
|
|
129
144
|
mutating operations (write, read back, assert the tenant from the response, roll
|
|
130
|
-
back on mismatch) is not engaged here
|
|
131
|
-
|
|
145
|
+
back on mismatch) is not engaged here, because nothing in the map writes. The
|
|
146
|
+
Web API fallback is sanctioned for READS only; it does not open a mutation path.
|
|
147
|
+
A future operation that mutates Sonar state — marking
|
|
132
148
|
a hotspot safe, changing an issue's status — is a write and MUST reconcile by
|
|
133
149
|
read-back before any retry.
|
|
134
150
|
|
|
135
151
|
## Invariants
|
|
136
152
|
|
|
137
|
-
- The official SonarQube MCP is the
|
|
153
|
+
- The official SonarQube MCP is the preferred substrate, not the only one. The
|
|
154
|
+
Sonar Web API with the same token is a sanctioned read-only fallback, and a
|
|
155
|
+
missing MCP is a demotion rather than a terminal failure.
|
|
138
156
|
- `SONARQUBE_CLI_*` values are resolved through `lisa-secrets-access` and
|
|
139
157
|
exported in-process, with the bare environment variables as the documented
|
|
140
158
|
fallback. Never write them to a dotfile or a `.env` on a local surface.
|