@mmerterden/multi-agent-pipeline 15.0.0 → 15.2.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 +127 -0
- package/README.md +9 -9
- package/README.tr.md +9 -9
- package/SECURITY.md +43 -0
- package/docs/adr/0007-multi-tool-adapter-framework.md +1 -1
- package/docs/architecture.md +9 -9
- package/docs/ecosystem.md +10 -10
- package/index.js +4 -1
- package/install/_common.mjs +44 -2
- package/install/_platform-filter.mjs +6 -131
- package/install/_plugin-skills.mjs +17 -28
- package/install/claude.mjs +111 -6
- package/install/codex.mjs +0 -3
- package/install/copilot.mjs +36 -1
- package/install/index.mjs +3 -1
- package/package.json +2 -2
- package/pipeline/commands/multi-agent/refactor/SKILL.md +36 -1
- package/pipeline/commands/multi-agent/scan/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/stack/SKILL.md +18 -8
- package/pipeline/commands/multi-agent/update/SKILL.md +31 -4
- package/pipeline/lib/parse-complaints.sh +12 -2
- package/pipeline/multi-agent-refs/complaint-analysis-template.md +1 -1
- package/pipeline/multi-agent-refs/phases/operations.md +7 -1
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +6 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +2 -1
- package/pipeline/preferences-template.json +6 -0
- package/pipeline/schemas/prefs.schema.json +27 -0
- package/pipeline/scripts/README.md +4 -3
- package/pipeline/scripts/check-derived-drift.mjs +5 -2
- package/pipeline/scripts/match-skills.mjs +4 -0
- package/pipeline/scripts/migrate-prefs.mjs +5 -1
- package/pipeline/scripts/phase-tracker.sh +19 -0
- package/pipeline/scripts/uninstall.mjs +3 -1
- package/pipeline/scripts/usage-report.mjs +457 -0
- package/pipeline/scripts/validate-complaint-doc.mjs +32 -11
- package/pipeline/skills/.skill-manifest.json +4 -4
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +153 -90
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +18 -8
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +1 -1
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usage-report.mjs - usage ping (opt-in) for the private mission-control dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Emits one compact activity event keyed by run id (`taskId`). Called at each
|
|
5
|
+
* phase boundary (status=running, via phase-tracker.sh) and once at the end
|
|
6
|
+
* (terminal status, from Phase 7 report or the halt path). The server upserts
|
|
7
|
+
* by run id, so per-phase calls update ONE record - it always reflects the last
|
|
8
|
+
* phase a run reached, including runs that halt before Phase 7. Fire-and-forget,
|
|
9
|
+
* never throws, never delays the run.
|
|
10
|
+
*
|
|
11
|
+
* Payload is activity metadata only - who, command, mode, input type, repo,
|
|
12
|
+
* phase, outcome, halt cause, review iterations, duration, token spend, cost,
|
|
13
|
+
* per-phase breakdown, language, stack, plugins, integrations, credential
|
|
14
|
+
* health, invoked skills, error tags, version. No prompts, code, diffs, token
|
|
15
|
+
* values, or absolute paths.
|
|
16
|
+
*
|
|
17
|
+
* Enable + configure via prefs.global.usageLog:
|
|
18
|
+
* { "enabled": true,
|
|
19
|
+
* "endpoint": "https://mmerterden.vercel.app/api/usage/ingest",
|
|
20
|
+
* "token": "<shared ingest token>" } // token may instead come from
|
|
21
|
+
* // env MULTI_AGENT_USAGE_TOKEN
|
|
22
|
+
* Nothing is sent when enabled is not true or no token resolves.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* node usage-report.mjs --state /path/to/agent-state.json
|
|
26
|
+
* node usage-report.mjs --task-id PROJ-123 # resolves the state path
|
|
27
|
+
*
|
|
28
|
+
* @module pipeline/scripts/usage-report
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { readFileSync, existsSync, readdirSync, appendFileSync } from "fs";
|
|
32
|
+
import { homedir, platform } from "os";
|
|
33
|
+
import { dirname, join } from "path";
|
|
34
|
+
import { fileURLToPath } from "url";
|
|
35
|
+
import { execFileSync } from "child_process";
|
|
36
|
+
import { costUsd } from "./_cost.mjs";
|
|
37
|
+
|
|
38
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const ENDPOINT_DEFAULT = "https://mmerterden.vercel.app/api/usage/ingest";
|
|
40
|
+
const TIMEOUT_MS = 2500;
|
|
41
|
+
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const out = { state: null, taskId: null, phase: null, status: null, dryRun: false };
|
|
44
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
45
|
+
const a = argv[i];
|
|
46
|
+
if (a === "--state") out.state = argv[++i];
|
|
47
|
+
else if (a === "--task-id") out.taskId = argv[++i];
|
|
48
|
+
else if (a === "--phase") out.phase = argv[++i];
|
|
49
|
+
else if (a === "--status") out.status = argv[++i];
|
|
50
|
+
else if (a === "--dry-run") out.dryRun = true;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readJson(path) {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveGlobalPrefs() {
|
|
64
|
+
const path =
|
|
65
|
+
process.env.MULTI_AGENT_PREFS ||
|
|
66
|
+
join(homedir(), ".claude", "multi-agent-preferences.json");
|
|
67
|
+
return readJson(path)?.global ?? {};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolvePrefs() {
|
|
71
|
+
return resolveGlobalPrefs().usageLog ?? {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the ingest token without ever storing it in a synced file.
|
|
76
|
+
* Order: env, then prefs.usageLog.token, then the Keychain entry mapped by
|
|
77
|
+
* prefs.global.keychainMapping.usage_ingest (via credential-store.sh). This
|
|
78
|
+
* lets update flip usageLog.enabled on while the secret stays in the Keychain.
|
|
79
|
+
*/
|
|
80
|
+
function resolveToken() {
|
|
81
|
+
if (process.env.MULTI_AGENT_USAGE_TOKEN) return process.env.MULTI_AGENT_USAGE_TOKEN;
|
|
82
|
+
const g = resolveGlobalPrefs();
|
|
83
|
+
const inline = g.usageLog?.token;
|
|
84
|
+
if (typeof inline === "string" && inline.trim()) return inline.trim();
|
|
85
|
+
const name = g.keychainMapping?.usage_ingest;
|
|
86
|
+
if (typeof name === "string" && name.trim()) {
|
|
87
|
+
const store = join(homedir(), ".claude", "lib", "credential-store.sh");
|
|
88
|
+
if (existsSync(store)) {
|
|
89
|
+
try {
|
|
90
|
+
const out = execFileSync("bash", [store, "get", name.trim()], {
|
|
91
|
+
encoding: "utf-8",
|
|
92
|
+
timeout: 4000,
|
|
93
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
94
|
+
}).trim();
|
|
95
|
+
if (out) return out;
|
|
96
|
+
} catch {
|
|
97
|
+
/* keychain unavailable - stay silent */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function enabledPlugins(state) {
|
|
105
|
+
const root = state.projectRoot || process.cwd();
|
|
106
|
+
const settings = readJson(join(root, ".claude", "settings.json"));
|
|
107
|
+
const raw = settings?.enabledPlugins ?? settings?.plugins ?? [];
|
|
108
|
+
const list = Array.isArray(raw) ? raw : Object.keys(raw || {});
|
|
109
|
+
return list
|
|
110
|
+
.map((p) => (typeof p === "string" ? p.split("@")[0] : null))
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.slice(0, 20);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function integrationTags(state) {
|
|
116
|
+
const apps = new Set();
|
|
117
|
+
if (state.jiraId) apps.add("jira");
|
|
118
|
+
if (state.remoteType) apps.add(String(state.remoteType).toLowerCase());
|
|
119
|
+
if (state.githubIssue) apps.add("github");
|
|
120
|
+
if (state.confluenceSpace) apps.add("confluence");
|
|
121
|
+
if (state.figmaAccess?.tier) apps.add("figma");
|
|
122
|
+
for (const call of state.telemetry?.mcpCalls ?? []) {
|
|
123
|
+
const tool = String(call?.tool || "");
|
|
124
|
+
const m = tool.match(/^mcp__([a-z0-9_]+?)__/i) || tool.match(/^mcp__([a-z0-9_]+)/i);
|
|
125
|
+
if (m) apps.add(m[1].replace(/_/g, "-").toLowerCase());
|
|
126
|
+
}
|
|
127
|
+
return Array.from(apps).slice(0, 20);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function credHealth() {
|
|
131
|
+
const g = resolveGlobalPrefs();
|
|
132
|
+
const mapping = g.keychainMapping ?? {};
|
|
133
|
+
const on = Object.entries(mapping)
|
|
134
|
+
.filter(([, v]) => typeof v === "string" && v.trim())
|
|
135
|
+
.map(([k]) => k)
|
|
136
|
+
.slice(0, 40);
|
|
137
|
+
|
|
138
|
+
const svc = {};
|
|
139
|
+
for (const [name, st] of Object.entries(g.serviceStatus ?? {})) {
|
|
140
|
+
const err = String(st?.error || "").toLowerCase();
|
|
141
|
+
const expired = /401|403|expired|unauthor|revoked|invalid.token/.test(err);
|
|
142
|
+
svc[name] = {
|
|
143
|
+
s: st?.ok ? "ok" : expired ? "expired" : "fail",
|
|
144
|
+
at: st?.checkedAt || null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { on, svc };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function invokedCalls(state) {
|
|
151
|
+
const names = new Set();
|
|
152
|
+
for (const c of state.telemetry?.skillCalls ?? []) {
|
|
153
|
+
if (c?.skill) names.add(String(c.skill));
|
|
154
|
+
}
|
|
155
|
+
return Array.from(names).slice(0, 30);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function detectOs() {
|
|
159
|
+
const p = platform();
|
|
160
|
+
if (p === "darwin") return "macos";
|
|
161
|
+
if (p === "win32") return "windows";
|
|
162
|
+
if (p === "linux") return "linux";
|
|
163
|
+
return p;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function detectCli() {
|
|
167
|
+
const e = process.env;
|
|
168
|
+
if (e.MULTI_AGENT_CLI) return String(e.MULTI_AGENT_CLI).toLowerCase();
|
|
169
|
+
if (e.CLAUDECODE || e.CLAUDE_CODE_ENTRYPOINT || e.ANTHROPIC_CLI) return "claude";
|
|
170
|
+
if (e.CODEX_HOME || e.CODEX_SANDBOX || e.CODEX_CLI) return "codex";
|
|
171
|
+
if (e.COPILOT_CLI || e.GITHUB_COPILOT_CLI || e.GH_COPILOT || e.COPILOT_AGENT_ID) {
|
|
172
|
+
return "copilot";
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function deriveContext(state) {
|
|
178
|
+
return {
|
|
179
|
+
lang: resolveGlobalPrefs().outputLanguage ?? null,
|
|
180
|
+
os: detectOs(),
|
|
181
|
+
cli: detectCli(),
|
|
182
|
+
stack: Array.isArray(state.stacks)
|
|
183
|
+
? state.stacks.slice(0, 10)
|
|
184
|
+
: state.stack
|
|
185
|
+
? [String(state.stack)]
|
|
186
|
+
: [],
|
|
187
|
+
plug: enabledPlugins(state),
|
|
188
|
+
apps: integrationTags(state),
|
|
189
|
+
creds: credHealth(),
|
|
190
|
+
calls: invokedCalls(state),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function resolveStatePath({ state, taskId }) {
|
|
195
|
+
if (state) return state;
|
|
196
|
+
if (process.env.MULTI_AGENT_STATE_FILE && existsSync(process.env.MULTI_AGENT_STATE_FILE)) {
|
|
197
|
+
return process.env.MULTI_AGENT_STATE_FILE;
|
|
198
|
+
}
|
|
199
|
+
if (!taskId) return null;
|
|
200
|
+
|
|
201
|
+
const logRoot = join(homedir(), ".claude", "logs", "multi-agent");
|
|
202
|
+
const direct = [
|
|
203
|
+
join(logRoot, taskId, "agent-state.json"),
|
|
204
|
+
join(process.cwd(), ".worktrees", taskId, "agent-state.json"),
|
|
205
|
+
join(process.cwd(), "agent-state.json"),
|
|
206
|
+
];
|
|
207
|
+
const hit = direct.find((p) => existsSync(p));
|
|
208
|
+
if (hit) return hit;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
for (const project of readdirSync(logRoot)) {
|
|
212
|
+
const p = join(logRoot, project, taskId, "agent-state.json");
|
|
213
|
+
if (existsSync(p)) return p;
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
/* logs dir missing */
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function packageVersion() {
|
|
222
|
+
const candidates = [
|
|
223
|
+
join(__dirname, "..", "..", "package.json"),
|
|
224
|
+
join(homedir(), ".claude", "multi-agent-pipeline", "package.json"),
|
|
225
|
+
];
|
|
226
|
+
for (const p of candidates) {
|
|
227
|
+
const v = readJson(p)?.version;
|
|
228
|
+
if (v) return v;
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function inputType(state) {
|
|
234
|
+
if (state.jiraId) return "jira";
|
|
235
|
+
if (state.githubIssue) return "github-issue";
|
|
236
|
+
return "free-text";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function commandOf(state) {
|
|
240
|
+
if (state.mode) return state.mode;
|
|
241
|
+
if (state.onlyDevelop) return "dev";
|
|
242
|
+
if (state.localMode) return "local";
|
|
243
|
+
return "full";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function resolveTracker(state) {
|
|
247
|
+
const id = state.taskId || state.shortId;
|
|
248
|
+
if (id == null) return null;
|
|
249
|
+
const root = join(homedir(), ".claude", "logs", "multi-agent");
|
|
250
|
+
const candidates = [
|
|
251
|
+
join(root, String(id), "tracker-state.json"),
|
|
252
|
+
state.artifactsPath ? join(state.artifactsPath, "tracker-state.json") : null,
|
|
253
|
+
].filter(Boolean);
|
|
254
|
+
return candidates.find((p) => existsSync(p)) ?? null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function phaseDurationSec(p) {
|
|
258
|
+
if (Number.isFinite(Number(p?.duration_ms))) {
|
|
259
|
+
return Math.round(Number(p.duration_ms) / 1000);
|
|
260
|
+
}
|
|
261
|
+
const s = p?.startedAt || p?.started_at || p?.start;
|
|
262
|
+
const f = p?.finishedAt || p?.finished_at || p?.completed_at || p?.end;
|
|
263
|
+
const sm = s ? Date.parse(s) : NaN;
|
|
264
|
+
const fm = f ? Date.parse(f) : NaN;
|
|
265
|
+
if (Number.isNaN(sm) || Number.isNaN(fm) || fm < sm) return null;
|
|
266
|
+
return Math.round((fm - sm) / 1000);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function trackerSummary(state) {
|
|
270
|
+
const path = resolveTracker(state);
|
|
271
|
+
if (!path) return { tk: null, cost: null, phs: null, failed: [], models: [] };
|
|
272
|
+
const tracker = readJson(path);
|
|
273
|
+
if (!tracker?.phases) return { tk: null, cost: null, phs: null, failed: [], models: [] };
|
|
274
|
+
|
|
275
|
+
let tin = 0;
|
|
276
|
+
let tout = 0;
|
|
277
|
+
let tcache = 0;
|
|
278
|
+
const phs = [];
|
|
279
|
+
const failed = [];
|
|
280
|
+
const models = new Set();
|
|
281
|
+
for (const [id, p] of Object.entries(tracker.phases)) {
|
|
282
|
+
if (p?.status === "failed") failed.push(id);
|
|
283
|
+
if (p?.model) models.add(String(p.model));
|
|
284
|
+
const pin = Number(p?.tokens_in || 0);
|
|
285
|
+
const pout = Number(p?.tokens_out || 0);
|
|
286
|
+
const pcache = Number(p?.tokens_cached || 0);
|
|
287
|
+
tin += pin;
|
|
288
|
+
tout += pout;
|
|
289
|
+
tcache += pcache;
|
|
290
|
+
phs.push({
|
|
291
|
+
p: Number.isNaN(Number(id)) ? id : Number(id),
|
|
292
|
+
st: p?.status || null,
|
|
293
|
+
du: phaseDurationSec(p),
|
|
294
|
+
tk: pin + pout + pcache || null,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
phs.sort((a, b) => Number(a.p) - Number(b.p));
|
|
298
|
+
|
|
299
|
+
const total = tin + tout + tcache;
|
|
300
|
+
const table = readJson(join(__dirname, "cost-table.json"));
|
|
301
|
+
const rate = table?.prices?.opus;
|
|
302
|
+
const cost =
|
|
303
|
+
total > 0 && rate ? Number(costUsd(rate, tin, tout, tcache).toFixed(4)) : null;
|
|
304
|
+
return {
|
|
305
|
+
tk: total || null,
|
|
306
|
+
cost,
|
|
307
|
+
phs: phs.length ? phs : null,
|
|
308
|
+
failed,
|
|
309
|
+
models: Array.from(models).slice(0, 10),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function deriveErrors(state, failedPhases) {
|
|
314
|
+
const errs = new Set();
|
|
315
|
+
if (state.haltReason) errs.add(String(state.haltReason));
|
|
316
|
+
for (const id of failedPhases ?? []) errs.add(`phase-${id}-failed`);
|
|
317
|
+
const s = String(state.status || "").toLowerCase();
|
|
318
|
+
if (s === "failed") errs.add("run-failed");
|
|
319
|
+
return Array.from(errs).slice(0, 20);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function durationSec(state) {
|
|
323
|
+
const s = state.startedAt ? Date.parse(state.startedAt) : NaN;
|
|
324
|
+
const f = state.finishedAt ? Date.parse(state.finishedAt) : NaN;
|
|
325
|
+
if (Number.isNaN(s) || Number.isNaN(f) || f < s) return null;
|
|
326
|
+
return Math.round((f - s) / 1000);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function runId(state) {
|
|
330
|
+
const base = state.taskId ?? state.shortId;
|
|
331
|
+
return base == null ? null : String(base);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function eventTimestamp(state) {
|
|
335
|
+
return state.finishedAt || state.updatedAt || new Date().toISOString();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const TERMINAL_STATUSES = ["complete", "completed", "failed", "paused", "halted", "error"];
|
|
339
|
+
|
|
340
|
+
function isTerminalStatus(state) {
|
|
341
|
+
if (state.haltReason) return true;
|
|
342
|
+
return TERMINAL_STATUSES.includes(String(state.status || "").toLowerCase());
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function appendErrorLedger(event) {
|
|
346
|
+
if (!event.errs || event.errs.length === 0) return;
|
|
347
|
+
const path = join(homedir(), ".claude", "logs", "multi-agent", "errors-ledger.jsonl");
|
|
348
|
+
const row = {
|
|
349
|
+
t: event.t,
|
|
350
|
+
id: event.id,
|
|
351
|
+
u: event.u,
|
|
352
|
+
c: event.c,
|
|
353
|
+
rp: event.rp,
|
|
354
|
+
ph: event.ph,
|
|
355
|
+
v: event.v,
|
|
356
|
+
errs: event.errs,
|
|
357
|
+
};
|
|
358
|
+
try {
|
|
359
|
+
appendFileSync(path, JSON.stringify(row) + "\n");
|
|
360
|
+
} catch {
|
|
361
|
+
/* logs dir missing or unwritable - never break the run */
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function buildEvent(state) {
|
|
366
|
+
const spend = trackerSummary(state);
|
|
367
|
+
const ctx = deriveContext(state);
|
|
368
|
+
return {
|
|
369
|
+
id: runId(state),
|
|
370
|
+
t: eventTimestamp(state),
|
|
371
|
+
u: state.identity?.username || null,
|
|
372
|
+
c: commandOf(state),
|
|
373
|
+
m: state.mode || null,
|
|
374
|
+
ap: Boolean(state.autopilot),
|
|
375
|
+
lo: Boolean(state.localMode),
|
|
376
|
+
dev: Boolean(state.onlyDevelop),
|
|
377
|
+
in: inputType(state),
|
|
378
|
+
rp: state.project || null,
|
|
379
|
+
ph: typeof state.currentPhase === "number" ? state.currentPhase : null,
|
|
380
|
+
st: state.status || null,
|
|
381
|
+
hr: state.haltReason || null,
|
|
382
|
+
ri: Array.isArray(state.reviewIterations)
|
|
383
|
+
? state.reviewIterations.length
|
|
384
|
+
: 0,
|
|
385
|
+
pr: Boolean(state.pr && (state.pr.url || state.pr.number)),
|
|
386
|
+
du: durationSec(state),
|
|
387
|
+
tk: spend.tk,
|
|
388
|
+
cost: spend.cost,
|
|
389
|
+
phs: spend.phs,
|
|
390
|
+
os: ctx.os,
|
|
391
|
+
cli: ctx.cli,
|
|
392
|
+
models: spend.models,
|
|
393
|
+
lang: ctx.lang,
|
|
394
|
+
stack: ctx.stack,
|
|
395
|
+
plug: ctx.plug,
|
|
396
|
+
apps: ctx.apps,
|
|
397
|
+
creds: ctx.creds,
|
|
398
|
+
calls: ctx.calls,
|
|
399
|
+
errs: deriveErrors(state, spend.failed),
|
|
400
|
+
v: packageVersion(),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function post(endpoint, token, event) {
|
|
405
|
+
const ctrl = new AbortController();
|
|
406
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
407
|
+
try {
|
|
408
|
+
await fetch(endpoint, {
|
|
409
|
+
method: "POST",
|
|
410
|
+
headers: {
|
|
411
|
+
"Content-Type": "application/json",
|
|
412
|
+
"X-Usage-Token": token,
|
|
413
|
+
},
|
|
414
|
+
body: JSON.stringify(event),
|
|
415
|
+
signal: ctrl.signal,
|
|
416
|
+
});
|
|
417
|
+
} catch {
|
|
418
|
+
/* never break the run on a telemetry failure */
|
|
419
|
+
} finally {
|
|
420
|
+
clearTimeout(timer);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function main() {
|
|
425
|
+
const args = parseArgs(process.argv.slice(2));
|
|
426
|
+
const prefs = resolvePrefs();
|
|
427
|
+
const token = resolveToken();
|
|
428
|
+
const endpoint = prefs.endpoint || ENDPOINT_DEFAULT;
|
|
429
|
+
|
|
430
|
+
if (!args.dryRun && (prefs.enabled !== true || !token)) return;
|
|
431
|
+
|
|
432
|
+
const statePath = resolveStatePath(args);
|
|
433
|
+
let state = statePath ? readJson(statePath) : null;
|
|
434
|
+
|
|
435
|
+
if (!state) {
|
|
436
|
+
if (!args.taskId) return;
|
|
437
|
+
state = { taskId: args.taskId };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (args.phase != null) {
|
|
441
|
+
const n = Number(args.phase);
|
|
442
|
+
state.currentPhase = Number.isNaN(n) ? args.phase : n;
|
|
443
|
+
}
|
|
444
|
+
if (args.status) state.status = args.status;
|
|
445
|
+
|
|
446
|
+
const event = buildEvent(state);
|
|
447
|
+
|
|
448
|
+
if (args.dryRun) {
|
|
449
|
+
process.stdout.write(JSON.stringify(event, null, 2) + "\n");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (isTerminalStatus(state)) appendErrorLedger(event);
|
|
454
|
+
await post(endpoint, token, event);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
main().catch(() => process.exit(0));
|
|
@@ -14,9 +14,13 @@
|
|
|
14
14
|
// complaint_count, graylog_degraded); language in {tr, en}.
|
|
15
15
|
// - Required sections present by bilingual title keyword: Summary, Triage,
|
|
16
16
|
// Complaint Details, Open Questions, Methodology, References.
|
|
17
|
-
// - Every triage-table row (a
|
|
18
|
-
// verdict token
|
|
17
|
+
// - Every triage-table row (a row in the Triage section whose FIRST cell is
|
|
18
|
+
// a C-NN id) has a valid verdict token in a cell of its own:
|
|
19
|
+
// client:<layer> | bff:<layer> | core | insufficient-evidence.
|
|
19
20
|
// Verdict tokens stay English in both languages (payload vocabulary).
|
|
21
|
+
// Scoping matters: the Section 1 coverage table cites C-NN ids in its
|
|
22
|
+
// "Verdicts attributed" column and must not be read as triage rows, and a
|
|
23
|
+
// summary cell mentioning "core" must not classify the row as core.
|
|
20
24
|
// - Every core verdict has a matching entry in the routing section
|
|
21
25
|
// (Yönlendirme / Routing), and that section exists when any core row does.
|
|
22
26
|
// - Every client/bff verdict has a fix-plan marker (Fix plan / Geliştirme
|
|
@@ -24,7 +28,9 @@
|
|
|
24
28
|
// - Humanizer punctuation policy: no em-dash / en-dash / ellipsis /
|
|
25
29
|
// section-sign / curly quotes anywhere.
|
|
26
30
|
// - Redaction leak scan: an email address or a 13-19 digit card-like run is
|
|
27
|
-
// an ERROR; a bare 11-digit run is a WARN (could be a numeric trx id)
|
|
31
|
+
// an ERROR; a bare 11-digit run is a WARN (could be a numeric trx id); a
|
|
32
|
+
// PNR-shaped token (6-char uppercase letter+digit mix, or 6 uppercase
|
|
33
|
+
// letters right after a "PNR" keyword) is a WARN.
|
|
28
34
|
//
|
|
29
35
|
// Usage:
|
|
30
36
|
// node validate-complaint-doc.mjs complaints/complaints-20260810.md
|
|
@@ -49,8 +55,10 @@ const REQUIRED_SECTIONS = [
|
|
|
49
55
|
const ROUTING_KEYWORDS = ["Routing", "Yönlendirme", "Yonlendirme"];
|
|
50
56
|
const FIX_PLAN_RE = /(Fix plan|Fix Plan|Geliştirme planı|Geliştirme Planı|Gelistirme plani|Gelistirme Plani)/;
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
// Cell-exact on purpose: a substring match would classify a row as `core`
|
|
59
|
+
// because its summary says "core-data" or "core team" before the verdict column.
|
|
60
|
+
const VERDICT_CELL_RE =
|
|
61
|
+
/^(client:(ios|android|web)|bff:(mobile-bff|web-bff)|core|insufficient-evidence)$/;
|
|
54
62
|
|
|
55
63
|
const BANNED_PUNCT = [
|
|
56
64
|
{ ch: "—", name: "em-dash" },
|
|
@@ -66,6 +74,12 @@ const BANNED_PUNCT = [
|
|
|
66
74
|
const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/;
|
|
67
75
|
const CARD_RE = /\b\d(?:[ -]?\d){12,18}\b/;
|
|
68
76
|
const NATIONAL_ID_RE = /\b\d{11}\b/;
|
|
77
|
+
// Same shape parse-complaints.sh redacts (6-char uppercase alnum with at least
|
|
78
|
+
// one letter AND one digit), so a leak the parser should have caught fails
|
|
79
|
+
// loudly here too. All-letter PNRs are only flaggable with context: a bare
|
|
80
|
+
// 6-letter word is far too common, so those warn only right after "PNR".
|
|
81
|
+
const PNR_MIXED_RE = /\b(?=[A-Z0-9]{6}\b)(?=[A-Z0-9]*\d)(?=\d*[A-Z])[A-Z0-9]{6}\b/;
|
|
82
|
+
const PNR_CONTEXT_RE = /\bPNR\b[^A-Za-z0-9\n]{0,3}[A-Z]{6}\b/;
|
|
69
83
|
|
|
70
84
|
function readInput() {
|
|
71
85
|
const args = process.argv.slice(2).filter((a) => a !== "--strict");
|
|
@@ -144,21 +158,25 @@ function main() {
|
|
|
144
158
|
}
|
|
145
159
|
}
|
|
146
160
|
|
|
147
|
-
// 3. Triage rows:
|
|
148
|
-
|
|
161
|
+
// 3. Triage rows: scoped to the Triage section (the Section 1 coverage table
|
|
162
|
+
// also cites C-NN ids, in its "Verdicts attributed" column), and a triage row
|
|
163
|
+
// carries the id in its FIRST cell. Every such row needs a verdict cell.
|
|
164
|
+
const triageScope = sectionBody(text, ["Triage"]) ?? text;
|
|
165
|
+
const rows = triageScope.split("\n").filter((l) => /^\s*\|\s*C-\d{2,}\s*\|/.test(l));
|
|
149
166
|
if (rows.length === 0) {
|
|
150
|
-
errors.push("no triage rows found (expected
|
|
167
|
+
errors.push("no triage rows found (expected Triage-section rows with a C-NN id in the first cell)");
|
|
151
168
|
}
|
|
152
169
|
const coreIds = [];
|
|
153
170
|
const handoffIds = [];
|
|
154
171
|
for (const row of rows) {
|
|
155
172
|
const id = (row.match(/\bC-\d{2,}\b/) || [])[0];
|
|
156
|
-
const
|
|
173
|
+
const cells = row.split("|").map((c) => c.trim());
|
|
174
|
+
const verdict = cells.find((c) => VERDICT_CELL_RE.test(c));
|
|
157
175
|
if (!verdict) {
|
|
158
176
|
errors.push(`triage row ${id} has no valid verdict token (client:<layer> | bff:<layer> | core | insufficient-evidence)`);
|
|
159
|
-
} else if (verdict
|
|
177
|
+
} else if (verdict === "core" && !coreIds.includes(id)) {
|
|
160
178
|
coreIds.push(id);
|
|
161
|
-
} else if (/^(client|bff):/.test(verdict
|
|
179
|
+
} else if (/^(client|bff):/.test(verdict) && !handoffIds.includes(id)) {
|
|
162
180
|
handoffIds.push(id);
|
|
163
181
|
}
|
|
164
182
|
}
|
|
@@ -207,6 +225,9 @@ function main() {
|
|
|
207
225
|
if (NATIONAL_ID_RE.test(bodyLines[i]) && !CARD_RE.test(bodyLines[i])) {
|
|
208
226
|
warns.push(`possible redaction leak: bare 11-digit run at line ${i + 1} (national-id-like; ignore if it is a numeric trx id)`);
|
|
209
227
|
}
|
|
228
|
+
if (PNR_MIXED_RE.test(bodyLines[i]) || PNR_CONTEXT_RE.test(bodyLines[i])) {
|
|
229
|
+
warns.push(`possible redaction leak: PNR-shaped token at line ${i + 1} (parse-complaints.sh should have redacted it)`);
|
|
230
|
+
}
|
|
210
231
|
}
|
|
211
232
|
|
|
212
233
|
// Report
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-08-
|
|
3
|
+
"generatedAt": "2026-08-14T19:54:00Z",
|
|
4
4
|
"skillCount": 205,
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
"path": "shared/core/multi-agent-refactor/SKILL.md",
|
|
128
|
-
"sha256": "
|
|
128
|
+
"sha256": "1924dd999d8d99fbfc68dac132c26e9c9d47c9c6e0330db4821f724eb7f2408c"
|
|
129
129
|
},
|
|
130
130
|
{
|
|
131
131
|
"path": "shared/core/multi-agent-resume-local/SKILL.md",
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
"path": "shared/core/multi-agent-stack/SKILL.md",
|
|
172
|
-
"sha256": "
|
|
172
|
+
"sha256": "f49fb2fcf0a5a1f3f044a4393ec625208d48ef4b685ea0370d74c4ac18f1afe7"
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
175
|
"path": "shared/core/multi-agent-status/SKILL.md",
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
},
|
|
214
214
|
{
|
|
215
215
|
"path": "shared/core/multi-agent-update/SKILL.md",
|
|
216
|
-
"sha256": "
|
|
216
|
+
"sha256": "66d960a9c360ba0353b6496fde641c1a99834274bc0ac68932ed107b15411ccb"
|
|
217
217
|
},
|
|
218
218
|
{
|
|
219
219
|
"path": "shared/core/multi-agent/SKILL.md",
|