@mmerterden/multi-agent-pipeline 20.11.0 → 20.12.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.
@@ -27,7 +27,15 @@
27
27
  * @module pipeline/scripts/usage-report
28
28
  */
29
29
 
30
- import { readFileSync, existsSync, readdirSync, appendFileSync } from "fs";
30
+ import {
31
+ readFileSync,
32
+ existsSync,
33
+ readdirSync,
34
+ appendFileSync,
35
+ mkdirSync,
36
+ renameSync,
37
+ writeFileSync,
38
+ } from "fs";
31
39
  import { canonicalEndpoint } from "../lib/usage-endpoint.mjs";
32
40
  import { homedir, platform } from "os";
33
41
  import { dirname, join } from "path";
@@ -709,27 +717,78 @@ function buildEvent(state, { event = null, login = null } = {}) {
709
717
  };
710
718
  }
711
719
 
720
+ function hostOf(url) {
721
+ try {
722
+ return new URL(url).hostname;
723
+ } catch {
724
+ return null;
725
+ }
726
+ }
727
+
728
+ /**
729
+ * The outcome of one POST. A redirect is never followed, because the ingest
730
+ * token would ride along to wherever it points; its target host is reported
731
+ * instead, so a moved endpoint is visible rather than silent.
732
+ */
712
733
  async function post(endpoint, token, event) {
713
734
  const ctrl = new AbortController();
714
735
  const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
715
736
  try {
716
- await fetch(endpoint, {
737
+ const res = await fetch(endpoint, {
717
738
  method: "POST",
718
739
  headers: {
719
740
  "Content-Type": "application/json",
720
741
  "X-Usage-Token": token,
721
742
  },
722
743
  body: JSON.stringify(event),
723
- redirect: "error",
744
+ redirect: "manual",
724
745
  signal: ctrl.signal,
725
746
  });
726
- } catch {
727
- /* never break the run on a telemetry failure */
747
+ if (res.status >= 300 && res.status < 400) {
748
+ return {
749
+ outcome: "redirected",
750
+ status: res.status,
751
+ location: hostOf(res.headers.get("location") || ""),
752
+ };
753
+ }
754
+ if (!res.ok) return { outcome: "rejected", status: res.status };
755
+ let body = null;
756
+ try {
757
+ body = await res.json();
758
+ } catch {
759
+ /* a 2xx without a JSON body is still a delivery */
760
+ }
761
+ return { outcome: body?.accepted === false ? "not-accepted" : "accepted", status: res.status };
762
+ } catch (err) {
763
+ const reason = ctrl.signal.aborted ? "timeout" : (err?.cause?.code ?? err?.name ?? "error");
764
+ return { outcome: "failed", reason: codeOf(String(reason), 40) };
728
765
  } finally {
729
766
  clearTimeout(timer);
730
767
  }
731
768
  }
732
769
 
770
+ export const LAST_SEND_FILE = "usage-last-send.json";
771
+
772
+ /**
773
+ * Overwrites the record of the most recent send attempt. The emitter runs
774
+ * detached with its output discarded, so this file is the only place a broken
775
+ * reporting path shows up; doctor reads it. It carries no token and no path.
776
+ */
777
+ function recordSend(entry) {
778
+ const dir = join(homedir(), ".claude", "logs", "multi-agent");
779
+ const target = join(dir, LAST_SEND_FILE);
780
+ const tmp = `${target}.${process.pid}.tmp`;
781
+ try {
782
+ mkdirSync(dir, { recursive: true });
783
+ writeFileSync(tmp, JSON.stringify({ t: new Date().toISOString(), ...entry }) + "\n", {
784
+ mode: 0o600,
785
+ });
786
+ renameSync(tmp, target);
787
+ } catch {
788
+ /* logs dir unwritable - never break the run */
789
+ }
790
+ }
791
+
733
792
  // The write-only ingest token rides in a header; sending it over http would
734
793
  // leak it in cleartext, contradicting the setup contract ("transmitted only
735
794
  // over TLS"). Accept https anywhere, and http ONLY for loopback (local dev).
@@ -759,10 +818,16 @@ async function main() {
759
818
  if (!args.dryRun) {
760
819
  if (prefs.optOut === true) return;
761
820
  if (prefs.enabled !== true) return;
762
- if (!endpointAllowed(endpoint)) return;
821
+ if (!endpointAllowed(endpoint)) {
822
+ recordSend({ id: args.taskId, outcome: "skipped", reason: "endpoint-not-allowed" });
823
+ return;
824
+ }
763
825
  }
764
826
  const token = args.dryRun ? "" : resolveToken();
765
- if (!args.dryRun && !token) return;
827
+ if (!args.dryRun && !token) {
828
+ recordSend({ id: args.taskId, outcome: "skipped", reason: "no-token" });
829
+ return;
830
+ }
766
831
 
767
832
  const statePath = resolveStatePath(args);
768
833
  let state = statePath ? readJson(statePath) : null;
@@ -782,7 +847,10 @@ async function main() {
782
847
  const who = resolveReportingLogin();
783
848
  if (!who.login) {
784
849
  process.stderr.write(`usage-report: not sent - ${who.reason}\n`);
785
- if (!args.dryRun) return;
850
+ if (!args.dryRun) {
851
+ recordSend({ id: runId(state), ev: kind, outcome: "skipped", reason: "no-login" });
852
+ return;
853
+ }
786
854
  }
787
855
 
788
856
  const event = buildEvent(state, { event: kind, login: who.login });
@@ -793,7 +861,14 @@ async function main() {
793
861
  }
794
862
 
795
863
  if (event.st !== "in_progress") appendErrorLedger(event);
796
- await post(endpoint, token, event);
864
+ const result = await post(endpoint, token, event);
865
+ recordSend({ id: event.id, ev: event.ev, host: hostOf(endpoint), ...result });
866
+ if (result.outcome !== "accepted") {
867
+ const detail = result.status ?? result.reason;
868
+ const where = result.location ? ` to ${result.location}` : "";
869
+ const verb = result.outcome === "not-accepted" ? "not accepted" : result.outcome;
870
+ process.stderr.write(`usage-report: ${verb}${where} (${detail}) by ${hostOf(endpoint)}\n`);
871
+ }
797
872
  }
798
873
 
799
874
  // Entry-point check: compare file URLs, never string-split a path.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-29T06:46:22Z",
3
+ "generatedAt": "2026-09-29T13:31:43Z",
4
4
  "skillCount": 218,
5
5
  "entries": [
6
6
  {
@@ -701,7 +701,7 @@
701
701
  },
702
702
  {
703
703
  "path": "shared/external/swift-architecture/SKILL.md",
704
- "sha256": "36566e7a79f2e6c760cc31fb1ee1a635c0e32988357c20af3502a03ecb371753"
704
+ "sha256": "295f6ce1488a93f64e5e78228b90863924fe21911769307844cc73f621c04ab1"
705
705
  },
706
706
  {
707
707
  "path": "shared/external/swift-charts/SKILL.md",
@@ -13,6 +13,10 @@ SwiftUI with Observation (iOS 17 or later) unless a UIKit type appears.
13
13
 
14
14
  ## Scope boundary
15
15
 
16
+ The samples here pass dependencies through initializers. In a repo that resolves
17
+ dependencies from a keyed container, `reference/architecture/dependency-injection.md`
18
+ takes precedence over initializer injection in every sample in this skill.
19
+
16
20
  This skill decides:
17
21
 
18
22
  - which pattern a feature uses,