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.
- package/README.md +38 -3
- package/dist/madeby.mjs +129 -31
- 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
|
|
130
|
-
isn't scored
|
|
131
|
-
|
|
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
|
|
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
|
|
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 :
|
|
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.
|
|
2613
|
+
var VERSION = "0.3.16";
|
|
2595
2614
|
|
|
2596
2615
|
// src/init-repo.ts
|
|
2597
|
-
var ACO_VERSION =
|
|
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
|
|
2608
|
-
- **with_ai** \u2014 a human authored it with AI assistance: \`npx
|
|
2609
|
-
- **human** \u2014 a human authored it: \`npx
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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.
|
|
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",
|