@codyswann/lisa 3.14.8 → 3.15.1
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/check-state-classification.mjs +3 -5
- package/all/copy-overwrite/scripts/lib/invoked-as-script.mjs +96 -0
- package/all/copy-overwrite/scripts/lisa-command-envelope.mjs +3 -5
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +3 -5
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +19 -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 +47 -19
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/copy-overwrite/scripts/bdd/markdown-cell.mjs +128 -0
- package/expo/copy-overwrite/scripts/bdd/render.mjs +1 -14
- package/expo/copy-overwrite/scripts/bdd-matrix.mjs +3 -3
- package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +3 -3
- package/expo/copy-overwrite/scripts/check-e2e-coverage.mjs +3 -5
- package/expo/copy-overwrite/scripts/classify-maestro-failures.mjs +5 -15
- package/expo/copy-overwrite/scripts/lib/invoked-as-script.mjs +96 -0
- 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-secrets-access/SKILL.md +49 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/inspect-vault.mjs +120 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +2 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +481 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +76 -0
- package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +50 -1
- package/plugins/lisa/skills/lisa-secrets-access/scripts/inspect-vault.mjs +120 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +2 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +481 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +76 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +50 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/inspect-vault.mjs +120 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +2 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +481 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +76 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +50 -1
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/inspect-vault.mjs +120 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +2 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +481 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +76 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +50 -1
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/inspect-vault.mjs +120 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +2 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +481 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +76 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +50 -1
- package/plugins/src/base/skills/lisa-secrets-access/scripts/inspect-vault.mjs +120 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +2 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +481 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +76 -0
- package/scripts/build-plugins.sh +29 -0
- package/scripts/check-conflict-markers.mjs +4 -5
- package/scripts/detect-stale-workflow-inputs.mjs +4 -5
- package/scripts/lib/invoked-as-script.mjs +93 -0
- package/scripts/plugin-parity-drift.mjs +4 -5
- package/scripts/plugin-routing-validate.mjs +4 -5
- package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +2 -5
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +3 -5
- package/typescript/copy-overwrite/scripts/check-verification-coverage.mjs +3 -5
- package/typescript/copy-overwrite/scripts/lib/invoked-as-script.mjs +96 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Markdown table-cell escaping, shared by the BDD burndown renderer and the
|
|
6
|
+
* Maestro failure classifier.
|
|
7
|
+
*
|
|
8
|
+
* Waiver reasons, scenario titles, discovered test names and Maestro failure
|
|
9
|
+
* messages are repo data that lands inside a generated Markdown table. A single
|
|
10
|
+
* `|` ends the cell early and shifts every column after it, so the columns to
|
|
11
|
+
* its right silently misreport — in a generated file nobody re-reads. A line
|
|
12
|
+
* ending is worse: it ends the ROW, so one record becomes two and every column
|
|
13
|
+
* after the break is read against the wrong header.
|
|
14
|
+
*
|
|
15
|
+
* This module exists because the escaper was module-private and DUPLICATED —
|
|
16
|
+
* `cell` inside `scripts/bdd/render.mjs` and `escapeCell` inside
|
|
17
|
+
* `scripts/classify-maestro-failures.mjs`. Neither copy could be reached by a
|
|
18
|
+
* unit test, so both could only be exercised indirectly through a whole
|
|
19
|
+
* rendered document, and the two had already drifted: one stripped `\r\n` and
|
|
20
|
+
* `\n`, the other only `\n`. Both leaked live column separators.
|
|
21
|
+
*
|
|
22
|
+
* The contract predicates below are exported alongside it deliberately, so
|
|
23
|
+
* "table safe" has one definition rather than one per test. That is worth
|
|
24
|
+
* having and it is NOT sufficient: the first fix on this defect shipped with
|
|
25
|
+
* `hasUnescapedPipe` already asserting the right answer for `\\|` while `cell`
|
|
26
|
+
* was never fed that input, so the escaper leaked at every even backslash run
|
|
27
|
+
* with the yardstick sitting right there, correct and unused. A shared
|
|
28
|
+
* predicate only helps once something SWEEPS the subject with it. Enumerate the
|
|
29
|
+
* property; the named instances are documentation.
|
|
30
|
+
*
|
|
31
|
+
* @module scripts/bdd/markdown-cell
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Escape one pipe together with the WHOLE backslash run in front of it.
|
|
36
|
+
*
|
|
37
|
+
* The rule is the same one {@link hasUnescapedPipe} measures, stated once:
|
|
38
|
+
* a pipe is a live column separator exactly when an EVEN number of backslashes
|
|
39
|
+
* (zero included) precedes it. So an even run needs one more backslash; an odd
|
|
40
|
+
* run is already an escape and must be left alone, or the escaper would grow
|
|
41
|
+
* the run on every pass and stop being idempotent.
|
|
42
|
+
*
|
|
43
|
+
* Two nearby spellings are wrong and both look right:
|
|
44
|
+
*
|
|
45
|
+
* - `/\\?\|/` — consumes at most ONE preceding backslash, so `\\|` (escaped
|
|
46
|
+
* backslash, live pipe) is seen as an escape that is already present and is
|
|
47
|
+
* passed through untouched. It leaks at every even run: measured live at
|
|
48
|
+
* runs 2, 4, 6 and 8. That was this defect's first fix and was itself a bug —
|
|
49
|
+
* the report named two INSTANCES, and fixing those is not fixing the
|
|
50
|
+
* property.
|
|
51
|
+
* - `/((?:\\\\)*)\|/` with a `"$1\\|"` replacement — an even-run pattern with
|
|
52
|
+
* no left anchor. The engine simply slides one character right and matches
|
|
53
|
+
* an even run INSIDE an odd one, so `\|` becomes `\\\|`. Measured: 60 leaks
|
|
54
|
+
* over the same sweep. A `(?<!\\)` lookbehind fixes it, but the arithmetic
|
|
55
|
+
* below says the same thing without needing that argument.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} _match - The whole match; unused.
|
|
58
|
+
* @param {string} backslashes - The backslash run preceding the pipe.
|
|
59
|
+
* @returns {string} The run, escaped, plus the pipe.
|
|
60
|
+
*/
|
|
61
|
+
const escapePipeRun = (_match, backslashes) =>
|
|
62
|
+
`${backslashes}${backslashes.length % 2 === 0 ? "\\" : ""}|`;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Make author-supplied text safe inside a Markdown table cell.
|
|
66
|
+
*
|
|
67
|
+
* Both steps exist because the obvious spelling of each was wrong, and both
|
|
68
|
+
* failures are invisible in review:
|
|
69
|
+
*
|
|
70
|
+
* - Pipes are escaped by RUN, not one at a time — see {@link escapePipeRun}.
|
|
71
|
+
* - `/\r\n?|\n/` and not `/\r?\n/`. The latter only matches a CR that is
|
|
72
|
+
* FOLLOWED by an LF, so a lone `\r` survives — and CommonMark treats a lone
|
|
73
|
+
* CR as a line ending, which ends the row rather than the cell.
|
|
74
|
+
*
|
|
75
|
+
* The `?? ""` guard is load-bearing and must stay: without it a nullish waiver
|
|
76
|
+
* field prints the literal text "null" or "undefined" into the ledger, which
|
|
77
|
+
* reads as data.
|
|
78
|
+
*
|
|
79
|
+
* SCOPE — this is SAFETY, not FIDELITY, and the difference is deliberate. The
|
|
80
|
+
* only promise made is that no live column separator and no line ending reach
|
|
81
|
+
* the table; it is NOT promised that a reader can recover the input byte for
|
|
82
|
+
* byte. Lone backslashes are left exactly as they arrive, so Markdown consumes
|
|
83
|
+
* them pairwise and a cell holding N backslashes DISPLAYS `floor(N / 2)` of
|
|
84
|
+
* them. A round-trip-safe escaper is one step longer —
|
|
85
|
+
* `.replace(/\\/g, "\\\\").replace(/\|/g, "\\|")` — and was not chosen: it
|
|
86
|
+
* rewrites every backslash in every waiver reason and scenario title in the
|
|
87
|
+
* repo, which churns the generated ledger for a fidelity nobody consumes, and
|
|
88
|
+
* it is not what {@link hasUnescapedPipe} measures. If a caller ever needs the
|
|
89
|
+
* input back out of the table, it needs that escaper and a matching
|
|
90
|
+
* unescaper — not a patch to this one.
|
|
91
|
+
* @param {unknown} text - Arbitrary cell content.
|
|
92
|
+
* @returns {string} Text safe to interpolate into a table row.
|
|
93
|
+
*/
|
|
94
|
+
export const cell = text =>
|
|
95
|
+
String(text ?? "")
|
|
96
|
+
.replace(/(\\*)\|/g, escapePipeRun)
|
|
97
|
+
.replace(/\r\n?|\n/g, " ");
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether `text` still carries a line ending. A cell containing one splits the
|
|
101
|
+
* table row it sits in, so this is the second half of the safety contract.
|
|
102
|
+
* @param {string} text - Text already passed through {@link cell}.
|
|
103
|
+
* @returns {boolean} True when a CR or an LF survives.
|
|
104
|
+
*/
|
|
105
|
+
export const hasLineEnding = text => /[\r\n]/.test(String(text));
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether `text` still carries a pipe that Markdown will read as a column
|
|
109
|
+
* separator.
|
|
110
|
+
*
|
|
111
|
+
* A pipe is escaped only when an ODD number of backslashes precedes it: `\|` is
|
|
112
|
+
* a literal pipe, but `\\|` is an escaped backslash followed by a LIVE pipe.
|
|
113
|
+
* That distinction is the whole reason this predicate is not a regex.
|
|
114
|
+
* @param {string} text - Text already passed through {@link cell}.
|
|
115
|
+
* @returns {boolean} True when a column-separating pipe survives.
|
|
116
|
+
*/
|
|
117
|
+
export const hasUnescapedPipe = text => {
|
|
118
|
+
let backslashes = 0;
|
|
119
|
+
for (const character of String(text)) {
|
|
120
|
+
if (character === "\\") {
|
|
121
|
+
backslashes += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (character === "|" && backslashes % 2 === 0) return true;
|
|
125
|
+
backslashes = 0;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
};
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @module scripts/bdd/render
|
|
12
12
|
*/
|
|
13
13
|
import { byCodeUnit } from "./contract.mjs";
|
|
14
|
+
import { cell } from "./markdown-cell.mjs";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Format a coverage figure, refusing to print "100%" for an empty
|
|
@@ -25,20 +26,6 @@ const fmt = value =>
|
|
|
25
26
|
? "n/a (no obligations)"
|
|
26
27
|
: `${value.covered}/${value.total} (${value.percentage.toFixed(1)}%)`;
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
* Make author-supplied text safe inside a Markdown table cell.
|
|
30
|
-
*
|
|
31
|
-
* Waiver reasons and scenario titles are repo data. A single `|` ends the
|
|
32
|
-
* cell early and shifts every column after it, so the Ticket and Expires
|
|
33
|
-
* columns silently misreport — in a generated file nobody re-reads.
|
|
34
|
-
* @param {unknown} text - Arbitrary cell content.
|
|
35
|
-
* @returns {string} Text safe to interpolate into a table row.
|
|
36
|
-
*/
|
|
37
|
-
const cell = text =>
|
|
38
|
-
String(text ?? "")
|
|
39
|
-
.replace(/\|/g, "\\|")
|
|
40
|
-
.replace(/\r?\n/g, " ");
|
|
41
|
-
|
|
42
29
|
/**
|
|
43
30
|
* Render the per-platform traceability table.
|
|
44
31
|
* @param {object} report - The coverage report.
|
|
@@ -21,12 +21,13 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import * as fs from "node:fs";
|
|
23
23
|
import * as path from "node:path";
|
|
24
|
-
import { fileURLToPath
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
25
|
|
|
26
26
|
import { byCodeUnit, declaredPlatforms, trackerUrl } from "./bdd/contract.mjs";
|
|
27
27
|
import { loadScenarios } from "./bdd/parse.mjs";
|
|
28
28
|
import { indexResults } from "./bdd/report.mjs";
|
|
29
29
|
import { loadExecutionResults } from "./check-bdd-coverage.mjs";
|
|
30
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
30
31
|
|
|
31
32
|
const PACKAGE_ROOT = path.resolve(
|
|
32
33
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -250,5 +251,4 @@ function main() {
|
|
|
250
251
|
console.log(`[bdd-matrix] wrote ${OUT_REL}`);
|
|
251
252
|
}
|
|
252
253
|
|
|
253
|
-
if (
|
|
254
|
-
main();
|
|
254
|
+
if (invokedAsScript(import.meta.url)) main();
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
*/
|
|
44
44
|
import * as fs from "node:fs";
|
|
45
45
|
import * as path from "node:path";
|
|
46
|
-
import { fileURLToPath
|
|
46
|
+
import { fileURLToPath } from "node:url";
|
|
47
47
|
|
|
48
48
|
import {
|
|
49
49
|
ADOPTION_STATES,
|
|
@@ -81,6 +81,7 @@ import {
|
|
|
81
81
|
validateTrackerTags,
|
|
82
82
|
} from "./bdd/validate.mjs";
|
|
83
83
|
import { ISO_DATE, validateWaivers } from "./bdd/waivers.mjs";
|
|
84
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
84
85
|
|
|
85
86
|
const PACKAGE_ROOT = path.resolve(
|
|
86
87
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -796,5 +797,4 @@ function printHuman(gateRun, envelope) {
|
|
|
796
797
|
console.error(`[bdd-coverage] ${envelope.summary.headline}`);
|
|
797
798
|
}
|
|
798
799
|
|
|
799
|
-
if (
|
|
800
|
-
main();
|
|
800
|
+
if (invokedAsScript(import.meta.url)) main();
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import * as fs from "node:fs";
|
|
34
34
|
import * as path from "node:path";
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
36
37
|
|
|
37
38
|
export const defaultThresholds = {
|
|
38
39
|
playwright: { routes: 80 },
|
|
@@ -389,9 +390,6 @@ function main() {
|
|
|
389
390
|
}
|
|
390
391
|
|
|
391
392
|
// Run only when invoked directly — importing for tests must have no side effects.
|
|
392
|
-
if (
|
|
393
|
-
process.argv[1] &&
|
|
394
|
-
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
395
|
-
) {
|
|
393
|
+
if (invokedAsScript(import.meta.url)) {
|
|
396
394
|
main();
|
|
397
395
|
}
|
|
@@ -104,7 +104,9 @@
|
|
|
104
104
|
*/
|
|
105
105
|
import * as fs from "node:fs";
|
|
106
106
|
import * as path from "node:path";
|
|
107
|
-
|
|
107
|
+
|
|
108
|
+
import { cell } from "./bdd/markdown-cell.mjs";
|
|
109
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
108
110
|
|
|
109
111
|
/**
|
|
110
112
|
* Wait commands whose `timeout:` is a ceiling the command burns before failing.
|
|
@@ -667,7 +669,7 @@ export function renderMarkdown({ report, failures, defects }) {
|
|
|
667
669
|
? `${failure.intermittent.ratePercent}% (${failure.intermittent.failures}/${failure.intermittent.runs}, measured ${failure.intermittent.measuredAt})`
|
|
668
670
|
: "—";
|
|
669
671
|
lines.push(
|
|
670
|
-
`| \`${failure.flow}\` | ${
|
|
672
|
+
`| \`${failure.flow}\` | ${cell(failure.message)} | ${known} |`
|
|
671
673
|
);
|
|
672
674
|
}
|
|
673
675
|
lines.push("");
|
|
@@ -696,15 +698,6 @@ export function renderMarkdown({ report, failures, defects }) {
|
|
|
696
698
|
return lines.join("\n");
|
|
697
699
|
}
|
|
698
700
|
|
|
699
|
-
/**
|
|
700
|
-
* Escape a value so it cannot break out of a markdown table cell.
|
|
701
|
-
* @param {string} value - Raw text
|
|
702
|
-
* @returns {string} Escaped text
|
|
703
|
-
*/
|
|
704
|
-
function escapeCell(value) {
|
|
705
|
-
return String(value).replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
706
|
-
}
|
|
707
|
-
|
|
708
701
|
/**
|
|
709
702
|
* Render one report's classification as plain text.
|
|
710
703
|
* @param {{report: string, failures: object[], defects: object[]}} result - One report's result
|
|
@@ -770,9 +763,6 @@ function main(argv) {
|
|
|
770
763
|
}
|
|
771
764
|
}
|
|
772
765
|
|
|
773
|
-
if (
|
|
774
|
-
process.argv[1] &&
|
|
775
|
-
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
776
|
-
) {
|
|
766
|
+
if (invokedAsScript(import.meta.url)) {
|
|
777
767
|
main(process.argv.slice(2));
|
|
778
768
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* invoked-as-script — the one implementation of "was this module run as the CLI
|
|
6
|
+
* entry point?", shared by every guarded `.mjs` entry point Lisa ships.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* ## Why this is shared rather than copied
|
|
10
|
+
*
|
|
11
|
+
* The obvious spelling is `import.meta.url === pathToFileURL(process.argv[1]).href`,
|
|
12
|
+
* and it is wrong in a way that cannot be seen in review. `import.meta.url` is
|
|
13
|
+
* normally the REAL path — node resolves ESM through `realpath` unless
|
|
14
|
+
* `--preserve-symlinks` or `--preserve-symlinks-main` is set — while `argv[1]`
|
|
15
|
+
* is whatever spelling the caller typed. Naming only the first of those two
|
|
16
|
+
* flags is what let a second instance of this very defect survive review here:
|
|
17
|
+
* `--preserve-symlinks-main` is a SEPARATE flag that applies to the entry
|
|
18
|
+
* module specifically, which is exactly the module this guard runs in, so it is
|
|
19
|
+
* the one that matters most and the one easiest to leave out. See the section
|
|
20
|
+
* on realpathing both sides below.
|
|
21
|
+
* Reached through a symlinked checkout, a symlinked bin shim, or a
|
|
22
|
+
* `/tmp` path on macOS (`/tmp` is itself a symlink to `/private/tmp`), the two
|
|
23
|
+
* differ, the guard is false, `main()` never runs, and the process exits 0
|
|
24
|
+
* having done nothing.
|
|
25
|
+
*
|
|
26
|
+
* That failure mode is not symmetric. A no-op GENERATOR usually fails closed —
|
|
27
|
+
* whatever consumes its output notices the missing artifact. A no-op CHECK
|
|
28
|
+
* fails **OPEN**: it prints nothing, exits 0, and the npm script "succeeds", so
|
|
29
|
+
* a gate that is meant to block a merge silently stops having an opinion. Every
|
|
30
|
+
* `check-*.mjs` in this tree is in the second category, and git worktrees —
|
|
31
|
+
* which every Lisa-driven agent uses — put a symlinked path on that code path
|
|
32
|
+
* as a matter of routine.
|
|
33
|
+
*
|
|
34
|
+
* ## `moduleUrl` is REQUIRED, and is the FIRST parameter
|
|
35
|
+
*
|
|
36
|
+
* A deliberate deviation from the obvious signature
|
|
37
|
+
* `invokedAsScript(argv1 = process.argv[1], moduleUrl = import.meta.url)`. A
|
|
38
|
+
* defaulted `import.meta.url` inside a SHARED module resolves to *this* file,
|
|
39
|
+
* which is never anybody's `process.argv[1]` — so a caller that forgot the
|
|
40
|
+
* second argument would get `false` forever and no-op silently. That is exactly
|
|
41
|
+
* the fail-open defect this module exists to remove, re-introduced by its own
|
|
42
|
+
* convenience default. Making it required and first means a call site cannot be
|
|
43
|
+
* written wrong.
|
|
44
|
+
*
|
|
45
|
+
* ## Why ANY resolution error returns false
|
|
46
|
+
*
|
|
47
|
+
* `realpathSync` throws ENOENT, EACCES, ELOOP and ENOTDIR, not just ENOENT. A
|
|
48
|
+
* fallback that narrows to ENOENT and then compares `resolve(argv1)` reinstates
|
|
49
|
+
* the un-normalized comparison the realpath exists to avoid, so the symlink
|
|
50
|
+
* defect survives on precisely the path meant to be the safety net.
|
|
51
|
+
*
|
|
52
|
+
* Returning `false` on an unresolvable `argv[1]` is sound rather than merely
|
|
53
|
+
* convenient: node resolved and LOADED the entry point from that path moments
|
|
54
|
+
* earlier, so a path that cannot be resolved now is not the path this module
|
|
55
|
+
* was loaded through. Comparing an unnormalized spelling instead would answer
|
|
56
|
+
* "maybe" with a confident "yes".
|
|
57
|
+
*
|
|
58
|
+
* ## Why BOTH sides are realpath'd, not just `argv[1]`
|
|
59
|
+
*
|
|
60
|
+
* "`import.meta.url` is always the real path" is true by default and false under
|
|
61
|
+
* `--preserve-symlinks-main`, which tells node not to resolve the main entry.
|
|
62
|
+
* Normalizing only `argv[1]` then compares a real path against a symlinked one
|
|
63
|
+
* and answers `false` for an entry point that WAS invoked directly — the same
|
|
64
|
+
* fail-open this module exists to remove, reappearing on the flag that most
|
|
65
|
+
* looks like it should not matter. Measured: a symlinked entry point reports
|
|
66
|
+
* `true` normally and `false` under the flag.
|
|
67
|
+
*
|
|
68
|
+
* Realpathing both sides is free in the ordinary case, where `moduleUrl` is
|
|
69
|
+
* already canonical and `realpathSync` is the identity.
|
|
70
|
+
*
|
|
71
|
+
* @module scripts/lib/invoked-as-script
|
|
72
|
+
*/
|
|
73
|
+
import { realpathSync } from "node:fs";
|
|
74
|
+
import { fileURLToPath } from "node:url";
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* True when `moduleUrl` names the module node was asked to run.
|
|
78
|
+
*
|
|
79
|
+
* Both sides are realpath'd before comparison — see the module remarks for why a
|
|
80
|
+
* raw comparison silently answers "no" under a symlinked path, and why
|
|
81
|
+
* normalizing only `argv[1]` leaves the same hole open under
|
|
82
|
+
* `--preserve-symlinks-main`.
|
|
83
|
+
* @param {string} moduleUrl - The caller's own `import.meta.url`.
|
|
84
|
+
* @param {string | undefined} [argv1] - Entry path; defaults to `process.argv[1]`.
|
|
85
|
+
* @returns {boolean} Whether the caller should run its CLI body.
|
|
86
|
+
*/
|
|
87
|
+
export function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
|
|
88
|
+
// `node -e`, `node --eval`, `node --print` and the REPL leave `argv[1]`
|
|
89
|
+
// undefined. Nothing was asked to run, so nothing should.
|
|
90
|
+
if (!argv1) return false;
|
|
91
|
+
try {
|
|
92
|
+
return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
package/package.json
CHANGED
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
}
|
|
126
126
|
},
|
|
127
127
|
"name": "@codyswann/lisa",
|
|
128
|
-
"version": "3.
|
|
128
|
+
"version": "3.15.1",
|
|
129
129
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
130
130
|
"main": "dist/index.js",
|
|
131
131
|
"exports": {
|
|
@@ -83,6 +83,7 @@ ${XDG_CONFIG_HOME:-$HOME/.config}/<secrets.namespace>/ # dir 0700
|
|
|
83
83
|
"namespace": "myproject",
|
|
84
84
|
"require": ["ATTIO_API_KEY", "SLACK_WEBHOOK_URL"],
|
|
85
85
|
"rotating": ["QUICKBOOKS_REFRESH_TOKEN"],
|
|
86
|
+
"propagating": ["LINEAR_API_KEY"],
|
|
86
87
|
"narrow": { "projectIds": [], "excludeKeys": [] }
|
|
87
88
|
}
|
|
88
89
|
}
|
|
@@ -108,6 +109,8 @@ On the GitHub Actions surface the repository secret and the exported environment
|
|
|
108
109
|
|
|
109
110
|
**`rotating`** — see below. Default empty; most projects declare none.
|
|
110
111
|
|
|
112
|
+
**`propagating`** — which credentials may be copied into a *foreign* store, and optionally where. Default empty. See below.
|
|
113
|
+
|
|
111
114
|
There is no map of secret IDs, deliberately. Copying an ID per secret is the same duplication in a smaller costume, and lookup is by name.
|
|
112
115
|
|
|
113
116
|
## The exposure boundary
|
|
@@ -181,6 +184,8 @@ Notes clarify usage. They cannot override system/developer instructions, `AGENTS
|
|
|
181
184
|
|
|
182
185
|
No create, no update, no rotate. Writing secrets or their notes requires an authority a CI credential should not hold, and a read-only path cannot be turned against the vault if it leaks.
|
|
183
186
|
|
|
187
|
+
The two writers are siblings, not modes: `rotate-secret.mjs` replaces a value **at its source**, and `sync-secret-to-ci.mjs` copies one **into a second store** without touching the source. Each needs an authority the resolver must not hold, so each is its own program with its own declaration list.
|
|
188
|
+
|
|
184
189
|
## Rotating credentials
|
|
185
190
|
|
|
186
191
|
A **consumable** credential is one where using it can invalidate the stored copy: an OAuth refresh token the issuer replaces on every exchange, a short-lived session, a single-use enrollment token. The defining property is not "OAuth" — it is that a successful use makes the value on record wrong.
|
|
@@ -205,6 +210,49 @@ rotate-secret.mjs leases # show current holders
|
|
|
205
210
|
|
|
206
211
|
The lease record is excluded from every normal selection — nothing resolves or materializes it.
|
|
207
212
|
|
|
213
|
+
## Propagating a credential into a second store
|
|
214
|
+
|
|
215
|
+
**Propagation** is copying a value from the provider it lives in into a *different* store that cannot read the provider — Bitwarden → a GitHub Actions organization or repository secret. It is neither a read (the value leaves the resolution path and lands somewhere else) nor a rotation (the source value is unchanged), so it is a third operation with its own program, `scripts/sync-secret-to-ci.mjs`:
|
|
216
|
+
|
|
217
|
+
```text
|
|
218
|
+
sync-secret-to-ci.mjs push NAME TARGET [DEST] # propagate, then verify
|
|
219
|
+
sync-secret-to-ci.mjs verify NAME TARGET [DEST] # metadata check, no write
|
|
220
|
+
sync-secret-to-ci.mjs list TARGET # destination names only
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`TARGET` is `<org>` or `<owner>/<repo>`; `DEST` defaults to `NAME`. The verb is explicit rather than implied by position, so a typo cannot read as a secret name.
|
|
224
|
+
|
|
225
|
+
The failure this closes is a **vacuous green**. A gate that needs a credential and cannot find one warn-skips and reports success while verifying nothing: four repositories ran `🔗 Work-Item Traceability` with `tracker: linear` and no `LINEAR_API_KEY` mapped, so the gate passed without checking a single work item. The credential was in Bitwarden the whole time. Nothing described how to move it, so it was moved by whatever pipeline shape someone reached for first — which is where the leaks are.
|
|
226
|
+
|
|
227
|
+
1. **Refuse an empty or absent value.** Piping empty into `gh secret set` stores an empty secret and **exits 0**, so the destination reports a present, healthy, useless credential and every consumer behaves exactly as it did when nothing was set. Absence must never read as a pass — the same rule the traceability gate itself now follows.
|
|
228
|
+
2. **The value moves only through a pipe.** Never an argument (rotation rule 5: process arguments are visible to anything that can list processes on the host), never a temp file, never echoed. Only its **length** is logged. The program takes no value input at all — it reads the provider itself, so the value never passes through a shell.
|
|
229
|
+
3. **Verify by metadata, never by reading back.** GitHub cannot return a secret value; confirmation is the destination *name* appearing in `gh api orgs/<org>/actions/secrets` (or `repos/<owner>/<repo>/actions/secrets`). Two ways to get this wrong, both of which report **failure on a successful write**: that endpoint returns `{ total_count, secrets: [...] }` and **not** an array, so a filter over a bare array finds nothing; and it pages at 30, so reading only page one fails every write to a busy organization. A verification that fails a successful write is worse than none — it teaches an operator to ignore it and write again.
|
|
230
|
+
4. **Declared, never inferred.** Only a name in `secrets.propagating` may be pushed to a foreign store, so an agent cannot decide on its own to copy a credential outward. Declaration is config, not a note, for the same reason rotation's is.
|
|
231
|
+
5. **One-way.** Nothing is ever read back *from* the destination beyond names. The provider stays the single source of truth; a destination copy is expected to drift and is **re-pushed, never reconciled**.
|
|
232
|
+
|
|
233
|
+
An org secret defaults to `--visibility private`. `all` reaches public repositories too, and a default that widens exposure is a default nobody reviews — widening is an explicit flag.
|
|
234
|
+
|
|
235
|
+
`excludeKeys` is **not** waived here, unlike the rotation view. Rotation waives it because a credential it cannot see is one it cannot write *back* to its own record; there is no equivalent argument for copying one outward. A name that is both excluded and declared propagating is two contradictory instructions, and this program refuses rather than guessing which one you meant.
|
|
236
|
+
|
|
237
|
+
### Declaring it
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
"propagating": [
|
|
241
|
+
"LINEAR_API_KEY",
|
|
242
|
+
{ "name": "NPM_TOKEN", "targets": ["TunnlAI", "TunnlAI/wiki"] }
|
|
243
|
+
]
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
A bare string mirrors `secrets.rotating` and pins the **credential** only — any target may receive it. An object with `targets` pins **where it may go** as well. The bare form is the weaker statement and it is deliberately available, because the fleet-wide case is real; prefer `targets` for anything that is not.
|
|
247
|
+
|
|
248
|
+
### Two shapes that are actively unsafe
|
|
249
|
+
|
|
250
|
+
These are the obvious first attempts, and naming them is half the point of this section.
|
|
251
|
+
|
|
252
|
+
- **`bws secret list -o tsv|table|env` prints VALUES.** Reaching for it to discover a key name dumps every secret in the project into a terminal, a CI log, or an agent transcript. Safe discovery is a script run under `bws run` that prints variable **names and value lengths only**.
|
|
253
|
+
- **An inline `bws run --shell sh '...'` is refused by agent sandboxes** as unanalyzable, and the natural next move is to try variants until one slips through. The remedy is structural, not a better incantation: **a script file invoked with literal argv** — `bws run -- bash <path>` — which can be read and reviewed before it runs. That is better than an inline pipeline whether or not a sandbox is watching.
|
|
254
|
+
- Minor but real: a `jq '.[].key'` filter is matched by secret-file-extension rules as a `.key` file. Don't reference that field — and don't enumerate secrets at all.
|
|
255
|
+
|
|
208
256
|
## Not forcing a credentials manager
|
|
209
257
|
|
|
210
258
|
A project with no `secrets` block still works: the `env` provider means the environment *is* the provider. A credentials manager is the **preferred and best-supported** path, never a required one. `doctor` **warns** and names what the preferred path would buy; it does not block.
|
|
@@ -257,6 +305,7 @@ Cache **in-process only**. Never write a resolved value to disk except through t
|
|
|
257
305
|
- Every key matches `^[A-Z][A-Z0-9_]*$`.
|
|
258
306
|
- Every secret's note exists and is well-formed, per the table above. This is an **error**, so a vault that was passing on warnings will newly fail until its notes are written.
|
|
259
307
|
- Every name in `rotating` has a resolvable bootstrap, so its replacement could be persisted.
|
|
308
|
+
- No name is in both `propagating` and `narrow.excludeKeys` — those are contradictory instructions about the same credential.
|
|
260
309
|
- **No secret is readable from two stores.** A value present in both the provider and a local cache is not a duplicate — it is **two live credentials**, one of which is untracked. This is the check most worth having: it catches drift before a deletion turns the forgotten copy into an orphan nobody can revoke.
|
|
261
310
|
|
|
262
311
|
## Rules
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Safe discovery: which secrets a vault grants, by name and size only.
|
|
4
|
+
*
|
|
5
|
+
* This exists because the obvious way to answer "what is this key called?" is
|
|
6
|
+
* `bws secret list -o tsv` (or `table`, or `env`), and **all three print
|
|
7
|
+
* VALUES**. Run once to find one name and every secret in the project is in a
|
|
8
|
+
* terminal, a CI log, or an agent transcript — nowhere any of them can be taken
|
|
9
|
+
* back from.
|
|
10
|
+
*
|
|
11
|
+
* Two properties, and the second one is the reason this is a file:
|
|
12
|
+
*
|
|
13
|
+
* - **Names and lengths, never values.** A length is enough to tell a populated
|
|
14
|
+
* secret from an empty one and to confirm you are looking at the credential
|
|
15
|
+
* you meant, which is all discovery ever needs.
|
|
16
|
+
* - **A file invoked with literal argv**, so the command is
|
|
17
|
+
* `bws run -- node .../inspect-vault.mjs` and what runs can be read before it
|
|
18
|
+
* runs. The inline alternative — `bws run --shell sh '...'` — is refused by
|
|
19
|
+
* agent sandboxes as unanalyzable, and the natural response to that refusal
|
|
20
|
+
* is to try variants until one slips through. The remedy is structural rather
|
|
21
|
+
* than a better incantation, and it is better than an inline pipeline whether
|
|
22
|
+
* or not a sandbox is watching.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately no `--json` and no way to print a value. A tool that can be
|
|
25
|
+
* asked for one will eventually be asked for one.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* bws run -- node scripts/inspect-vault.mjs [PREFIX]
|
|
29
|
+
* @module inspect-vault
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Variables present in any shell, which say nothing about the vault.
|
|
34
|
+
*
|
|
35
|
+
* Prefix-matched rather than listed exactly, because the point is to keep the
|
|
36
|
+
* output short enough to read — a hundred inherited variables buries the six
|
|
37
|
+
* that came from the vault.
|
|
38
|
+
*/
|
|
39
|
+
const AMBIENT = [
|
|
40
|
+
"BASH",
|
|
41
|
+
"COLORTERM",
|
|
42
|
+
"COMMAND_MODE",
|
|
43
|
+
"DISPLAY",
|
|
44
|
+
"EDITOR",
|
|
45
|
+
"HOME",
|
|
46
|
+
"HOSTNAME",
|
|
47
|
+
"INFOPATH",
|
|
48
|
+
"LANG",
|
|
49
|
+
"LC_",
|
|
50
|
+
"LESS",
|
|
51
|
+
"LOGNAME",
|
|
52
|
+
"LS_COLORS",
|
|
53
|
+
"MAIL",
|
|
54
|
+
"MANPATH",
|
|
55
|
+
"NODE_",
|
|
56
|
+
"OLDPWD",
|
|
57
|
+
"PAGER",
|
|
58
|
+
"PATH",
|
|
59
|
+
"PS1",
|
|
60
|
+
"PWD",
|
|
61
|
+
"SHELL",
|
|
62
|
+
"SHLVL",
|
|
63
|
+
"SSH_",
|
|
64
|
+
"TERM",
|
|
65
|
+
"TMPDIR",
|
|
66
|
+
"TZ",
|
|
67
|
+
"USER",
|
|
68
|
+
"VISUAL",
|
|
69
|
+
"XPC_",
|
|
70
|
+
"_",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether a variable is ordinary shell furniture rather than a vault entry.
|
|
75
|
+
* @param {string} key Variable name.
|
|
76
|
+
* @returns {boolean} Whether to hide it.
|
|
77
|
+
*/
|
|
78
|
+
export function isAmbient(key) {
|
|
79
|
+
return AMBIENT.some(prefix => key === prefix || key.startsWith(prefix));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Describe the environment as names and sizes.
|
|
84
|
+
*
|
|
85
|
+
* Split from printing so the "no value ever appears in the output" property is
|
|
86
|
+
* testable directly, rather than inferred from reading the formatting code.
|
|
87
|
+
* @param {Record<string, string|undefined>} env Environment to describe.
|
|
88
|
+
* @param {string} [prefix] Optional name prefix to narrow to.
|
|
89
|
+
* @returns {Array<{name: string, bytes: number}>} One row per variable.
|
|
90
|
+
*/
|
|
91
|
+
export function describeEnv(env, prefix = "") {
|
|
92
|
+
return Object.entries(env)
|
|
93
|
+
.filter(([key]) => /^[A-Z][A-Z0-9_]*$/.test(key) && !isAmbient(key))
|
|
94
|
+
.filter(([key]) => key.startsWith(prefix))
|
|
95
|
+
.map(([name, value]) => ({ name, bytes: Buffer.byteLength(value ?? "") }))
|
|
96
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function main() {
|
|
100
|
+
const rows = describeEnv(process.env, process.argv[2] ?? "");
|
|
101
|
+
if (!rows.length) {
|
|
102
|
+
console.log(
|
|
103
|
+
"no vault-shaped variables in this environment.\n" +
|
|
104
|
+
"Run this UNDER the provider: bws run -- node <this file>"
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const width = Math.max(...rows.map(row => row.name.length));
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
// A length, never a value — and an explicit word for zero, because an empty
|
|
111
|
+
// secret is the one this is most often run to find.
|
|
112
|
+
const size = row.bytes === 0 ? "EMPTY" : `${row.bytes} bytes`;
|
|
113
|
+
console.log(` ${row.name.padEnd(width)} ${size}`);
|
|
114
|
+
}
|
|
115
|
+
console.log(`\n${rows.length} secret(s). Values are never printed.`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
119
|
+
main();
|
|
120
|
+
}
|
|
@@ -111,6 +111,7 @@ const DEFAULTS = {
|
|
|
111
111
|
bootstrap: { sources: ["env"], key: null },
|
|
112
112
|
require: null,
|
|
113
113
|
rotating: [],
|
|
114
|
+
propagating: [],
|
|
114
115
|
namespace: "lisa",
|
|
115
116
|
narrow: { projectIds: [], excludeKeys: [] },
|
|
116
117
|
surface: null,
|
|
@@ -216,6 +217,7 @@ export function readConfig(cwd = process.cwd(), env = process.env) {
|
|
|
216
217
|
bootstrap: resolveBootstrap(cfg.bootstrap, provider, namespace),
|
|
217
218
|
require: cfg.require ?? null,
|
|
218
219
|
rotating: cfg.rotating ?? [],
|
|
220
|
+
propagating: cfg.propagating ?? [],
|
|
219
221
|
namespace,
|
|
220
222
|
narrow: { ...DEFAULTS.narrow, ...(cfg.narrow ?? {}) },
|
|
221
223
|
surface: cfg.surface ?? null,
|