@mmerterden/multi-agent-pipeline 17.5.1 → 18.0.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 +276 -0
- package/README.md +59 -1
- package/README.tr.md +57 -0
- package/docs/adr/0011-dormant-ci.md +25 -1
- package/docs/features.md +24 -0
- package/docs/server-readiness.md +188 -0
- package/docs/token-budget-history.md +1 -1
- package/index.js +16 -1
- package/install/_common.mjs +42 -17
- package/install/_dev-only-files.mjs +8 -0
- package/install/_unattended-profile.mjs +113 -0
- package/install/index.mjs +48 -0
- package/install/templates/claude-hooks.json +13 -1
- package/manifest.json +1049 -0
- package/package.json +5 -2
- package/pipeline/commands/multi-agent/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/feedback/SKILL.md +7 -1
- package/pipeline/commands/multi-agent/graph/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/issue/SKILL.md +13 -1
- package/pipeline/commands/multi-agent/jira/SKILL.md +13 -1
- package/pipeline/commands/multi-agent/resume/SKILL.md +16 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +14 -16
- package/pipeline/commands/multi-agent/status/SKILL.md +52 -21
- package/pipeline/commands/multi-agent/update/SKILL.md +13 -56
- package/pipeline/lib/_jira-auth.sh +8 -0
- package/pipeline/lib/analysis-jira-write.sh +32 -0
- package/pipeline/lib/ask-choice.sh +13 -2
- package/pipeline/lib/autopilot-state.sh +8 -0
- package/pipeline/lib/fatal.mjs +129 -0
- package/pipeline/lib/figma-mcp-refresh.sh +18 -0
- package/pipeline/lib/figma-screenshot.sh +18 -0
- package/pipeline/lib/invoked-directly.mjs +43 -0
- package/pipeline/lib/jira-publish.sh +42 -0
- package/pipeline/lib/md2confluence-v3.py +47 -0
- package/pipeline/lib/outbound-gate.mjs +175 -0
- package/pipeline/lib/plan-todos.sh +27 -6
- package/pipeline/lib/post-pr-review.sh +77 -8
- package/pipeline/lib/repo-hygiene.sh +8 -3
- package/pipeline/lib/require-jq.sh +40 -0
- package/pipeline/lib/run-paths.sh +335 -0
- package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +70 -0
- package/pipeline/multi-agent-refs/features/code-graph.md +20 -0
- package/pipeline/multi-agent-refs/features/cost-analysis.md +93 -0
- package/pipeline/multi-agent-refs/features/doctor.md +68 -0
- package/pipeline/multi-agent-refs/features/maturity-followup.md +166 -0
- package/pipeline/multi-agent-refs/features/package-manager.md +80 -0
- package/pipeline/multi-agent-refs/features/usage-reporting.md +79 -0
- package/pipeline/multi-agent-refs/features/verify-by-test.md +1 -1
- package/pipeline/multi-agent-refs/features/verify.md +83 -0
- package/pipeline/multi-agent-refs/phases/operations.md +13 -2
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +6 -3
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +8 -2
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +1 -1
- package/pipeline/multi-agent-refs/picker-contract.md +1 -1
- package/pipeline/multi-agent-refs/unattended-contract.md +129 -0
- package/pipeline/preferences-template.json +1 -1
- package/pipeline/schemas/agent-state.schema.json +122 -11
- package/pipeline/schemas/prefs.schema.json +35 -0
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/_run-paths.mjs +372 -0
- package/pipeline/scripts/aggregate-metrics.mjs +64 -64
- package/pipeline/scripts/autopilot-arming.mjs +2 -1
- package/pipeline/scripts/autopilot-intake.mjs +2 -1
- package/pipeline/scripts/autopilot-runner.mjs +206 -2
- package/pipeline/scripts/build-references.mjs +2 -1
- package/pipeline/scripts/build-stack-plugins.mjs +10 -2
- package/pipeline/scripts/capture-evidence.sh +7 -2
- package/pipeline/scripts/classify-plan-safety.mjs +2 -1
- package/pipeline/scripts/cost-analyze.mjs +600 -0
- package/pipeline/scripts/cost-budget-check.mjs +4 -12
- package/pipeline/scripts/council-view.mjs +2 -1
- package/pipeline/scripts/crush-json.mjs +2 -1
- package/pipeline/scripts/diff-explain.mjs +6 -9
- package/pipeline/scripts/diff-risk-score.mjs +2 -1
- package/pipeline/scripts/doctor.mjs +203 -4
- package/pipeline/scripts/evidence-gate.mjs +9 -3
- package/pipeline/scripts/feedback-send.mjs +13 -3
- package/pipeline/scripts/gc-abandoned.sh +29 -13
- package/pipeline/scripts/gc-worktrees.sh +11 -4
- package/pipeline/scripts/github-ssh-setup.sh +64 -7
- package/pipeline/scripts/graph-mermaid.mjs +4 -2
- package/pipeline/scripts/graph-report.mjs +155 -1
- package/pipeline/scripts/keychain-save.sh +101 -30
- package/pipeline/scripts/learn-from-transcripts.mjs +2 -1
- package/pipeline/scripts/learning-curve.mjs +34 -29
- package/pipeline/scripts/make-manifest.mjs +199 -0
- package/pipeline/scripts/maturity-followup.mjs +294 -0
- package/pipeline/scripts/migrate-prefs.mjs +2 -1
- package/pipeline/scripts/migrate-state.mjs +94 -4
- package/pipeline/scripts/package-manager.mjs +310 -0
- package/pipeline/scripts/phase-banner.sh +6 -2
- package/pipeline/scripts/phase-tracker.sh +41 -3
- package/pipeline/scripts/plan-coverage-gate.mjs +6 -2
- package/pipeline/scripts/pre-commit-check.sh +7 -0
- package/pipeline/scripts/pre-push-check.sh +7 -0
- package/pipeline/scripts/purge.sh +23 -6
- package/pipeline/scripts/render-agent-log-cost.sh +9 -2
- package/pipeline/scripts/render-cost-summary.sh +9 -2
- package/pipeline/scripts/render-work-summary.sh +11 -4
- package/pipeline/scripts/review-file-filter.mjs +4 -2
- package/pipeline/scripts/review-scope.mjs +2 -1
- package/pipeline/scripts/routine-registry.mjs +2 -1
- package/pipeline/scripts/run-aggregator.mjs +13 -14
- package/pipeline/scripts/run-metrics.mjs +3 -1
- package/pipeline/scripts/runs-index.mjs +343 -0
- package/pipeline/scripts/scorecard-snapshot.mjs +178 -0
- package/pipeline/scripts/search-logs.sh +18 -0
- package/pipeline/scripts/test-gap-scan.mjs +2 -1
- package/pipeline/scripts/test-integrity-gate.mjs +2 -1
- package/pipeline/scripts/update-issue-progress.sh +56 -7
- package/pipeline/scripts/usage-register.mjs +271 -0
- package/pipeline/scripts/usage-report.mjs +14 -3
- package/pipeline/scripts/validate-analysis-doc.mjs +2 -1
- package/pipeline/scripts/validate-code-graph.mjs +6 -3
- package/pipeline/scripts/validate-complaint-doc.mjs +2 -1
- package/pipeline/scripts/validate-diff-risk.mjs +6 -3
- package/pipeline/scripts/validate-test-gap.mjs +6 -3
- package/pipeline/scripts/validate-triage.mjs +3 -1
- package/pipeline/scripts/verify-citations.mjs +4 -2
- package/pipeline/scripts/verify.mjs +327 -0
- package/pipeline/scripts/worktree-finalize.sh +13 -4
- package/pipeline/scripts/write-state.mjs +154 -15
- package/pipeline/skills/.skill-manifest.json +6 -6
- package/pipeline/skills/.skills-index.json +56 -1
- package/pipeline/skills/shared/README.md +8 -3
- package/pipeline/skills/shared/core/multi-agent-issue/SKILL.md +14 -0
- package/pipeline/skills/shared/core/multi-agent-jira/SKILL.md +14 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +13 -0
- package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +33 -9
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +6 -0
- package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/package_app.sh +4 -1
- package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/setup_dev_signing.sh +4 -1
- package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +2 -1
- package/pipeline/skills/skills-index.md +6 -1
|
@@ -34,6 +34,7 @@ import { execFileSync } from "node:child_process";
|
|
|
34
34
|
import { join, dirname, resolve } from "node:path";
|
|
35
35
|
import { parseFlags } from "./graph-build.mjs";
|
|
36
36
|
import { loadGraph, defaultGraphPath } from "./graph-query.mjs";
|
|
37
|
+
import { loadRules } from "./_code-graph.mjs";
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Top-level source directory of a path, used as the module bucket.
|
|
@@ -86,7 +87,109 @@ export function summarize(graph, top) {
|
|
|
86
87
|
const kinds = {};
|
|
87
88
|
for (const s of symbols) kinds[s.symbolKind] = (kinds[s.symbolKind] || 0) + 1;
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
// The stack's rules say which symbol kinds can be reference targets at all.
|
|
91
|
+
// Without them the unreferenced question has no honest answer, so the section
|
|
92
|
+
// says that rather than listing symbols that never could have had an edge.
|
|
93
|
+
let referenceKinds;
|
|
94
|
+
try {
|
|
95
|
+
referenceKinds = loadRules(graph.stack).referenceKinds;
|
|
96
|
+
} catch {
|
|
97
|
+
// A stack with no rule file on disk, or one this build cannot parse. The
|
|
98
|
+
// section says so instead of guessing.
|
|
99
|
+
referenceKinds = null;
|
|
100
|
+
}
|
|
101
|
+
const unreferenced = referenceKinds ? unreferencedSymbols(graph, referenceKinds) : null;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
hubs,
|
|
105
|
+
modules,
|
|
106
|
+
externals,
|
|
107
|
+
orphans,
|
|
108
|
+
kinds,
|
|
109
|
+
fileCount: files.length,
|
|
110
|
+
referenceKinds,
|
|
111
|
+
unreferenced,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Symbols no OTHER file in this graph names.
|
|
117
|
+
*
|
|
118
|
+
* The orphan section finds FILES with no edge at all. A file imported for one
|
|
119
|
+
* symbol while three of its other exports are dead has edges, so it is invisible
|
|
120
|
+
* there - which is the gap this closes. The data needed was already in the graph:
|
|
121
|
+
* a symbol's only incoming edge kinds are `defines` (from its own file) and
|
|
122
|
+
* `references` (from another file).
|
|
123
|
+
*
|
|
124
|
+
* CANDIDATES, NOT VERDICTS. ADR-0010 records what the extractor is: regex over
|
|
125
|
+
* comment-stripped source, not a parser. Dynamic dispatch, reflection,
|
|
126
|
+
* string-keyed lookup, a public API consumed outside this repo, and anything
|
|
127
|
+
* reached through a name the rules do not treat as a reference target all look
|
|
128
|
+
* identical to dead code from here. So this reports and never gates, and the
|
|
129
|
+
* exclusions below exist so the list does not fill with symbols that COULD NOT
|
|
130
|
+
* have an edge no matter how heavily used they are:
|
|
131
|
+
*
|
|
132
|
+
* - a nested declaration is never a reference target by construction
|
|
133
|
+
* - a symbolKind outside this stack's referenceKinds is never a target
|
|
134
|
+
* - a name declared in two places gets no reference edge at all (the builder
|
|
135
|
+
* drops ambiguous tokens), so absence proves nothing about it
|
|
136
|
+
* - a symbol declared in a test file is not the subject of this question
|
|
137
|
+
*
|
|
138
|
+
* Same-file use also produces no edge, so "unreferenced" here means precisely
|
|
139
|
+
* "no other file in this repo names it", which is what the section says.
|
|
140
|
+
*
|
|
141
|
+
* @param {object} graph
|
|
142
|
+
* @param {Set<string>|null} referenceKinds - null when the stack's rules did not load
|
|
143
|
+
* @returns {{candidates: object[], testOnly: object[], excluded: object}}
|
|
144
|
+
*/
|
|
145
|
+
export function unreferencedSymbols(graph, referenceKinds) {
|
|
146
|
+
const symbols = graph.nodes.filter((n) => n.kind === "symbol");
|
|
147
|
+
const fileById = new Map(graph.nodes.filter((n) => n.kind === "file").map((n) => [n.id, n]));
|
|
148
|
+
|
|
149
|
+
const nameCount = new Map();
|
|
150
|
+
for (const sym of symbols) {
|
|
151
|
+
if (sym.nested === true) continue;
|
|
152
|
+
nameCount.set(sym.name, (nameCount.get(sym.name) || 0) + 1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const incoming = new Map();
|
|
156
|
+
for (const e of graph.edges) {
|
|
157
|
+
if (e.kind !== "references") continue;
|
|
158
|
+
if (!incoming.has(e.to)) incoming.set(e.to, []);
|
|
159
|
+
incoming.get(e.to).push(e.from);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const excluded = { nested: 0, kindNotATarget: 0, ambiguousName: 0, test: 0 };
|
|
163
|
+
const candidates = [];
|
|
164
|
+
const testOnly = [];
|
|
165
|
+
|
|
166
|
+
for (const sym of symbols) {
|
|
167
|
+
if (sym.isTest) {
|
|
168
|
+
excluded.test += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (sym.nested === true) {
|
|
172
|
+
excluded.nested += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (referenceKinds && !referenceKinds.has(sym.symbolKind)) {
|
|
176
|
+
excluded.kindNotATarget += 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if ((nameCount.get(sym.name) || 0) > 1) {
|
|
180
|
+
excluded.ambiguousName += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const from = incoming.get(sym.id) || [];
|
|
184
|
+
if (from.length === 0) {
|
|
185
|
+
candidates.push(sym);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (from.every((fid) => fileById.get(fid)?.isTest === true)) testOnly.push(sym);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const bySymbol = (a, b) => a.path.localeCompare(b.path) || a.name.localeCompare(b.name);
|
|
192
|
+
return { candidates: candidates.sort(bySymbol), testOnly: testOnly.sort(bySymbol), excluded };
|
|
90
193
|
}
|
|
91
194
|
|
|
92
195
|
/**
|
|
@@ -160,6 +263,57 @@ export function render(graph, top) {
|
|
|
160
263
|
}
|
|
161
264
|
L.push("");
|
|
162
265
|
|
|
266
|
+
L.push(`## Symbols nothing else references`);
|
|
267
|
+
L.push("");
|
|
268
|
+
if (!s.referenceKinds) {
|
|
269
|
+
L.push(
|
|
270
|
+
`Not computed: this stack's code-graph rules did not load, so which symbol kinds can be ` +
|
|
271
|
+
`reference targets at all is unknown, and every answer would be a guess.`,
|
|
272
|
+
);
|
|
273
|
+
} else {
|
|
274
|
+
const u = s.unreferenced;
|
|
275
|
+
L.push(
|
|
276
|
+
`Candidates, not verdicts. These are symbols no OTHER file in this repo names. ` +
|
|
277
|
+
`The extractor is regex over comment-stripped source, not a parser, so dynamic dispatch, ` +
|
|
278
|
+
`reflection, string-keyed lookup and a public API consumed outside this repo all look ` +
|
|
279
|
+
`exactly like dead code from here. Nothing gates on this list.`,
|
|
280
|
+
);
|
|
281
|
+
L.push("");
|
|
282
|
+
L.push(
|
|
283
|
+
`Excluded because they could not carry a reference edge either way: ` +
|
|
284
|
+
`${u.excluded.kindNotATarget} of a kind this stack never treats as a target, ` +
|
|
285
|
+
`${u.excluded.ambiguousName} declared under a name that exists in more than one place, ` +
|
|
286
|
+
`${u.excluded.nested} nested, ${u.excluded.test} declared in test files.`,
|
|
287
|
+
);
|
|
288
|
+
L.push("");
|
|
289
|
+
if (u.candidates.length === 0) {
|
|
290
|
+
L.push(`None: every eligible symbol is named by at least one other file.`);
|
|
291
|
+
} else {
|
|
292
|
+
L.push(`${u.candidates.length} symbol(s) with no reference from any other file:`);
|
|
293
|
+
L.push("");
|
|
294
|
+
L.push(`| Symbol | Kind | Path |`);
|
|
295
|
+
L.push(`|---|---|---|`);
|
|
296
|
+
for (const c of u.candidates.slice(0, top)) {
|
|
297
|
+
L.push(`| \`${c.name}\` | ${c.symbolKind} | \`${c.path}:${c.line || 1}\` |`);
|
|
298
|
+
}
|
|
299
|
+
if (u.candidates.length > top) L.push("");
|
|
300
|
+
if (u.candidates.length > top) L.push(`... ${u.candidates.length - top} more`);
|
|
301
|
+
}
|
|
302
|
+
L.push("");
|
|
303
|
+
if (u.testOnly.length > 0) {
|
|
304
|
+
L.push(
|
|
305
|
+
`${u.testOnly.length} more are referenced ONLY from test files. That is not dead code - ` +
|
|
306
|
+
`it is code whose only consumer is its own test, which is worth knowing before a plan ` +
|
|
307
|
+
`treats it as load-bearing:`,
|
|
308
|
+
);
|
|
309
|
+
L.push("");
|
|
310
|
+
for (const t of u.testOnly.slice(0, top))
|
|
311
|
+
L.push(`- \`${t.name}\` - \`${t.path}:${t.line || 1}\``);
|
|
312
|
+
if (u.testOnly.length > top) L.push(`- ... ${u.testOnly.length - top} more`);
|
|
313
|
+
L.push("");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
163
317
|
return L.join("\n");
|
|
164
318
|
}
|
|
165
319
|
|
|
@@ -2,55 +2,126 @@
|
|
|
2
2
|
|
|
3
3
|
set -uo pipefail
|
|
4
4
|
|
|
5
|
-
# Kullanım:
|
|
6
|
-
# Örnek:
|
|
7
|
-
#
|
|
5
|
+
# Kullanım (interaktif): ./keychain-save.sh <service-adı>
|
|
6
|
+
# Örnek: ./keychain-save.sh github-pat
|
|
7
|
+
# ./keychain-save.sh firebase-sa
|
|
8
|
+
#
|
|
9
|
+
# HEADLESS: terminal yokken script SORMAZ. Üç `read` koruma olmadan duruyordu
|
|
10
|
+
# ve bir launchd ajanının altında ilki sonsuza kadar bekler; kurulum "asılı
|
|
11
|
+
# kaldı" diye görünür.
|
|
12
|
+
#
|
|
13
|
+
# MULTI_AGENT_UNATTENDED=1 terminal olsa bile sorulmaz
|
|
14
|
+
# ./keychain-save.sh <ad> --stdin token stdin'den okunur (satır değil,
|
|
15
|
+
# tüm gövde - JSON çok satırlıdır)
|
|
16
|
+
# ./keychain-save.sh <ad> --json <yol> JSON dosyadan okunur
|
|
17
|
+
#
|
|
18
|
+
# Secret İÇİN env değişkeni bilerek YOK. Bir env değeri çocuk süreçlere
|
|
19
|
+
# miras kalır ve bazı sistemlerde `ps e` ile görünür; borudan okumak onu
|
|
20
|
+
# yalnız bu sürecin belleğinde tutar. Aynı sebeple argv'ye de hiç değmez.
|
|
8
21
|
|
|
9
22
|
SERVICE_NAME="${1:-}"
|
|
23
|
+
MODE="${2:-}"
|
|
24
|
+
MODE_ARG="${3:-}"
|
|
25
|
+
|
|
26
|
+
usage() {
|
|
27
|
+
echo "Kullanım: $0 <service-adı> (interaktif)"
|
|
28
|
+
echo " $0 <service-adı> --stdin (token borudan)"
|
|
29
|
+
echo " $0 <service-adı> --json <yol> (JSON dosyadan)"
|
|
30
|
+
}
|
|
10
31
|
|
|
11
32
|
if [ -z "$SERVICE_NAME" ]; then
|
|
12
|
-
|
|
13
|
-
echo "Örnek: $0 github-pat"
|
|
33
|
+
usage
|
|
14
34
|
exit 1
|
|
15
35
|
fi
|
|
16
36
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
# Başsızken mod verilmediyse, soruyu sormak yerine reddet. Sessizce beklemek
|
|
38
|
+
# en kötü seçenek: dışarı hiçbir şey yazmaz ve kimse neyi beklediğini bilmez.
|
|
39
|
+
if [ -z "$MODE" ] && { [ ! -t 0 ] || [ "${MULTI_AGENT_UNATTENDED:-}" = "1" ]; }; then
|
|
40
|
+
# Sebebi doğru söyle. "terminal yok" derken terminalin VAR olması,
|
|
41
|
+
# okuyanı kendi kurulumunu yanlış yerde aramaya gönderir.
|
|
42
|
+
if [ "${MULTI_AGENT_UNATTENDED:-}" = "1" ]; then
|
|
43
|
+
echo "Hata: MULTI_AGENT_UNATTENDED=1 ve mod verilmedi." >&2
|
|
44
|
+
else
|
|
45
|
+
echo "Hata: terminal yok ve mod verilmedi." >&2
|
|
46
|
+
fi
|
|
47
|
+
usage >&2
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
read_json_file() {
|
|
52
|
+
local path="$1"
|
|
53
|
+
# Kullanıcının girdiği tırnak işaretlerini temizle
|
|
54
|
+
path="${path//\'/}"
|
|
55
|
+
path="${path//\"/}"
|
|
56
|
+
# Tilde (~) expand
|
|
57
|
+
path="${path/#\~/$HOME}"
|
|
58
|
+
if [ ! -f "$path" ]; then
|
|
59
|
+
echo "Hata: Dosya bulunamadı: $path" >&2
|
|
60
|
+
return 1
|
|
61
|
+
fi
|
|
62
|
+
# JSON olduğu gibi saklanır. Kimlik deposu çok satırlı değeri
|
|
63
|
+
# bayt bayt geri verir; base64 sarmalı okuma tarafında çözülmeyen
|
|
64
|
+
# bir katman ekliyordu.
|
|
65
|
+
cat "$path"
|
|
66
|
+
}
|
|
21
67
|
|
|
22
|
-
case "$
|
|
23
|
-
|
|
24
|
-
read
|
|
25
|
-
|
|
68
|
+
case "$MODE" in
|
|
69
|
+
--stdin)
|
|
70
|
+
# `read` DEĞİL `cat`: `read` ilk satırda durur ve bir service-account
|
|
71
|
+
# JSON'unun geri kalanını sessizce atar.
|
|
72
|
+
SECRET=$(cat)
|
|
26
73
|
if [ -z "$SECRET" ]; then
|
|
27
|
-
echo "Hata:
|
|
74
|
+
echo "Hata: stdin boş." >&2
|
|
28
75
|
exit 1
|
|
29
76
|
fi
|
|
30
77
|
;;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
JSON_PATH="${JSON_PATH//\"/}"
|
|
36
|
-
# Tilde (~) expand
|
|
37
|
-
JSON_PATH="${JSON_PATH/#\~/$HOME}"
|
|
38
|
-
if [ ! -f "$JSON_PATH" ]; then
|
|
39
|
-
echo "Hata: Dosya bulunamadı: $JSON_PATH"
|
|
78
|
+
--json)
|
|
79
|
+
if [ -z "$MODE_ARG" ]; then
|
|
80
|
+
echo "Hata: --json bir dosya yolu ister." >&2
|
|
81
|
+
usage >&2
|
|
40
82
|
exit 1
|
|
41
83
|
fi
|
|
42
|
-
|
|
43
|
-
# bayt bayt geri verir; base64 sarmalı okuma tarafında çözülmeyen
|
|
44
|
-
# bir katman ekliyordu.
|
|
45
|
-
SECRET=$(cat "$JSON_PATH")
|
|
84
|
+
SECRET=$(read_json_file "$MODE_ARG") || exit 1
|
|
46
85
|
if [ -z "$SECRET" ]; then
|
|
47
|
-
echo "Hata: Dosya okunamadı."
|
|
86
|
+
echo "Hata: Dosya okunamadı." >&2
|
|
48
87
|
exit 1
|
|
49
88
|
fi
|
|
50
|
-
|
|
89
|
+
;;
|
|
90
|
+
"")
|
|
91
|
+
echo "Ne tür bir secret kaydedeceksin?"
|
|
92
|
+
echo " 1) Personal Access Token / API Key"
|
|
93
|
+
echo " 2) JSON dosyası (service account vb.)"
|
|
94
|
+
# EOF (Ctrl-D) bir cevaptır: korumasız `read` onu boş bir seçime
|
|
95
|
+
# çevirip aşağıdaki "Geçersiz seçim"e düşürür, ki doğru sonuç.
|
|
96
|
+
read -rp "Seçim (1/2): " CHOICE || CHOICE=""
|
|
97
|
+
|
|
98
|
+
case "$CHOICE" in
|
|
99
|
+
1)
|
|
100
|
+
read -rsp "Token'ı yapıştır (gizli yazılır): " SECRET || SECRET=""
|
|
101
|
+
echo ""
|
|
102
|
+
if [ -z "$SECRET" ]; then
|
|
103
|
+
echo "Hata: Token boş olamaz."
|
|
104
|
+
exit 1
|
|
105
|
+
fi
|
|
106
|
+
;;
|
|
107
|
+
2)
|
|
108
|
+
read -rp "JSON dosya yolunu gir: " JSON_PATH || JSON_PATH=""
|
|
109
|
+
SECRET=$(read_json_file "$JSON_PATH") || exit 1
|
|
110
|
+
if [ -z "$SECRET" ]; then
|
|
111
|
+
echo "Hata: Dosya okunamadı."
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
echo "JSON olduğu gibi saklanacak."
|
|
115
|
+
;;
|
|
116
|
+
*)
|
|
117
|
+
echo "Geçersiz seçim."
|
|
118
|
+
exit 1
|
|
119
|
+
;;
|
|
120
|
+
esac
|
|
51
121
|
;;
|
|
52
122
|
*)
|
|
53
|
-
echo "
|
|
123
|
+
echo "Hata: bilinmeyen mod: $MODE" >&2
|
|
124
|
+
usage >&2
|
|
54
125
|
exit 1
|
|
55
126
|
;;
|
|
56
127
|
esac
|
|
@@ -66,6 +66,7 @@ import { execFileSync } from "node:child_process";
|
|
|
66
66
|
import { basename, dirname, join } from "node:path";
|
|
67
67
|
import { homedir, tmpdir } from "node:os";
|
|
68
68
|
import { fileURLToPath } from "node:url";
|
|
69
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
69
70
|
|
|
70
71
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
71
72
|
const argv = process.argv.slice(2);
|
|
@@ -622,4 +623,4 @@ function main() {
|
|
|
622
623
|
}
|
|
623
624
|
}
|
|
624
625
|
|
|
625
|
-
main
|
|
626
|
+
runMain("learn-from-transcripts", main);
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Exit: 0 always (advisory; empty/missing metrics -> friendly note).
|
|
21
21
|
|
|
22
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
23
|
import { homedir } from "node:os";
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
|
|
@@ -39,8 +39,11 @@ for (const a of args) {
|
|
|
39
39
|
const METRICS_FILE =
|
|
40
40
|
process.env.METRICS_FILE || join(homedir(), ".claude", "logs", "multi-agent", "metrics.jsonl");
|
|
41
41
|
|
|
42
|
+
// Called from module scope and has to stop the script, so the write goes to
|
|
43
|
+
// fd 1 synchronously: console.log would still be in the pipe when process.exit
|
|
44
|
+
// tore the process down.
|
|
42
45
|
function emitEmpty(reason) {
|
|
43
|
-
if (opts.json)
|
|
46
|
+
if (opts.json) writeFileSync(1, `${JSON.stringify({ ok: false, reason, buckets: [] })}\n`);
|
|
44
47
|
else console.error(`learning-curve: ${reason}`);
|
|
45
48
|
process.exit(0);
|
|
46
49
|
}
|
|
@@ -183,35 +186,37 @@ if (buckets.length >= 2) {
|
|
|
183
186
|
|
|
184
187
|
const out = { ok: true, bucketDays: opts.bucketDays, buckets, trend };
|
|
185
188
|
|
|
189
|
+
// else, not process.exit(0) after the write: stdout to a pipe is asynchronous
|
|
190
|
+
// and exiting drops whatever has not drained, so the JSON arrived truncated to
|
|
191
|
+
// anything reading it through a pipe.
|
|
186
192
|
if (opts.json) {
|
|
187
193
|
console.log(JSON.stringify(out, null, 2));
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
};
|
|
198
|
-
console.log(`# Learning curve (${opts.bucketDays}-day buckets)\n`);
|
|
199
|
-
console.log(
|
|
200
|
-
"| Bucket | Tasks | First-pass clean | Avg review cycles | Rework/task | Tokens/task | Cache ratio | Memory precision | Mined lessons/task | Offload bytes saved/task |",
|
|
201
|
-
);
|
|
202
|
-
console.log("|---|---|---|---|---|---|---|---|---|---|");
|
|
203
|
-
for (const b of buckets) {
|
|
194
|
+
} else {
|
|
195
|
+
// Markdown / text (default)
|
|
196
|
+
const arrow = (v, goodIsUp) => {
|
|
197
|
+
if (v == null || v === 0) return "-";
|
|
198
|
+
const up = v > 0;
|
|
199
|
+
const good = up === goodIsUp;
|
|
200
|
+
return `${up ? "+" : ""}${v} ${good ? "(better)" : "(worse)"}`;
|
|
201
|
+
};
|
|
202
|
+
console.log(`# Learning curve (${opts.bucketDays}-day buckets)\n`);
|
|
204
203
|
console.log(
|
|
205
|
-
|
|
204
|
+
"| Bucket | Tasks | First-pass clean | Avg review cycles | Rework/task | Tokens/task | Cache ratio | Memory precision | Mined lessons/task | Offload bytes saved/task |",
|
|
206
205
|
);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
206
|
+
console.log("|---|---|---|---|---|---|---|---|---|---|");
|
|
207
|
+
for (const b of buckets) {
|
|
208
|
+
console.log(
|
|
209
|
+
`| ${b.bucketStart} | ${b.tasks} | ${b.firstPassCleanRate ?? "-"} | ${b.avgReviewCycles ?? "-"} | ${b.reworkPerTask} | ${b.tokensPerTask} | ${b.cacheRatio ?? "-"} | ${b.memoryPrecision ?? "-"} | ${b.minedLessonsPerTask ?? "-"} | ${b.offloadBytesSavedPerTask ?? "-"} |`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (trend) {
|
|
213
|
+
console.log(`\n**Trend ${trend.from} -> ${trend.to}:**`);
|
|
214
|
+
console.log(`- First-pass clean: ${arrow(trend.firstPassCleanRate, true)}`);
|
|
215
|
+
console.log(`- Avg review cycles: ${arrow(trend.avgReviewCycles, false)}`);
|
|
216
|
+
console.log(`- Tokens/task: ${arrow(trend.tokensPerTask, false)}`);
|
|
217
|
+
console.log(`- Cache ratio: ${arrow(trend.cacheRatio, true)}`);
|
|
218
|
+
console.log(`- Memory precision: ${arrow(trend.memoryPrecision, true)}`);
|
|
219
|
+
} else {
|
|
220
|
+
console.log("\n(need >= 2 buckets for a trend; keep running the pipeline)");
|
|
221
|
+
}
|
|
217
222
|
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* make-manifest.mjs - a digest of everything the package ships.
|
|
4
|
+
*
|
|
5
|
+
* "Is my install the one that was published, or has something edited it" is a
|
|
6
|
+
* question this tree could not answer. The install is a COPY: `install.js`
|
|
7
|
+
* writes into ~/.claude, ~/.copilot and ~/.codex, and from that moment the two
|
|
8
|
+
* halves drift independently. An edit made in the installed copy looks like a
|
|
9
|
+
* feature nobody can find the source of; a file the installer failed to write
|
|
10
|
+
* looks like a bug in a script that was never there.
|
|
11
|
+
*
|
|
12
|
+
* The manifest turns both into one command. It records a SHA-256 for every
|
|
13
|
+
* file the package ships, the version, and the commit it was built from, and
|
|
14
|
+
* `multi-agent-pipeline verify` compares an installed tree against it, file by
|
|
15
|
+
* file.
|
|
16
|
+
*
|
|
17
|
+
* The file set is not guessed: it comes from `npm pack --dry-run --json`, so
|
|
18
|
+
* it is by construction the same set npm will publish, `files` globs and all.
|
|
19
|
+
* `--ignore-scripts` is on the inner call because this script runs FROM
|
|
20
|
+
* prepack, and without it npm would run prepack again.
|
|
21
|
+
*
|
|
22
|
+
* Two things the manifest cannot cover, said here rather than discovered later:
|
|
23
|
+
* it cannot hash itself (it does not exist when the list is taken), and a
|
|
24
|
+
* signature over it does not authenticate the tarball - the verifier and the
|
|
25
|
+
* public key travel inside the same tarball, so anyone able to rewrite one can
|
|
26
|
+
* rewrite the others. What a signature DOES catch is a manifest damaged in
|
|
27
|
+
* transit or edited after install, which is the failure this file exists for.
|
|
28
|
+
* Provenance belongs to npm's own integrity field, not to us.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* make-manifest.mjs write manifest.json at the repo root
|
|
32
|
+
* make-manifest.mjs --sign also write manifest.sig (needs a signing key)
|
|
33
|
+
* make-manifest.mjs --check recompute and report drift, write nothing
|
|
34
|
+
* make-manifest.mjs --json
|
|
35
|
+
*
|
|
36
|
+
* Exit codes:
|
|
37
|
+
* 0 written, or --check found no drift
|
|
38
|
+
* 1 --check found drift
|
|
39
|
+
* 2 usage or environment error
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { execFileSync } from "node:child_process";
|
|
43
|
+
import { createHash, createPrivateKey, sign as cryptoSign } from "node:crypto";
|
|
44
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
45
|
+
import { dirname, join } from "node:path";
|
|
46
|
+
import { fileURLToPath } from "node:url";
|
|
47
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
48
|
+
|
|
49
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
50
|
+
const MANIFEST = join(ROOT, "manifest.json");
|
|
51
|
+
|
|
52
|
+
const argv = process.argv.slice(2);
|
|
53
|
+
const has = (f) => argv.includes(f);
|
|
54
|
+
|
|
55
|
+
/** The exact set npm would publish, in npm's own words. */
|
|
56
|
+
function shippedFiles() {
|
|
57
|
+
const out = execFileSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], {
|
|
58
|
+
cwd: ROOT,
|
|
59
|
+
encoding: "utf-8",
|
|
60
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
61
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
62
|
+
});
|
|
63
|
+
const parsed = JSON.parse(out);
|
|
64
|
+
const files = parsed?.[0]?.files;
|
|
65
|
+
if (!Array.isArray(files) || !files.length) {
|
|
66
|
+
throw new Error("npm pack --dry-run returned no file list");
|
|
67
|
+
}
|
|
68
|
+
return files
|
|
69
|
+
.map((f) => f.path)
|
|
70
|
+
.filter((p) => p !== "manifest.json" && p !== "manifest.sig")
|
|
71
|
+
.sort();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sha256(path) {
|
|
75
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function gitCommit() {
|
|
79
|
+
try {
|
|
80
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
81
|
+
cwd: ROOT,
|
|
82
|
+
encoding: "utf-8",
|
|
83
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
84
|
+
}).trim();
|
|
85
|
+
} catch {
|
|
86
|
+
// A tarball unpacked outside a checkout has no git. The manifest is still
|
|
87
|
+
// valid; only the provenance line is unknown, and saying so beats writing
|
|
88
|
+
// a placeholder that reads like a real commit.
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function build() {
|
|
94
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
|
|
95
|
+
const files = {};
|
|
96
|
+
for (const rel of shippedFiles()) {
|
|
97
|
+
const abs = join(ROOT, rel);
|
|
98
|
+
if (!existsSync(abs)) continue;
|
|
99
|
+
files[rel] = sha256(abs);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
name: pkg.name,
|
|
103
|
+
version: pkg.version,
|
|
104
|
+
commit: gitCommit(),
|
|
105
|
+
generatedAt: new Date().toISOString(),
|
|
106
|
+
algorithm: "sha256",
|
|
107
|
+
fileCount: Object.keys(files).length,
|
|
108
|
+
files,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Sign the manifest with an ed25519 key. Optional, and silent about its absence
|
|
114
|
+
* on the common path: a package with no signature still verifies on hashes
|
|
115
|
+
* alone, which is the property that actually gets used.
|
|
116
|
+
*
|
|
117
|
+
* The key is read through the credential store, never taken from argv and never
|
|
118
|
+
* printed. MULTI_AGENT_SIGNING_KEY is the fallback for a build host that has no
|
|
119
|
+
* store (CI), and it holds the PEM itself.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} manifestJson exact bytes that were written to disk
|
|
122
|
+
* @returns {string|null} base64 signature, or null when no key is configured
|
|
123
|
+
*/
|
|
124
|
+
function sign(manifestJson) {
|
|
125
|
+
let pem = process.env.MULTI_AGENT_SIGNING_KEY || "";
|
|
126
|
+
if (!pem) {
|
|
127
|
+
try {
|
|
128
|
+
pem = execFileSync(
|
|
129
|
+
join(ROOT, "pipeline", "lib", "credential-store.sh"),
|
|
130
|
+
["get", "pipeline_signing"],
|
|
131
|
+
{
|
|
132
|
+
encoding: "utf-8",
|
|
133
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
134
|
+
},
|
|
135
|
+
).trim();
|
|
136
|
+
} catch {
|
|
137
|
+
pem = "";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!pem) {
|
|
141
|
+
process.stderr.write(
|
|
142
|
+
"make-manifest: no signing key configured - manifest written unsigned (hashes still verify)\n",
|
|
143
|
+
);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const privateKey = createPrivateKey(pem);
|
|
147
|
+
return cryptoSign(null, Buffer.from(manifestJson), privateKey).toString("base64");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function main() {
|
|
151
|
+
if (has("--check")) {
|
|
152
|
+
if (!existsSync(MANIFEST)) {
|
|
153
|
+
process.stderr.write("make-manifest: no manifest.json to check\n");
|
|
154
|
+
process.exit(2);
|
|
155
|
+
}
|
|
156
|
+
const stored = JSON.parse(readFileSync(MANIFEST, "utf-8"));
|
|
157
|
+
const fresh = build();
|
|
158
|
+
const changed = [];
|
|
159
|
+
for (const [rel, hash] of Object.entries(fresh.files)) {
|
|
160
|
+
if (stored.files[rel] !== hash) changed.push(rel);
|
|
161
|
+
}
|
|
162
|
+
const gone = Object.keys(stored.files).filter((r) => !(r in fresh.files));
|
|
163
|
+
const report = { changed, gone, stale: changed.length + gone.length > 0 };
|
|
164
|
+
if (has("--json")) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
165
|
+
else if (!report.stale)
|
|
166
|
+
process.stdout.write(`manifest.json is current (${fresh.fileCount} files)\n`);
|
|
167
|
+
else {
|
|
168
|
+
process.stdout.write(
|
|
169
|
+
`manifest.json is stale: ${changed.length} changed, ${gone.length} gone\n`,
|
|
170
|
+
);
|
|
171
|
+
for (const r of [...changed, ...gone].slice(0, 20)) process.stdout.write(` ${r}\n`);
|
|
172
|
+
}
|
|
173
|
+
process.exitCode = report.stale ? 1 : 0;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const manifest = build();
|
|
178
|
+
const json = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
179
|
+
writeFileSync(MANIFEST, json);
|
|
180
|
+
if (has("--sign")) {
|
|
181
|
+
const sig = sign(json);
|
|
182
|
+
if (sig) writeFileSync(join(ROOT, "manifest.sig"), `${sig}\n`);
|
|
183
|
+
}
|
|
184
|
+
if (has("--json"))
|
|
185
|
+
process.stdout.write(
|
|
186
|
+
`${JSON.stringify({ written: MANIFEST, ...manifest, files: undefined }, null, 2)}\n`,
|
|
187
|
+
);
|
|
188
|
+
else
|
|
189
|
+
// STDERR, not stdout. This runs as `prepack`, and npm passes a prepack
|
|
190
|
+
// script's stdout straight through - so one friendly line here lands INSIDE
|
|
191
|
+
// `npm pack --dry-run --json` and every consumer that parses that JSON
|
|
192
|
+
// fails with "Unexpected token 'm'". Two gates caught it within minutes;
|
|
193
|
+
// a publish would not have.
|
|
194
|
+
process.stderr.write(
|
|
195
|
+
`manifest.json: ${manifest.fileCount} files, version ${manifest.version}, commit ${manifest.commit?.slice(0, 12) ?? "unknown"}\n`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
runMain("make-manifest", main);
|