@mmerterden/multi-agent-pipeline 20.22.0 → 20.23.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 +29 -0
- package/docs/adr/0010-own-code-graph.md +3 -2
- package/docs/facts.json +3 -3
- package/docs/features.md +2 -2
- package/install/_usage-lifecycle.mjs +44 -0
- package/install/catalog-history.json +1 -1
- package/install/index.mjs +2 -0
- package/manifest.json +52 -49
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/update/SKILL.md +3 -3
- package/pipeline/lib/json-file-lock.mjs +24 -2
- package/pipeline/multi-agent-refs/analysis/evidence.md +1 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +1 -0
- package/pipeline/multi-agent-refs/features/analysis-jira.md +5 -0
- package/pipeline/multi-agent-refs/features/code-graph.md +40 -3
- package/pipeline/multi-agent-refs/features/usage-reporting.md +40 -23
- package/pipeline/multi-agent-refs/generate-issue.md +11 -1
- package/pipeline/multi-agent-refs/knowledge.md +18 -0
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -2
- package/pipeline/schemas/code-graph.schema.json +3 -3
- package/pipeline/schemas/prefs.schema.json +26 -4
- package/pipeline/scripts/_code-graph.mjs +121 -12
- package/pipeline/scripts/_diff-impact.mjs +12 -5
- package/pipeline/scripts/_supertypes.mjs +133 -0
- package/pipeline/scripts/analysis-story-body.mjs +24 -8
- package/pipeline/scripts/capture-flush.sh +4 -1
- package/pipeline/scripts/code-graph-rules/android.json +1 -0
- package/pipeline/scripts/code-graph-rules/ios.json +5 -0
- package/pipeline/scripts/code-graph-rules/node.json +1 -0
- package/pipeline/scripts/code-graph-rules/python.json +1 -0
- package/pipeline/scripts/git-path.mjs +21 -0
- package/pipeline/scripts/graph-affected.mjs +2 -1
- package/pipeline/scripts/graph-confirm.mjs +140 -0
- package/pipeline/scripts/learnings-ledger.mjs +3 -2
- package/pipeline/scripts/product-context.mjs +30 -19
- package/pipeline/scripts/symbol-existence-gate.mjs +21 -9
- package/pipeline/scripts/test-gap-scan.mjs +4 -1
- package/pipeline/scripts/test-strength.mjs +7 -8
- package/pipeline/scripts/usage-identity.mjs +1 -1
- package/pipeline/scripts/usage-register.mjs +181 -15
- package/pipeline/scripts/validate-code-graph.mjs +3 -3
- package/pipeline/scripts/valven-gate.mjs +9 -1
- package/pipeline/scripts/valven-port.mjs +16 -5
- package/pipeline/scripts/valven-split.mjs +6 -4
- package/pipeline/skills/.skill-manifest.json +3 -3
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +3 -3
|
@@ -16,6 +16,11 @@ import { escapeJiraWiki, wikiLink } from "./jira-wiki-escape.mjs";
|
|
|
16
16
|
import { redesignSections } from "../lib/analysis-sections.mjs";
|
|
17
17
|
|
|
18
18
|
const MAX_CRITERIA = 8;
|
|
19
|
+
|
|
20
|
+
// Panel border colours of a story section, the ones testers read stories in:
|
|
21
|
+
// the user story, the acceptance criteria, and every other section.
|
|
22
|
+
const PANEL = { story: "#d4d39e", criteria: "#d4a19e", info: "#9eb6d4" };
|
|
23
|
+
const panel = (colour, lines) => [`{panel:borderColor=${colour}}`, ...lines, "{panel}", ""];
|
|
19
24
|
const MAX_DESIGN_LINKS = 5;
|
|
20
25
|
|
|
21
26
|
// A criterion that describes a refusal, an error or an alternative path. One is
|
|
@@ -152,19 +157,30 @@ function linksOf(story, lines, ctx) {
|
|
|
152
157
|
|
|
153
158
|
function render({ userStory, acceptance, currentVsTarget, links }, sourceIds) {
|
|
154
159
|
const out = [];
|
|
155
|
-
if (userStory) out.push("h3. User story", escapeJiraWiki(userStory)
|
|
160
|
+
if (userStory) out.push(...panel(PANEL.story, ["h3. User story", escapeJiraWiki(userStory)]));
|
|
156
161
|
if (acceptance.length) {
|
|
157
|
-
out.push(
|
|
162
|
+
out.push(
|
|
163
|
+
...panel(PANEL.criteria, [
|
|
164
|
+
"h3. Acceptance criteria",
|
|
165
|
+
...acceptance.map((a) => `# ${escapeJiraWiki(a)}`),
|
|
166
|
+
]),
|
|
167
|
+
);
|
|
158
168
|
}
|
|
159
169
|
if (currentVsTarget.length) {
|
|
160
|
-
out.push(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
out.push(
|
|
171
|
+
...panel(PANEL.info, [
|
|
172
|
+
"h3. Current vs target",
|
|
173
|
+
"||Id||Status||Where||",
|
|
174
|
+
...currentVsTarget.map(
|
|
175
|
+
(r) => `|${escapeJiraWiki(r.id)}|${escapeJiraWiki(r.status)}|${escapeJiraWiki(r.where)}|`,
|
|
176
|
+
),
|
|
177
|
+
]),
|
|
178
|
+
);
|
|
165
179
|
}
|
|
166
180
|
if (links.length) {
|
|
167
|
-
out.push(
|
|
181
|
+
out.push(
|
|
182
|
+
...panel(PANEL.info, ["h3. Links", ...links.map((l) => `* ${wikiLink(l.label, l.url)}`)]),
|
|
183
|
+
);
|
|
168
184
|
}
|
|
169
185
|
out.push(
|
|
170
186
|
`Sources: ${escapeJiraWiki(sourceIds.length ? sourceIds.join(", ") : "derived from the section heading")}`,
|
|
@@ -66,7 +66,10 @@ LOGS="${LOGS_ROOT:-$HOME/.claude/logs/multi-agent}"
|
|
|
66
66
|
SCRIPTS="$HOME/.claude/scripts"
|
|
67
67
|
[ -d "$SCRIPTS" ] || SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
68
68
|
|
|
69
|
-
say() {
|
|
69
|
+
say() {
|
|
70
|
+
[ "$QUIET" -eq 1 ] && return 0
|
|
71
|
+
if [ "$JSON" -eq 1 ]; then printf '%s\n' "$1" >&2; else printf '%s\n' "$1"; fi
|
|
72
|
+
}
|
|
70
73
|
|
|
71
74
|
# Resolve the run to flush.
|
|
72
75
|
#
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
}
|
|
52
52
|
],
|
|
53
53
|
"referenceKinds": ["class", "interface", "object", "enum", "record", "typealias"],
|
|
54
|
+
"supertypes": { "syntax": "colon", "kinds": ["class", "interface", "object", "enum"] },
|
|
54
55
|
"referenceKindsNote": "fun is deliberately absent, for the reason Swift's rules record for func: a bare lowercase name matched across files is almost never a call to that exact declaration, and on a large app the generic ones (invoke, build, create, get) take the top god-node slots purely because each happened to be declared once. Composables are the tempting exception - they are capitalised and behave like types - but they are declared with fun, so including them means including every other fun with them.",
|
|
55
56
|
"ignoredIdentifiers": [
|
|
56
57
|
"this",
|
|
@@ -51,6 +51,11 @@
|
|
|
51
51
|
}
|
|
52
52
|
],
|
|
53
53
|
"referenceKinds": ["class", "struct", "enum", "protocol", "actor", "typealias"],
|
|
54
|
+
"supertypes": {
|
|
55
|
+
"syntax": "colon",
|
|
56
|
+
"kinds": ["class", "struct", "enum", "protocol", "actor"],
|
|
57
|
+
"extensionPattern": "\\bextension\\s+([A-Z][A-Za-z0-9_.]*)"
|
|
58
|
+
},
|
|
54
59
|
"referenceKindsNote": "func is deliberately absent. A bare lowercase method name matched across files is almost never a call to that exact declaration: on a large Swift app `with`, `size`, `name`, `image` and `model` took the top god-node slots purely because each happened to be declared in exactly one file. Functions still reach the graph through their `defines` edge, so they stay findable by name.",
|
|
55
60
|
"ignoredIdentifiers": [
|
|
56
61
|
"self",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
48
|
"referenceKinds": ["class", "interface", "type", "enum", "const"],
|
|
49
|
+
"supertypes": { "syntax": "extends", "kinds": ["class", "interface"] },
|
|
49
50
|
"referenceKindsNote": "function is deliberately absent, for the reason Swift's rules record for func: a bare name matched across files is almost never a call to that exact declaration, and the generic ones (run, main, get, parse, handler) would take the top god-node slots purely because each happened to be declared once. Functions still reach the graph through their defines edge, so they stay findable by name. const is present but narrowed to an uppercase-initial declaration at column 0, which is what a component, a singleton or an exported table looks like; a lowercase const is a local and is not a declaration anyone references across files. Measured on this repo (126 sources): including function put `ok` at degree 52, `f` at 38, `tokens` at 15 and `arg` at 12 - local helper names that happen to be declared exactly once, so the ambiguity rule does not catch them either. Excluding it leaves a thin symbol layer, and that is the honest shape of this stack: a JavaScript module's exported unit is usually a function, so the useful graph here is the import graph between files. `graph-affected \"<file>.mjs\"` answers who imports a module, which is the question this stack actually gets asked.",
|
|
50
51
|
"ignoredIdentifiers": [
|
|
51
52
|
"this",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
}
|
|
31
31
|
],
|
|
32
32
|
"referenceKinds": ["class"],
|
|
33
|
+
"supertypes": { "syntax": "parens", "kinds": ["class"] },
|
|
33
34
|
"referenceKindsNote": "def is deliberately absent, for the reason Swift's rules record for func and the node rules record with measurements: a bare lowercase name matched across files is almost never a call to that exact declaration, and Python's convention of short module-level helpers (main, run, load, parse, setup) makes it worse than most. Functions still reach the graph through their defines edge, so they stay findable by name.",
|
|
34
35
|
"ignoredIdentifiers": [
|
|
35
36
|
"self",
|
|
@@ -61,3 +61,24 @@ export function unquotePathIfNeeded(s) {
|
|
|
61
61
|
}
|
|
62
62
|
return s;
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The path a `---` or `+++` diff header names (the text after that marker): a
|
|
67
|
+
* quoted path ends at its closing quote, which git may follow with a tab, and
|
|
68
|
+
* an unquoted one ends at a tab.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} text
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function diffHeaderPath(text) {
|
|
74
|
+
if (text.startsWith('"')) {
|
|
75
|
+
for (let i = 1; i < text.length; i++) {
|
|
76
|
+
if (text[i] === "\\") {
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (text[i] === '"') return unquoteGitPath(text.slice(1, i));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return text.replace(/\t.*$/, "");
|
|
84
|
+
}
|
|
@@ -171,7 +171,8 @@ function runDiff(flags, graph, depth, kinds) {
|
|
|
171
171
|
text = diffFor(root, flags.diff);
|
|
172
172
|
}
|
|
173
173
|
const freshness = graphFreshness(graph, { root });
|
|
174
|
-
const
|
|
174
|
+
const range = flags["diff-file"] && flags["diff-file"] !== true ? undefined : flags.diff;
|
|
175
|
+
const side = graphSide(graph, { root, range });
|
|
175
176
|
const mapped = changedSymbols(graph, parseHunkRanges(text, side));
|
|
176
177
|
const hits = affected({ graph, seeds: mapped.seeds, depth, kinds });
|
|
177
178
|
const result = {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file graph-confirm.mjs - a name-based blast radius, checked by a language server.
|
|
5
|
+
*
|
|
6
|
+
* The code graph joins files to declarations by name, so a dependent it lists
|
|
7
|
+
* may only share a name with the changed type, and a dependent behind an
|
|
8
|
+
* ambiguous name is missing. The toolkit's `code_references` answers the same
|
|
9
|
+
* question semantically when the project has an index. This merges the two
|
|
10
|
+
* without trusting either blindly:
|
|
11
|
+
*
|
|
12
|
+
* confirmed a direct dependent the server also found
|
|
13
|
+
* name-only a direct dependent a ready semantic index did not find
|
|
14
|
+
* unchecked a deeper ring the server did not name (it depends on an
|
|
15
|
+
* intermediate declaration), or no answer could check it
|
|
16
|
+
*
|
|
17
|
+
* A deeper ring the server names directly is confirmed too.
|
|
18
|
+
*
|
|
19
|
+
* `lspOnly` lists files the server found that the graph did not. Only a result
|
|
20
|
+
* with `semantic: true` and `indexReady: true` confirms or demotes anything; a
|
|
21
|
+
* fallback answer or an index still building leaves every file unchecked,
|
|
22
|
+
* because "not found" from an unready index is not evidence.
|
|
23
|
+
*
|
|
24
|
+
* Inputs:
|
|
25
|
+
* --affected <path> graph-affected --json output
|
|
26
|
+
* --references <path> JSON array of code_references results, one per changed type
|
|
27
|
+
* --root <path> repository root the server's absolute paths are under
|
|
28
|
+
*
|
|
29
|
+
* Exit codes: 0 answered, 64 usage error.
|
|
30
|
+
*
|
|
31
|
+
* Zero runtime dependencies (ADR-0004).
|
|
32
|
+
*
|
|
33
|
+
* @module pipeline/scripts/graph-confirm
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
37
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
38
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {{affected: Array<{path: string|null, dist: number}>}} affectedDoc
|
|
42
|
+
* @param {Array<{semantic?: boolean, indexReady?: boolean, results?: Array<{file: string}>}>} references
|
|
43
|
+
* @param {{root: string}} opts
|
|
44
|
+
*/
|
|
45
|
+
export function confirmImpact(affectedDoc, references, { root }) {
|
|
46
|
+
const usable = (references || []).filter(
|
|
47
|
+
(r) => r && r.semantic === true && r.indexReady === true,
|
|
48
|
+
);
|
|
49
|
+
const checked = usable.length > 0 && usable.length === (references || []).length;
|
|
50
|
+
const real = (p) => {
|
|
51
|
+
const abs = resolve(p);
|
|
52
|
+
try {
|
|
53
|
+
return realpathSync(abs);
|
|
54
|
+
} catch {
|
|
55
|
+
const parent = dirname(abs);
|
|
56
|
+
return parent === abs ? abs : join(real(parent), basename(abs));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const repo = real(root);
|
|
60
|
+
// A server answers with paths relative to its own workspace root (the
|
|
61
|
+
// nearest package), absolute ones through whatever spelling of a symlinked
|
|
62
|
+
// directory it was given, and files outside the repository (SDKs).
|
|
63
|
+
const rel = (file, serverRoot) => {
|
|
64
|
+
const abs = isAbsolute(file) ? file : resolve(serverRoot || root, file);
|
|
65
|
+
const r = relative(repo, real(abs));
|
|
66
|
+
return r && !r.startsWith("..") && !isAbsolute(r) ? r : null;
|
|
67
|
+
};
|
|
68
|
+
const found = new Set();
|
|
69
|
+
for (const r of usable)
|
|
70
|
+
for (const loc of r.results || []) {
|
|
71
|
+
const f = loc?.file ? rel(loc.file, r.root) : null;
|
|
72
|
+
if (f) found.add(f);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const seedPaths = new Set((affectedDoc.changedSymbols || []).map((c) => c.path).filter(Boolean));
|
|
76
|
+
for (const id of affectedDoc.seeds || []) {
|
|
77
|
+
const s = String(id);
|
|
78
|
+
if (s.startsWith("sym:")) seedPaths.add(s.slice(4, s.lastIndexOf("#")));
|
|
79
|
+
else if (s.startsWith("file:")) seedPaths.add(s.slice(5));
|
|
80
|
+
}
|
|
81
|
+
const graphPaths = new Set();
|
|
82
|
+
const affected = (affectedDoc.affected || []).map((row) => {
|
|
83
|
+
if (row.path) graphPaths.add(row.path);
|
|
84
|
+
let confirmation = "unchecked";
|
|
85
|
+
if (checked && row.path && found.has(row.path)) confirmation = "confirmed";
|
|
86
|
+
else if (checked && row.dist === 1 && row.path) confirmation = "name-only";
|
|
87
|
+
return { ...row, confirmation };
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const lspOnly = checked
|
|
91
|
+
? [...found].filter((f) => !graphPaths.has(f) && !seedPaths.has(f)).sort()
|
|
92
|
+
: [];
|
|
93
|
+
const byPath = new Map();
|
|
94
|
+
for (const a of affected) if (a.path && !byPath.has(a.path)) byPath.set(a.path, a.confirmation);
|
|
95
|
+
const count = (c) => [...byPath.values()].filter((v) => v === c).length;
|
|
96
|
+
return {
|
|
97
|
+
affected,
|
|
98
|
+
lspOnly,
|
|
99
|
+
summary: {
|
|
100
|
+
confirmed: count("confirmed"),
|
|
101
|
+
nameOnly: count("name-only"),
|
|
102
|
+
unchecked: count("unchecked"),
|
|
103
|
+
lspOnly: lspOnly.length,
|
|
104
|
+
checked,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function flag(argv, name) {
|
|
110
|
+
const i = argv.indexOf(name);
|
|
111
|
+
const v = i >= 0 ? argv[i + 1] : undefined;
|
|
112
|
+
return v && !v.startsWith("--") ? v : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function main(argv) {
|
|
116
|
+
const affectedPath = flag(argv, "--affected");
|
|
117
|
+
const referencesPath = flag(argv, "--references");
|
|
118
|
+
const root = flag(argv, "--root");
|
|
119
|
+
if (!affectedPath || !referencesPath || !root) {
|
|
120
|
+
process.stderr.write(
|
|
121
|
+
"usage: graph-confirm.mjs --affected <json> --references <json> --root <path>\n",
|
|
122
|
+
);
|
|
123
|
+
process.exitCode = 64;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
let affectedDoc;
|
|
127
|
+
let references;
|
|
128
|
+
try {
|
|
129
|
+
affectedDoc = JSON.parse(readFileSync(affectedPath, "utf8"));
|
|
130
|
+
references = JSON.parse(readFileSync(referencesPath, "utf8"));
|
|
131
|
+
} catch (e) {
|
|
132
|
+
process.stderr.write(`graph-confirm: ${e.message}\n`);
|
|
133
|
+
process.exitCode = 64;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!Array.isArray(references)) references = [references];
|
|
137
|
+
process.stdout.write(`${JSON.stringify(confirmImpact(affectedDoc, references, { root }))}\n`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (invokedDirectly(import.meta.url)) main(process.argv.slice(2));
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
* Distill the triage's rejected[] findings into rejected-preference
|
|
42
42
|
* entries so reviewers stop re-raising them. SAFETY: blocking-
|
|
43
43
|
* severity rejections are NOT distilled - a wrong rejection of a
|
|
44
|
-
* blocking issue must never permanently suppress that class.
|
|
44
|
+
* blocking issue must never permanently suppress that class. Exit 2
|
|
45
|
+
* when nothing new was distilled (a repeat of the same triage).
|
|
45
46
|
* forget [--statement "..."] [--kind <kind>] [--repo-slug <slug>]
|
|
46
47
|
* Remove ledger entries matching the statement substring and/or
|
|
47
48
|
* kind (rewrites the file). Use to clear a bad/stale learning.
|
|
@@ -311,7 +312,7 @@ function cmdFromTriage() {
|
|
|
311
312
|
}
|
|
312
313
|
}
|
|
313
314
|
process.stdout.write(JSON.stringify({ ok: true, slug, added: written, skippedBlocking }) + "\n");
|
|
314
|
-
process.exitCode = 0;
|
|
315
|
+
process.exitCode = written ? 0 : 2;
|
|
315
316
|
return;
|
|
316
317
|
}
|
|
317
318
|
|
|
@@ -40,19 +40,11 @@
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { execFileSync } from "node:child_process";
|
|
43
|
-
import {
|
|
44
|
-
appendFileSync,
|
|
45
|
-
chmodSync,
|
|
46
|
-
existsSync,
|
|
47
|
-
mkdirSync,
|
|
48
|
-
readFileSync,
|
|
49
|
-
renameSync,
|
|
50
|
-
statSync,
|
|
51
|
-
writeFileSync,
|
|
52
|
-
} from "node:fs";
|
|
43
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
53
44
|
import { homedir } from "node:os";
|
|
54
45
|
import { dirname, join } from "node:path";
|
|
55
46
|
import { fileURLToPath } from "node:url";
|
|
47
|
+
import { lockTimeoutFrom, withFileLockSync, writeJsonAtomicSync } from "../lib/json-file-lock.mjs";
|
|
56
48
|
|
|
57
49
|
export const STACKS = ["ios", "android", "web", "backend", "resources"];
|
|
58
50
|
const AUTH_SCHEMES = ["basic", "bearer"];
|
|
@@ -429,13 +421,26 @@ function defaultPrefsPath() {
|
|
|
429
421
|
);
|
|
430
422
|
}
|
|
431
423
|
|
|
432
|
-
|
|
424
|
+
/**
|
|
425
|
+
* Read, change and replace the preferences file under the lock write-state and
|
|
426
|
+
* migrate-prefs take, so a setup or a second run writing at the same moment is
|
|
427
|
+
* never overwritten. A file that cannot be parsed stops the write instead of
|
|
428
|
+
* being replaced; a symlinked file is written through to its target, at its
|
|
429
|
+
* own mode.
|
|
430
|
+
*/
|
|
431
|
+
function updatePrefs(file, mutate) {
|
|
433
432
|
mkdirSync(dirname(file), { recursive: true });
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
433
|
+
return withFileLockSync(
|
|
434
|
+
file,
|
|
435
|
+
() => {
|
|
436
|
+
const current = existsSync(file) ? readJson(file) : { global: {}, projects: {} };
|
|
437
|
+
const next = mutate(current);
|
|
438
|
+
const mode = existsSync(file) ? statSync(file).mode & 0o777 : 0o600;
|
|
439
|
+
writeJsonAtomicSync(file, next, { mode, followSymlink: true });
|
|
440
|
+
return next;
|
|
441
|
+
},
|
|
442
|
+
{ timeoutMs: lockTimeoutFrom(process.env) },
|
|
443
|
+
);
|
|
439
444
|
}
|
|
440
445
|
|
|
441
446
|
function readJson(file) {
|
|
@@ -491,11 +496,13 @@ function main() {
|
|
|
491
496
|
fail("learn needs --project, --product and --repos");
|
|
492
497
|
let next;
|
|
493
498
|
try {
|
|
494
|
-
|
|
499
|
+
const repos = readJson(args.repos);
|
|
500
|
+
next = updatePrefs(prefsFile, (prefs) =>
|
|
501
|
+
learnProduct(prefs, args.project, args.product, repos),
|
|
502
|
+
);
|
|
495
503
|
} catch (e) {
|
|
496
504
|
fail(e.message);
|
|
497
505
|
}
|
|
498
|
-
writePrefsAtomic(prefsFile, next);
|
|
499
506
|
process.stdout.write(
|
|
500
507
|
`${JSON.stringify({ product: args.product, repos: next.global.products[args.product].repos.length })}\n`,
|
|
501
508
|
);
|
|
@@ -510,7 +517,11 @@ function main() {
|
|
|
510
517
|
}
|
|
511
518
|
case "decline": {
|
|
512
519
|
if (!args.project) fail("decline needs --project");
|
|
513
|
-
|
|
520
|
+
try {
|
|
521
|
+
updatePrefs(prefsFile, (prefs) => declineProduct(prefs, args.project));
|
|
522
|
+
} catch (e) {
|
|
523
|
+
fail(e.message);
|
|
524
|
+
}
|
|
514
525
|
process.stdout.write(`${JSON.stringify({ project: args.project, product: "" })}\n`);
|
|
515
526
|
break;
|
|
516
527
|
}
|
|
@@ -46,6 +46,7 @@ import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
|
46
46
|
import { attendedVerdict } from "../lib/unattended.mjs";
|
|
47
47
|
import { getAdapter, resolveStack, AdapterError } from "./_stack-adapter.mjs";
|
|
48
48
|
import { globToRegExp } from "./glob-match.mjs";
|
|
49
|
+
import { diffHeaderPath } from "./git-path.mjs";
|
|
49
50
|
import { gateContext, recordQuietly } from "./gate-ledger.mjs";
|
|
50
51
|
|
|
51
52
|
const GATE = "symbol-existence";
|
|
@@ -208,13 +209,13 @@ function keyExists(ref, { keys, idents }) {
|
|
|
208
209
|
|
|
209
210
|
function listFiles(dir, head) {
|
|
210
211
|
if (head) {
|
|
211
|
-
const r = git(dir, ["ls-tree", "-r", "--name-only", head]);
|
|
212
|
+
const r = git(dir, ["ls-tree", "-r", "-z", "--name-only", head]);
|
|
212
213
|
if (!r.ok) throw new UsageError(`cannot list ${head}: ${r.err}`);
|
|
213
|
-
return r.out.split("\
|
|
214
|
+
return r.out.split("\0").filter(Boolean);
|
|
214
215
|
}
|
|
215
|
-
const tracked = git(dir, ["ls-files", "--cached"]);
|
|
216
|
-
const untracked = git(dir, ["ls-files", "--others", "--exclude-standard"]);
|
|
217
|
-
return [...new Set([...tracked.out.split("\
|
|
216
|
+
const tracked = git(dir, ["ls-files", "-z", "--cached"]);
|
|
217
|
+
const untracked = git(dir, ["ls-files", "-z", "--others", "--exclude-standard"]);
|
|
218
|
+
return [...new Set([...tracked.out.split("\0"), ...untracked.out.split("\0")].filter(Boolean))];
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
function readAt(dir, head, file) {
|
|
@@ -236,14 +237,25 @@ function readAt(dir, head, file) {
|
|
|
236
237
|
*/
|
|
237
238
|
export function addedLines(dir, base, head) {
|
|
238
239
|
const range = head ? [base, head] : [base];
|
|
239
|
-
const d = git(dir, [
|
|
240
|
+
const d = git(dir, [
|
|
241
|
+
"diff",
|
|
242
|
+
"-U0",
|
|
243
|
+
"--no-color",
|
|
244
|
+
"--no-ext-diff",
|
|
245
|
+
"--src-prefix=a/",
|
|
246
|
+
"--dst-prefix=b/",
|
|
247
|
+
"--diff-filter=AMR",
|
|
248
|
+
"--end-of-options",
|
|
249
|
+
...range,
|
|
250
|
+
]);
|
|
240
251
|
if (!d.ok) throw new UsageError(`git diff failed: ${d.err}`);
|
|
241
252
|
const out = [];
|
|
242
253
|
let file = null;
|
|
243
254
|
let line = 0;
|
|
244
255
|
for (const raw of d.out.split("\n")) {
|
|
245
256
|
if (raw.startsWith("+++ ")) {
|
|
246
|
-
|
|
257
|
+
const path = diffHeaderPath(raw.slice(4));
|
|
258
|
+
file = path === "/dev/null" ? null : path.replace(/^b\//, "");
|
|
247
259
|
continue;
|
|
248
260
|
}
|
|
249
261
|
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
|
|
@@ -258,8 +270,8 @@ export function addedLines(dir, base, head) {
|
|
|
258
270
|
}
|
|
259
271
|
}
|
|
260
272
|
if (!head) {
|
|
261
|
-
const untracked = git(dir, ["ls-files", "--others", "--exclude-standard"]);
|
|
262
|
-
for (const f of untracked.out.split("\
|
|
273
|
+
const untracked = git(dir, ["ls-files", "-z", "--others", "--exclude-standard"]);
|
|
274
|
+
for (const f of untracked.out.split("\0").filter(Boolean)) {
|
|
263
275
|
const text = readAt(dir, null, f);
|
|
264
276
|
if (text === null || text.includes("\u0000")) continue;
|
|
265
277
|
text.split("\n").forEach((t, i) => out.push({ file: f, line: i + 1, text: t }));
|
|
@@ -207,7 +207,10 @@ function isTestPath(path) {
|
|
|
207
207
|
rules.testPathPrefixes.some((p) => basename(path).startsWith(p))
|
|
208
208
|
)
|
|
209
209
|
return true;
|
|
210
|
-
if (
|
|
210
|
+
if (
|
|
211
|
+
Array.isArray(rules.testPathContains) &&
|
|
212
|
+
rules.testPathContains.some((c) => `/${path}`.includes(c))
|
|
213
|
+
)
|
|
211
214
|
return true;
|
|
212
215
|
return false;
|
|
213
216
|
}
|
|
@@ -236,15 +236,14 @@ function prepare(opts) {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
function changedFiles(root, base, head) {
|
|
239
|
-
const r = git(root, ["diff", "--no-renames", "--name-status", base, head]);
|
|
239
|
+
const r = git(root, ["diff", "--no-renames", "--name-status", "-z", base, head]);
|
|
240
240
|
if (!r.ok) throw new UsageError(`git diff failed: ${r.err}`);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
});
|
|
241
|
+
const fields = r.out.split("\0");
|
|
242
|
+
const out = [];
|
|
243
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
244
|
+
if (fields[i]) out.push({ status: fields[i][0], path: fields[i + 1] });
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
248
247
|
}
|
|
249
248
|
|
|
250
249
|
function testCommandFor(opts, adapter, worktree, file) {
|
|
@@ -67,7 +67,7 @@ const HOST = "github.com";
|
|
|
67
67
|
const API_DEFAULT = "https://api.github.com";
|
|
68
68
|
const DEFAULT_KEYS = ["id_ed25519", "id_ecdsa", "id_rsa"];
|
|
69
69
|
export const LOGIN_METHODS = ["gh", "credential", "ssh"];
|
|
70
|
-
const LOGIN_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,38}$/;
|
|
70
|
+
export const LOGIN_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,38}$/;
|
|
71
71
|
const REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
72
72
|
|
|
73
73
|
export function defaultPrefsPath(env = process.env) {
|