madeby 0.3.14 → 0.3.16

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.
Files changed (3) hide show
  1. package/README.md +38 -3
  2. package/dist/madeby.mjs +129 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,6 +27,12 @@ sign-offs, SPDX AI-disclosure tags — and lets a project **gate PRs** on it (`m
27
27
  guesses whether code is AI; no tool can do that honestly. It reports only what contributors **put on
28
28
  the record**.
29
29
 
30
+ The commands `init` writes into `AGENTS.md` pin the exact madeby release (`npx madeby@0.3.16 …`),
31
+ like the CI file does: agents run them on developers' machines, which hold credentials, so they run
32
+ the release the team chose rather than whatever is newest. When a newer release is out, `init`
33
+ reports the rule as outdated, and `npx madeby@latest init --update` moves the pin in one commit to
34
+ review.
35
+
30
36
  Zero hosted infrastructure — runs against your local git history, on a laptop or in any CI. Nothing
31
37
  leaves your machine.
32
38
 
@@ -43,6 +49,7 @@ npm i -g madeby
43
49
  ```
44
50
  madeby init Set MadeBy up: write .madeby/policy.json + the CI disclosure check.
45
51
  --record-model (opt-in) also asks agents to name the model version.
52
+ In a repo that uses Claude Code, also turns its commit trailer on.
46
53
  madeby check [<range>] The gate: do commits meet .madeby/policy.json? (a verdict — exit code
47
54
  gates). <range> e.g. origin/main..HEAD — a PR's own commits. --json too.
48
55
  madeby coverage [<range>] The scoreboard: how much of the repo's authorship is disclosed (known)
@@ -74,6 +81,11 @@ A commit **discloses who wrote it** if it carries any of:
74
81
  - an **affirmative human-authorship trailer** — `Authored-by-human: <you>` (see below);
75
82
  - a **`madeby ai`** attestation (your own AI session's witnessed spans).
76
83
 
84
+ It checks the same commits `coverage` scores: every non-merge commit, plus any merge where someone
85
+ changed what git would have produced on its own (a conflict resolved by hand). A merge git made by
86
+ itself authors nothing and is never checked. Disclose a resolution like any commit: `madeby me
87
+ --with-ai <tool>` right after committing it, or `--for` later.
88
+
77
89
  A **DCO `Signed-off-by:`** line or a **commit signature** is recognized too, but on its own it says
78
90
  who is accountable for the commit, not who wrote it, so it counts only if the policy's `accept`
79
91
  lists it. `check` reports commits that carry only one of those apart from those that carry nothing.
@@ -126,9 +138,11 @@ model versions are listed exactly as the AI commits name them — a commit namin
126
138
  (`Assisted-by: Claude Code` + `Co-Authored-By: Claude Opus 5.5`) counts under both, so those rows can
127
139
  sum past 100%. `--by author` shows one row per commit author (grouped by email): how many of their
128
140
  commits disclose, and the mix — who discloses and who never does. `--json` (which includes `byAuthor`) emits
129
- counts only (percentages are derived at the edge, never stored). A plain merge authors nothing and
130
- isn't scored; a merge that changes content of its own (a conflict resolution) is, like any other
131
- commit — chosen by what it changes, never by whether it discloses. Use it to see your gap, then
141
+ counts only (percentages are derived at the edge, never stored). A merge git made on its own authors nothing
142
+ and isn't scored, even when both sides changed the same file; a merge where someone changed what git
143
+ would have produced (a conflict resolved by hand) is, like any other commit — chosen by what it
144
+ changes, never by whether it discloses. That takes git 2.36 or later (`--remerge-diff`); with older
145
+ git, merges git made cleanly may be counted too, and the scope says the count is approximate. Use it to see your gap, then
132
146
  `madeby init --required` to close it going forward.
133
147
 
134
148
  ### `madeby me` — affirm human authorship
@@ -198,6 +212,27 @@ Fail-safe by design: a missing or malformed policy degrades to `advisory`. `made
198
212
  on its own error, and never invents a stricter gate than the maintainer wrote. It never passes a
199
213
  `required` check it couldn't actually run, either: missing history fails it (see `check` above).
200
214
 
215
+ ### Claude Code's own trailer
216
+
217
+ Claude Code adds a `Co-Authored-By` trailer to its commits unless a developer turns it off in their
218
+ personal settings — and then the `AGENTS.md` rule alone may not bring it back. So in a repo that uses
219
+ Claude Code (it has a `CLAUDE.md` or a `.claude/` folder; `--claude-code` to force), `madeby init`
220
+ sets the trailer in the shared `.claude/settings.json`. It looks at any depth: Claude Code reads the
221
+ settings of the folder it's launched from, so a monorepo whose app keeps its `CLAUDE.md` in
222
+ `application/` gets `application/.claude/settings.json`, and the disclosure rule merged into that
223
+ `CLAUDE.md`. If it finds none, it says so.
224
+
225
+ ```json
226
+ { "attribution": { "commit": "Co-Authored-By: Claude Code <noreply@anthropic.com>" } }
227
+ ```
228
+
229
+ The shared project file overrides a developer's personal settings, so everyone in the repo gets the
230
+ trailer back. It's fixed text — Claude Code's setting has no placeholder for the model — so to record
231
+ the model use `--record-model`. It merges into an existing file and never changes a value the team
232
+ already set, including turning attribution off on purpose. A developer's own
233
+ `.claude/settings.local.json` or instructions can still turn it off; the `required` check is what
234
+ holds the line.
235
+
201
236
  ## Use it as a PR check
202
237
 
203
238
  A ready-made GitHub Action, an Azure DevOps Pipelines template, and a GitLab CI job wrap
package/dist/madeby.mjs CHANGED
@@ -59,6 +59,18 @@ function readGitLog(repoPath, limit, range) {
59
59
  return [];
60
60
  }
61
61
  }
62
+ var remergeDiff;
63
+ function supportsRemergeDiff() {
64
+ if (remergeDiff === void 0) {
65
+ try {
66
+ const v = /git version (\d+)\.(\d+)/.exec(execFileSync("git", ["--version"], { encoding: "utf8" }));
67
+ remergeDiff = !!v && (Number(v[1]) > 2 || Number(v[1]) === 2 && Number(v[2]) >= 36);
68
+ } catch {
69
+ remergeDiff = false;
70
+ }
71
+ }
72
+ return remergeDiff;
73
+ }
62
74
  function readAuthoredMerges(repoPath, opts = {}) {
63
75
  try {
64
76
  const args = [
@@ -66,7 +78,7 @@ function readAuthoredMerges(repoPath, opts = {}) {
66
78
  repoPath,
67
79
  "log",
68
80
  "--merges",
69
- "--cc",
81
+ supportsRemergeDiff() ? "--remerge-diff" : "--cc",
70
82
  "--name-only",
71
83
  `--format=${RECORD}%H${FIELD}%an${FIELD}%ae${FIELD}%G?${FIELD}%B${GROUP}`,
72
84
  ...opts.since !== void 0 && !opts.range ? [`--since=@${opts.since}`] : [],
@@ -86,6 +98,14 @@ function commitTime(repoPath, sha) {
86
98
  return void 0;
87
99
  }
88
100
  }
101
+ function readScoredCommits(repoPath, opts) {
102
+ const limit = opts.range ? void 0 : opts.limit;
103
+ const authored = readGitLog(repoPath, limit, opts.range);
104
+ const oldest = authored.at(-1)?.sha;
105
+ const since = limit && oldest && authored.length >= limit ? commitTime(repoPath, oldest) : void 0;
106
+ const merges = authored.length > 0 && (!limit || authored.length < limit || since !== void 0) ? readAuthoredMerges(repoPath, { range: opts.range, since }) : [];
107
+ return { authored, merges };
108
+ }
89
109
 
90
110
  // ../classify/src/classify.ts
91
111
  var AI_PATTERNS = [
@@ -1149,7 +1169,8 @@ function readDisclosures(root, opts = {}) {
1149
1169
  }
1150
1170
  function readDisclosuresWithRejects(root, opts) {
1151
1171
  const late = readDisclosureCommits(root, rangeHead(opts.range));
1152
- const commits = applyLateDisclosures(readGitLog(root, opts.range ? void 0 : opts.limit ?? 200, opts.range), late);
1172
+ const { authored, merges } = readScoredCommits(root, { range: opts.range, limit: opts.limit ?? 200 });
1173
+ const commits = applyLateDisclosures([...authored, ...merges], late);
1153
1174
  return {
1154
1175
  commits: commits.map((c) => ({
1155
1176
  sha: c.sha ?? "",
@@ -1396,11 +1417,7 @@ function agentName(raw) {
1396
1417
  return name || raw.trim();
1397
1418
  }
1398
1419
  function repoCoverage(root, opts = {}) {
1399
- const limit = opts.range ? void 0 : opts.limit ?? 500;
1400
- const authored = readGitLog(root, limit, opts.range);
1401
- const oldest = authored.at(-1)?.sha;
1402
- const since = limit && oldest && authored.length >= limit ? commitTime(root, oldest) : void 0;
1403
- const merges = authored.length > 0 && (!limit || authored.length < limit || since !== void 0) ? readAuthoredMerges(root, { range: opts.range, since }) : [];
1420
+ const { authored, merges } = readScoredCommits(root, { range: opts.range, limit: opts.limit ?? 500 });
1404
1421
  const commits = applyLateDisclosures([...authored, ...merges], readDisclosureCommits(root, rangeHead(opts.range)));
1405
1422
  const byCategory = { human: 0, with_ai: 0, ai: 0, bot: 0 };
1406
1423
  const byTier = { asserted: 0, verified: 0 };
@@ -1440,8 +1457,8 @@ function repoCoverage(root, opts = {}) {
1440
1457
  }
1441
1458
  return {
1442
1459
  isEstimate: true,
1443
- methodology: "commit-level authorship disclosure (estimate); undisclosed = unknown, never inferred; merges scored only when they change content of their own; a disclosure commit (Discloses:) fills in commits disclosed afterwards and isn't scored itself",
1444
- scope: (opts.range ? `range ${opts.range}` : `last ${authored.length} commits`) + (merges.length ? ` + ${merges.length} merges with changes of their own` : ""),
1460
+ methodology: "commit-level authorship disclosure (estimate); undisclosed = unknown, never inferred; merges scored only when they change content of their own" + (supportsRemergeDiff() ? " (beyond git's automatic merge)" : " (approximate: this git predates --remerge-diff, so merges git made cleanly may be counted too)") + "; a disclosure commit (Discloses:) fills in commits disclosed afterwards and isn't scored itself",
1461
+ scope: (opts.range ? `range ${opts.range}` : `last ${authored.length} commits`) + (merges.length ? ` + ${merges.length} merges with changes of their own${supportsRemergeDiff() ? "" : " (approximate \u2014 git < 2.36)"}` : ""),
1445
1462
  total: commits.length,
1446
1463
  known,
1447
1464
  unknown: commits.length - known,
@@ -2245,10 +2262,11 @@ function parseFor(args) {
2245
2262
  if (i === -1) return void 0;
2246
2263
  const a = args[i];
2247
2264
  const v = a.startsWith("--for=") ? a.slice("--for=".length) : args[i + 1];
2248
- return v && !v.startsWith("-") ? v : void 0;
2265
+ return v && !v.startsWith("-") ? v.trim() : "";
2249
2266
  }
2250
2267
  function discloseFor(root, target, opts) {
2251
2268
  const none = { disclosed: [], skipped: [], lines: [] };
2269
+ if (!target.trim()) return { ok: false, reason: "bad-target", ...none };
2252
2270
  let email = "";
2253
2271
  try {
2254
2272
  email = git4(root, ["config", "user.email"]).toLowerCase();
@@ -2298,7 +2316,7 @@ function renderDiscloseFor(cmd2, target, r) {
2298
2316
  if (!r.ok) {
2299
2317
  if (r.reason === "no-identity") console.error(`madeby ${cmd2}: set your git identity first (git config user.name / user.email).`);
2300
2318
  else if (r.reason === "staged-changes") console.error(`madeby ${cmd2}: you have staged changes. A disclosure commit carries no content \u2014 commit or unstage them first.`);
2301
- else console.error(`madeby ${cmd2}: no commits found for ${target}. Give a commit or a range, e.g. --for origin/dev..HEAD`);
2319
+ else console.error(`madeby ${cmd2}: no commits found for ${target ? `'${target}'` : "an empty --for"}. Give a commit or a range, e.g. --for origin/dev..HEAD. Nothing was changed.`);
2302
2320
  process.exit(1);
2303
2321
  return;
2304
2322
  }
@@ -2328,7 +2346,7 @@ function meCommand(args) {
2328
2346
  return;
2329
2347
  }
2330
2348
  const forTarget = parseFor(args);
2331
- if (forTarget) {
2349
+ if (forTarget !== void 0) {
2332
2350
  renderDiscloseFor("me", forTarget, discloseFor(root, forTarget, { category: withAi ? "with_ai" : "human", tool, model }));
2333
2351
  return;
2334
2352
  }
@@ -2417,7 +2435,7 @@ function aiCommand(args) {
2417
2435
  }
2418
2436
  const tool = positional[0] || "AI";
2419
2437
  const forTarget = parseFor(args);
2420
- if (forTarget) {
2438
+ if (forTarget !== void 0) {
2421
2439
  renderDiscloseFor("ai", forTarget, discloseFor(root, forTarget, { category: "ai", tool, model }));
2422
2440
  return;
2423
2441
  }
@@ -2587,37 +2605,40 @@ function coverageCommand(args) {
2587
2605
  }
2588
2606
 
2589
2607
  // src/init-repo.ts
2608
+ import { execFileSync as execFileSync7 } from "node:child_process";
2590
2609
  import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
2591
2610
  import { dirname as dirname2, join as join8 } from "node:path";
2592
2611
 
2593
2612
  // src/version.ts
2594
- var VERSION = "0.3.14";
2613
+ var VERSION = "0.3.16";
2595
2614
 
2596
2615
  // src/init-repo.ts
2597
- var ACO_VERSION = 2;
2616
+ var ACO_VERSION = 3;
2598
2617
  var ACO_OPEN = `<!-- madeby:aco v${ACO_VERSION} -->`;
2599
2618
  var ACO_CLOSE = `<!-- /madeby:aco -->`;
2600
2619
  var ACO_OPEN_RE = /<!-- madeby:aco(?: v(\d+))? -->/;
2620
+ var PKG = `madeby@${VERSION}`;
2601
2621
  var ACO_BODY = `## Authorship disclosure (ACO)
2602
2622
 
2603
2623
  When you author or modify content here, **disclose who authored it** \u2014 per
2604
2624
  [ACO, the Authorship Certificate of Origin](https://github.com/MadeByFYI/MadeBy/blob/main/ACO.md).
2605
2625
  After you commit, run the command for the honest category (no install, no setup):
2606
2626
 
2607
- - **ai** \u2014 an AI authored it (the committer stays accountable): \`npx madeby@latest ai [tool]\`
2608
- - **with_ai** \u2014 a human authored it with AI assistance: \`npx madeby@latest me --with-ai [tool]\`
2609
- - **human** \u2014 a human authored it: \`npx madeby@latest me\`
2627
+ - **ai** \u2014 an AI authored it (the committer stays accountable): \`npx ${PKG} ai [tool]\`
2628
+ - **with_ai** \u2014 a human authored it with AI assistance: \`npx ${PKG} me --with-ai [tool]\`
2629
+ - **human** \u2014 a human authored it: \`npx ${PKG} me\`
2610
2630
 
2611
2631
  If your platform makes the commit for you (a cloud coding agent that pushes through its own tool),
2612
2632
  amending afterwards can be lost \u2014 put the disclosure in the message instead, with \`--print\`:
2613
- \`git commit -m "<message>" -m "$(npx madeby@latest ai [tool] --print)"\`.
2633
+ \`git commit -m "<message>" -m "$(npx ${PKG} ai [tool] --print)"\`.
2614
2634
 
2615
- Disclose per commit, honestly. Undisclosed content stays *unknown*, never guessed.
2635
+ Disclose per commit, honestly. Undisclosed content stays *unknown*, never guessed. (The madeby
2636
+ version is pinned on purpose; \`npx madeby@latest init --update\` moves it to the newest release.)
2616
2637
  `;
2617
2638
  var MODEL_LINE = `
2618
2639
  **This repo also records the model.** Name your tool as usual \u2014 madeby reads the model from the
2619
2640
  tool's own record of the session where it can (Cursor, Copilot CLI, Claude Code), e.g.
2620
- \`npx madeby@latest me --with-ai Cursor\` writes \`Assisted-by: Cursor:<model>\`. Elsewhere add
2641
+ \`npx ${PKG} me --with-ai Cursor\` writes \`Assisted-by: Cursor:<model>\`. Elsewhere add
2621
2642
  \`--model <version>\` with the exact model you are running as. If you don't know it, leave it off \u2014
2622
2643
  never guess.
2623
2644
  `;
@@ -2644,6 +2665,13 @@ function acoStatus(content) {
2644
2665
  if (!loc) return "absent";
2645
2666
  if (loc.version === null || loc.version < ACO_VERSION) return "outdated";
2646
2667
  if (loc.version > ACO_VERSION) return "newer";
2668
+ const pin = /npx madeby@(\d+)\.(\d+)\.(\d+)/.exec(content.slice(loc.start, loc.end));
2669
+ if (pin) {
2670
+ const [a, b, c] = VERSION.split(".").map(Number);
2671
+ const cmp = Number(pin[1]) - a || Number(pin[2]) - b || Number(pin[3]) - c;
2672
+ if (cmp < 0) return "outdated";
2673
+ if (cmp > 0) return "newer";
2674
+ }
2647
2675
  return "current";
2648
2676
  }
2649
2677
  function replaceAcoBlock(content, loc, block) {
@@ -2687,6 +2715,44 @@ function ensurePolicy(abs, rel, want, res) {
2687
2715
  }
2688
2716
  return { mode: raise ? "required" : current.mode, recordModel: modelOn || current.recordModel === true, malformed: !isObject };
2689
2717
  }
2718
+ var CLAUDE_TRAILER = "Co-Authored-By: Claude Code <noreply@anthropic.com>";
2719
+ function claudeCodeDirs(root) {
2720
+ const dirs = /* @__PURE__ */ new Set();
2721
+ if (existsSync7(join8(root, "CLAUDE.md")) || existsSync7(join8(root, ".claude"))) dirs.add("");
2722
+ let files = [];
2723
+ try {
2724
+ files = execFileSync7("git", ["-C", root, "-c", "core.quotePath=false", "ls-files", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 }).split("\n");
2725
+ } catch {
2726
+ }
2727
+ for (const f of files) {
2728
+ const parts = f.split("/");
2729
+ const at = parts.indexOf(".claude");
2730
+ if (at >= 0 && at < parts.length - 1) dirs.add(parts.slice(0, at).join("/"));
2731
+ else if (parts[parts.length - 1] === "CLAUDE.md") dirs.add(parts.slice(0, -1).join("/"));
2732
+ }
2733
+ return [...dirs].sort();
2734
+ }
2735
+ function ensureClaudeSettings(abs) {
2736
+ if (!existsSync7(abs)) {
2737
+ mkdirSync2(dirname2(abs), { recursive: true });
2738
+ writeFileSync2(abs, JSON.stringify({ attribution: { commit: CLAUDE_TRAILER } }, null, 2) + "\n");
2739
+ return "created";
2740
+ }
2741
+ let raw;
2742
+ try {
2743
+ raw = JSON.parse(readFileSync9(abs, "utf8"));
2744
+ } catch {
2745
+ return "malformed";
2746
+ }
2747
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "malformed";
2748
+ const settings = raw;
2749
+ const attribution = settings.attribution;
2750
+ if (attribution === false || attribution && typeof attribution === "object" && "commit" in attribution) return "kept";
2751
+ if (attribution === void 0 && settings.includeCoAuthoredBy === false) return "kept";
2752
+ const next = { ...settings, attribution: { ...attribution && typeof attribution === "object" ? attribution : {}, commit: CLAUDE_TRAILER } };
2753
+ writeFileSync2(abs, JSON.stringify(next, null, 2) + "\n");
2754
+ return "merged";
2755
+ }
2690
2756
  function ensureAco(abs, rel, res, opts) {
2691
2757
  const block = acoBlock(opts.recordModel);
2692
2758
  if (!existsSync7(abs)) {
@@ -2737,6 +2803,17 @@ function initRepo(root, opts = {}) {
2737
2803
  const aco = { update, recordModel: policy.recordModel, modelRequested };
2738
2804
  ensureAco(join8(root, "AGENTS.md"), "AGENTS.md", res, { createIfAbsent: true, ...aco });
2739
2805
  ensureAco(join8(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false, ...aco });
2806
+ const claudeDirs = claudeCodeDirs(root);
2807
+ if (opts.claudeCode && !claudeDirs.includes("")) claudeDirs.unshift("");
2808
+ const claudeSettings = [];
2809
+ for (const dir of claudeDirs) {
2810
+ const rel = (f) => dir ? `${dir}/${f}` : f;
2811
+ if (dir) ensureAco(join8(root, dir, "CLAUDE.md"), rel("CLAUDE.md"), res, { createIfAbsent: false, ...aco });
2812
+ const file = rel(".claude/settings.json");
2813
+ const status = ensureClaudeSettings(join8(root, file));
2814
+ if (status === "created") created.push(file);
2815
+ claudeSettings.push({ file, status });
2816
+ }
2740
2817
  if (host === "github") {
2741
2818
  writeIfAbsent(join8(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
2742
2819
  } else if (host === "azure-devops") {
@@ -2753,7 +2830,7 @@ function initRepo(root, opts = {}) {
2753
2830
  nextSteps.push('.madeby/policy.json is not a valid policy, so the check treats it as advisory. It was left untouched \u2014 fix it (e.g. { "version": 0, "mode": "required" }) to enforce.');
2754
2831
  }
2755
2832
  if (created.includes("AGENTS.md")) {
2756
- nextSteps.push("An AGENTS.md ACO convention was added \u2014 agents that read it disclose authorship (e.g. 'npx madeby@latest ai [tool]'). Tune it to your repo's tools.");
2833
+ nextSteps.push(`An AGENTS.md ACO convention was added \u2014 agents that read it disclose authorship (e.g. 'npx ${PKG} ai [tool]'), with madeby pinned to this release. Tune it to your repo's tools.`);
2757
2834
  }
2758
2835
  if (merged.length > 0) {
2759
2836
  nextSteps.push(`Merged the ACO disclosure rule into your existing ${merged.join(" + ")} \u2014 review the appended "Authorship disclosure (ACO)" section and tune it to your repo.`);
@@ -2762,7 +2839,7 @@ function initRepo(root, opts = {}) {
2762
2839
  nextSteps.push(`Refreshed an outdated ACO convention in ${updated.join(" + ")} to the current version \u2014 review the change before committing.`);
2763
2840
  }
2764
2841
  if (outdated.length > 0) {
2765
- nextSteps.push(`${outdated.join(" + ")} carries an OUTDATED ACO convention \u2014 run 'madeby init --update' to refresh it in place (only the ACO block changes).`);
2842
+ nextSteps.push(`${outdated.join(" + ")} carries an OUTDATED ACO convention (an older convention or an older pinned madeby) \u2014 run 'npx madeby@latest init --update' to refresh it in place (only the ACO block changes).`);
2766
2843
  }
2767
2844
  if (host === "azure-devops") {
2768
2845
  nextSteps.push("In Azure DevOps, create a pipeline from .madeby/azure-pipelines.yml (a PR-only pipeline: no credentials kept, madeby pinned) and add it to the protected branch as a build-validation policy.");
@@ -2773,8 +2850,23 @@ function initRepo(root, opts = {}) {
2773
2850
  nextSteps.push(`When ready to enforce: run 'madeby init --required' (or set "mode": "required" in .madeby/policy.json), then make the check required on the protected branch \u2014 the host's control plane (GitHub: gh api branch protection; Azure: a build-validation policy). See the MCP resource madeby://guide/enforce for exact commands.`);
2774
2851
  }
2775
2852
  nextSteps.push("Contributors disclose origin with a Co-Authored-By / Generated-by trailer (AI), an Authored-by-human trailer ('madeby me'), or an Authored-by-ai trailer ('madeby ai' \u2014 AI authored, committer accountable). A DCO Signed-off-by or a signed commit alone doesn't say who wrote the code, so it counts only if the policy's 'accept' lists it. Evidence upgrade: 'madeby ai --witness' records your own AI session's witnessed spans.");
2853
+ const set = claudeSettings.filter((c) => c.status === "created" || c.status === "merged").map((c) => c.file);
2854
+ const kept = claudeSettings.filter((c) => c.status === "kept").map((c) => c.file);
2855
+ const broken = claudeSettings.filter((c) => c.status === "malformed").map((c) => c.file);
2856
+ if (claudeSettings.length === 0) {
2857
+ nextSteps.push("No Claude Code files found (a CLAUDE.md or .claude/ folder, at any depth), so Claude Code's commit trailer wasn't set. If developers use Claude Code here, run 'madeby init --claude-code'.");
2858
+ }
2859
+ if (set.length > 0) {
2860
+ nextSteps.push(`Claude Code now adds '${CLAUDE_TRAILER}' to its commits when launched where ${set.join(", ")} applies, for everyone \u2014 including developers who turned the trailer off in their personal settings. Their .claude/settings.local.json, or an instruction of their own, can still turn it off; the check is what holds the line.`);
2861
+ }
2862
+ if (kept.length > 0) {
2863
+ nextSteps.push(`${kept.join(", ")} already set${kept.length === 1 ? "s" : ""} Claude Code's commit attribution, so it was left as the team set it. Clear that setting for the default trailer, or set attribution.commit to name the AI.`);
2864
+ }
2865
+ if (broken.length > 0) {
2866
+ nextSteps.push(`${broken.join(", ")} isn't valid JSON, so Claude Code's commit trailer wasn't set there. Fix it and run 'madeby init' again.`);
2867
+ }
2776
2868
  nextSteps.push("Existing history is accounted for honestly, not fabricated: run 'madeby check' over full history to recognize what's already disclosed; pre-adoption commits stay unknown. See madeby://guide/backfill.");
2777
- return { created, skipped, merged, updated, outdated, raised, modelOn, policyMode: policy.mode, recordModel: policy.recordModel, host, nextSteps };
2869
+ return { created, skipped, merged, updated, outdated, raised, modelOn, policyMode: policy.mode, recordModel: policy.recordModel, claudeSettings, host, nextSteps };
2778
2870
  }
2779
2871
  var GH_WORKFLOW = `name: Disclosure
2780
2872
  on: pull_request
@@ -2823,9 +2915,11 @@ function initCommand(args) {
2823
2915
  const recordModel = args.includes("--record-model");
2824
2916
  const hostIdx = args.indexOf("--host");
2825
2917
  const host = hostIdx >= 0 ? args[hostIdx + 1] : void 0;
2826
- const r = initRepo(root, { mode, host, update, recordModel });
2918
+ const claudeCode = args.includes("--claude-code");
2919
+ const r = initRepo(root, { mode, host, update, recordModel, claudeCode });
2827
2920
  for (const f of r.created) console.log(` + ${f}`);
2828
2921
  for (const f of r.merged) console.log(` ~ ${f} (merged in the ACO rule)`);
2922
+ for (const c of r.claudeSettings) if (c.status === "merged") console.log(` ~ ${c.file} (merged in Claude Code's commit trailer)`);
2829
2923
  for (const f of r.updated) console.log(` \u2191 ${f} (ACO convention refreshed to the current version)`);
2830
2924
  for (const f of r.raised) console.log(` \u2191 ${f} (mode raised to required)`);
2831
2925
  for (const f of r.modelOn) console.log(` \u2191 ${f} (now records the model version)`);
@@ -2838,13 +2932,13 @@ madeby init \u2014 policy: ${r.policyMode}${r.recordModel ? " + records model" :
2838
2932
  }
2839
2933
 
2840
2934
  // src/mcp.ts
2841
- import { execFileSync as execFileSync7 } from "node:child_process";
2935
+ import { execFileSync as execFileSync8 } from "node:child_process";
2842
2936
  import { createInterface } from "node:readline";
2843
2937
  var DEFAULT_PROTOCOL = "2024-11-05";
2844
2938
  var SERVER_INFO = { name: "madeby", version: VERSION };
2845
2939
  function rootFor(path) {
2846
2940
  const cwd = path && path.length ? path : process.cwd();
2847
- return execFileSync7("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
2941
+ return execFileSync8("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
2848
2942
  }
2849
2943
  var repoInput = {
2850
2944
  type: "object",
@@ -2864,14 +2958,16 @@ var TOOLS = [
2864
2958
  mode: { enum: ["advisory", "required"], description: "policy mode to seed (default: advisory)" },
2865
2959
  host: { type: "string", description: "CI host to wire: github (default) or azure-devops" },
2866
2960
  update: { type: "boolean", description: "refresh an outdated ACO convention block in place (drift update); default false (report only)" },
2867
- record_model: { type: "boolean", description: "opt in: ask AI disclosures to also name the model version (tool:model) \u2014 policy recordModel + an AGENTS.md line. Never turned off by init; default false." }
2961
+ record_model: { type: "boolean", description: "opt in: ask AI disclosures to also name the model version (tool:model) \u2014 policy recordModel + an AGENTS.md line. Never turned off by init; default false." },
2962
+ claude_code: { type: "boolean", description: "set Claude Code's commit trailer in .claude/settings.json even if the repo shows no sign of Claude Code (it is set automatically when there is a CLAUDE.md or .claude/). A value the team already set is never changed." }
2868
2963
  }
2869
2964
  },
2870
2965
  handler: (args) => initRepo(rootFor(args.path), {
2871
2966
  mode: args.mode,
2872
2967
  host: args.host,
2873
2968
  update: args.update,
2874
- recordModel: args.record_model
2969
+ recordModel: args.record_model,
2970
+ claudeCode: args.claude_code
2875
2971
  })
2876
2972
  },
2877
2973
  {
@@ -2925,9 +3021,9 @@ var TOOLS = [
2925
3021
  const tool = args.tool;
2926
3022
  const model = args.model;
2927
3023
  const category = args.category;
2928
- if (typeof args.for === "string" && args.for) {
3024
+ if (args.for !== void 0 && args.for !== null) {
2929
3025
  const c = category === "ai" ? "ai" : category === "with_ai" || args.with_ai === true ? "with_ai" : "human";
2930
- return discloseFor(root, args.for, { category: c, tool, model: c === "human" ? void 0 : model });
3026
+ return discloseFor(root, typeof args.for === "string" ? args.for : "", { category: c, tool, model: c === "human" ? void 0 : model });
2931
3027
  }
2932
3028
  if (args.print === true) {
2933
3029
  const c = category === "ai" ? "ai" : category === "with_ai" || args.with_ai === true ? "with_ai" : "human";
@@ -3177,6 +3273,8 @@ Usage:
3177
3273
  + the CI disclosure check. --update refreshes an outdated ACO block in
3178
3274
  place; --required to enforce now; --host azure-devops for Azure Pipelines;
3179
3275
  --record-model to also ask agents for the model version (opt-in).
3276
+ In a repo that uses Claude Code (CLAUDE.md or .claude/; --claude-code to
3277
+ force) it also turns Claude Code's commit trailer on in .claude/settings.json.
3180
3278
  madeby check [<range>] The gate: do commits meet .madeby/policy.json? (a verdict \u2014 exit
3181
3279
  code gates). <range> e.g. origin/main..HEAD; in CI it's auto-detected.
3182
3280
  --json for a machine-readable result.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "madeby",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "mcpName": "io.github.MadeByFYI/madeby",
5
5
  "description": "Disclose AI authorship of code. `npx madeby ai` adds an Authored-by-ai git trailer; AI coding agents disclose per the ACO standard (human/with_ai/ai) from an AGENTS.md convention. Disclosure, not detection. Gate PRs with `madeby check`.",
6
6
  "license": "MIT",