@mmerterden/multi-agent-pipeline 20.23.1 → 20.24.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/CHANGELOG.md +30 -1
- package/README.md +7 -6
- package/README.tr.md +6 -5
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +3 -3
- package/install/catalog-history.json +1 -1
- package/install/templates/claude-hooks.json +14 -1
- package/manifest.json +77 -56
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +11 -2
- package/pipeline/commands/multi-agent/postmortem/SKILL.md +78 -0
- package/pipeline/commands/multi-agent/setup/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
- package/pipeline/lib/analysis-jira-write.sh +105 -22
- package/pipeline/lib/decisions.mjs +130 -0
- package/pipeline/lib/plan-todos.sh +156 -33
- package/pipeline/lib/task-graph.mjs +446 -0
- package/pipeline/lib/tdd-evidence.mjs +579 -0
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +2 -0
- package/pipeline/multi-agent-refs/analysis-template.md +4 -2
- package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
- package/pipeline/multi-agent-refs/features/analysis-jira.md +51 -3
- package/pipeline/multi-agent-refs/features/autopilot-decisions.md +34 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +4 -2
- package/pipeline/multi-agent-refs/features/mutation-probe.md +58 -0
- package/pipeline/multi-agent-refs/features/plan-checks.md +67 -0
- package/pipeline/multi-agent-refs/features/plan-todos.md +40 -1
- package/pipeline/multi-agent-refs/features/rework-escalation.md +49 -0
- package/pipeline/multi-agent-refs/features/scope-check.md +1 -0
- package/pipeline/multi-agent-refs/generate-issue.md +1 -1
- package/pipeline/multi-agent-refs/help/en.md +1 -0
- package/pipeline/multi-agent-refs/help/tr.md +1 -0
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -3
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +5 -7
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +13 -11
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +3 -3
- package/pipeline/multi-agent-refs/picker-contract.md +3 -2
- package/pipeline/multi-agent-refs/response-shape.md +65 -0
- package/pipeline/rules/response-shape.md +18 -0
- package/pipeline/schemas/agent-state.schema.json +31 -1
- package/pipeline/schemas/plan-todos.schema.json +101 -0
- package/pipeline/schemas/planning-output.schema.json +46 -0
- package/pipeline/schemas/prefs.schema.json +117 -1
- package/pipeline/scripts/_jsonl.mjs +151 -0
- package/pipeline/scripts/_plan-overlap.mjs +231 -0
- package/pipeline/scripts/_redact.mjs +124 -10
- package/pipeline/scripts/analysis-story-body.mjs +26 -10
- package/pipeline/scripts/analysis-story-tree.mjs +210 -9
- package/pipeline/scripts/autopilot-publish.mjs +12 -1
- package/pipeline/scripts/cost-analyze.mjs +6 -24
- package/pipeline/scripts/decisions-log.mjs +118 -0
- package/pipeline/scripts/edit-syntax-gate.mjs +240 -0
- package/pipeline/scripts/edit-syntax-gate.sh +29 -0
- package/pipeline/scripts/find-polluter.mjs +281 -0
- package/pipeline/scripts/gate-ledger.mjs +46 -13
- package/pipeline/scripts/mutation-probe.mjs +484 -0
- package/pipeline/scripts/plan-complexity.mjs +259 -0
- package/pipeline/scripts/review-decision-gate.mjs +33 -1
- package/pipeline/scripts/rework-escalation.mjs +264 -0
- package/pipeline/scripts/test-strength.mjs +5 -3
- package/pipeline/scripts/transcript-index.mjs +950 -0
- package/pipeline/scripts/validate-planning.mjs +48 -6
- package/pipeline/skills/.skill-manifest.json +11 -7
- package/pipeline/skills/.skills-index.json +12 -1
- package/pipeline/skills/shared/README.md +4 -3
- package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +11 -2
- package/pipeline/skills/shared/core/multi-agent-postmortem/SKILL.md +66 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +15 -0
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +13 -0
- package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +8 -0
- package/pipeline/skills/skills-index.md +2 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file _plan-overlap.mjs - where two plan tasks meet, and whether they agree.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 hands Phase 2 a list of tasks that may run in parallel. Two tasks
|
|
5
|
+
* that touch the same file, or name the same interface, are where a plan
|
|
6
|
+
* contradicts itself without any single task being wrong: both create the
|
|
7
|
+
* same new file, one deletes or renames what the other still uses, or a task
|
|
8
|
+
* consumes something its producer is not ordered before.
|
|
9
|
+
*
|
|
10
|
+
* Every pair that shares a name becomes an anchored row:
|
|
11
|
+
*
|
|
12
|
+
* { a, b, kind: "file"|"symbol", name, aRole, bRole, order, finding }
|
|
13
|
+
*
|
|
14
|
+
* `a` precedes `b` in plan order. A role is what the task does to the name:
|
|
15
|
+
*
|
|
16
|
+
* file creates (in `creates`) | removes (in `removes`) | edits (in `files`)
|
|
17
|
+
* symbol produces (in `produces`) | removes (in `removes`) | consumes (in `consumes`)
|
|
18
|
+
*
|
|
19
|
+
* `removes` covers deletion and the old side of a rename, so one field holds
|
|
20
|
+
* files and symbols alike. A name is a file when it contains a `/`, ends in a
|
|
21
|
+
* known file extension, is a well-known extensionless file (Makefile,
|
|
22
|
+
* Dockerfile, Podfile, ...) or is a dotfile; anything else, `UserService.fetch`
|
|
23
|
+
* included, is a symbol. File names are compared normalised, so `./a` and `a`
|
|
24
|
+
* are one file. A task that names something in more than one field takes its
|
|
25
|
+
* strongest role: removes, then creates / produces, then edits / consumes.
|
|
26
|
+
*
|
|
27
|
+
* `order` comes from `dependsOn`, transitively: `a-first`, `b-first`, or
|
|
28
|
+
* `unordered` when neither task waits for the other.
|
|
29
|
+
*
|
|
30
|
+
* Findings:
|
|
31
|
+
*
|
|
32
|
+
* double-create two tasks create the same file. Contradiction:
|
|
33
|
+
* the second one overwrites the first.
|
|
34
|
+
* removes-used one task removes what the other edits,
|
|
35
|
+
* consumes, creates or produces. Warning.
|
|
36
|
+
* consumer-not-after-producer the user of a created file or produced symbol
|
|
37
|
+
* is not ordered after its producer. Warning.
|
|
38
|
+
* unordered-edit two tasks edit one file with no order between
|
|
39
|
+
* them, so parallel workers collide. Warning.
|
|
40
|
+
*
|
|
41
|
+
* Only the roles the plan states are judged. A plan without `creates`,
|
|
42
|
+
* `removes`, `produces` or `consumes` still gets its shared-file rows, and the
|
|
43
|
+
* unordered-edit finding, from `files` alone.
|
|
44
|
+
*
|
|
45
|
+
* Zero runtime dependencies (ADR-0004).
|
|
46
|
+
*
|
|
47
|
+
* @module pipeline/scripts/_plan-overlap
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
const RANK = { removes: 3, creates: 2, produces: 2, edits: 1, consumes: 1 };
|
|
51
|
+
|
|
52
|
+
const list = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === "string" && x) : []);
|
|
53
|
+
|
|
54
|
+
const FILE_EXTENSIONS = new Set(
|
|
55
|
+
(
|
|
56
|
+
"swift m mm h hh hpp c cc cpp cxx kt kts java gradle groovy scala js mjs cjs jsx ts tsx mts cts " +
|
|
57
|
+
"vue svelte py pyi rb go rs php cs dart lua pl sh bash zsh fish ps1 bat cmd sql graphql gql " +
|
|
58
|
+
"proto json jsonc json5 yaml yml toml ini cfg conf xml plist html htm css scss sass less md " +
|
|
59
|
+
"mdx markdown txt rst adoc csv tsv lock resolved xcconfig xcstrings strings stringsdict " +
|
|
60
|
+
"storyboard xib entitlements pbxproj xcscheme xcworkspacedata xcassets xcodeproj xcworkspace " +
|
|
61
|
+
"xcframework xctestplan png jpg jpeg gif svg webp heic pdf ico ttf otf woff woff2 mp3 mp4 mov " +
|
|
62
|
+
"wav zip gz tgz tar jar aar apk aab ipa dylib so tf hcl cmake mk patch diff pem p12 mobileprovision"
|
|
63
|
+
).split(" "),
|
|
64
|
+
);
|
|
65
|
+
const FILE_NAMES = new Set([
|
|
66
|
+
"Makefile",
|
|
67
|
+
"GNUmakefile",
|
|
68
|
+
"Dockerfile",
|
|
69
|
+
"Containerfile",
|
|
70
|
+
"Podfile",
|
|
71
|
+
"Gemfile",
|
|
72
|
+
"Rakefile",
|
|
73
|
+
"Fastfile",
|
|
74
|
+
"Appfile",
|
|
75
|
+
"Matchfile",
|
|
76
|
+
"Pluginfile",
|
|
77
|
+
"Brewfile",
|
|
78
|
+
"Cartfile",
|
|
79
|
+
"Mintfile",
|
|
80
|
+
"Procfile",
|
|
81
|
+
"Jenkinsfile",
|
|
82
|
+
"Vagrantfile",
|
|
83
|
+
"Pipfile",
|
|
84
|
+
"Justfile",
|
|
85
|
+
"Package.swift",
|
|
86
|
+
"Package.resolved",
|
|
87
|
+
"Podfile.lock",
|
|
88
|
+
"Gemfile.lock",
|
|
89
|
+
"go.mod",
|
|
90
|
+
"go.sum",
|
|
91
|
+
"gradlew",
|
|
92
|
+
"mvnw",
|
|
93
|
+
"BUILD",
|
|
94
|
+
"WORKSPACE",
|
|
95
|
+
"LICENSE",
|
|
96
|
+
"README",
|
|
97
|
+
"CHANGELOG",
|
|
98
|
+
"CODEOWNERS",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
/** `./a`, `a` and `a//b/.` name the same file. */
|
|
102
|
+
export function normalizeFile(name) {
|
|
103
|
+
const parts = [];
|
|
104
|
+
for (const seg of name.split("/")) {
|
|
105
|
+
if (seg === "" || seg === ".") continue;
|
|
106
|
+
if (seg === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
|
|
107
|
+
else parts.push(seg);
|
|
108
|
+
}
|
|
109
|
+
const joined = parts.join("/");
|
|
110
|
+
return name.startsWith("/") ? `/${joined}` : joined || name;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const isFileName = (name) => {
|
|
114
|
+
if (name.includes("/")) return true;
|
|
115
|
+
if (FILE_NAMES.has(name) || name.startsWith(".")) return true;
|
|
116
|
+
const dot = name.lastIndexOf(".");
|
|
117
|
+
return dot > 0 && FILE_EXTENSIONS.has(name.slice(dot + 1).toLowerCase());
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** name -> role for one task, per kind, keeping the strongest role. */
|
|
121
|
+
function rolesOf(task) {
|
|
122
|
+
const files = new Map();
|
|
123
|
+
const symbols = new Map();
|
|
124
|
+
const put = (map, name, role) => {
|
|
125
|
+
const cur = map.get(name);
|
|
126
|
+
if (!cur || RANK[role] > RANK[cur]) map.set(name, role);
|
|
127
|
+
};
|
|
128
|
+
for (const f of list(task.files)) put(files, normalizeFile(f), "edits");
|
|
129
|
+
for (const f of list(task.creates)) put(files, normalizeFile(f), "creates");
|
|
130
|
+
for (const s of list(task.consumes)) put(symbols, s, "consumes");
|
|
131
|
+
for (const s of list(task.produces)) put(symbols, s, "produces");
|
|
132
|
+
for (const n of list(task.removes)) {
|
|
133
|
+
if (isFileName(n)) put(files, normalizeFile(n), "removes");
|
|
134
|
+
else put(symbols, n, "removes");
|
|
135
|
+
}
|
|
136
|
+
return { file: files, symbol: symbols };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** id -> set of every id it waits for, through dependsOn. */
|
|
140
|
+
function ancestors(tasks) {
|
|
141
|
+
const deps = new Map(tasks.map((t) => [String(t.id), list(t.dependsOn)]));
|
|
142
|
+
const memo = new Map();
|
|
143
|
+
const walk = (id, trail = new Set()) => {
|
|
144
|
+
if (memo.has(id)) return memo.get(id);
|
|
145
|
+
const out = new Set();
|
|
146
|
+
if (trail.has(id)) return out;
|
|
147
|
+
trail.add(id);
|
|
148
|
+
for (const d of deps.get(id) || []) {
|
|
149
|
+
out.add(d);
|
|
150
|
+
for (const x of walk(d, trail)) out.add(x);
|
|
151
|
+
}
|
|
152
|
+
trail.delete(id);
|
|
153
|
+
memo.set(id, out);
|
|
154
|
+
return out;
|
|
155
|
+
};
|
|
156
|
+
return (id) => walk(id);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const PRODUCER = new Set(["creates", "produces"]);
|
|
160
|
+
|
|
161
|
+
function judge(aRole, bRole, order) {
|
|
162
|
+
if (aRole === "creates" && bRole === "creates") return "double-create";
|
|
163
|
+
if ((aRole === "removes") !== (bRole === "removes")) return "removes-used";
|
|
164
|
+
if (aRole === "removes") return null;
|
|
165
|
+
const aMakes = PRODUCER.has(aRole);
|
|
166
|
+
const bMakes = PRODUCER.has(bRole);
|
|
167
|
+
if (aMakes && !bMakes && order !== "a-first") return "consumer-not-after-producer";
|
|
168
|
+
if (bMakes && !aMakes && order !== "b-first") return "consumer-not-after-producer";
|
|
169
|
+
if (aRole === "edits" && bRole === "edits" && order === "unordered") return "unordered-edit";
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function sentence(row) {
|
|
174
|
+
const { a, b, name, aRole, bRole, order, finding } = row;
|
|
175
|
+
switch (finding) {
|
|
176
|
+
case "double-create":
|
|
177
|
+
return order === "unordered"
|
|
178
|
+
? `${a} and ${b} both create ${name}; one task must create it and the other depend on it`
|
|
179
|
+
: `${a} and ${b} both create ${name}; they are already ordered, so drop ${name} from the creates of one (the later task edits it)`;
|
|
180
|
+
case "removes-used": {
|
|
181
|
+
const [rm, user, role] = aRole === "removes" ? [a, b, bRole] : [b, a, aRole];
|
|
182
|
+
return `${a} and ${b}: ${rm} removes ${name}, which ${user} ${role}`;
|
|
183
|
+
}
|
|
184
|
+
case "consumer-not-after-producer": {
|
|
185
|
+
const [maker, user, role] = PRODUCER.has(aRole) ? [a, b, bRole] : [b, a, aRole];
|
|
186
|
+
return `${a} and ${b}: ${user} ${role} ${name} but is not ordered after ${maker}, which ${PRODUCER.has(aRole) ? aRole : bRole} it`;
|
|
187
|
+
}
|
|
188
|
+
case "unordered-edit":
|
|
189
|
+
return `${a} and ${b} both edit ${name} with no dependsOn between them`;
|
|
190
|
+
default:
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {Array<{id: string, files?: string[], creates?: string[], removes?: string[],
|
|
197
|
+
* produces?: string[], consumes?: string[], dependsOn?: string[]}>} tasks
|
|
198
|
+
* @returns {{
|
|
199
|
+
* rows: Array<{a: string, b: string, kind: "file"|"symbol", name: string, aRole: string,
|
|
200
|
+
* bRole: string, order: "a-first"|"b-first"|"unordered", finding: string|null}>,
|
|
201
|
+
* contradictions: string[],
|
|
202
|
+
* warnings: string[],
|
|
203
|
+
* }}
|
|
204
|
+
*/
|
|
205
|
+
export function planOverlaps(tasks) {
|
|
206
|
+
const ts = (Array.isArray(tasks) ? tasks : []).filter((t) => t && t.id != null);
|
|
207
|
+
const roles = ts.map(rolesOf);
|
|
208
|
+
const before = ancestors(ts);
|
|
209
|
+
const rows = [];
|
|
210
|
+
for (const kind of ["file", "symbol"]) {
|
|
211
|
+
for (let i = 0; i < ts.length; i++) {
|
|
212
|
+
for (let j = i + 1; j < ts.length; j++) {
|
|
213
|
+
const a = String(ts[i].id);
|
|
214
|
+
const b = String(ts[j].id);
|
|
215
|
+
const order = before(b).has(a) ? "a-first" : before(a).has(b) ? "b-first" : "unordered";
|
|
216
|
+
for (const [name, aRole] of roles[i][kind]) {
|
|
217
|
+
const bRole = roles[j][kind].get(name);
|
|
218
|
+
if (!bRole) continue;
|
|
219
|
+
rows.push({ a, b, kind, name, aRole, bRole, order, finding: judge(aRole, bRole, order) });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const contradictions = [];
|
|
225
|
+
const warnings = [];
|
|
226
|
+
for (const row of rows) {
|
|
227
|
+
if (!row.finding) continue;
|
|
228
|
+
(row.finding === "double-create" ? contradictions : warnings).push(sentence(row));
|
|
229
|
+
}
|
|
230
|
+
return { rows, contradictions, warnings };
|
|
231
|
+
}
|
|
@@ -51,22 +51,136 @@ const SYSTEM_PATH = /(?<![\w.~\-/])\/(?:Volumes|private|var|opt|tmp)(?![\w.-])\S
|
|
|
51
51
|
/** Any user's home directory, reduced to ~ so the rest of the path still reads. */
|
|
52
52
|
const HOME_PATH = /(?<![\w.~\-/])\/(?:Users|home)(?:\/[^\s/)"'\]]+)?(?![\w.-])/g;
|
|
53
53
|
|
|
54
|
+
/** The replacement chain, in order, with the kind each step counts as. */
|
|
55
|
+
const STEPS = [
|
|
56
|
+
[SCHEME_LINK, "[link]", "link"],
|
|
57
|
+
[WWW_LINK, "[link]", "link"],
|
|
58
|
+
[EMAIL, "[email]", "email"],
|
|
59
|
+
[BARE_LINK, "[link]", "link"],
|
|
60
|
+
[IPV4, "[host]", "host"],
|
|
61
|
+
[DOTTED_NAME, hostOrSelf, "host"],
|
|
62
|
+
[SYSTEM_PATH, "[path]", "path"],
|
|
63
|
+
[HOME_PATH, "~", "home-path"],
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* scrub, plus how many replacements of each kind it made. A dotted name that
|
|
68
|
+
* hostOrSelf leaves alone is not counted.
|
|
69
|
+
*
|
|
70
|
+
* @returns {{text: string, counts: Record<string, number>}}
|
|
71
|
+
*/
|
|
72
|
+
export function scrubCounted(text) {
|
|
73
|
+
const counts = {};
|
|
74
|
+
let out = String(text ?? "");
|
|
75
|
+
for (const [re, replacement, kind] of STEPS) {
|
|
76
|
+
out = out.replace(re, (match, ...groups) => {
|
|
77
|
+
const next = typeof replacement === "function" ? replacement(match, ...groups) : replacement;
|
|
78
|
+
if (next !== match) counts[kind] = (counts[kind] || 0) + 1;
|
|
79
|
+
return next;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return { text: out.trim(), counts };
|
|
83
|
+
}
|
|
84
|
+
|
|
54
85
|
/**
|
|
55
86
|
* Free text with URLs (with or without a scheme), e-mail addresses, IPv4
|
|
56
87
|
* addresses, host names, absolute machine paths and home directories replaced
|
|
57
88
|
* by markers.
|
|
58
89
|
*/
|
|
59
90
|
export function scrub(text) {
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
return scrubCounted(text).text;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- credentials the provider rules (lib/redact.mjs) have no prefix for ------
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A variable, flag or header name that holds a credential: any word ending in
|
|
98
|
+
* TOKEN, SECRET, PASSWORD, PASSWD, PWD or API_KEY (PGPASSWORD, MYSQL_PWD,
|
|
99
|
+
* myAppToken, db-password, x-api-key), or a separated word ending in KEY.
|
|
100
|
+
*/
|
|
101
|
+
const SECRET_NAME = String.raw`[A-Za-z0-9_-]*(?:TOKEN|SECRET|PASSWORD|PASSWD|PWD|API[_-]?KEY)|(?:[A-Za-z0-9]+[_-])+KEY`;
|
|
102
|
+
const NOT_MARKER = String.raw`(?!\[redacted:)`;
|
|
103
|
+
const PEM_END = String.raw`-----END [A-Z ]*PRIVATE KEY-----`;
|
|
104
|
+
const JSON_KEY = String.raw`[A-Za-z0-9_-]*(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credentials?)`;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Credential shapes the provider rules miss, as [pattern, replacer, kind].
|
|
108
|
+
* Each keeps the name or header in front of the value so the line still says
|
|
109
|
+
* what was configured, and drops only the value. They run after
|
|
110
|
+
* lib/redact.mjs, so they also finish what its markers leave behind: the body
|
|
111
|
+
* of a private key whose BEGIN line it replaced, and the host and port after
|
|
112
|
+
* a credential URL.
|
|
113
|
+
*/
|
|
114
|
+
const CREDENTIAL_STEPS = [
|
|
115
|
+
[
|
|
116
|
+
new RegExp(
|
|
117
|
+
String.raw`(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\[redacted:private-key-block\])[\s\S]*?(?:${PEM_END}|$(?![\s\S]))`,
|
|
118
|
+
"g",
|
|
119
|
+
),
|
|
120
|
+
() => "[redacted:private-key-block]",
|
|
121
|
+
"private-key-block",
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@\S*|\[redacted:url-with-credentials\]@\S*/gi,
|
|
125
|
+
() => "[redacted:url-with-credentials]",
|
|
126
|
+
"url-with-credentials",
|
|
127
|
+
],
|
|
128
|
+
[/\bAT(?:ATT|CTT)[A-Za-z0-9_=-]{20,}/g, () => "[redacted:atlassian-token]", "atlassian-token"],
|
|
129
|
+
[
|
|
130
|
+
new RegExp(
|
|
131
|
+
String.raw`\b(Authorization\s*:\s*)${NOT_MARKER}(?:(?:Bearer|Basic|Digest|token)\s+)?${NOT_MARKER}[^\s"',;]+`,
|
|
132
|
+
"gi",
|
|
133
|
+
),
|
|
134
|
+
(_, head) => `${head}[redacted:authorization-header]`,
|
|
135
|
+
"authorization-header",
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
/\b(Bearer\s+)(?!\[redacted:)(?=[A-Za-z0-9._~+/=-]*\d)([A-Za-z0-9._~+/=-]{12,})/g,
|
|
139
|
+
(_, head) => `${head}[redacted:bearer-token]`,
|
|
140
|
+
"bearer-token",
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
new RegExp(
|
|
144
|
+
String.raw`(\\?")(${JSON_KEY})\1(\s*:\s*)\1${NOT_MARKER}(?:\\[^"]|[^"\\])+?\1`,
|
|
145
|
+
"gi",
|
|
146
|
+
),
|
|
147
|
+
(_, q, key, sep) => `${q}${key}${q}${sep}${q}[redacted:json-secret]${q}`,
|
|
148
|
+
"json-secret",
|
|
149
|
+
],
|
|
150
|
+
[
|
|
151
|
+
new RegExp(
|
|
152
|
+
String.raw`(?<![\w-])(${SECRET_NAME})(\s*[=:]\s*)${NOT_MARKER}("[^"\n]+"|'[^'\n]+'|[^\s"',;]+)`,
|
|
153
|
+
"gi",
|
|
154
|
+
),
|
|
155
|
+
(_, name, sep) => `${name}${sep}[redacted:env-secret]`,
|
|
156
|
+
"env-secret",
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
/(\bcurl\b[^\n]*?\s(?:-u\s*|--user[=\s]\s*)(["']?)[^\s:"']+:)(?!\[redacted:)[^\s"']+\2/g,
|
|
160
|
+
(_, head, q) => `${head}[redacted:basic-auth]${q}`,
|
|
161
|
+
"basic-auth",
|
|
162
|
+
],
|
|
163
|
+
[
|
|
164
|
+
/\b((?:mysql|mysqldump|mysqladmin|mariadb|mariadb-dump)\b[^\n]*?\s-p)(?!\s)(?!\[redacted:)\S+/g,
|
|
165
|
+
(_, head) => `${head}[redacted:password-flag]`,
|
|
166
|
+
"password-flag",
|
|
167
|
+
],
|
|
168
|
+
[
|
|
169
|
+
/(--password[= ])(?!\[redacted:)\S+/g,
|
|
170
|
+
(_, head) => `${head}[redacted:password-flag]`,
|
|
171
|
+
"password-flag",
|
|
172
|
+
],
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* `text` with the credentials CREDENTIAL_STEPS recognise replaced by
|
|
177
|
+
* `[redacted:<kind>]` markers. Meant to run after lib/redact.mjs; markers it
|
|
178
|
+
* already wrote are never redacted a second time.
|
|
179
|
+
*/
|
|
180
|
+
export function redactCredentials(text) {
|
|
181
|
+
let out = String(text ?? "");
|
|
182
|
+
for (const [re, replacer] of CREDENTIAL_STEPS) out = out.replace(re, replacer);
|
|
183
|
+
return out;
|
|
70
184
|
}
|
|
71
185
|
|
|
72
186
|
/** A copy of `obj` without `keys`. */
|
|
@@ -17,10 +17,22 @@ import { redesignSections } from "../lib/analysis-sections.mjs";
|
|
|
17
17
|
|
|
18
18
|
const MAX_CRITERIA = 8;
|
|
19
19
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
const
|
|
20
|
+
// Default panel border colours of a story section: the user story, the
|
|
21
|
+
// acceptance criteria, and every other section. issueTree.storyPanels
|
|
22
|
+
// overrides any of them, or turns panels off with false.
|
|
23
|
+
const PANEL_DEFAULTS = { story: "#d4d39e", criteria: "#d4a19e", other: "#9eb6d4" };
|
|
24
|
+
const HEX_COLOUR = /^#[0-9a-fA-F]{6}$/;
|
|
25
|
+
|
|
26
|
+
/** The colours to draw with, or null for plain sections. */
|
|
27
|
+
export function panelColours(setting) {
|
|
28
|
+
if (setting === false) return null;
|
|
29
|
+
const own = setting && typeof setting === "object" ? setting : {};
|
|
30
|
+
const pick = (k) => (HEX_COLOUR.test(own[k] ?? "") ? own[k] : PANEL_DEFAULTS[k]);
|
|
31
|
+
return { story: pick("story"), criteria: pick("criteria"), other: pick("other") };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const section = (colours, kind, lines) =>
|
|
35
|
+
colours ? [`{panel:borderColor=${colours[kind]}}`, ...lines, "{panel}", ""] : [...lines, ""];
|
|
24
36
|
const MAX_DESIGN_LINKS = 5;
|
|
25
37
|
|
|
26
38
|
// A criterion that describes a refusal, an error or an alternative path. One is
|
|
@@ -155,12 +167,13 @@ function linksOf(story, lines, ctx) {
|
|
|
155
167
|
return links;
|
|
156
168
|
}
|
|
157
169
|
|
|
158
|
-
function render({ userStory, acceptance, currentVsTarget, links }, sourceIds) {
|
|
170
|
+
function render({ userStory, acceptance, currentVsTarget, links }, sourceIds, colours) {
|
|
159
171
|
const out = [];
|
|
160
|
-
if (userStory)
|
|
172
|
+
if (userStory)
|
|
173
|
+
out.push(...section(colours, "story", ["h3. User story", escapeJiraWiki(userStory)]));
|
|
161
174
|
if (acceptance.length) {
|
|
162
175
|
out.push(
|
|
163
|
-
...
|
|
176
|
+
...section(colours, "criteria", [
|
|
164
177
|
"h3. Acceptance criteria",
|
|
165
178
|
...acceptance.map((a) => `# ${escapeJiraWiki(a)}`),
|
|
166
179
|
]),
|
|
@@ -168,7 +181,7 @@ function render({ userStory, acceptance, currentVsTarget, links }, sourceIds) {
|
|
|
168
181
|
}
|
|
169
182
|
if (currentVsTarget.length) {
|
|
170
183
|
out.push(
|
|
171
|
-
...
|
|
184
|
+
...section(colours, "other", [
|
|
172
185
|
"h3. Current vs target",
|
|
173
186
|
"||Id||Status||Where||",
|
|
174
187
|
...currentVsTarget.map(
|
|
@@ -179,7 +192,10 @@ function render({ userStory, acceptance, currentVsTarget, links }, sourceIds) {
|
|
|
179
192
|
}
|
|
180
193
|
if (links.length) {
|
|
181
194
|
out.push(
|
|
182
|
-
...
|
|
195
|
+
...section(colours, "other", [
|
|
196
|
+
"h3. Links",
|
|
197
|
+
...links.map((l) => `* ${wikiLink(l.label, l.url)}`),
|
|
198
|
+
]),
|
|
183
199
|
);
|
|
184
200
|
}
|
|
185
201
|
out.push(
|
|
@@ -208,7 +224,7 @@ export function buildStoryBody(story, ctx) {
|
|
|
208
224
|
if (!acceptance.some((a) => NEGATIVE.test(a))) warnings.push("no negative acceptance criterion");
|
|
209
225
|
|
|
210
226
|
const body = { userStory, acceptance, currentVsTarget, links };
|
|
211
|
-
return { ...body, warnings, wiki: render(body, story.sourceIds || []) };
|
|
227
|
+
return { ...body, warnings, wiki: render(body, story.sourceIds || [], panelColours(ctx.panels)) };
|
|
212
228
|
}
|
|
213
229
|
|
|
214
230
|
/**
|