@codyswann/lisa 3.4.0 → 3.5.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/README.md +22 -0
- package/all/copy-contents/gitignore +7 -0
- package/all/copy-overwrite/scripts/lisa-command-envelope.mjs +44 -4
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +31 -0
- package/all/copy-overwrite/scripts/lisa-schema-validate.mjs +125 -47
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +24 -2
- package/cdk/merge/.oxlintrc.json +1 -1
- package/dist/cli/apply.d.ts.map +1 -1
- package/dist/cli/apply.js +48 -13
- package/dist/cli/apply.js.map +1 -1
- package/dist/cli/doctor-apply-freshness.d.ts +29 -0
- package/dist/cli/doctor-apply-freshness.d.ts.map +1 -0
- package/dist/cli/doctor-apply-freshness.js +194 -0
- package/dist/cli/doctor-apply-freshness.js.map +1 -0
- package/dist/cli/doctor-learnings-merge-driver.d.ts +23 -0
- package/dist/cli/doctor-learnings-merge-driver.d.ts.map +1 -0
- package/dist/cli/doctor-learnings-merge-driver.js +73 -0
- package/dist/cli/doctor-learnings-merge-driver.js.map +1 -0
- package/dist/cli/doctor-readiness-workflows.js +2 -2
- package/dist/cli/doctor-readiness-workflows.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +13 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/ui-ci-quality-jobs-parse.js +2 -2
- package/dist/cli/ui-ci-quality-jobs-parse.js.map +1 -1
- package/dist/cli/ui-deploy-pipeline-model.d.ts +4 -0
- package/dist/cli/ui-deploy-pipeline-model.d.ts.map +1 -1
- package/dist/cli/ui-deploy-pipeline-model.js +2 -2
- package/dist/cli/ui-deploy-pipeline-model.js.map +1 -1
- package/dist/codex/agent-transformer.js +2 -2
- package/dist/codex/agent-transformer.js.map +1 -1
- package/dist/codex/command-skill-transformer.js +3 -3
- package/dist/codex/command-skill-transformer.js.map +1 -1
- package/dist/codex/scripts/block-no-verify.sh +53 -4
- package/dist/core/apply-receipt.d.ts +68 -0
- package/dist/core/apply-receipt.d.ts.map +1 -0
- package/dist/core/apply-receipt.js +107 -0
- package/dist/core/apply-receipt.js.map +1 -0
- package/dist/core/learnings-merge-driver-install.d.ts +34 -0
- package/dist/core/learnings-merge-driver-install.d.ts.map +1 -1
- package/dist/core/learnings-merge-driver-install.js +36 -0
- package/dist/core/learnings-merge-driver-install.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +62 -39
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/migrations/ensure-lisa-postinstall.d.ts +33 -0
- package/dist/migrations/ensure-lisa-postinstall.d.ts.map +1 -1
- package/dist/migrations/ensure-lisa-postinstall.js +47 -5
- package/dist/migrations/ensure-lisa-postinstall.js.map +1 -1
- package/dist/migrations/ensure-oxlint-base-configs.d.ts +42 -0
- package/dist/migrations/ensure-oxlint-base-configs.d.ts.map +1 -0
- package/dist/migrations/ensure-oxlint-base-configs.js +212 -0
- package/dist/migrations/ensure-oxlint-base-configs.js.map +1 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +3 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/opencode/command-transformer.js +3 -3
- package/dist/opencode/command-transformer.js.map +1 -1
- package/dist/utils/yaml.d.ts +54 -0
- package/dist/utils/yaml.d.ts.map +1 -0
- package/dist/utils/yaml.js +124 -0
- package/dist/utils/yaml.js.map +1 -0
- package/expo/copy-overwrite/scripts/bdd/baseline.mjs +104 -19
- package/expo/copy-overwrite/scripts/bdd/contract.mjs +20 -1
- package/expo/copy-overwrite/scripts/bdd/discover.mjs +53 -10
- package/expo/copy-overwrite/scripts/bdd/parse.mjs +25 -3
- package/expo/copy-overwrite/scripts/bdd/render.mjs +2 -1
- package/expo/copy-overwrite/scripts/bdd/report.mjs +26 -12
- package/expo/copy-overwrite/scripts/bdd/validate.mjs +2 -2
- package/expo/copy-overwrite/scripts/bdd/waivers.mjs +19 -4
- package/expo/copy-overwrite/scripts/bdd-matrix.mjs +2 -2
- package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +58 -16
- package/expo/create-only/.github/required-checks.json +10 -4
- package/expo/merge/.oxlintrc.json +1 -1
- package/harper-fabric/merge/.oxlintrc.json +1 -1
- package/nestjs/create-only/.github/required-checks.json +57 -0
- package/nestjs/merge/.oxlintrc.json +1 -1
- package/package.json +1 -1
- package/phaser/merge/.oxlintrc.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-atlassian-access/SKILL.md +7 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-jam-access/SKILL.md +37 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +7 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-posthog-access/SKILL.md +40 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-sentry-access/SKILL.md +44 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-sonarcloud-access/SKILL.md +56 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa/hooks/block-no-verify.agy.sh +53 -4
- package/plugins/lisa/hooks/block-no-verify.sh +31 -0
- package/plugins/lisa/rules/eager/credential-substrate-precedence.md +3 -1
- package/plugins/lisa/rules/eager/tracked-work.md +1 -1
- package/plugins/lisa/rules/reference/credential-substrate-precedence.md +20 -0
- package/plugins/lisa/rules/reference/tracked-work.md +4 -2
- package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +7 -0
- package/plugins/lisa/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-jam-access/SKILL.md +37 -2
- package/plugins/lisa/skills/lisa-notion-access/SKILL.md +7 -0
- package/plugins/lisa/skills/lisa-posthog-access/SKILL.md +40 -3
- package/plugins/lisa/skills/lisa-sentry-access/SKILL.md +44 -3
- package/plugins/lisa/skills/lisa-sonarcloud-access/SKILL.md +56 -0
- package/plugins/lisa/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +53 -4
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +7 -0
- package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-jam-access/SKILL.md +37 -2
- package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +7 -0
- package/plugins/lisa-agy/skills/lisa-posthog-access/SKILL.md +40 -3
- package/plugins/lisa-agy/skills/lisa-sentry-access/SKILL.md +44 -3
- package/plugins/lisa-agy/skills/lisa-sonarcloud-access/SKILL.md +56 -0
- package/plugins/lisa-agy/skills/lisa-track/SKILL.md +1 -1
- 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-no-verify.sh +31 -0
- package/plugins/lisa-copilot/rules/eager/credential-substrate-precedence.md +3 -1
- package/plugins/lisa-copilot/rules/eager/tracked-work.md +1 -1
- package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +20 -0
- package/plugins/lisa-copilot/rules/reference/tracked-work.md +4 -2
- package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +7 -0
- package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-jam-access/SKILL.md +37 -2
- package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +7 -0
- package/plugins/lisa-copilot/skills/lisa-posthog-access/SKILL.md +40 -3
- package/plugins/lisa-copilot/skills/lisa-sentry-access/SKILL.md +44 -3
- package/plugins/lisa-copilot/skills/lisa-sonarcloud-access/SKILL.md +56 -0
- package/plugins/lisa-copilot/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +31 -0
- package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +20 -0
- package/plugins/lisa-cursor/rules/credential-substrate-precedence.mdc +3 -1
- package/plugins/lisa-cursor/rules/tracked-work-reference.mdc +4 -2
- package/plugins/lisa-cursor/rules/tracked-work.mdc +1 -1
- package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +7 -0
- package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-jam-access/SKILL.md +37 -2
- package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +7 -0
- package/plugins/lisa-cursor/skills/lisa-posthog-access/SKILL.md +40 -3
- package/plugins/lisa-cursor/skills/lisa-sentry-access/SKILL.md +44 -3
- package/plugins/lisa-cursor/skills/lisa-sonarcloud-access/SKILL.md +56 -0
- package/plugins/lisa-cursor/skills/lisa-track/SKILL.md +1 -1
- 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-no-verify.agy.sh +53 -4
- package/plugins/src/base/hooks/block-no-verify.sh +31 -0
- package/plugins/src/base/rules/eager/credential-substrate-precedence.md +3 -1
- package/plugins/src/base/rules/eager/tracked-work.md +1 -1
- package/plugins/src/base/rules/reference/credential-substrate-precedence.md +20 -0
- package/plugins/src/base/rules/reference/tracked-work.md +4 -2
- package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +7 -0
- package/plugins/src/base/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-jam-access/SKILL.md +37 -2
- package/plugins/src/base/skills/lisa-notion-access/SKILL.md +7 -0
- package/plugins/src/base/skills/lisa-posthog-access/SKILL.md +40 -3
- package/plugins/src/base/skills/lisa-sentry-access/SKILL.md +44 -3
- package/plugins/src/base/skills/lisa-sonarcloud-access/SKILL.md +56 -0
- package/plugins/src/base/skills/lisa-track/SKILL.md +1 -1
- package/scripts/install-claude-plugins.sh +5 -1
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +298 -26
- package/typescript/create-only/.github/required-checks.json +11 -6
- package/typescript/create-only/.github/workflows/required-checks-drift.yml +84 -0
- package/typescript/merge/.oxlintrc.json +1 -1
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import {
|
|
20
20
|
REPORT_SCHEMA_VERSION,
|
|
21
|
+
byCodeUnit,
|
|
21
22
|
runnersByPlatform,
|
|
22
23
|
trackerUrl,
|
|
23
24
|
} from "./contract.mjs";
|
|
@@ -73,19 +74,24 @@ function buildTestInventory(discovery, contract) {
|
|
|
73
74
|
* required here" and "everything required here is covered" are different
|
|
74
75
|
* claims, and printing the second for the first is how a platform with no
|
|
75
76
|
* obligations comes to look fully covered.
|
|
77
|
+
*
|
|
78
|
+
* `percentage` is ROUNDED FOR DISPLAY and `exact` is not. Only `exact` is ever
|
|
79
|
+
* compared against a floor: 2000 of 2001 obligations is 99.95002%, which
|
|
80
|
+
* rounds to 100.0, and a genuinely-below-floor platform reporting `ok: true`
|
|
81
|
+
* because of a display convention is the same class of false headline this
|
|
82
|
+
* whole report is built to prevent.
|
|
76
83
|
* @param {readonly object[]} subset - Obligations to count.
|
|
77
84
|
* @param {ReadonlySet<string>} coveredKeys - Keys with an aligned mapping.
|
|
78
|
-
* @returns {object} Covered/total/percentage.
|
|
85
|
+
* @returns {object} Covered/total/percentage/exact.
|
|
79
86
|
*/
|
|
80
87
|
function summarize(subset, coveredKeys) {
|
|
81
88
|
const covered = subset.filter(item => coveredKeys.has(item.key)).length;
|
|
89
|
+
const exact = subset.length === 0 ? null : (covered / subset.length) * 100;
|
|
82
90
|
return {
|
|
83
91
|
covered,
|
|
84
92
|
total: subset.length,
|
|
85
|
-
percentage:
|
|
86
|
-
|
|
87
|
-
? null
|
|
88
|
-
: Number(((covered / subset.length) * 100).toFixed(1)),
|
|
93
|
+
percentage: exact === null ? null : Number(exact.toFixed(1)),
|
|
94
|
+
exact,
|
|
89
95
|
};
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -212,7 +218,7 @@ export function buildExecution(mappings, runs) {
|
|
|
212
218
|
notRunTests: outcomes
|
|
213
219
|
.filter(item => item.result === null)
|
|
214
220
|
.map(item => `${item.runner} ${item.file} :: ${item.evidence}`)
|
|
215
|
-
.sort(),
|
|
221
|
+
.sort(byCodeUnit),
|
|
216
222
|
};
|
|
217
223
|
}
|
|
218
224
|
|
|
@@ -268,18 +274,23 @@ function classifyFloor(declared) {
|
|
|
268
274
|
*/
|
|
269
275
|
export function evaluateFloor(contract, byPlatform, platforms) {
|
|
270
276
|
const floors = contract.coverageFloor ?? {};
|
|
271
|
-
const entries = [...platforms].sort().map(platform => {
|
|
277
|
+
const entries = [...platforms].sort(byCodeUnit).map(platform => {
|
|
272
278
|
const declared = classifyFloor(floors[platform]);
|
|
273
279
|
// A platform with no obligations reports `null`, not 100. It cannot clear
|
|
274
280
|
// a positive floor by having nothing to measure.
|
|
275
281
|
const actual = byPlatform[platform]?.percentage ?? null;
|
|
282
|
+
// The UNROUNDED value decides. `actual` above is the display figure and is
|
|
283
|
+
// kept only so the report and the operator message agree with the rest of
|
|
284
|
+
// the traceability block.
|
|
285
|
+
const exact = byPlatform[platform]?.exact ?? null;
|
|
276
286
|
return [
|
|
277
287
|
platform,
|
|
278
288
|
{
|
|
279
289
|
floor: declared.value,
|
|
280
290
|
state: declared.state,
|
|
281
291
|
actual,
|
|
282
|
-
|
|
292
|
+
exact,
|
|
293
|
+
ok: isFloorMet(declared, exact),
|
|
283
294
|
},
|
|
284
295
|
];
|
|
285
296
|
});
|
|
@@ -302,7 +313,7 @@ export function evaluateFloor(contract, byPlatform, platforms) {
|
|
|
302
313
|
* disable enforcement. An unset floor has nothing to clear here; the enforced
|
|
303
314
|
* mode reports its absence separately.
|
|
304
315
|
* @param {{state: string, value: number|null}} declared - The classified floor.
|
|
305
|
-
* @param {number|null} actual - Measured percentage, or null when there are no obligations.
|
|
316
|
+
* @param {number|null} actual - Measured UNROUNDED percentage, or null when there are no obligations.
|
|
306
317
|
* @returns {boolean} Whether the floor is satisfied.
|
|
307
318
|
*/
|
|
308
319
|
function isFloorMet(declared, actual) {
|
|
@@ -338,7 +349,10 @@ export function buildTrackerIndex(scenarios, trackers) {
|
|
|
338
349
|
scenario => scenario.trackers.length === 0
|
|
339
350
|
).length,
|
|
340
351
|
tags: [...tags.values()]
|
|
341
|
-
.map(entry => ({
|
|
352
|
+
.map(entry => ({
|
|
353
|
+
...entry,
|
|
354
|
+
scenarios: [...entry.scenarios].sort(byCodeUnit),
|
|
355
|
+
}))
|
|
342
356
|
.sort((a, b) => a.tag.localeCompare(b.tag)),
|
|
343
357
|
};
|
|
344
358
|
}
|
|
@@ -366,7 +380,7 @@ export function buildReport({
|
|
|
366
380
|
const obligations = declared.filter(item => !waivedKeys.has(item.key));
|
|
367
381
|
const coveredKeys = coverageKeys(scenarios, contract, unresolved);
|
|
368
382
|
const byPlatform = Object.fromEntries(
|
|
369
|
-
[...platforms].sort().map(platform => [
|
|
383
|
+
[...platforms].sort(byCodeUnit).map(platform => [
|
|
370
384
|
platform,
|
|
371
385
|
summarize(
|
|
372
386
|
obligations.filter(item => item.platform === platform),
|
|
@@ -460,7 +474,7 @@ function countScenarios(scenarios) {
|
|
|
460
474
|
function byRunnerSummary(contract, obligations, coveredKeys) {
|
|
461
475
|
return Object.fromEntries(
|
|
462
476
|
Object.keys(contract.runnerPlatforms ?? {})
|
|
463
|
-
.sort()
|
|
477
|
+
.sort(byCodeUnit)
|
|
464
478
|
.map(runner => [
|
|
465
479
|
runner,
|
|
466
480
|
summarize(
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
|
|
12
|
-
import { runnersByPlatform } from "./contract.mjs";
|
|
12
|
+
import { byCodeUnit, runnersByPlatform } from "./contract.mjs";
|
|
13
13
|
import { resolveInsideRepo } from "./parse.mjs";
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -43,7 +43,7 @@ export function validateScenarios(scenarios, platforms) {
|
|
|
43
43
|
defects.push(
|
|
44
44
|
defect(
|
|
45
45
|
"scenario-platform",
|
|
46
|
-
`${at} declares no platform; expected one of ${[...platforms].sort().join(", ") || "(none configured)"}`
|
|
46
|
+
`${at} declares no platform; expected one of ${[...platforms].sort(byCodeUnit).join(", ") || "(none configured)"}`
|
|
47
47
|
)
|
|
48
48
|
);
|
|
49
49
|
}
|
|
@@ -22,10 +22,16 @@ const REQUIRED_FIELDS = [
|
|
|
22
22
|
];
|
|
23
23
|
|
|
24
24
|
/** ISO calendar date, the only accepted date shape. */
|
|
25
|
-
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
25
|
+
export const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Validate every waiver in the coverage map.
|
|
29
|
+
*
|
|
30
|
+
* The evaluation date is validated FIRST and reported as a defect of its own.
|
|
31
|
+
* Expiry is a lexical `<` comparison against an ISO date, and every comparison
|
|
32
|
+
* with a non-date string is false — so an unusable `BDD_TODAY` did not merely
|
|
33
|
+
* skip the expiry check, it silently passed EVERY expired waiver in the repo.
|
|
34
|
+
* A date the gate cannot read is a finding, never a free pass.
|
|
29
35
|
* @param {object} input - Scenarios, contract, and the evaluation date.
|
|
30
36
|
* @returns {object[]} Defects found.
|
|
31
37
|
*/
|
|
@@ -34,7 +40,15 @@ export function validateWaivers({ scenarios, contract, today }) {
|
|
|
34
40
|
const platformRunners = runnersByPlatform(contract.runnerPlatforms);
|
|
35
41
|
const mapped = mappedKeys(contract);
|
|
36
42
|
const seen = new Set();
|
|
37
|
-
const
|
|
43
|
+
const usableToday = typeof today === "string" && ISO_DATE.test(today);
|
|
44
|
+
const defects = usableToday
|
|
45
|
+
? []
|
|
46
|
+
: [
|
|
47
|
+
defect(
|
|
48
|
+
"waiver-metadata",
|
|
49
|
+
`the evaluation date ${JSON.stringify(today ?? null)} is not an ISO date (YYYY-MM-DD), so no waiver expiry could be evaluated. Set BDD_TODAY to a calendar date or leave it unset.`
|
|
50
|
+
),
|
|
51
|
+
];
|
|
38
52
|
for (const [index, waiver] of (contract.platformWaivers ?? []).entries()) {
|
|
39
53
|
const at = `coverage-map.platformWaivers[${index}] ${waiver.scenario ?? "(no scenario)"}`;
|
|
40
54
|
const scenario = byId.get(waiver.scenario);
|
|
@@ -43,7 +57,7 @@ export function validateWaivers({ scenarios, contract, today }) {
|
|
|
43
57
|
defects.push(blocking);
|
|
44
58
|
continue;
|
|
45
59
|
}
|
|
46
|
-
defects.push(...metadataDefects(waiver, at, today));
|
|
60
|
+
defects.push(...metadataDefects(waiver, at, usableToday ? today : null));
|
|
47
61
|
defects.push(...runnerDefects(waiver, platformRunners, at));
|
|
48
62
|
defects.push(...platformDefects(waiver, scenario, { mapped, seen }, at));
|
|
49
63
|
}
|
|
@@ -96,7 +110,7 @@ function blockingError(waiver, scenario, at) {
|
|
|
96
110
|
* waiver — the time-box is what stops an IOU from becoming permanent.
|
|
97
111
|
* @param {object} waiver - Raw waiver entry.
|
|
98
112
|
* @param {string} at - Location label.
|
|
99
|
-
* @param {string} today - ISO date the run is evaluated against.
|
|
113
|
+
* @param {string|null} today - ISO date the run is evaluated against, or null when unusable.
|
|
100
114
|
* @returns {object[]} Defects found.
|
|
101
115
|
*/
|
|
102
116
|
function metadataDefects(waiver, at, today) {
|
|
@@ -125,6 +139,7 @@ function metadataDefects(waiver, at, today) {
|
|
|
125
139
|
);
|
|
126
140
|
}
|
|
127
141
|
if (
|
|
142
|
+
today !== null &&
|
|
128
143
|
waiver.expiresAt &&
|
|
129
144
|
ISO_DATE.test(waiver.expiresAt) &&
|
|
130
145
|
waiver.expiresAt < today
|
|
@@ -20,7 +20,7 @@ import * as fs from "node:fs";
|
|
|
20
20
|
import * as path from "node:path";
|
|
21
21
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
22
22
|
|
|
23
|
-
import { declaredPlatforms, trackerUrl } from "./bdd/contract.mjs";
|
|
23
|
+
import { byCodeUnit, declaredPlatforms, trackerUrl } from "./bdd/contract.mjs";
|
|
24
24
|
import { loadScenarios } from "./bdd/parse.mjs";
|
|
25
25
|
import { indexResults } from "./bdd/report.mjs";
|
|
26
26
|
import { loadExecutionResults } from "./check-bdd-coverage.mjs";
|
|
@@ -128,7 +128,7 @@ export function renderMatrix(root, resultFiles) {
|
|
|
128
128
|
const results = indexResults(execution.runs);
|
|
129
129
|
const byScenario = mappingsByScenario(contract);
|
|
130
130
|
const waived = waivedKeys(contract);
|
|
131
|
-
const runners = Object.keys(contract.runnerPlatforms ?? {}).sort();
|
|
131
|
+
const runners = Object.keys(contract.runnerPlatforms ?? {}).sort(byCodeUnit);
|
|
132
132
|
const byFeature = new Map();
|
|
133
133
|
for (const scenario of scenarios) {
|
|
134
134
|
byFeature.set(scenario.feature, [
|
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
validateScenarios,
|
|
78
78
|
validateTrackerTags,
|
|
79
79
|
} from "./bdd/validate.mjs";
|
|
80
|
-
import { validateWaivers } from "./bdd/waivers.mjs";
|
|
80
|
+
import { ISO_DATE, validateWaivers } from "./bdd/waivers.mjs";
|
|
81
81
|
|
|
82
82
|
const PACKAGE_ROOT = path.resolve(
|
|
83
83
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -178,6 +178,17 @@ export function validateAdoption(contract, mode, today) {
|
|
|
178
178
|
*/
|
|
179
179
|
function bootstrapDefects(adoption, today) {
|
|
180
180
|
const defects = [];
|
|
181
|
+
if (!ISO_DATE.test(String(today))) {
|
|
182
|
+
// The expiry is a lexical comparison against an ISO date, and every
|
|
183
|
+
// comparison with a non-date is false — so an unreadable evaluation date
|
|
184
|
+
// would make the time-box unreachable and bootstrap permanent.
|
|
185
|
+
defects.push(
|
|
186
|
+
defect(
|
|
187
|
+
"bootstrap-metadata",
|
|
188
|
+
`the evaluation date ${JSON.stringify(today ?? null)} is not an ISO date (YYYY-MM-DD), so the bootstrap expiry could not be evaluated`
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
181
192
|
if (!adoption.owner) {
|
|
182
193
|
defects.push(
|
|
183
194
|
defect(
|
|
@@ -193,14 +204,14 @@ function bootstrapDefects(adoption, today) {
|
|
|
193
204
|
"bootstrap requires adoption.expiresAt (an ISO date); a bootstrap with no time-box never ends"
|
|
194
205
|
)
|
|
195
206
|
);
|
|
196
|
-
} else if (
|
|
207
|
+
} else if (!ISO_DATE.test(adoption.expiresAt)) {
|
|
197
208
|
defects.push(
|
|
198
209
|
defect(
|
|
199
210
|
"bootstrap-metadata",
|
|
200
211
|
"adoption.expiresAt must be an ISO date (YYYY-MM-DD)"
|
|
201
212
|
)
|
|
202
213
|
);
|
|
203
|
-
} else if (adoption.expiresAt < today) {
|
|
214
|
+
} else if (ISO_DATE.test(String(today)) && adoption.expiresAt < today) {
|
|
204
215
|
defects.push(
|
|
205
216
|
defect(
|
|
206
217
|
"bootstrap-expired",
|
|
@@ -278,7 +289,7 @@ function validateAll({
|
|
|
278
289
|
...defects,
|
|
279
290
|
defect(
|
|
280
291
|
"baseline",
|
|
281
|
-
`base revision ${options.baseSha} is not readable, so the non-regression checks could not run. A gate that cannot compare against a base does not get to report that nothing regressed.`
|
|
292
|
+
`base revision ${options.baseSha} is not readable${baseline.error ? ` (${baseline.error})` : ""}, so the non-regression checks could not run. A gate that cannot compare against a base does not get to report that nothing regressed.`
|
|
282
293
|
),
|
|
283
294
|
];
|
|
284
295
|
}
|
|
@@ -343,16 +354,16 @@ function floorIntegrityDefects(report) {
|
|
|
343
354
|
|
|
344
355
|
/**
|
|
345
356
|
* Defects that only exist in enforced mode, where absence must fail.
|
|
346
|
-
*
|
|
357
|
+
*
|
|
358
|
+
* The platform vocabulary is deliberately NOT a parameter: every platform this
|
|
359
|
+
* function cares about already reaches it through `report.floor`, which was
|
|
360
|
+
* built from that same vocabulary. It used to be passed and discarded behind a
|
|
361
|
+
* `void`, which reads as "unused for now" and hides whether an intended check
|
|
362
|
+
* was ever written.
|
|
363
|
+
* @param {object} input - Contract, scenarios, report, and discovery.
|
|
347
364
|
* @returns {object[]} Defects found.
|
|
348
365
|
*/
|
|
349
|
-
function enforcedDefects({
|
|
350
|
-
contract,
|
|
351
|
-
scenarios,
|
|
352
|
-
report,
|
|
353
|
-
platforms,
|
|
354
|
-
discovery,
|
|
355
|
-
}) {
|
|
366
|
+
function enforcedDefects({ contract, scenarios, report, discovery }) {
|
|
356
367
|
const defects = [];
|
|
357
368
|
if (scenarios.length === 0) {
|
|
358
369
|
defects.push(
|
|
@@ -392,15 +403,28 @@ function enforcedDefects({
|
|
|
392
403
|
defects.push(
|
|
393
404
|
defect(
|
|
394
405
|
"floor-regression",
|
|
395
|
-
`${platform} traceability coverage ${value
|
|
406
|
+
`${platform} traceability coverage ${measured(value)}% is below its committed floor of ${value.floor}%`
|
|
396
407
|
)
|
|
397
408
|
);
|
|
398
409
|
}
|
|
399
410
|
}
|
|
400
|
-
void platforms;
|
|
401
411
|
return defects;
|
|
402
412
|
}
|
|
403
413
|
|
|
414
|
+
/**
|
|
415
|
+
* The measured percentage, printed at enough precision to explain the verdict.
|
|
416
|
+
*
|
|
417
|
+
* The floor is decided on the unrounded value, so a platform sitting at
|
|
418
|
+
* 99.95% must not be told it failed at "100%" — a message that contradicts
|
|
419
|
+
* its own finding is how an operator concludes the gate is broken.
|
|
420
|
+
* @param {{actual: number|null, exact: number|null}} value - A floor entry.
|
|
421
|
+
* @returns {string} The percentage to print.
|
|
422
|
+
*/
|
|
423
|
+
function measured(value) {
|
|
424
|
+
if (value.exact === null) return String(value.actual);
|
|
425
|
+
return String(Number(value.exact.toFixed(4)));
|
|
426
|
+
}
|
|
427
|
+
|
|
404
428
|
/**
|
|
405
429
|
* Evaluate the gate.
|
|
406
430
|
* @param {string} root - Repo root.
|
|
@@ -452,7 +476,7 @@ export function run(root, options) {
|
|
|
452
476
|
discovery,
|
|
453
477
|
}),
|
|
454
478
|
...(mode === "enforced"
|
|
455
|
-
? enforcedDefects({ contract, scenarios, report,
|
|
479
|
+
? enforcedDefects({ contract, scenarios, report, discovery })
|
|
456
480
|
: []),
|
|
457
481
|
];
|
|
458
482
|
return result({ mode, defects, report, contract });
|
|
@@ -689,6 +713,14 @@ async function main() {
|
|
|
689
713
|
* When the shared module is absent the same object is emitted unvalidated
|
|
690
714
|
* rather than nothing: a gate that produces no result is indistinguishable
|
|
691
715
|
* from one that passed.
|
|
716
|
+
*
|
|
717
|
+
* When the shared module is present but REFUSES the envelope — a schema this
|
|
718
|
+
* gate's fields no longer satisfy, or a half-copied `scripts/` directory whose
|
|
719
|
+
* schema document never arrived — that refusal is caught rather than thrown.
|
|
720
|
+
* An uncaught rejection here produced no envelope at all and a Node-supplied
|
|
721
|
+
* exit code, which is the one outcome this function exists to rule out. The
|
|
722
|
+
* fields are emitted with `status: "invalid"`, so the run is machine-readable,
|
|
723
|
+
* operator-readable, and NONZERO.
|
|
692
724
|
* @param {object} input - The gate run, CLI options, and files written.
|
|
693
725
|
* @returns {Promise<object>} The envelope.
|
|
694
726
|
*/
|
|
@@ -708,7 +740,17 @@ async function sealEnvelope({ gateRun, options, filesWritten }) {
|
|
|
708
740
|
);
|
|
709
741
|
return { schemaVersion: "lisa-command-envelope-v1", ...fields };
|
|
710
742
|
}
|
|
711
|
-
|
|
743
|
+
try {
|
|
744
|
+
return shared.buildEnvelope(fields);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
console.error(`[bdd-coverage] ${error.message}`);
|
|
747
|
+
return {
|
|
748
|
+
schemaVersion: "lisa-command-envelope-v1",
|
|
749
|
+
...fields,
|
|
750
|
+
status: "invalid",
|
|
751
|
+
reason: `the result could not be sealed into a valid command envelope: ${error.message}`,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
712
754
|
}
|
|
713
755
|
|
|
714
756
|
/**
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_readme": [
|
|
3
3
|
"See typescript/create-only/.github/required-checks.json in Lisa for the full rationale. In short: GitHub counts a SKIPPED required status check as SATISFIED, so a `skip_jobs` token that silences a required context makes the merge gate decorative.",
|
|
4
|
-
"This
|
|
5
|
-
"
|
|
6
|
-
"
|
|
4
|
+
"This seed declares the three tokens the shipped ci.yml skips, so that the day one of them is required the guard can say so instead of the gate quietly going hollow. The `ruleset_required: false` on each is a DEFAULT, not a measurement — it is only meaningful once you have transcribed `required_contexts` from your own ruleset, which is what stamping `baseline_fetched_at` asserts.",
|
|
5
|
+
"⚠️ `required_contexts` SHIPS EMPTY AND UNSTAMPED ON PURPOSE. Lisa cannot know what YOUR ruleset requires, and an earlier version of this seed shipped a guess that was measured WRONG (#2476): it claimed `🔗 Work-Item Traceability` was required when no ruleset required it, and omitted six contexts that were. Until `ruleset.baseline_fetched_at` carries the date you transcribed the real list, the guard reports NOT CHECKED rather than answering from fiction.",
|
|
6
|
+
"To arm it: gh api repos/OWNER/NAME/rulesets --jq '.[] | \"\\\\(.id) \\\\(.name)\"' to find the ids, then gh api repos/OWNER/NAME/rulesets/ID --jq '.rules[] | select(.type==\"required_status_checks\") | .parameters.required_status_checks[].context' for the contexts. Paste them into `required_contexts` BYTE FOR BYTE, fill in `ruleset.repo` / `ruleset.ids`, and set `baseline_fetched_at` to today.",
|
|
7
|
+
"A transcription expires after 90 days, because a ruleset can be edited with no signal in this repository. The shipped `.github/workflows/required-checks-drift.yml` runs `--remote` weekly to catch that; `--remote` reads the ruleset live and so answers even when the cache is untrusted.",
|
|
8
|
+
"Lisa's quality.yml runs the offline arm on every pull request. This seed ships `\"enforcement\": \"warn\"`, which downgrades findings AND the NOT-CHECKED refusal to reports so a fresh install does not go red on arrival. Delete the key once you have transcribed the list — then it blocks.",
|
|
9
|
+
"`skip_jobs` is matched as an exact comma-delimited token and GitHub Actions expression syntax has no string-replace, so write the list with no spaces: `skip_jobs: 'a,b'`, never `skip_jobs: 'a, b'`. A spaced token matches nothing and the job runs — reported as `whitespace_in_skip_token`.",
|
|
10
|
+
"`_example_required_contexts` below is a STARTING POINT FOR TYPING, never read by the guard. Verify every line against your own ruleset before promoting any of it."
|
|
7
11
|
],
|
|
12
|
+
"enforcement": "warn",
|
|
8
13
|
"ruleset": {
|
|
9
14
|
"repo": "",
|
|
10
15
|
"ids": [],
|
|
@@ -14,7 +19,8 @@
|
|
|
14
19
|
".github/workflows/ci.yml"
|
|
15
20
|
],
|
|
16
21
|
"exemption_ticket_pattern": "^[A-Z][A-Z0-9]+-\\d+$",
|
|
17
|
-
"required_contexts": [
|
|
22
|
+
"required_contexts": [],
|
|
23
|
+
"_example_required_contexts": [
|
|
18
24
|
"🔍 Quality Checks / 🧹 Lint",
|
|
19
25
|
"🔍 Quality Checks / 🔍 Type Check",
|
|
20
26
|
"🔍 Quality Checks / 🏗️ Build",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_readme": [
|
|
3
|
+
"See typescript/create-only/.github/required-checks.json in Lisa for the full rationale. In short: GitHub counts a SKIPPED required status check as SATISFIED, so a `skip_jobs` token that silences a required context makes the merge gate decorative.",
|
|
4
|
+
"This seed declares the three tokens the shipped ci.yml skips, so that the day one of them is required the guard can say so instead of the gate quietly going hollow. The `ruleset_required: false` on each is a DEFAULT, not a measurement — it is only meaningful once you have transcribed `required_contexts` from your own ruleset, which is what stamping `baseline_fetched_at` asserts.",
|
|
5
|
+
"⚠️ `required_contexts` SHIPS EMPTY AND UNSTAMPED ON PURPOSE. Lisa cannot know what YOUR ruleset requires, and an earlier version of this seed shipped a guess that was measured WRONG (#2476): it claimed `🔗 Work-Item Traceability` was required when no ruleset required it, and omitted six contexts that were. Until `ruleset.baseline_fetched_at` carries the date you transcribed the real list, the guard reports NOT CHECKED rather than answering from fiction.",
|
|
6
|
+
"To arm it: gh api repos/OWNER/NAME/rulesets --jq '.[] | \"\\\\(.id) \\\\(.name)\"' to find the ids, then gh api repos/OWNER/NAME/rulesets/ID --jq '.rules[] | select(.type==\"required_status_checks\") | .parameters.required_status_checks[].context' for the contexts. Paste them into `required_contexts` BYTE FOR BYTE, fill in `ruleset.repo` / `ruleset.ids`, and set `baseline_fetched_at` to today.",
|
|
7
|
+
"A transcription expires after 90 days, because a ruleset can be edited with no signal in this repository. The shipped `.github/workflows/required-checks-drift.yml` runs `--remote` weekly to catch that; `--remote` reads the ruleset live and so answers even when the cache is untrusted.",
|
|
8
|
+
"Lisa's quality.yml runs the offline arm on every pull request. This seed ships `\"enforcement\": \"warn\"`, which downgrades findings AND the NOT-CHECKED refusal to reports so a fresh install does not go red on arrival. Delete the key once you have transcribed the list — then it blocks.",
|
|
9
|
+
"`skip_jobs` is matched as an exact comma-delimited token and GitHub Actions expression syntax has no string-replace, so write the list with no spaces: `skip_jobs: 'a,b'`, never `skip_jobs: 'a, b'`. A spaced token matches nothing and the job runs — reported as `whitespace_in_skip_token`.",
|
|
10
|
+
"`_example_required_contexts` below is a STARTING POINT FOR TYPING, never read by the guard. Verify every line against your own ruleset before promoting any of it."
|
|
11
|
+
],
|
|
12
|
+
"enforcement": "warn",
|
|
13
|
+
"ruleset": {
|
|
14
|
+
"repo": "",
|
|
15
|
+
"ids": [],
|
|
16
|
+
"baseline_fetched_at": ""
|
|
17
|
+
},
|
|
18
|
+
"workflows": [
|
|
19
|
+
".github/workflows/ci.yml"
|
|
20
|
+
],
|
|
21
|
+
"exemption_ticket_pattern": "^[A-Z][A-Z0-9]+-\\d+$",
|
|
22
|
+
"required_contexts": [],
|
|
23
|
+
"_example_required_contexts": [
|
|
24
|
+
"🔍 Quality Checks / 🧹 Lint",
|
|
25
|
+
"🔍 Quality Checks / 🔍 Type Check",
|
|
26
|
+
"🔍 Quality Checks / 🏗️ Build",
|
|
27
|
+
"🔍 Quality Checks / 📐 Check Formatting",
|
|
28
|
+
"🔍 Quality Checks / 🔒 Security Scan",
|
|
29
|
+
"🔍 Quality Checks / 🧪 Run Unit Tests",
|
|
30
|
+
"🔍 Quality Checks / 🧪 Run Integration Tests",
|
|
31
|
+
"🔍 Quality Checks / 🔗 Work-Item Traceability"
|
|
32
|
+
],
|
|
33
|
+
"skip_job_declarations": {
|
|
34
|
+
"test:e2e": {
|
|
35
|
+
"suppressed_contexts": [
|
|
36
|
+
"🔍 Quality Checks / 🧪 Run E2E Tests"
|
|
37
|
+
],
|
|
38
|
+
"ruleset_required": false,
|
|
39
|
+
"reason": "A NestJS API's end-to-end suite needs a database and a booted app, which the shipped ci.yml does not provision. Lisa's quality callee guards each job on script existence, so un-skipping it without that scaffolding would produce a run that succeeds having tested nothing — a worse false green than the skip. Not a required context, so skipping removes a check rather than faking one."
|
|
40
|
+
},
|
|
41
|
+
"playwright_e2e": {
|
|
42
|
+
"suppressed_contexts": [
|
|
43
|
+
"🔍 Quality Checks / 🎭 Playwright Shard Setup",
|
|
44
|
+
"🔍 Quality Checks / 🎭 Playwright E2E Tests"
|
|
45
|
+
],
|
|
46
|
+
"ruleset_required": false,
|
|
47
|
+
"reason": "Note this ONE token silences TWO jobs — the mapping is not a mechanical transform, which is why this file is a reviewed snapshot rather than something derived. Neither is a required context: a headless API has no browser surface for Playwright to drive."
|
|
48
|
+
},
|
|
49
|
+
"zap_baseline": {
|
|
50
|
+
"suppressed_contexts": [
|
|
51
|
+
"🔍 Quality Checks / 🕷️ OWASP ZAP Baseline"
|
|
52
|
+
],
|
|
53
|
+
"ruleset_required": false,
|
|
54
|
+
"reason": "The shipped ci.yml runs ZAP as its own `zap` job against a deployed target after quality passes, not inside the quality callee. Not a required context."
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
}
|
|
121
121
|
},
|
|
122
122
|
"name": "@codyswann/lisa",
|
|
123
|
-
"version": "3.
|
|
123
|
+
"version": "3.5.0",
|
|
124
124
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
125
125
|
"main": "dist/index.js",
|
|
126
126
|
"exports": {
|
|
@@ -69,6 +69,13 @@ read_atlassian_token() {
|
|
|
69
69
|
# Legacy fallback: the OS keychain written by the guided /lisa:setup:atlassian
|
|
70
70
|
# flow, for projects that have not adopted a credentials provider. Reached only
|
|
71
71
|
# when the chokepoint is absent or has no entry.
|
|
72
|
+
#
|
|
73
|
+
# This rung is REMOVED ON 2026-11-01 — a dated migration ramp, not a standing
|
|
74
|
+
# exemption (see credential-substrate-precedence, "Legacy OS-keychain fallback
|
|
75
|
+
# — removal date"). A keychain entry is machine-local ambient state no headless
|
|
76
|
+
# surface can reach, so a project resting on it has no working tier 1 in cron,
|
|
77
|
+
# CI, or a cloud session. Re-run /lisa:setup:atlassian before that date to
|
|
78
|
+
# store ATLASSIAN_API_TOKEN through the chokepoint instead.
|
|
72
79
|
case "$(uname -s)" in
|
|
73
80
|
Darwin) security find-generic-password -s lisa-atlassian -a "$email" -w 2>/dev/null ;;
|
|
74
81
|
Linux) command -v secret-tool >/dev/null && secret-tool lookup service lisa-atlassian account "$email" 2>/dev/null ;;
|
|
@@ -64,7 +64,7 @@ The input-resolver invokes `lisa-track $ARGUMENTS` and owns its complete resolve
|
|
|
64
64
|
- **Bind before durable work:** only after the verified claim, run:
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
|
-
node scripts/lisa-work-item.mjs
|
|
67
|
+
node scripts/lisa-work-item.mjs link <canonical-ref>
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
Require a successful readback of that worktree-local binding. On detached HEAD, `branch: null` is the expected pending binding; after branch creation the mandatory `attach-branch` step below must replace it before any commit. Tracker or binding failure stops the flow; never continue untracked.
|
|
@@ -39,20 +39,55 @@ headlessly — which is why it leads. The CLI tier uses:
|
|
|
39
39
|
```bash
|
|
40
40
|
curl -fsSL https://native.jam.dev/install | bash
|
|
41
41
|
export PATH="$HOME/.local/bin:$PATH"
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
# Resolve the PAT through the chokepoint before giving up on the environment.
|
|
44
|
+
# `$JAM_PAT` is the documented fallback, not the only rung: without this, a
|
|
45
|
+
# project that keeps its credentials in Bitwarden, Doppler, or AWS has no tier 1
|
|
46
|
+
# path at all and silently resolves through the interactive MCP — the exact
|
|
47
|
+
# divergence `credential-substrate-precedence` exists to remove.
|
|
48
|
+
read_jam_pat() {
|
|
49
|
+
[ -n "${JAM_PAT:-}" ] && { echo "$JAM_PAT"; return; }
|
|
50
|
+
local resolver
|
|
51
|
+
for resolver in .claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs \
|
|
52
|
+
.agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs; do
|
|
53
|
+
if [ -f "$resolver" ]; then
|
|
54
|
+
local via_lisa
|
|
55
|
+
via_lisa=$(node "$resolver" get JAM_PAT 2>/dev/null) \
|
|
56
|
+
&& [ -n "$via_lisa" ] && { echo "$via_lisa"; return; }
|
|
57
|
+
break
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Piped, never written to disk and never passed as an argv token: a PAT on a
|
|
64
|
+
# command line is readable from the process table by every user on the host.
|
|
65
|
+
read_jam_pat | jam auth login --token
|
|
43
66
|
jam skills install
|
|
44
67
|
```
|
|
45
68
|
|
|
46
69
|
If neither tier works, fail with:
|
|
47
70
|
|
|
48
71
|
```text
|
|
49
|
-
Error: no Jam access substrate available. Authenticate the Jam MCP or
|
|
72
|
+
Error: no Jam access substrate available. Authenticate the Jam MCP, set JAM_PAT, or store JAM_PAT in this project's secrets provider.
|
|
50
73
|
```
|
|
51
74
|
|
|
75
|
+
## Mutation boundary
|
|
76
|
+
|
|
77
|
+
Every operation in the Invocation Contract is **read-only** — fetching a trace,
|
|
78
|
+
a recording, or a bug report. So the `credential-substrate-precedence` guarded
|
|
79
|
+
fallback for mutating operations (write, read back, assert the tenant from the
|
|
80
|
+
response, roll back on mismatch) is not engaged here, and a failed tier is
|
|
81
|
+
simply skipped. A future operation that mutates Jam state — commenting on or
|
|
82
|
+
deleting a Jam — is a write and MUST reconcile by read-back before any retry.
|
|
83
|
+
|
|
52
84
|
## Invariants
|
|
53
85
|
|
|
54
86
|
- Tier order is `credential-substrate-precedence`: `JAM_PAT` CLI first, Jam MCP as
|
|
55
87
|
a preserved first-class fallback. Do not retry a failed tier blindly.
|
|
88
|
+
- The PAT is resolved through `lisa-secrets-access`, with the bare `JAM_PAT`
|
|
89
|
+
environment variable as the documented fallback. Never read a second
|
|
90
|
+
credential store directly.
|
|
56
91
|
- Never commit a Jam PAT into `.mcp.json` or any generated setup artifact.
|
|
57
92
|
- Headless Jam access requires `native.jam.dev` for the installer and
|
|
58
93
|
`api.jam.dev` for CLI/API calls in any custom remote network allowlist.
|
|
@@ -70,6 +70,13 @@ read_notion_token() {
|
|
|
70
70
|
# Legacy fallback: the OS keychain written by the guided /lisa:setup:notion
|
|
71
71
|
# flow, for projects that have not adopted a credentials provider. Reached only
|
|
72
72
|
# when the chokepoint is absent or has no entry.
|
|
73
|
+
#
|
|
74
|
+
# This rung is REMOVED ON 2026-11-01 — a dated migration ramp, not a standing
|
|
75
|
+
# exemption (see credential-substrate-precedence, "Legacy OS-keychain fallback
|
|
76
|
+
# — removal date"). A keychain entry is machine-local ambient state no headless
|
|
77
|
+
# surface can reach, so a project resting on it has no working tier 1 in cron,
|
|
78
|
+
# CI, or a cloud session. Re-run /lisa:setup:notion before that date to store
|
|
79
|
+
# NOTION_API_TOKEN through the chokepoint instead.
|
|
73
80
|
case "$(uname -s)" in
|
|
74
81
|
Darwin) security find-generic-password -s lisa-notion -a "$workspace" -w 2>/dev/null ;;
|
|
75
82
|
Linux) command -v secret-tool >/dev/null && \
|