madeby 0.3.13 → 0.3.15

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 -0
  2. package/dist/madeby.mjs +276 -51
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -43,6 +43,7 @@ npm i -g madeby
43
43
  ```
44
44
  madeby init Set MadeBy up: write .madeby/policy.json + the CI disclosure check.
45
45
  --record-model (opt-in) also asks agents to name the model version.
46
+ In a repo that uses Claude Code, also turns its commit trailer on.
46
47
  madeby check [<range>] The gate: do commits meet .madeby/policy.json? (a verdict — exit code
47
48
  gates). <range> e.g. origin/main..HEAD — a PR's own commits. --json too.
48
49
  madeby coverage [<range>] The scoreboard: how much of the repo's authorship is disclosed (known)
@@ -148,6 +149,25 @@ of AI disclosure); a sworn declaration is *sworn*. The one asymmetry is evidence
148
149
  leave a re-checkable artifact (a session log, `madeby ai`); human authorship is your firsthand
149
150
  testimony. If you used AI, disclose the AI — don't claim human.
150
151
 
152
+ ### Disclosing earlier commits — `--for`
153
+
154
+ `me` and `ai` amend HEAD. When that isn't possible — the commits are already pushed, or the repo
155
+ forbids amending and rebasing — disclose them with **one new commit** instead:
156
+
157
+ ```sh
158
+ madeby me --with-ai "Claude Code" --for origin/dev..HEAD # your commits on this branch
159
+ madeby ai "Claude Code" --for 4f1c2a9e # one commit
160
+ ```
161
+
162
+ It makes an empty **disclosure commit** whose `Discloses: <hash>` lines name the commits and whose
163
+ trailers say who wrote them. Nothing is rewritten. Push it as usual: on a pull request the check
164
+ re-runs, and it travels with the branch into every target it's merged into. Only your own commits
165
+ (author email = `git config user.email`) that don't disclose yet are included; the rest are listed
166
+ as skipped. The model is named only if you pass `--model`, since the session you're in now didn't
167
+ write those commits. `check`, `coverage` and `who` read disclosure commits by the rules in
168
+ [ACO §5](https://github.com/MadeByFYI/MadeBy/blob/main/ACO.md#5-carriers-bindings--subsume-dont-compete), and `coverage` counts how many
169
+ disclosures arrived late.
170
+
151
171
  **The policy file is optional.** With no `.madeby/policy.json`, `check` defaults to **advisory** —
152
172
  it reports coverage and never blocks, so you can run it (or add the CI check) with zero config. Add
153
173
  the file only when you want to enforce:
@@ -179,6 +199,24 @@ Fail-safe by design: a missing or malformed policy degrades to `advisory`. `made
179
199
  on its own error, and never invents a stricter gate than the maintainer wrote. It never passes a
180
200
  `required` check it couldn't actually run, either: missing history fails it (see `check` above).
181
201
 
202
+ ### Claude Code's own trailer
203
+
204
+ Claude Code adds a `Co-Authored-By` trailer to its commits unless a developer turns it off in their
205
+ personal settings — and then the `AGENTS.md` rule alone may not bring it back. So in a repo that uses
206
+ Claude Code (it has a `CLAUDE.md` or a `.claude/` folder; `--claude-code` to force), `madeby init`
207
+ sets the trailer in the shared `.claude/settings.json`:
208
+
209
+ ```json
210
+ { "attribution": { "commit": "Co-Authored-By: Claude Code <noreply@anthropic.com>" } }
211
+ ```
212
+
213
+ The shared project file overrides a developer's personal settings, so everyone in the repo gets the
214
+ trailer back. It's fixed text — Claude Code's setting has no placeholder for the model — so to record
215
+ the model use `--record-model`. It merges into an existing file and never changes a value the team
216
+ already set, including turning attribution off on purpose. A developer's own
217
+ `.claude/settings.local.json` or instructions can still turn it off; the `required` check is what
218
+ holds the line.
219
+
182
220
  ## Use it as a PR check
183
221
 
184
222
  A ready-made GitHub Action, an Azure DevOps Pipelines template, and a GitLab CI job wrap
package/dist/madeby.mjs CHANGED
@@ -17,19 +17,19 @@ var parseMeta = (rec) => {
17
17
  };
18
18
  };
