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.
- package/README.md +38 -0
- package/dist/madeby.mjs +276 -51
- 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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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 =
|
|
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
|
|
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
|
|
1145
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
1700
|
-
return
|
|
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 =
|
|
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:
|
|
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
|
|
1944
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
1854
1945
|
function repoRoot() {
|
|
1855
1946
|
try {
|
|
1856
|
-
return
|
|
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
|
|
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
|
|
2037
|
-
return
|
|
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
|
-
|
|
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 =
|
|
2071
|
-
message =
|
|
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
|
-
|
|
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 =
|
|
2095
|
-
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 =
|
|
2113
|
-
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
|
-
|
|
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 =
|
|
2126
|
-
message =
|
|
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
|
-
|
|
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("--") &&
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|