@mmerterden/multi-agent-pipeline 20.23.2 → 20.25.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +8 -6
  3. package/README.tr.md +7 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +7 -6
  7. package/docs/features.md +9 -4
  8. package/install/catalog-history.json +1 -1
  9. package/install/templates/claude-hooks.json +14 -1
  10. package/install/templates/codex-hooks.json +18 -0
  11. package/manifest.json +126 -98
  12. package/package.json +1 -1
  13. package/pipeline/agents/task-clarifier.md +1 -1
  14. package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +4 -2
  15. package/pipeline/commands/multi-agent/deps/SKILL.md +61 -0
  16. package/pipeline/commands/multi-agent/model/SKILL.md +2 -2
  17. package/pipeline/commands/multi-agent/postmortem/SKILL.md +90 -0
  18. package/pipeline/commands/multi-agent/review/SKILL.md +5 -5
  19. package/pipeline/commands/multi-agent/route-on/SKILL.md +1 -1
  20. package/pipeline/commands/multi-agent/setup/SKILL.md +7 -5
  21. package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
  22. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +2 -3
  23. package/pipeline/contract/fixtures/runs-failed.json +1 -1
  24. package/pipeline/contract/fixtures/runs-old-schema.json +1 -1
  25. package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
  26. package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
  27. package/pipeline/contract/fixtures/runs-running.json +1 -1
  28. package/pipeline/lib/analysis-jira-write.sh +70 -0
  29. package/pipeline/lib/credential-inventory.sh +5 -5
  30. package/pipeline/lib/decisions.mjs +130 -0
  31. package/pipeline/lib/fetch-crashlytics.sh +6 -8
  32. package/pipeline/lib/model-rung.sh +1 -1
  33. package/pipeline/lib/plan-todos.sh +177 -37
  34. package/pipeline/lib/post-pr-review.sh +4 -6
  35. package/pipeline/lib/route-state.sh +1 -1
  36. package/pipeline/lib/shadow-git.sh +4 -5
  37. package/pipeline/lib/task-graph.mjs +446 -0
  38. package/pipeline/lib/tdd-evidence.mjs +579 -0
  39. package/pipeline/multi-agent-refs/analysis-template-corporate.md +2 -0
  40. package/pipeline/multi-agent-refs/analysis-template.md +4 -2
  41. package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -11
  42. package/pipeline/multi-agent-refs/features/analysis-jira.md +42 -0
  43. package/pipeline/multi-agent-refs/features/autopilot-decisions.md +34 -0
  44. package/pipeline/multi-agent-refs/features/cost-analysis.md +4 -0
  45. package/pipeline/multi-agent-refs/features/dep-bump.md +176 -0
  46. package/pipeline/multi-agent-refs/features/model-fallback.md +21 -14
  47. package/pipeline/multi-agent-refs/features/mutation-probe.md +58 -0
  48. package/pipeline/multi-agent-refs/features/plan-checks.md +67 -0
  49. package/pipeline/multi-agent-refs/features/plan-todos.md +40 -1
  50. package/pipeline/multi-agent-refs/features/rework-escalation.md +49 -0
  51. package/pipeline/multi-agent-refs/features/scope-check.md +1 -0
  52. package/pipeline/multi-agent-refs/help/en.md +2 -0
  53. package/pipeline/multi-agent-refs/help/tr.md +2 -0
  54. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  55. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -3
  56. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +7 -7
  57. package/pipeline/multi-agent-refs/phases/phase-3-review.md +18 -16
  58. package/pipeline/multi-agent-refs/phases/phase-5-report.md +3 -3
  59. package/pipeline/multi-agent-refs/picker-contract.md +3 -2
  60. package/pipeline/multi-agent-refs/response-shape.md +65 -0
  61. package/pipeline/multi-agent-refs/tracker-contract.md +6 -6
  62. package/pipeline/rules/response-shape.md +18 -0
  63. package/pipeline/schemas/agent-state.schema.json +36 -1
  64. package/pipeline/schemas/phases.json +4 -0
  65. package/pipeline/schemas/plan-todos.schema.json +104 -3
  66. package/pipeline/schemas/planning-output.schema.json +46 -0
  67. package/pipeline/schemas/prefs.schema.json +122 -3
  68. package/pipeline/scripts/_code-graph.mjs +2 -2
  69. package/pipeline/scripts/_jsonl.mjs +151 -0
  70. package/pipeline/scripts/_plan-overlap.mjs +231 -0
  71. package/pipeline/scripts/_redact.mjs +124 -10
  72. package/pipeline/scripts/_transcript-hosts.mjs +406 -0
  73. package/pipeline/scripts/analysis-story-tree.mjs +153 -8
  74. package/pipeline/scripts/autopilot-publish.mjs +12 -1
  75. package/pipeline/scripts/build-stack-plugins.mjs +3 -3
  76. package/pipeline/scripts/capture-flush.sh +7 -7
  77. package/pipeline/scripts/code-graph-rules/android.json +1 -1
  78. package/pipeline/scripts/cost-analyze.mjs +32 -43
  79. package/pipeline/scripts/cost-table.json +35 -19
  80. package/pipeline/scripts/decisions-log.mjs +118 -0
  81. package/pipeline/scripts/dep-bump.mjs +1556 -0
  82. package/pipeline/scripts/edit-syntax-gate.mjs +362 -0
  83. package/pipeline/scripts/edit-syntax-gate.sh +32 -0
  84. package/pipeline/scripts/find-polluter.mjs +281 -0
  85. package/pipeline/scripts/gate-ledger.mjs +46 -13
  86. package/pipeline/scripts/gc-abandoned.sh +2 -2
  87. package/pipeline/scripts/gen-mode-dispatch.mjs +3 -3
  88. package/pipeline/scripts/git-path.mjs +1 -2
  89. package/pipeline/scripts/host-hooks.mjs +265 -0
  90. package/pipeline/scripts/jira-attach.sh +1 -1
  91. package/pipeline/scripts/keychain.py +3 -3
  92. package/pipeline/scripts/mutation-probe.mjs +484 -0
  93. package/pipeline/scripts/output-quality-check.sh +1 -1
  94. package/pipeline/scripts/phase-tracker.sh +5 -6
  95. package/pipeline/scripts/phase0-exit-gate.mjs +3 -3
  96. package/pipeline/scripts/plan-complexity.mjs +259 -0
  97. package/pipeline/scripts/pre-push-check.sh +3 -6
  98. package/pipeline/scripts/review-decision-gate.mjs +33 -1
  99. package/pipeline/scripts/rework-escalation.mjs +264 -0
  100. package/pipeline/scripts/test-strength.mjs +5 -3
  101. package/pipeline/scripts/transcript-index.mjs +1131 -0
  102. package/pipeline/scripts/uninstall.mjs +90 -4
  103. package/pipeline/scripts/update-issue-progress.sh +4 -6
  104. package/pipeline/scripts/usage-report.mjs +2 -2
  105. package/pipeline/scripts/validate-planning.mjs +48 -6
  106. package/pipeline/skills/.skill-manifest.json +20 -12
  107. package/pipeline/skills/.skills-index.json +23 -1
  108. package/pipeline/skills/shared/README.md +5 -3
  109. package/pipeline/skills/shared/core/multi-agent/SKILL.md +6 -6
  110. package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +4 -2
  111. package/pipeline/skills/shared/core/multi-agent-deps/SKILL.md +60 -0
  112. package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +2 -2
  113. package/pipeline/skills/shared/core/multi-agent-postmortem/SKILL.md +69 -0
  114. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -5
  115. package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +1 -1
  116. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +7 -5
  117. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -4
  118. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +2 -3
  119. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +15 -0
  120. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +13 -0
  121. package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +8 -0
  122. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
  123. package/pipeline/skills/skills-index.md +3 -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 String(text ?? "")
61
- .replace(SCHEME_LINK, "[link]")
62
- .replace(WWW_LINK, "[link]")
63
- .replace(EMAIL, "[email]")
64
- .replace(BARE_LINK, "[link]")
65
- .replace(IPV4, "[host]")
66
- .replace(DOTTED_NAME, hostOrSelf)
67
- .replace(SYSTEM_PATH, "[path]")
68
- .replace(HOME_PATH, "~")
69
- .trim();
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`. */