@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
|
@@ -29,6 +29,54 @@
|
|
|
29
29
|
|
|
30
30
|
set -euo pipefail
|
|
31
31
|
|
|
32
|
+
# jq is not optional on this path. Without the guard below a missing binary
|
|
33
|
+
# renders as EMPTY DATA and the work continues on it; see lib/require-jq.sh.
|
|
34
|
+
for _rq in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/require-jq.sh" \
|
|
35
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/require-jq.sh" \
|
|
36
|
+
"$HOME/.claude/lib/require-jq.sh" \
|
|
37
|
+
"$HOME/.copilot/lib/require-jq.sh" \
|
|
38
|
+
"$HOME/.codex/lib/require-jq.sh"; do
|
|
39
|
+
[ -f "$_rq" ] || continue
|
|
40
|
+
# shellcheck source=/dev/null
|
|
41
|
+
. "$_rq" && break
|
|
42
|
+
done
|
|
43
|
+
unset _rq
|
|
44
|
+
if ! command -v ma_require_jq >/dev/null 2>&1; then
|
|
45
|
+
# The helper itself is missing, which is an install problem, not a jq one.
|
|
46
|
+
ma_require_jq() { command -v jq >/dev/null 2>&1 || { echo "jq not found - cannot ${1:-continue}." >&2; return 1; }; }
|
|
47
|
+
fi
|
|
48
|
+
ma_require_jq "update the issue" || exit 3
|
|
49
|
+
|
|
50
|
+
# Outbound leak gate. Every byte below is composed at runtime out of command
|
|
51
|
+
# output, error text and file excerpts, any of which can carry a token that was
|
|
52
|
+
# on this machine a second earlier - and once it is in a comment it is in
|
|
53
|
+
# someone else's database. The repo's own leak scanner looks at FILES IN THE
|
|
54
|
+
# REPO and never sees this text. See lib/outbound-gate.mjs.
|
|
55
|
+
ma_outbound_gate() {
|
|
56
|
+
local body_file="$1" og=""
|
|
57
|
+
for c in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/outbound-gate.mjs" \
|
|
58
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/outbound-gate.mjs" \
|
|
59
|
+
"$HOME/.claude/lib/outbound-gate.mjs" \
|
|
60
|
+
"$HOME/.copilot/lib/outbound-gate.mjs" \
|
|
61
|
+
"$HOME/.codex/lib/outbound-gate.mjs"; do
|
|
62
|
+
[ -f "$c" ] && { og="$c"; break; }
|
|
63
|
+
done
|
|
64
|
+
# Missing gate is NOT an open door: refusing to publish beats publishing
|
|
65
|
+
# unchecked, and the only way this file is absent is a broken install.
|
|
66
|
+
if [ -z "$og" ]; then
|
|
67
|
+
echo "outbound-gate.mjs not found - refusing to publish unchecked text." >&2
|
|
68
|
+
return 7
|
|
69
|
+
fi
|
|
70
|
+
node "$og" --file "$body_file"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Run-state path resolution: pipeline/lib/run-paths.sh owns the two layouts
|
|
74
|
+
# (nested <root>/<project>/<id>/ and flat <root>/<id>/) and every id spelling.
|
|
75
|
+
_MA_RP_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
76
|
+
# shellcheck source=/dev/null
|
|
77
|
+
. "$_MA_RP_HERE/../lib/run-paths.sh" 2>/dev/null || . "$HOME/.claude/lib/run-paths.sh"
|
|
78
|
+
|
|
79
|
+
|
|
32
80
|
# Rewrite the `### Progress` block of the body file ($2) with $1 and print the
|
|
33
81
|
# result to stdout. The replaced region is bounded at the FIRST of:
|
|
34
82
|
# - a line starting with `<!--` (the legend comment, printed and kept), or
|
|
@@ -90,13 +138,10 @@ fi
|
|
|
90
138
|
# per-project directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/)
|
|
91
139
|
# - not the ~/.claude/projects/*/state/ path this used to glob, which no
|
|
92
140
|
# writer in the pipeline ever populates.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
break
|
|
98
|
-
fi
|
|
99
|
-
done
|
|
141
|
+
# This globbed ONLY the nested layout. phase-tracker.sh and most Phase 0 paths
|
|
142
|
+
# write flat, so on a real machine the majority of runs resolved to nothing and
|
|
143
|
+
# this script reported "no agent-state.json" for a run whose state existed.
|
|
144
|
+
AGENT_STATE="$(ma_resolve_run_file "$TASK_ID" agent-state.json 2>/dev/null || true)"
|
|
100
145
|
|
|
101
146
|
if [ -z "$AGENT_STATE" ]; then
|
|
102
147
|
echo "update-issue-progress: no agent-state.json found for task=$TASK_ID" >&2
|
|
@@ -154,6 +199,10 @@ if cmp -s "$TMP_BODY" "$TMP_NEW"; then
|
|
|
154
199
|
fi
|
|
155
200
|
|
|
156
201
|
# Apply.
|
|
202
|
+
if ! ma_outbound_gate "$TMP_NEW"; then
|
|
203
|
+
echo "update-issue-progress: outbound gate refused the new body; #$ISSUE_NUM left unchanged" >&2
|
|
204
|
+
exit 7
|
|
205
|
+
fi
|
|
157
206
|
if ! gh issue edit "$ISSUE_NUM" --repo "$ORG_REPO" --body-file "$TMP_NEW" >/dev/null; then
|
|
158
207
|
echo "update-issue-progress: gh issue edit failed for #$ISSUE_NUM" >&2
|
|
159
208
|
exit 4
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* usage-register.mjs - ask the reporting endpoint for this machine's token.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS
|
|
6
|
+
*
|
|
7
|
+
* Operational reporting needs two things to work: `usageLog.enabled` true AND a
|
|
8
|
+
* token that resolves. Since v15.8.0 both are arranged automatically - but only
|
|
9
|
+
* inside `/multi-agent:update`, as forty lines of shell embedded in a skill. A
|
|
10
|
+
* new user installs the package, runs `/multi-agent:setup` (which explicitly
|
|
11
|
+
* says registration happens in update), works for weeks, and never registers.
|
|
12
|
+
* Their runs emit nothing, and the panel cannot tell that apart from nobody
|
|
13
|
+
* using the pipeline at all - which is exactly what it looked like.
|
|
14
|
+
*
|
|
15
|
+
* So the registration becomes one deterministic call that three places make:
|
|
16
|
+
* setup (onboarding), update (existing path), and the first run of a machine
|
|
17
|
+
* that reached neither.
|
|
18
|
+
*
|
|
19
|
+
* WHAT IT NEVER DOES
|
|
20
|
+
*
|
|
21
|
+
* - It never ships a secret: the token is REQUESTED, and it is write-only.
|
|
22
|
+
* - It never writes the token to a file. The credential store holds it; prefs
|
|
23
|
+
* hold only the name of the entry.
|
|
24
|
+
* - It never registers when `usageLog.optOut` is true. That is permanent and
|
|
25
|
+
* checked before anything else.
|
|
26
|
+
* - It never fails a caller. Offline, endpoint down, ingest disabled by the
|
|
27
|
+
* admin, no credential store: one status line, exit 0.
|
|
28
|
+
* - It never reports run CONTENT. That is usage-report.mjs's contract, and it
|
|
29
|
+
* is coarse metadata only.
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* node usage-register.mjs [--prefs <path>] [--json] [--quiet] [--dry-run] [--feedback]
|
|
33
|
+
*
|
|
34
|
+
* Exit: always 0. `--json` says what happened; the shell does not need to care.
|
|
35
|
+
*
|
|
36
|
+
* @module pipeline/scripts/usage-register
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
40
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
41
|
+
import { join } from "node:path";
|
|
42
|
+
import { homedir, hostname, userInfo } from "node:os";
|
|
43
|
+
import { pathToFileURL } from "node:url";
|
|
44
|
+
|
|
45
|
+
const DEFAULT_ENDPOINT = "https://mmerterden.com/api/usage/ingest";
|
|
46
|
+
const TIMEOUT_MS = 10000;
|
|
47
|
+
|
|
48
|
+
function readJson(file) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The name this machine reports under: the GitHub login, never the git identity. */
|
|
57
|
+
export function reportingUser(prefs, { env = process.env } = {}) {
|
|
58
|
+
const declared = prefs?.global?.identities?.[0]?.username;
|
|
59
|
+
if (typeof declared === "string" && declared.trim()) return declared.trim();
|
|
60
|
+
try {
|
|
61
|
+
const out = execFileSync("gh", ["api", "user", "--jq", ".login"], {
|
|
62
|
+
encoding: "utf8",
|
|
63
|
+
timeout: 5000,
|
|
64
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
65
|
+
}).trim();
|
|
66
|
+
if (out) return out;
|
|
67
|
+
} catch {
|
|
68
|
+
/* gh is optional */
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return env.USER || userInfo().username || "unknown";
|
|
72
|
+
} catch {
|
|
73
|
+
return "unknown";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Should this machine register at all?
|
|
79
|
+
*
|
|
80
|
+
* The three "no" answers are deliberately distinguishable: the caller prints
|
|
81
|
+
* them, and "you opted out" and "we could not reach the endpoint" are different
|
|
82
|
+
* facts about the same silence.
|
|
83
|
+
*/
|
|
84
|
+
export function decideRegistration(
|
|
85
|
+
prefs,
|
|
86
|
+
{ env = process.env, hasStoredToken = false, forFeedback = false } = {},
|
|
87
|
+
) {
|
|
88
|
+
const g = prefs?.global || {};
|
|
89
|
+
const u = g.usageLog || {};
|
|
90
|
+
// `optOut` is a choice about PASSIVE collection. /multi-agent:feedback is the
|
|
91
|
+
// opposite: somebody typed a message meant to be read, and it rides the same
|
|
92
|
+
// token. So a feedback run may mint one - and it must NOT switch telemetry on
|
|
93
|
+
// when it does, which is what `enableTelemetry` below is for.
|
|
94
|
+
if (u.optOut === true && !forFeedback) return { act: false, reason: "opted out" };
|
|
95
|
+
if (env.MULTI_AGENT_USAGE_TOKEN) return { act: false, reason: "token in environment" };
|
|
96
|
+
if (typeof u.token === "string" && u.token.trim())
|
|
97
|
+
return { act: false, reason: "token in prefs" };
|
|
98
|
+
if (hasStoredToken) return { act: false, reason: "token already onboarded" };
|
|
99
|
+
return { act: true, reason: "no token resolves" };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The /register URL derived from whatever endpoint the prefs name. */
|
|
103
|
+
export function registerUrl(endpoint) {
|
|
104
|
+
const ep = (endpoint || DEFAULT_ENDPOINT).trim().replace(/\/+$/, "");
|
|
105
|
+
return ep.endsWith("/ingest") ? `${ep.slice(0, -"/ingest".length)}/register` : `${ep}/register`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function storedToken(prefs, credStore) {
|
|
109
|
+
const name = prefs?.global?.keychainMapping?.usage_ingest;
|
|
110
|
+
if (!name || !existsSync(credStore)) return null;
|
|
111
|
+
const r = spawnSync("bash", [credStore, "get", name], { encoding: "utf8", timeout: 8000 });
|
|
112
|
+
const out = (r.stdout || "").trim();
|
|
113
|
+
return out || null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function requestToken(url, body) {
|
|
117
|
+
const ctrl = new AbortController();
|
|
118
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetch(url, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { "Content-Type": "application/json" },
|
|
123
|
+
body: JSON.stringify(body),
|
|
124
|
+
signal: ctrl.signal,
|
|
125
|
+
});
|
|
126
|
+
if (!res.ok) return { token: null, why: `endpoint answered ${res.status}` };
|
|
127
|
+
const json = await res.json().catch(() => null);
|
|
128
|
+
const token = typeof json?.token === "string" ? json.token.trim() : "";
|
|
129
|
+
return token ? { token, why: null } : { token: null, why: "endpoint returned no token" };
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return {
|
|
132
|
+
token: null,
|
|
133
|
+
why: e?.name === "AbortError" ? "endpoint timed out" : "endpoint unreachable",
|
|
134
|
+
};
|
|
135
|
+
} finally {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Write the switch, and the credential-store ENTRY NAME when there is one.
|
|
142
|
+
*
|
|
143
|
+
* `entryName` is optional on purpose: a token can resolve from the environment
|
|
144
|
+
* or from `usageLog.token`, and those machines still need the switch flipped.
|
|
145
|
+
* Claiming "enabled" while writing nothing is the failure this whole feature is
|
|
146
|
+
* about, one level smaller.
|
|
147
|
+
*
|
|
148
|
+
* The token itself is never written here, whatever the caller passes.
|
|
149
|
+
*/
|
|
150
|
+
export function persistPrefs(prefsPath, entryName = null, { enableTelemetry = true } = {}) {
|
|
151
|
+
const j = readJson(prefsPath);
|
|
152
|
+
if (!j) return false;
|
|
153
|
+
j.global = j.global || {};
|
|
154
|
+
if (entryName) {
|
|
155
|
+
j.global.keychainMapping = j.global.keychainMapping || {};
|
|
156
|
+
j.global.keychainMapping.usage_ingest = entryName;
|
|
157
|
+
}
|
|
158
|
+
j.global.usageLog = j.global.usageLog || {};
|
|
159
|
+
if (enableTelemetry) j.global.usageLog.enabled = true;
|
|
160
|
+
writeFileSync(prefsPath, `${JSON.stringify(j, null, 2)}\n`);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function main(argv) {
|
|
165
|
+
const opts = { quiet: false, json: false, dryRun: false, prefs: null, feedback: false };
|
|
166
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
167
|
+
const a = argv[i];
|
|
168
|
+
if (a === "--quiet") opts.quiet = true;
|
|
169
|
+
else if (a === "--json") opts.json = true;
|
|
170
|
+
else if (a === "--dry-run") opts.dryRun = true;
|
|
171
|
+
else if (a === "--feedback") opts.feedback = true;
|
|
172
|
+
else if (a === "--prefs") opts.prefs = argv[++i];
|
|
173
|
+
}
|
|
174
|
+
const HOME = homedir();
|
|
175
|
+
const prefsPath = opts.prefs || join(HOME, ".claude", "multi-agent-preferences.json");
|
|
176
|
+
const credStore = join(HOME, ".claude", "lib", "credential-store.sh");
|
|
177
|
+
|
|
178
|
+
const say = (line) => {
|
|
179
|
+
if (!opts.quiet) process.stdout.write(`${line}\n`);
|
|
180
|
+
};
|
|
181
|
+
const done = (status, detail) => {
|
|
182
|
+
if (opts.json) process.stdout.write(`${JSON.stringify({ status, detail })}\n`);
|
|
183
|
+
return 0;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const prefs = readJson(prefsPath);
|
|
187
|
+
if (!prefs) {
|
|
188
|
+
// Setup writes this file; nothing here creates it. An installer that seeds
|
|
189
|
+
// state is the one thing the install contract forbids.
|
|
190
|
+
say(" -> operational reporting: no preferences file yet (run /multi-agent:setup)");
|
|
191
|
+
return done("skipped", "no prefs");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const has = Boolean(storedToken(prefs, credStore));
|
|
195
|
+
const decision = decideRegistration(prefs, { hasStoredToken: has, forFeedback: opts.feedback });
|
|
196
|
+
if (!decision.act) {
|
|
197
|
+
if (decision.reason !== "opted out") {
|
|
198
|
+
// Already has a token: make sure the switch is actually on. A token with
|
|
199
|
+
// enabled:false is the other half of the same silence.
|
|
200
|
+
if (prefs.global?.usageLog?.enabled !== true && !opts.dryRun) {
|
|
201
|
+
const name = prefs.global?.keychainMapping?.usage_ingest || null;
|
|
202
|
+
if (persistPrefs(prefsPath, name)) {
|
|
203
|
+
say(" -> operational reporting: enabled (a token was already onboarded)");
|
|
204
|
+
return done("enabled", decision.reason);
|
|
205
|
+
}
|
|
206
|
+
say(" -> operational reporting: could not write preferences");
|
|
207
|
+
return done("unavailable", "prefs write failed");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
say(` -> operational reporting: unchanged (${decision.reason})`);
|
|
211
|
+
return done("skipped", decision.reason);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const url = registerUrl(prefs.global?.usageLog?.endpoint);
|
|
215
|
+
const user = reportingUser(prefs);
|
|
216
|
+
let host = "unknown";
|
|
217
|
+
try {
|
|
218
|
+
host = hostname().split(".")[0] || "unknown";
|
|
219
|
+
} catch {
|
|
220
|
+
/* a nameless host is still a host */
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (opts.dryRun) {
|
|
224
|
+
say(` -> operational reporting: would register ${user} with ${url}`);
|
|
225
|
+
return done("dry-run", url);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const { token, why } = await requestToken(url, { u: user, c: host });
|
|
229
|
+
if (!token) {
|
|
230
|
+
say(` -> operational reporting left off (${why})`);
|
|
231
|
+
return done("unavailable", why);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const optedOut = prefs.global?.usageLog?.optOut === true;
|
|
235
|
+
const entry = `${process.env.USER || user}_Usage_Ingest_Token`;
|
|
236
|
+
const set = spawnSync("bash", [credStore, "set", entry], {
|
|
237
|
+
input: token,
|
|
238
|
+
encoding: "utf8",
|
|
239
|
+
timeout: 10000,
|
|
240
|
+
});
|
|
241
|
+
if (set.status !== 0) {
|
|
242
|
+
say(" -> operational reporting left off (the credential store refused the token)");
|
|
243
|
+
return done("unavailable", "credential store write failed");
|
|
244
|
+
}
|
|
245
|
+
persistPrefs(prefsPath, entry, { enableTelemetry: !optedOut });
|
|
246
|
+
if (optedOut) {
|
|
247
|
+
say(" -> feedback token registered; telemetry stays off (usageLog.optOut is set)");
|
|
248
|
+
return done("registered-feedback-only", entry);
|
|
249
|
+
}
|
|
250
|
+
say(
|
|
251
|
+
" -> operational reporting: registered this machine (write-only token in the credential store)",
|
|
252
|
+
);
|
|
253
|
+
say(" opt out any time: set global.usageLog.optOut=true in multi-agent-preferences.json");
|
|
254
|
+
return done("registered", entry);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function isEntrypoint() {
|
|
258
|
+
const arg = process.argv[1];
|
|
259
|
+
if (!arg) return false;
|
|
260
|
+
try {
|
|
261
|
+
return import.meta.url === pathToFileURL(arg).href;
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (isEntrypoint()) {
|
|
268
|
+
main(process.argv.slice(2)).then((code) => {
|
|
269
|
+
process.exitCode = code;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Enable + configure via prefs.global.usageLog:
|
|
11
11
|
* { "enabled": true,
|
|
12
|
-
* "endpoint": "https://mmerterden.
|
|
12
|
+
* "endpoint": "https://mmerterden.com/api/usage/ingest",
|
|
13
13
|
* "token": "<shared ingest token>" } // token may instead come from
|
|
14
14
|
* // env MULTI_AGENT_USAGE_TOKEN
|
|
15
15
|
* Nothing is sent when enabled is not true or no token resolves.
|
|
@@ -30,7 +30,7 @@ import { createHash } from "crypto";
|
|
|
30
30
|
import { costUsd } from "./_cost.mjs";
|
|
31
31
|
|
|
32
32
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
|
-
const ENDPOINT_DEFAULT = "https://mmerterden.
|
|
33
|
+
const ENDPOINT_DEFAULT = "https://mmerterden.com/api/usage/ingest";
|
|
34
34
|
const TIMEOUT_MS = 2500;
|
|
35
35
|
|
|
36
36
|
// The emitter ships verbatim into ~/.claude, ~/.copilot and ~/.codex, so it must
|
|
@@ -609,7 +609,18 @@ async function main() {
|
|
|
609
609
|
// Windows-safe entry-point check: compare file URLs, never string-split a path.
|
|
610
610
|
const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
611
611
|
if (invokedDirectly) {
|
|
612
|
-
|
|
612
|
+
// Exit 0 stays: usage telemetry must never be the reason a run fails, and a
|
|
613
|
+
// non-zero exit here would propagate into whatever called it.
|
|
614
|
+
//
|
|
615
|
+
// But the silence goes. `catch(() => process.exit(0))` reported success for
|
|
616
|
+
// every failure, so a telemetry path that had been broken for weeks looked
|
|
617
|
+
// exactly like one that worked - there was no surface on which anyone could
|
|
618
|
+
// notice. One line on stderr costs nothing and is the difference between a
|
|
619
|
+
// degraded feature and an invisible one.
|
|
620
|
+
main().catch((err) => {
|
|
621
|
+
process.stderr.write(`usage-report: not sent - ${err?.message ?? err}\n`);
|
|
622
|
+
process.exit(0);
|
|
623
|
+
});
|
|
613
624
|
}
|
|
614
625
|
|
|
615
626
|
export {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
// Exit: 0 valid, 1 invalid (or WARN under --strict), 64 usage error.
|
|
42
42
|
|
|
43
43
|
import { readFileSync } from "node:fs";
|
|
44
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
44
45
|
|
|
45
46
|
// A repo-less run (Locked 35) still splits by channel, derived from the evidence:
|
|
46
47
|
// "mobile" and "web" are its platform values. "none" is the narrower case where the
|
|
@@ -1019,4 +1020,4 @@ function main() {
|
|
|
1019
1020
|
console.log(`validate-analysis-doc: OK (mode=${mode}, ${warns.length} warning(s))`);
|
|
1020
1021
|
}
|
|
1021
1022
|
|
|
1022
|
-
main
|
|
1023
|
+
runMain("validate-analysis-doc", main);
|
|
@@ -162,13 +162,16 @@ export function validate(obj) {
|
|
|
162
162
|
obj = JSON.parse(raw);
|
|
163
163
|
} catch (e) {
|
|
164
164
|
console.log(JSON.stringify({ ok: false, errors: [`JSON parse error: ${e.message}`] }));
|
|
165
|
-
process.
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
return;
|
|
166
167
|
}
|
|
167
168
|
const errors = validate(obj);
|
|
168
169
|
if (errors.length === 0) {
|
|
169
170
|
console.log(JSON.stringify({ ok: true, errors: [] }));
|
|
170
|
-
|
|
171
|
+
return;
|
|
171
172
|
}
|
|
173
|
+
// `errors` is one entry per violation and has no bound, so process.exit(1)
|
|
174
|
+
// here truncated the report exactly when there was most of it to read.
|
|
172
175
|
console.log(JSON.stringify({ ok: false, errors }));
|
|
173
|
-
process.
|
|
176
|
+
process.exitCode = 1;
|
|
174
177
|
})();
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
// Exit: 0 valid, 1 invalid (or WARN under --strict), 64 usage error.
|
|
41
41
|
|
|
42
42
|
import { readFileSync } from "node:fs";
|
|
43
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
43
44
|
|
|
44
45
|
const REQUIRED_FM = ["run_name", "generated_at", "language", "complaint_count", "graylog_degraded"];
|
|
45
46
|
|
|
@@ -263,4 +264,4 @@ function main() {
|
|
|
263
264
|
process.exit(0);
|
|
264
265
|
}
|
|
265
266
|
|
|
266
|
-
main
|
|
267
|
+
runMain("validate-complaint-doc", main);
|
|
@@ -120,13 +120,16 @@ function validate(obj) {
|
|
|
120
120
|
obj = JSON.parse(raw);
|
|
121
121
|
} catch (e) {
|
|
122
122
|
console.log(JSON.stringify({ ok: false, errors: [`JSON parse error: ${e.message}`] }));
|
|
123
|
-
process.
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
return;
|
|
124
125
|
}
|
|
125
126
|
const errors = validate(obj);
|
|
126
127
|
if (errors.length === 0) {
|
|
127
128
|
console.log(JSON.stringify({ ok: true, errors: [] }));
|
|
128
|
-
|
|
129
|
+
return;
|
|
129
130
|
}
|
|
131
|
+
// `errors` is one entry per violation and has no bound, so process.exit(1)
|
|
132
|
+
// here truncated the report exactly when there was most of it to read.
|
|
130
133
|
console.log(JSON.stringify({ ok: false, errors }));
|
|
131
|
-
process.
|
|
134
|
+
process.exitCode = 1;
|
|
132
135
|
})();
|
|
@@ -88,13 +88,16 @@ function validate(obj) {
|
|
|
88
88
|
obj = JSON.parse(raw);
|
|
89
89
|
} catch (e) {
|
|
90
90
|
console.log(JSON.stringify({ ok: false, errors: [`JSON parse error: ${e.message}`] }));
|
|
91
|
-
process.
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return;
|
|
92
93
|
}
|
|
93
94
|
const errors = validate(obj);
|
|
94
95
|
if (errors.length === 0) {
|
|
95
96
|
console.log(JSON.stringify({ ok: true, errors: [] }));
|
|
96
|
-
|
|
97
|
+
return;
|
|
97
98
|
}
|
|
99
|
+
// `errors` is one entry per violation and has no bound, so process.exit(1)
|
|
100
|
+
// here truncated the report exactly when there was most of it to read.
|
|
98
101
|
console.log(JSON.stringify({ ok: false, errors }));
|
|
99
|
-
process.
|
|
102
|
+
process.exitCode = 1;
|
|
100
103
|
})();
|
|
@@ -292,4 +292,6 @@ function validate(raw) {
|
|
|
292
292
|
const raw = await readInput();
|
|
293
293
|
const result = validate(raw);
|
|
294
294
|
console.log(JSON.stringify(result, null, 2));
|
|
295
|
-
process.exit(
|
|
295
|
+
// Assigned rather than called: process.exit() would cut this payload short when
|
|
296
|
+
// stdout is a pipe, which is how every caller reads it.
|
|
297
|
+
process.exitCode = result.ok ? result.code : 1;
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
46
46
|
import { resolve as resolvePath, sep } from "node:path";
|
|
47
47
|
import { spawnSync } from "node:child_process";
|
|
48
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
49
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
52
|
* Reject a path before it reaches git.
|
|
@@ -423,6 +425,6 @@ function main() {
|
|
|
423
425
|
process.exit(report.unresolved.length === 0 ? 0 : 1);
|
|
424
426
|
}
|
|
425
427
|
|
|
426
|
-
if (import.meta.url
|
|
427
|
-
main
|
|
428
|
+
if (invokedDirectly(import.meta.url)) {
|
|
429
|
+
runMain("verify-citations", main);
|
|
428
430
|
}
|