19
19
  function historyProblem(repoPath, range) {
20
- const git4 = (...args) => {
20
+ const git5 = (...args) => {
21
21
  try {
22
22
  return execFileSync("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
23
23
  } catch {
24
24
  return null;
25
25
  }
26
26
  };
27
- if (git4("rev-parse", "--is-shallow-repository") === "true") {
27
+ if (git5("rev-parse", "--is-shallow-repository") === "true") {
28
28
  return "this is a shallow clone, so the history may be cut short. Fetch the full history (Azure Pipelines: fetchDepth: 0 on the checkout step; GitHub Actions: fetch-depth: 0).";
29
29
  }
30
30
  if (range) {
31
31
  for (const end of range.split(/\.{2,3}/).filter(Boolean)) {
32
- if (git4("rev-parse", "--verify", "--quiet", `${end}^{commit}`) === null) {
32
+ if (git5("rev-parse", "--verify", "--quiet", `${end}^{commit}`) === null) {
33
33
  return `the range ${range} can't be read: ${end} isn't in this clone. Fetch it (full history), or pass a range that exists.`;
34
34
  }
35
35
  }
@@ -971,6 +971,16 @@ function commitDisclosureKinds(commit) {
971
971
  if (commit.signed === true) kinds.push("commit-signature");
972
972
  return kinds;
973
973
  }
974
+ function hasAuthorshipDisclosure(message) {
975
+ return commitDisclosureKinds({ message }).some((k) => DISCLOSURE_KINDS[k].authorship);
976
+ }
977
+ var DISCLOSES_RE = /^[ \t]*Discloses[ \t]*:[ \t]*([0-9a-f]{64}|[0-9a-f]{40})[ \t]*$/gim;
978
+ function disclosedTargets(message) {
979
+ return [...new Set([...message.matchAll(DISCLOSES_RE)].map((m) => m[1].toLowerCase()))];
980
+ }
981
+ function disclosureCommitTrailers(message) {
982
+ return message.split("\n").slice(1).filter((l) => !/^[ \t]*Discloses[ \t]*:/i.test(l)).join("\n").trim();
983
+ }
974
984
 
975
985
  // ../core/src/policy.ts
976
986
  var DEFAULT_POLICY = { version: 0, mode: "advisory" };
@@ -1070,13 +1080,85 @@ registerHostAdapter(githubAdapter);
1070
1080
  // ../analyzer/src/read-disclosures.ts
1071
1081
  import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1072
1082
  import { join } from "node:path";
1083
+
1084
+ // ../analyzer/src/late-disclosure.ts
1085
+ import { execFileSync as execFileSync2 } from "node:child_process";
1086
+ var FIELD2 = "";
1087
+ var RECORD2 = "";
1088
+ var git = (root, args) => {
1089
+ try {
1090
+ return execFileSync2("git", ["-C", root, ...args], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
1091
+ } catch {
1092
+ return null;
1093
+ }
1094
+ };
1095
+ function rangeHead(range) {
1096
+ if (!range) return "HEAD";
1097
+ const parts = range.split(/\.{2,3}/);
1098
+ return parts[parts.length - 1] || "HEAD";
1099
+ }
1100
+ function readDisclosureCommits(root, head = "HEAD") {
1101
+ const out = { applied: /* @__PURE__ */ new Map(), disclosureCommits: /* @__PURE__ */ new Set(), rejected: [] };
1102
+ const log = git(root, ["log", "--no-merges", "-E", "--grep=^[ ]*Discloses[ ]*:[ ]*[0-9a-f]{40}", `--format=%H${FIELD2}%ae${FIELD2}%B${RECORD2}`, head]);
1103
+ if (!log) return out;
1104
+ const candidates = log.split(RECORD2).map((r) => r.replace(/^\s+/, "")).filter(Boolean).map((r) => {
1105
+ const [sha = "", email = "", ...msg] = r.split(FIELD2);
1106
+ return { sha, email: email.toLowerCase(), message: msg.join(FIELD2) };
1107
+ });
1108
+ for (const d of candidates) {
1109
+ const targets = disclosedTargets(d.message);
1110
+ if (targets.length === 0) continue;
1111
+ const changed = git(root, ["diff-tree", "--root", "--no-commit-id", "--name-only", "-r", d.sha]);
1112
+ if (changed === null || changed.trim() !== "") continue;
1113
+ out.disclosureCommits.add(d.sha);
1114
+ const trailers = disclosureCommitTrailers(d.message);
1115
+ const emails = /* @__PURE__ */ new Map();
1116
+ const shown = git(root, ["show", "-s", "--format=%H %ae", ...targets]) ?? targets.map((t) => git(root, ["show", "-s", "--format=%H %ae", t]) ?? "").join("\n");
1117
+ for (const line of shown.split("\n")) {
1118
+ const [sha, email] = line.trim().split(" ");
1119
+ if (sha && email !== void 0) emails.set(sha, email.toLowerCase());
1120
+ }
1121
+ for (const t of targets) {
1122
+ if (out.applied.has(t)) continue;
1123
+ const email = emails.get(t);
1124
+ if (email === void 0) out.rejected.push({ sha: t, via: d.sha, reason: "unknown-commit" });
1125
+ else if (email !== d.email) out.rejected.push({ sha: t, via: d.sha, reason: "other-author" });
1126
+ else if (git(root, ["merge-base", "--is-ancestor", t, d.sha]) === null) out.rejected.push({ sha: t, via: d.sha, reason: "not-an-ancestor" });
1127
+ else out.applied.set(t, { via: d.sha, trailers });
1128
+ }
1129
+ }
1130
+ return out;
1131
+ }
1132
+ function applyLateDisclosures(commits, d) {
1133
+ const out = [];
1134
+ for (const c of commits) {
1135
+ const sha = (c.sha ?? "").toLowerCase();
1136
+ if (d.disclosureCommits.has(sha)) continue;
1137
+ const late = d.applied.get(sha);
1138
+ if (late && !hasAuthorshipDisclosure(c.message)) out.push({ ...c, message: `${c.message}
1139
+
1140
+ ${late.trailers}`, late: late.via });
1141
+ else out.push(c);
1142
+ }
1143
+ return out;
1144
+ }
1145
+
1146
+ // ../analyzer/src/read-disclosures.ts
1073
1147
  function readDisclosures(root, opts = {}) {
1074
- const commits = readGitLog(root, opts.range ? void 0 : opts.limit ?? 200, opts.range);
1075
- return commits.map((c) => ({
1076
- sha: c.sha ?? "",
1077
- subject: c.message.split("\n")[0] ?? "",
1078
- disclosures: commitDisclosureKinds({ message: c.message, signed: c.signed })
1079
- }));
1148
+ return readDisclosuresWithRejects(root, opts).commits;
1149
+ }
1150
+ function readDisclosuresWithRejects(root, opts) {
1151
+ const late = readDisclosureCommits(root, rangeHead(opts.range));
1152
+ const commits = applyLateDisclosures(readGitLog(root, opts.range ? void 0 : opts.limit ?? 200, opts.range), late);
1153
+ return {
1154
+ commits: commits.map((c) => ({
1155
+ sha: c.sha ?? "",
1156
+ subject: c.message.split("\n")[0] ?? "",
1157
+ disclosures: commitDisclosureKinds({ message: c.message, signed: c.signed }),
1158
+ ...c.late ? { late: c.late } : {}
1159
+ })),
1160
+ rejected: late.rejected
1161
+ };
1080
1162
  }
1081
1163
  function evaluateRepoDisclosure(root, opts = {}) {
1082
1164
  let policy = DEFAULT_POLICY;
@@ -1089,7 +1171,7 @@ function evaluateRepoDisclosure(root, opts = {}) {
1089
1171
  warning = ".madeby/policy.json is not valid JSON \u2014 degrading to advisory (report-only, never blocks).";
1090
1172
  }
1091
1173
  }
1092
- const commits = readDisclosures(root, opts);
1174
+ const { commits, rejected } = readDisclosuresWithRejects(root, opts);
1093
1175
  const disclosures = commits.map((c) => ({ ref: (c.sha || "").slice(0, 8) || "(unknown)", kinds: c.disclosures }));
1094
1176
  const result = evaluateDisclosurePolicy(policy, disclosures);
1095
1177
  const subjectFor = (ref) => commits.find((c) => (c.sha || "").startsWith(ref))?.subject ?? "";
@@ -1099,12 +1181,13 @@ function evaluateRepoDisclosure(root, opts = {}) {
1099
1181
  summary: result.summary,
1100
1182
  total: disclosures.length,
1101
1183
  undisclosed: result.nonCompliant.map((v) => ({ ref: v.ref, subject: subjectFor(v.ref) })),
1184
+ lateRejected: rejected.filter((r) => commits.some((c) => c.sha === r.sha)),
1102
1185
  ...warning ? { warning } : {}
1103
1186
  };
1104
1187
  }
1105
1188
 
1106
1189
  // ../analyzer/src/path-provenance.ts
1107
- import { execFileSync as execFileSync2 } from "node:child_process";
1190
+ import { execFileSync as execFileSync3 } from "node:child_process";
1108
1191
  import { readFileSync as readFileSync4, existsSync as existsSync2 } from "node:fs";
1109
1192
  import { join as join3 } from "node:path";
1110
1193
 
@@ -1141,13 +1224,13 @@ function readSpanManifestsFromDir(repoDir) {
1141
1224
 
1142
1225
  // ../analyzer/src/path-provenance.ts
1143
1226
  var overlaps = (aS, aE, bS, bE) => bS === void 0 || bE === void 0 || aS <= bE && aE >= bS;
1144
- function git(root, args) {
1145
- return execFileSync2("git", ["-C", root, "-c", "core.quotePath=false", ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
1227
+ function git2(root, args) {
1228
+ return execFileSync3("git", ["-C", root, "-c", "core.quotePath=false", ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
1146
1229
  }
1147
1230
  var US = "";
1148
1231
  function commitMeta(root, sha) {
1149
1232
  try {
1150
- const out = git(root, ["show", "-s", `--format=%G?${US}%an${US}%ae${US}%B`, sha]);
1233
+ const out = git2(root, ["show", "-s", `--format=%G?${US}%an${US}%ae${US}%B`, sha]);
1151
1234
  const parts = out.split(US);
1152
1235
  const sig = parts[0] ?? "N";
1153
1236
  return { signed: sig !== "N" && sig !== "", name: parts[1] ?? "", email: parts[2] ?? "", message: (parts.slice(3).join(US) ?? "").trim() };
@@ -1217,12 +1300,17 @@ function provenanceOf(root, path, opts = {}) {
1217
1300
  if (opts.useBlame !== false) {
1218
1301
  try {
1219
1302
  const range = opts.startLine !== void 0 ? [`-L`, `${opts.startLine},${opts.endLine ?? opts.startLine}`] : [];
1220
- const porcelain = git(root, ["blame", "--porcelain", ...range, "--", path]);
1303
+ const porcelain = git2(root, ["blame", "--porcelain", ...range, "--", path]);
1221
1304
  const metaCache = /* @__PURE__ */ new Map();
1305
+ const late = readDisclosureCommits(root);
1222
1306
  for (const g of blameGroups(porcelain)) {
1223
1307
  let meta = metaCache.get(g.sha);
1224
1308
  if (meta === void 0) {
1225
1309
  meta = commitMeta(root, g.sha);
1310
+ const filled = meta && late.applied.get(g.sha.toLowerCase());
1311
+ if (meta && filled && !hasAuthorshipDisclosure(meta.message)) meta = { ...meta, message: `${meta.message}
1312
+
1313
+ ${filled.trailers}` };
1226
1314
  metaCache.set(g.sha, meta);
1227
1315
  }
1228
1316
  if (!meta) continue;
@@ -1273,7 +1361,7 @@ function provenanceMap(root, opts = {}) {
1273
1361
  const witnessed = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
1274
1362
  let trackedFiles = 0;
1275
1363
  try {
1276
- trackedFiles = git(root, ["ls-files", ...prefix ? ["--", prefix] : []]).split("\n").filter(Boolean).length;
1364
+ trackedFiles = git2(root, ["ls-files", ...prefix ? ["--", prefix] : []]).split("\n").filter(Boolean).length;
1277
1365
  } catch {
1278
1366
  }
1279
1367
  const commits = readDisclosures(root, { limit: opts.commitLimit ?? 500 });
@@ -1313,10 +1401,11 @@ function repoCoverage(root, opts = {}) {
1313
1401
  const oldest = authored.at(-1)?.sha;
1314
1402
  const since = limit && oldest && authored.length >= limit ? commitTime(root, oldest) : void 0;
1315
1403
  const merges = authored.length > 0 && (!limit || authored.length < limit || since !== void 0) ? readAuthoredMerges(root, { range: opts.range, since }) : [];
1316
- const commits = [...authored, ...merges];
1404
+ const commits = applyLateDisclosures([...authored, ...merges], readDisclosureCommits(root, rangeHead(opts.range)));
1317
1405
  const byCategory = { human: 0, with_ai: 0, ai: 0, bot: 0 };
1318
1406
  const byTier = { asserted: 0, verified: 0 };
1319
1407
  let known = 0;
1408
+ let late = 0;
1320
1409
  const agents = /* @__PURE__ */ new Map();
1321
1410
  const authors = /* @__PURE__ */ new Map();
1322
1411
  for (const c of commits) {
@@ -1331,6 +1420,7 @@ function repoCoverage(root, opts = {}) {
1331
1420
  author.known += 1;
1332
1421
  author.byCategory[category] += 1;
1333
1422
  known += 1;
1423
+ if (c.late) late += 1;
1334
1424
  byCategory[category] += 1;
1335
1425
  if (c.signed) byTier.verified += 1;
1336
1426
  else byTier.asserted += 1;
@@ -1350,11 +1440,12 @@ function repoCoverage(root, opts = {}) {
1350
1440
  }
1351
1441
  return {
1352
1442
  isEstimate: true,
1353
- methodology: "commit-level authorship disclosure (estimate); undisclosed = unknown, never inferred; merges scored only when they change content of their own",
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",
1354
1444
  scope: (opts.range ? `range ${opts.range}` : `last ${authored.length} commits`) + (merges.length ? ` + ${merges.length} merges with changes of their own` : ""),
1355
1445
  total: commits.length,
1356
1446
  known,
1357
1447
  unknown: commits.length - known,
1448
+ late,
1358
1449
  byCategory,
1359
1450
  byTier,
1360
1451
  byAgent: [...agents.values()].sort((a, b) => b.commits - a.commits || a.name.localeCompare(b.name)),
@@ -1363,7 +1454,7 @@ function repoCoverage(root, opts = {}) {
1363
1454
  }
1364
1455
 
1365
1456
  // ../analyzer/src/record-spans.ts
1366
- import { execFileSync as execFileSync3 } from "node:child_process";
1457
+ import { execFileSync as execFileSync4 } from "node:child_process";
1367
1458
  import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync, writeFileSync } from "node:fs";
1368
1459
  import { homedir } from "node:os";
1369
1460
  import { dirname, join as join4 } from "node:path";
@@ -1696,8 +1787,8 @@ ${body}
1696
1787
  }
1697
1788
 
1698
1789
  // ../analyzer/src/record-spans.ts
1699
- function git2(root, ...a) {
1700
- return execFileSync3("git", ["-C", root, "-c", "core.quotePath=false", ...a], { encoding: "utf8" }).trim();
1790
+ function git3(root, ...a) {
1791
+ return execFileSync4("git", ["-C", root, "-c", "core.quotePath=false", ...a], { encoding: "utf8" }).trim();
1701
1792
  }
1702
1793
  function autodiscover(root) {
1703
1794
  const enc3 = root.replace(/[/_]/g, "-");
@@ -1713,7 +1804,7 @@ function recordAiSpans(root, opts = {}) {
1713
1804
  if (!transcriptPath || !existsSync3(transcriptPath)) {
1714
1805
  return { written: false, matched: 0, discarded: 0, message: "transcript not found \u2014 pass a transcript path", error: "transcript-not-found" };
1715
1806
  }
1716
- const commit = git2(root, "rev-parse", opts.ref ?? "HEAD");
1807
+ const commit = git3(root, "rev-parse", opts.ref ?? "HEAD");
1717
1808
  const transcriptText = readFileSync5(transcriptPath, "utf8");
1718
1809
  const parser = detectParser(transcriptText);
1719
1810
  if (!parser) {
@@ -1731,7 +1822,7 @@ function recordAiSpans(root, opts = {}) {
1731
1822
  }
1732
1823
  },
1733
1824
  commit,
1734
- operatorId: git2(root, "show", "-s", "--format=%ae", commit),
1825
+ operatorId: git3(root, "show", "-s", "--format=%ae", commit),
1735
1826
  now: opts.now ?? (/* @__PURE__ */ new Date()).toISOString()
1736
1827
  });
1737
1828
  if (result.manifest.attestations.length === 0) {
@@ -1850,10 +1941,10 @@ var gitlabAdapter = {
1850
1941
  registerHostAdapter(gitlabAdapter);
1851
1942
 
1852
1943
  // src/scope.ts
1853
- import { execFileSync as execFileSync4 } from "node:child_process";
1944
+ import { execFileSync as execFileSync5 } from "node:child_process";
1854
1945
  function repoRoot() {
1855
1946
  try {
1856
- return execFileSync4("git", ["-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
1947
+ return execFileSync5("git", ["-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
1857
1948
  } catch {
1858
1949
  return null;
1859
1950
  }
@@ -1905,16 +1996,20 @@ Undisclosed commits (${result.undisclosed.length}):`);
1905
1996
  console.log(` \u2717 ${v.ref} ${v.subject.slice(0, 60)}`);
1906
1997
  }
1907
1998
  if (result.undisclosed.length > 20) console.log(` \u2026 and ${result.undisclosed.length - 20} more`);
1999
+ const why = { "other-author": "it names another author's commit", "not-an-ancestor": "it names a commit that isn't before it", "unknown-commit": "it names a commit that isn't here" };
2000
+ for (const r of result.lateRejected.slice(0, 20)) {
2001
+ console.log(` ! ${r.sha.slice(0, 8)} is named by disclosure commit ${r.via.slice(0, 8)}, which can't disclose it: ${why[r.reason]}.`);
2002
+ }
1908
2003
  console.log(
1909
2004
  `
1910
- To disclose who wrote it: 'madeby ai <tool>' (an AI wrote it), 'madeby me --with-ai <tool>' (you, with AI), or 'madeby me' (you alone); a Co-Authored-By trailer naming an AI tool also counts. A DCO sign-off or a signature alone doesn't say who wrote the code. (Disclosure, not a ban \u2014 what this project requires is set in .madeby/policy.json.)`
2005
+ To disclose who wrote it: 'madeby ai <tool>' (an AI wrote it), 'madeby me --with-ai <tool>' (you, with AI), or 'madeby me' (you alone); a Co-Authored-By trailer naming an AI tool also counts. A DCO sign-off or a signature alone doesn't say who wrote the code. Already pushed, or no amending allowed? Add --for <range> (e.g. madeby me --with-ai <tool> --for origin/main..HEAD): one new commit discloses the earlier ones, nothing rewritten. (Disclosure, not a ban \u2014 what this project requires is set in .madeby/policy.json.)`
1911
2006
  );
1912
2007
  }
1913
2008
  process.exit(result.pass ? 0 : 1);
1914
2009
  }
1915
2010
 
1916
2011
  // src/commands/me.ts
1917
- import { execFileSync as execFileSync5 } from "node:child_process";
2012
+ import { execFileSync as execFileSync6 } from "node:child_process";
1918
2013
  import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
1919
2014
  import { homedir as homedir3 } from "node:os";
1920
2015
  import { join as join6 } from "node:path";
@@ -2033,8 +2128,8 @@ function parseWithAi(args) {
2033
2128
  const next = args[i + 1];
2034
2129
  return { withAi: true, tool: next && !next.startsWith("-") ? next : "AI" };
2035
2130
  }
2036
- function git3(root, args) {
2037
- return execFileSync5("git", ["-C", root, ...args], { encoding: "utf8" }).trim();
2131
+ function git4(root, args) {
2132
+ return execFileSync6("git", ["-C", root, ...args], { encoding: "utf8" }).trim();
2038
2133
  }
2039
2134
  var HUMAN_ATTEST2 = /^[ \t]*(?:Authored-by-human|Human-authored(?:-by)?)[ \t]*:/im;
2040
2135
  var ASSIST = /^[ \t]*Assisted-by[ \t]*:/im;
@@ -2060,15 +2155,15 @@ function attestAiAuthorship(root, opts = {}) {
2060
2155
  const tool = ai.value;
2061
2156
  const base = { ok: false, changed: false, withAi: false, tool, model: ai.model, modelSource: ai.modelSource, added: [], category: "ai" };
2062
2157
  try {
2063
- execFileSync5("git", ["-C", root, "diff", "--cached", "--quiet"]);
2158
+ execFileSync6("git", ["-C", root, "diff", "--cached", "--quiet"]);
2064
2159
  } catch {
2065
2160
  return { ...base, reason: "staged-changes" };
2066
2161
  }
2067
2162
  let subject = "";
2068
2163
  let message = "";
2069
2164
  try {
2070
- subject = git3(root, ["show", "-s", "--format=%s", "HEAD"]);
2071
- message = git3(root, ["show", "-s", "--format=%B", "HEAD"]);
2165
+ subject = git4(root, ["show", "-s", "--format=%s", "HEAD"]);
2166
+ message = git4(root, ["show", "-s", "--format=%B", "HEAD"]);
2072
2167
  } catch {
2073
2168
  return { ...base, reason: "no-commit" };
2074
2169
  }
@@ -2079,7 +2174,7 @@ function attestAiAuthorship(root, opts = {}) {
2079
2174
  added.push("Authored-by-ai");
2080
2175
  }
2081
2176
  if (trailers.length > 0) {
2082
- execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", "--allow-empty", ...trailers]);
2177
+ execFileSync6("git", ["-C", root, "commit", "--amend", "--no-edit", "--allow-empty", ...trailers]);
2083
2178
  }
2084
2179
  return { ...base, ok: true, changed: trailers.length > 0, subject, added };
2085
2180
  }
@@ -2091,8 +2186,8 @@ function disclosureTrailers(root, opts) {
2091
2186
  let name = "";
2092
2187
  let email = "";
2093
2188
  try {
2094
- name = git3(root, ["config", "user.name"]);
2095
- email = git3(root, ["config", "user.email"]);
2189
+ name = git4(root, ["config", "user.name"]);
2190
+ email = git4(root, ["config", "user.email"]);
2096
2191
  } catch {
2097
2192
  }
2098
2193
  if (!name || !email) return { ok: false, reason: "no-identity", lines: [] };
@@ -2109,21 +2204,21 @@ function affirmAuthorship(root, opts = {}) {
2109
2204
  let name = "";
2110
2205
  let email = "";
2111
2206
  try {
2112
- name = git3(root, ["config", "user.name"]);
2113
- email = git3(root, ["config", "user.email"]);
2207
+ name = git4(root, ["config", "user.name"]);
2208
+ email = git4(root, ["config", "user.email"]);
2114
2209
  } catch {
2115
2210
  }
2116
2211
  if (!name || !email) return { ...base, reason: "no-identity" };
2117
2212
  try {
2118
- execFileSync5("git", ["-C", root, "diff", "--cached", "--quiet"]);
2213
+ execFileSync6("git", ["-C", root, "diff", "--cached", "--quiet"]);
2119
2214
  } catch {
2120
2215
  return { ...base, reason: "staged-changes", name, email };
2121
2216
  }
2122
2217
  let subject = "";
2123
2218
  let message = "";
2124
2219
  try {
2125
- subject = git3(root, ["show", "-s", "--format=%s", "HEAD"]);
2126
- message = git3(root, ["show", "-s", "--format=%B", "HEAD"]);
2220
+ subject = git4(root, ["show", "-s", "--format=%s", "HEAD"]);
2221
+ message = git4(root, ["show", "-s", "--format=%B", "HEAD"]);
2127
2222
  } catch {
2128
2223
  return { ...base, reason: "no-commit" };
2129
2224
  }
@@ -2140,11 +2235,84 @@ function affirmAuthorship(root, opts = {}) {
2140
2235
  added.push("Assisted-by");
2141
2236
  }
2142
2237
  if (trailers.length > 0) {
2143
- execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", "--allow-empty", ...trailers]);
2238
+ execFileSync6("git", ["-C", root, "commit", "--amend", "--no-edit", "--allow-empty", ...trailers]);
2144
2239
  }
2145
2240
  const category = hasAssist || added.includes("Assisted-by") ? "with_ai" : "human";
2146
2241
  return { ...base, ok: true, changed: trailers.length > 0, name, email, subject, added, category };
2147
2242
  }
2243
+ function parseFor(args) {
2244
+ const i = args.findIndex((a2) => a2 === "--for" || a2.startsWith("--for="));
2245
+ if (i === -1) return void 0;
2246
+ const a = args[i];
2247
+ const v = a.startsWith("--for=") ? a.slice("--for=".length) : args[i + 1];
2248
+ return v && !v.startsWith("-") ? v : void 0;
2249
+ }
2250
+ function discloseFor(root, target, opts) {
2251
+ const none = { disclosed: [], skipped: [], lines: [] };
2252
+ let email = "";
2253
+ try {
2254
+ email = git4(root, ["config", "user.email"]).toLowerCase();
2255
+ } catch {
2256
+ }
2257
+ if (!email) return { ok: false, reason: "no-identity", ...none };
2258
+ try {
2259
+ execFileSync6("git", ["-C", root, "diff", "--cached", "--quiet"]);
2260
+ } catch {
2261
+ return { ok: false, reason: "staged-changes", ...none };
2262
+ }
2263
+ let commits;
2264
+ if (target.includes("..")) {
2265
+ commits = [...readGitLog(root, void 0, target), ...readAuthoredMerges(root, { range: target })];
2266
+ } else {
2267
+ try {
2268
+ const sha = git4(root, ["rev-parse", "--verify", "--quiet", `${target}^{commit}`]);
2269
+ commits = readGitLog(root, 1, sha);
2270
+ if (commits[0]?.sha !== sha) commits = readAuthoredMerges(root, { range: `${sha}^!` });
2271
+ } catch {
2272
+ return { ok: false, reason: "bad-target", ...none };
2273
+ }
2274
+ }
2275
+ if (commits.length === 0) return { ok: false, reason: "bad-target", ...none };
2276
+ const later = readDisclosureCommits(root);
2277
+ const disclosed = [];
2278
+ const skipped = [];
2279
+ for (const c of commits) {
2280
+ const sha = (c.sha ?? "").toLowerCase();
2281
+ const subject2 = c.message.split("\n")[0] ?? "";
2282
+ if (later.disclosureCommits.has(sha)) continue;
2283
+ if ((c.authorEmail ?? "").toLowerCase() !== email) skipped.push({ sha, subject: subject2, reason: "not-yours" });
2284
+ else if (hasAuthorshipDisclosure(c.message)) skipped.push({ sha, subject: subject2, reason: "already-discloses" });
2285
+ else if (later.applied.has(sha)) skipped.push({ sha, subject: subject2, reason: "already-disclosed-later" });
2286
+ else disclosed.push({ sha, subject: subject2 });
2287
+ }
2288
+ const t = disclosureTrailers(root, { category: opts.category, tool: opts.tool, model: opts.model, env: {}, home: "/nonexistent" });
2289
+ if (!t.ok) return { ok: false, reason: "no-identity", ...none };
2290
+ if (disclosed.length === 0) return { ok: true, disclosed, skipped, lines: t.lines };
2291
+ const n = disclosed.length;
2292
+ const subject = `Disclose authorship of ${n} earlier commit${n === 1 ? "" : "s"}`;
2293
+ const body = [...disclosed.map((d) => `Discloses: ${d.sha}`), ...t.lines].join("\n");
2294
+ execFileSync6("git", ["-C", root, "commit", "--allow-empty", "-m", subject, "-m", body], { stdio: ["ignore", "ignore", "pipe"] });
2295
+ return { ok: true, commit: git4(root, ["rev-parse", "HEAD"]), disclosed, skipped, lines: t.lines };
2296
+ }
2297
+ function renderDiscloseFor(cmd2, target, r) {
2298
+ if (!r.ok) {
2299
+ if (r.reason === "no-identity") console.error(`madeby ${cmd2}: set your git identity first (git config user.name / user.email).`);
2300
+ 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`);
2302
+ process.exit(1);
2303
+ return;
2304
+ }
2305
+ const why = { "not-yours": "another author's \u2014 they disclose their own", "already-discloses": "its message already discloses", "already-disclosed-later": "a disclosure commit already covers it" };
2306
+ for (const s of r.skipped) console.log(` skipped ${s.sha.slice(0, 8)} ${s.subject.slice(0, 50)} (${why[s.reason]})`);
2307
+ if (!r.commit) {
2308
+ console.log(`madeby ${cmd2}: nothing to disclose in ${target}.`);
2309
+ return;
2310
+ }
2311
+ console.log(
2312
+ `madeby ${cmd2}: made disclosure commit ${r.commit.slice(0, 8)} for ${r.disclosed.length} earlier commit${r.disclosed.length === 1 ? "" : "s"} (${r.lines.join("; ")}).
2313
+ Nothing was rewritten. Push it as usual; on a pull request the check re-runs.`
2314
+ );
2315
+ }
2148
2316
  function meCommand(args) {
2149
2317
  const { withAi, tool } = parseWithAi(args);
2150
2318
  const model = parseModel(args);
@@ -2159,6 +2327,11 @@ function meCommand(args) {
2159
2327
  process.exit(2);
2160
2328
  return;
2161
2329
  }
2330
+ const forTarget = parseFor(args);
2331
+ if (forTarget) {
2332
+ renderDiscloseFor("me", forTarget, discloseFor(root, forTarget, { category: withAi ? "with_ai" : "human", tool, model }));
2333
+ return;
2334
+ }
2162
2335
  if (args.includes("--print")) {
2163
2336
  const t = disclosureTrailers(root, { category: withAi ? "with_ai" : "human", tool, model });
2164
2337
  if (!t.ok) {
@@ -2201,7 +2374,7 @@ function aiCommand(args) {
2201
2374
  const makeSample = args.includes("--make-parser-sample");
2202
2375
  const witness = args.includes("--witness");
2203
2376
  const model = parseModel(args);
2204
- const positional = args.filter((a, i) => !a.startsWith("--") && !(args[i - 1] === "--model"));
2377
+ const positional = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--model" && args[i - 1] !== "--for");
2205
2378
  if (makeSample) {
2206
2379
  const s = writeParserSample(root, { transcriptPath: positional[0] });
2207
2380
  if (s.error === "transcript-not-found") {
@@ -2243,6 +2416,11 @@ function aiCommand(args) {
2243
2416
  return;
2244
2417
  }
2245
2418
  const tool = positional[0] || "AI";
2419
+ const forTarget = parseFor(args);
2420
+ if (forTarget) {
2421
+ renderDiscloseFor("ai", forTarget, discloseFor(root, forTarget, { category: "ai", tool, model }));
2422
+ return;
2423
+ }
2246
2424
  if (args.includes("--print")) {
2247
2425
  console.log(disclosureTrailers(root, { category: "ai", tool, model }).lines.join("\n"));
2248
2426
  return;
@@ -2413,7 +2591,7 @@ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as rea
2413
2591
  import { dirname as dirname2, join as join8 } from "node:path";
2414
2592
 
2415
2593
  // src/version.ts
2416
- var VERSION = "0.3.13";
2594
+ var VERSION = "0.3.15";
2417
2595
 
2418
2596
  // src/init-repo.ts
2419
2597
  var ACO_VERSION = 2;
@@ -2509,6 +2687,31 @@ function ensurePolicy(abs, rel, want, res) {
2509
2687
  }
2510
2688
  return { mode: raise ? "required" : current.mode, recordModel: modelOn || current.recordModel === true, malformed: !isObject };
2511
2689
  }
2690
+ var CLAUDE_TRAILER = "Co-Authored-By: Claude Code <noreply@anthropic.com>";
2691
+ function ensureClaudeSettings(root, force) {
2692
+ const abs = join8(root, ".claude", "settings.json");
2693
+ const usesClaude = force || existsSync7(join8(root, "CLAUDE.md")) || existsSync7(join8(root, ".claude"));
2694
+ if (!usesClaude) return "not-used";
2695
+ if (!existsSync7(abs)) {
2696
+ mkdirSync2(dirname2(abs), { recursive: true });
2697
+ writeFileSync2(abs, JSON.stringify({ attribution: { commit: CLAUDE_TRAILER } }, null, 2) + "\n");
2698
+ return "created";
2699
+ }
2700
+ let raw;
2701
+ try {
2702
+ raw = JSON.parse(readFileSync9(abs, "utf8"));
2703
+ } catch {
2704
+ return "malformed";
2705
+ }
2706
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "malformed";
2707
+ const settings = raw;
2708
+ const attribution = settings.attribution;
2709
+ if (attribution === false || attribution && typeof attribution === "object" && "commit" in attribution) return "kept";
2710
+ if (attribution === void 0 && settings.includeCoAuthoredBy === false) return "kept";
2711
+ const next = { ...settings, attribution: { ...attribution && typeof attribution === "object" ? attribution : {}, commit: CLAUDE_TRAILER } };
2712
+ writeFileSync2(abs, JSON.stringify(next, null, 2) + "\n");
2713
+ return "merged";
2714
+ }
2512
2715
  function ensureAco(abs, rel, res, opts) {
2513
2716
  const block = acoBlock(opts.recordModel);
2514
2717
  if (!existsSync7(abs)) {
@@ -2559,6 +2762,8 @@ function initRepo(root, opts = {}) {
2559
2762
  const aco = { update, recordModel: policy.recordModel, modelRequested };
2560
2763
  ensureAco(join8(root, "AGENTS.md"), "AGENTS.md", res, { createIfAbsent: true, ...aco });
2561
2764
  ensureAco(join8(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false, ...aco });
2765
+ const claudeSettings = ensureClaudeSettings(root, opts.claudeCode ?? false);
2766
+ if (claudeSettings === "created") created.push(".claude/settings.json");
2562
2767
  if (host === "github") {
2563
2768
  writeIfAbsent(join8(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
2564
2769
  } else if (host === "azure-devops") {
@@ -2595,8 +2800,15 @@ function initRepo(root, opts = {}) {
2595
2800
  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.`);
2596
2801
  }
2597
2802
  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.");
2803
+ if (claudeSettings === "created" || claudeSettings === "merged") {
2804
+ nextSteps.push(`Claude Code now adds '${CLAUDE_TRAILER}' to its commits in this repo (.claude/settings.json), 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.`);
2805
+ } else if (claudeSettings === "kept") {
2806
+ nextSteps.push("This repo's .claude/settings.json already sets 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.");
2807
+ } else if (claudeSettings === "malformed") {
2808
+ nextSteps.push(".claude/settings.json isn't valid JSON, so Claude Code's commit trailer wasn't set. Fix the file and run 'madeby init' again.");
2809
+ }
2598
2810
  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.");
2599
- return { created, skipped, merged, updated, outdated, raised, modelOn, policyMode: policy.mode, recordModel: policy.recordModel, host, nextSteps };
2811
+ return { created, skipped, merged, updated, outdated, raised, modelOn, policyMode: policy.mode, recordModel: policy.recordModel, claudeSettings, host, nextSteps };
2600
2812
  }
2601
2813
  var GH_WORKFLOW = `name: Disclosure
2602
2814
  on: pull_request
@@ -2645,9 +2857,11 @@ function initCommand(args) {
2645
2857
  const recordModel = args.includes("--record-model");
2646
2858
  const hostIdx = args.indexOf("--host");
2647
2859
  const host = hostIdx >= 0 ? args[hostIdx + 1] : void 0;
2648
- const r = initRepo(root, { mode, host, update, recordModel });
2860
+ const claudeCode = args.includes("--claude-code");
2861
+ const r = initRepo(root, { mode, host, update, recordModel, claudeCode });
2649
2862
  for (const f of r.created) console.log(` + ${f}`);
2650
2863
  for (const f of r.merged) console.log(` ~ ${f} (merged in the ACO rule)`);
2864
+ if (r.claudeSettings === "merged") console.log(" ~ .claude/settings.json (merged in Claude Code's commit trailer)");
2651
2865
  for (const f of r.updated) console.log(` \u2191 ${f} (ACO convention refreshed to the current version)`);
2652
2866
  for (const f of r.raised) console.log(` \u2191 ${f} (mode raised to required)`);
2653
2867
  for (const f of r.modelOn) console.log(` \u2191 ${f} (now records the model version)`);
@@ -2660,13 +2874,13 @@ madeby init \u2014 policy: ${r.policyMode}${r.recordModel ? " + records model" :
2660
2874
  }
2661
2875
 
2662
2876
  // src/mcp.ts
2663
- import { execFileSync as execFileSync6 } from "node:child_process";
2877
+ import { execFileSync as execFileSync7 } from "node:child_process";
2664
2878
  import { createInterface } from "node:readline";
2665
2879
  var DEFAULT_PROTOCOL = "2024-11-05";
2666
2880
  var SERVER_INFO = { name: "madeby", version: VERSION };
2667
2881
  function rootFor(path) {
2668
2882
  const cwd = path && path.length ? path : process.cwd();
2669
- return execFileSync6("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
2883
+ return execFileSync7("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
2670
2884
  }
2671
2885
  var repoInput = {
2672
2886
  type: "object",
@@ -2686,14 +2900,16 @@ var TOOLS = [
2686
2900
  mode: { enum: ["advisory", "required"], description: "policy mode to seed (default: advisory)" },
2687
2901
  host: { type: "string", description: "CI host to wire: github (default) or azure-devops" },
2688
2902
  update: { type: "boolean", description: "refresh an outdated ACO convention block in place (drift update); default false (report only)" },
2689
- 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." }
2903
+ 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." },
2904
+ 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." }
2690
2905
  }
2691
2906
  },
2692
2907
  handler: (args) => initRepo(rootFor(args.path), {
2693
2908
  mode: args.mode,
2694
2909
  host: args.host,
2695
2910
  update: args.update,
2696
- recordModel: args.record_model
2911
+ recordModel: args.record_model,
2912
+ claudeCode: args.claude_code
2697
2913
  })
2698
2914
  },
2699
2915
  {
@@ -2729,7 +2945,7 @@ var TOOLS = [
2729
2945
  },
2730
2946
  {
2731
2947
  name: "disclose",
2732
- description: "Disclose authorship on HEAD per ACO \u2014 the agent-native disclosure of work you just did. Pick the honest category: 'ai' (an AI authored it; you, the committer, are accountable) adds Authored-by-ai; 'with_ai' (a human authored it WITH AI assistance) adds Authored-by-human + Assisted-by; 'human' adds Authored-by-human. IMPORTANT: if the AI wrote the code and a human is merely committing it, that is 'ai' \u2014 do NOT claim 'with_ai'/human authorship the human didn't do. Amends HEAD's MESSAGE only (a testimony, never a content change) and refuses if there are staged changes. Evidence upgrade for the ai category (witnessed spans from a session log) is the CLI `madeby ai --witness`. Read the standard: madeby://guide/aco.",
2948
+ description: "Disclose authorship on HEAD per ACO \u2014 the agent-native disclosure of work you just did. Pick the honest category: 'ai' (an AI authored it; you, the committer, are accountable) adds Authored-by-ai; 'with_ai' (a human authored it WITH AI assistance) adds Authored-by-human + Assisted-by; 'human' adds Authored-by-human. IMPORTANT: if the AI wrote the code and a human is merely committing it, that is 'ai' \u2014 do NOT claim 'with_ai'/human authorship the human didn't do. Amends HEAD's MESSAGE only (a testimony, never a content change) and refuses if there are staged changes. Evidence upgrade for the ai category (witnessed spans from a session log) is the CLI `madeby ai --witness`. To disclose EARLIER commits without rewriting them (a repo that forbids amend/rebase), pass `for` (a commit or range): it makes one empty disclosure commit naming them (Discloses: <hash>), only for the git user's own undisclosed commits; push it and the PR check re-runs. Read the standard: madeby://guide/aco.",
2733
2949
  inputSchema: {
2734
2950
  type: "object",
2735
2951
  properties: {
@@ -2738,7 +2954,8 @@ var TOOLS = [
2738
2954
  with_ai: { type: "boolean", description: "legacy shorthand: true \u21D2 category with_ai, false \u21D2 human. Prefer `category`." },
2739
2955
  tool: { type: "string", description: "the AI tool to name (Assisted-by for with_ai, Authored-by-ai for ai). Default: AI." },
2740
2956
  print: { type: "boolean", description: "return the disclosure trailer lines instead of amending HEAD \u2014 for platforms that make the commit themselves; put them in the commit message's last paragraph" },
2741
- model: { type: "string", description: "the model version you are running as, e.g. claude-sonnet-5 \u2014 written as tool:model. Give it when the repo's AGENTS.md asks; omit it rather than guess." }
2957
+ model: { type: "string", description: "the model version you are running as, e.g. claude-sonnet-5 \u2014 written as tool:model. Give it when the repo's AGENTS.md asks; omit it rather than guess." },
2958
+ for: { type: "string", description: "disclose earlier commits instead of HEAD: a commit or a range (e.g. origin/dev..HEAD). Makes an empty disclosure commit; nothing is rewritten. The model is named only if you pass `model` for those commits." }
2742
2959
  }
2743
2960
  },
2744
2961
  handler: (args) => {
@@ -2746,6 +2963,10 @@ var TOOLS = [
2746
2963
  const tool = args.tool;
2747
2964
  const model = args.model;
2748
2965
  const category = args.category;
2966
+ if (typeof args.for === "string" && args.for) {
2967
+ const c = category === "ai" ? "ai" : category === "with_ai" || args.with_ai === true ? "with_ai" : "human";
2968
+ return discloseFor(root, args.for, { category: c, tool, model: c === "human" ? void 0 : model });
2969
+ }
2749
2970
  if (args.print === true) {
2750
2971
  const c = category === "ai" ? "ai" : category === "with_ai" || args.with_ai === true ? "with_ai" : "human";
2751
2972
  return disclosureTrailers(root, { category: c, tool, model });
@@ -2994,6 +3215,8 @@ Usage:
2994
3215
  + the CI disclosure check. --update refreshes an outdated ACO block in
2995
3216
  place; --required to enforce now; --host azure-devops for Azure Pipelines;
2996
3217
  --record-model to also ask agents for the model version (opt-in).
3218
+ In a repo that uses Claude Code (CLAUDE.md or .claude/; --claude-code to
3219
+ force) it also turns Claude Code's commit trailer on in .claude/settings.json.
2997
3220
  madeby check [<range>] The gate: do commits meet .madeby/policy.json? (a verdict \u2014 exit
2998
3221
  code gates). <range> e.g. origin/main..HEAD; in CI it's auto-detected.
2999
3222
  --json for a machine-readable result.
@@ -3011,6 +3234,8 @@ Usage:
3011
3234
  --model <version> names the model too (Assisted-by: <tool>:<version>).
3012
3235
  --print (me / ai) prints the trailer lines instead of amending HEAD,
3013
3236
  for the commit message itself: git commit -m <msg> -m "$(madeby ai <tool> --print)".
3237
+ --for <commit|range> (me / ai) discloses EARLIER commits instead, with one
3238
+ new empty commit naming them (Discloses: <hash>) \u2014 nothing rewritten.
3014
3239
  madeby mcp Run the MCP server (stdio) \u2014 the tools, agent-callable.
3015
3240
  madeby help Show this help.
3016
3241
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "madeby",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
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",