madeby 0.3.8 → 0.3.10
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 +24 -2
- package/dist/madeby.mjs +312 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ npm i -g madeby
|
|
|
42
42
|
|
|
43
43
|
```
|
|
44
44
|
madeby init Set MadeBy up: write .madeby/policy.json + the CI disclosure check.
|
|
45
|
+
--record-model (opt-in) also asks agents to name the model version.
|
|
45
46
|
madeby check [<range>] The gate: do commits meet .madeby/policy.json? (a verdict — exit code
|
|
46
47
|
gates). <range> e.g. origin/main..HEAD — a PR's own commits. --json too.
|
|
47
48
|
madeby coverage [<range>] The scoreboard: how much of the repo's authorship is disclosed (known)
|
|
@@ -49,9 +50,13 @@ madeby coverage [<range>] The scoreboard: how much of the repo's authorship
|
|
|
49
50
|
madeby who [<path>] Made by whom? What's on the record about who made this — one file's
|
|
50
51
|
(alias: whom) origin, or the whole repo with no path. --lines=A-B to scope. --json too.
|
|
51
52
|
madeby ai [tool] Made by AI: attest AI authorship of HEAD (an Authored-by-ai trailer).
|
|
53
|
+
--model <version> names the model too (Authored-by-ai: <tool>:<version>).
|
|
52
54
|
`--witness [log]` instead records witnessed spans from a session log.
|
|
53
55
|
madeby me [--with-ai [t]] Made by me: affirm you authored HEAD (Authored-by-human); --with-ai adds
|
|
54
|
-
Assisted-by → with_ai.
|
|
56
|
+
Assisted-by → with_ai. --model <version> names the model too.
|
|
57
|
+
--print (me / ai) print the trailer lines instead of amending HEAD — for a
|
|
58
|
+
platform that makes the commit itself (a cloud coding agent):
|
|
59
|
+
git commit -m "<msg>" -m "$(madeby ai <tool> --print)".
|
|
55
60
|
madeby mcp Run the MCP server (stdio) — the tools, agent-callable.
|
|
56
61
|
madeby help
|
|
57
62
|
```
|
|
@@ -97,12 +102,20 @@ authorship coverage — last 500 commits (estimate · disclosure, not detection
|
|
|
97
102
|
of the 310 known: human 41% · with_ai 38% · ai 21%
|
|
98
103
|
trust: asserted 88% · verified 12% — self-reported; sign commits to verify
|
|
99
104
|
|
|
105
|
+
agents & models named in the 183 AI commits:
|
|
106
|
+
Claude Code 121 66%
|
|
107
|
+
Claude Opus 5.5 98 54%
|
|
108
|
+
Cursor 62 34%
|
|
109
|
+
|
|
100
110
|
→ 38% unknown. Require disclosure going forward: madeby init --required
|
|
101
111
|
```
|
|
102
112
|
|
|
103
113
|
It is a **labeled estimate**, honest by construction: an undisclosed commit is **unknown**, never
|
|
104
114
|
inferred as human — disclosure, not detection. The human/with_ai/ai mix is reported *of the known*,
|
|
105
|
-
never of the whole repo, and the asserted-vs-verified split shows how much is signed.
|
|
115
|
+
never of the whole repo, and the asserted-vs-verified split shows how much is signed. The agents and
|
|
116
|
+
model versions are listed exactly as the AI commits name them — a commit naming a tool and its model
|
|
117
|
+
(`Assisted-by: Claude Code` + `Co-Authored-By: Claude Opus 5.5`) counts under both, so those rows can
|
|
118
|
+
sum past 100%. `--json` emits
|
|
106
119
|
counts only (percentages are derived at the edge, never stored). A plain merge authors nothing and
|
|
107
120
|
isn't scored; a merge that changes content of its own (a conflict resolution) is, like any other
|
|
108
121
|
commit — chosen by what it changes, never by whether it discloses. Use it to see your gap, then
|
|
@@ -140,6 +153,15 @@ Two modes, nothing more:
|
|
|
140
153
|
- `required` — fail on any commit that discloses nothing your policy accepts.
|
|
141
154
|
- optional `"accept": ["dco-signoff", "ai-trailer", …]` — restrict what satisfies the policy
|
|
142
155
|
(omit ⇒ any recognized disclosure counts).
|
|
156
|
+
- optional `"recordModel": true` — written by `madeby init --record-model`: the repo asks AI
|
|
157
|
+
disclosures to name the model version as well as the tool. It adds one line to the AGENTS.md rule,
|
|
158
|
+
and `me --with-ai` / `ai` then read the model from the **tool's own record of the session** — Cursor's
|
|
159
|
+
`CURSOR_MODEL`, the Copilot CLI and Claude Code session logs (locally; only the model id is read) —
|
|
160
|
+
which wins over an agent's `--model`, since an agent routed by "Auto" may not know which model served
|
|
161
|
+
it. Elsewhere `--model <version>` is used, never guessed. So `coverage` can list each **agent · model** pairing
|
|
162
|
+
(`Cursor · gpt-5.3-codex` vs `Claude Code · claude-sonnet-5`). A request, not a gate — an AI
|
|
163
|
+
disclosure without a model still satisfies the policy. Opt-in because naming models is a project's
|
|
164
|
+
call: the Linux kernel, for one, asks only that an LLM was used, not which.
|
|
143
165
|
|
|
144
166
|
(There's no "off" — that's just not running the check. Advisory is the floor.)
|
|
145
167
|
|
package/dist/madeby.mjs
CHANGED
|
@@ -86,6 +86,20 @@ function matchAi(text) {
|
|
|
86
86
|
}
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
89
|
+
var TOOL_MODEL = /^([^:\s<>][^:<>]*?)[ \t]*:[ \t]*([^\s:<>/][^:<>]*?)$/;
|
|
90
|
+
function splitToolModel(value) {
|
|
91
|
+
const v = value.replace(/<[^>]*>/g, "").trim();
|
|
92
|
+
const m = TOOL_MODEL.exec(v);
|
|
93
|
+
return m ? { tool: m[1].trim(), model: m[2].trim() } : null;
|
|
94
|
+
}
|
|
95
|
+
function matchAiTrailer(who) {
|
|
96
|
+
const tm = splitToolModel(who);
|
|
97
|
+
if (tm) {
|
|
98
|
+
const p = matchAi(tm.tool) ?? matchAi(tm.model) ?? genericAiDisclosure(tm.tool);
|
|
99
|
+
if (p) return { ...p, raw: who.trim(), tool: tm.tool, modelVersion: tm.model };
|
|
100
|
+
}
|
|
101
|
+
return matchAi(who) ?? genericAiDisclosure(who);
|
|
102
|
+
}
|
|
89
103
|
var GENERIC_AI_TRAILER = /^(?:AI|an? AI|AI (?:assistant|tool|pair)|artificial intelligence)$/i;
|
|
90
104
|
function genericAiDisclosure(who) {
|
|
91
105
|
const w = who.trim();
|
|
@@ -125,13 +139,13 @@ function classifyCommit(commit) {
|
|
|
125
139
|
const aiAuthored = [...commit.message.matchAll(AI_AUTHOR_TRAILER)].map((m) => m[1].trim());
|
|
126
140
|
if (aiAuthored.length > 0 && !HUMAN_ATTEST.test(commit.message)) {
|
|
127
141
|
const who = aiAuthored[0];
|
|
128
|
-
const c =
|
|
142
|
+
const c = matchAiTrailer(who) ?? { provider: "unspecified", raw: who };
|
|
129
143
|
return { class: "ai", confidence: CONF_AI, signals: [`authored-by-ai:${c.provider}`], aiContributors: [c] };
|
|
130
144
|
}
|
|
131
145
|
const trailers = [...commit.message.matchAll(TRAILER_RE)].map((m) => m[1].trim());
|
|
132
146
|
let humanCoAuthors = 0;
|
|
133
147
|
for (const who of trailers) {
|
|
134
|
-
const ai =
|
|
148
|
+
const ai = matchAiTrailer(who);
|
|
135
149
|
if (ai) {
|
|
136
150
|
aiContributors.push(ai);
|
|
137
151
|
signals.push(`co-authored-by:${ai.provider}`);
|
|
@@ -140,7 +154,7 @@ function classifyCommit(commit) {
|
|
|
140
154
|
}
|
|
141
155
|
}
|
|
142
156
|
for (const who of aiAuthored) {
|
|
143
|
-
const ai =
|
|
157
|
+
const ai = matchAiTrailer(who) ?? { provider: "unspecified", raw: who };
|
|
144
158
|
aiContributors.push(ai);
|
|
145
159
|
signals.push(`authored-by-ai:${ai.provider}`);
|
|
146
160
|
}
|
|
@@ -925,7 +939,12 @@ function parseDisclosurePolicy(input) {
|
|
|
925
939
|
const mode = MODES.includes(o.mode) ? o.mode : "advisory";
|
|
926
940
|
const version = typeof o.version === "number" ? o.version : 0;
|
|
927
941
|
const accept = Array.isArray(o.accept) ? o.accept.filter((k) => typeof k === "string") : void 0;
|
|
928
|
-
return
|
|
942
|
+
return {
|
|
943
|
+
version,
|
|
944
|
+
mode,
|
|
945
|
+
...accept && accept.length > 0 ? { accept } : {},
|
|
946
|
+
...o.recordModel === true ? { recordModel: true } : {}
|
|
947
|
+
};
|
|
929
948
|
}
|
|
930
949
|
function evaluateDisclosurePolicy(policy, commits) {
|
|
931
950
|
const accept = policy.accept;
|
|
@@ -1231,6 +1250,10 @@ function provenanceMap(root, opts = {}) {
|
|
|
1231
1250
|
}
|
|
1232
1251
|
|
|
1233
1252
|
// ../analyzer/src/coverage.ts
|
|
1253
|
+
function agentName(raw) {
|
|
1254
|
+
const name = raw.replace(/<[^>]*>/g, " ").split(/\s+/).filter((w) => !w.includes("@")).join(" ").replace(/(\s*\([^)]*\))+\s*$/, "").trim();
|
|
1255
|
+
return name || raw.trim();
|
|
1256
|
+
}
|
|
1234
1257
|
function repoCoverage(root, opts = {}) {
|
|
1235
1258
|
const limit = opts.range ? void 0 : opts.limit ?? 500;
|
|
1236
1259
|
const authored = readGitLog(root, limit, opts.range);
|
|
@@ -1241,15 +1264,29 @@ function repoCoverage(root, opts = {}) {
|
|
|
1241
1264
|
const byCategory = { human: 0, with_ai: 0, ai: 0, bot: 0 };
|
|
1242
1265
|
const byTier = { asserted: 0, verified: 0 };
|
|
1243
1266
|
let known = 0;
|
|
1267
|
+
const agents = /* @__PURE__ */ new Map();
|
|
1244
1268
|
for (const c of commits) {
|
|
1245
1269
|
const kinds = commitDisclosureKinds({ message: c.message, signed: c.signed });
|
|
1246
|
-
const cls = classifyCommit({ message: c.message, authorName: c.authorName, authorEmail: c.authorEmail, signed: c.signed, sha: c.sha })
|
|
1270
|
+
const { class: cls, aiContributors } = classifyCommit({ message: c.message, authorName: c.authorName, authorEmail: c.authorEmail, signed: c.signed, sha: c.sha });
|
|
1247
1271
|
const category = cls === "human" && !kinds.includes("human-attestation") ? "unknown" : cls;
|
|
1248
1272
|
if (category === "unknown") continue;
|
|
1249
1273
|
known += 1;
|
|
1250
1274
|
byCategory[category] += 1;
|
|
1251
1275
|
if (c.signed) byTier.verified += 1;
|
|
1252
1276
|
else byTier.asserted += 1;
|
|
1277
|
+
if (category === "with_ai" || category === "ai") {
|
|
1278
|
+
const named = /* @__PURE__ */ new Set();
|
|
1279
|
+
for (const a of aiContributors) {
|
|
1280
|
+
const paired = a.tool && a.modelVersion ? { tool: a.tool, model: a.modelVersion } : void 0;
|
|
1281
|
+
const name = paired ? `${paired.tool} \xB7 ${paired.model}` : agentName(a.raw);
|
|
1282
|
+
const key = name.toLowerCase();
|
|
1283
|
+
if (named.has(key)) continue;
|
|
1284
|
+
named.add(key);
|
|
1285
|
+
const e = agents.get(key) ?? { name, provider: a.provider, ...paired, commits: 0 };
|
|
1286
|
+
e.commits += 1;
|
|
1287
|
+
agents.set(key, e);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1253
1290
|
}
|
|
1254
1291
|
return {
|
|
1255
1292
|
isEstimate: true,
|
|
@@ -1259,7 +1296,8 @@ function repoCoverage(root, opts = {}) {
|
|
|
1259
1296
|
known,
|
|
1260
1297
|
unknown: commits.length - known,
|
|
1261
1298
|
byCategory,
|
|
1262
|
-
byTier
|
|
1299
|
+
byTier,
|
|
1300
|
+
byAgent: [...agents.values()].sort((a, b) => b.commits - a.commits || a.name.localeCompare(b.name))
|
|
1263
1301
|
};
|
|
1264
1302
|
}
|
|
1265
1303
|
|
|
@@ -1795,6 +1833,94 @@ To disclose: add a Co-Authored-By / Generated-by trailer, affirm your own author
|
|
|
1795
1833
|
|
|
1796
1834
|
// src/commands/me.ts
|
|
1797
1835
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
1836
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
|
|
1837
|
+
import { homedir as homedir3 } from "node:os";
|
|
1838
|
+
import { join as join6 } from "node:path";
|
|
1839
|
+
|
|
1840
|
+
// src/model-source.ts
|
|
1841
|
+
import { closeSync, existsSync as existsSync4, openSync, readdirSync as readdirSync3, readSync, statSync as statSync2 } from "node:fs";
|
|
1842
|
+
import { homedir as homedir2 } from "node:os";
|
|
1843
|
+
import { join as join5 } from "node:path";
|
|
1844
|
+
var SESSION_ID = /^[0-9a-f-]{8,64}$/i;
|
|
1845
|
+
var TAIL_BYTES = 2 * 1024 * 1024;
|
|
1846
|
+
function lastModelIn(path) {
|
|
1847
|
+
let fd;
|
|
1848
|
+
try {
|
|
1849
|
+
const size = statSync2(path).size;
|
|
1850
|
+
const len = Math.min(size, TAIL_BYTES);
|
|
1851
|
+
const buf = Buffer.alloc(len);
|
|
1852
|
+
fd = openSync(path, "r");
|
|
1853
|
+
readSync(fd, buf, 0, len, size - len);
|
|
1854
|
+
const models = [...buf.toString("utf8").matchAll(/"(?:model|newModel)"\s*:\s*"([^"<][^"]{0,80})"/g)].map((m) => m[1]);
|
|
1855
|
+
return models.at(-1) ?? null;
|
|
1856
|
+
} catch {
|
|
1857
|
+
return null;
|
|
1858
|
+
} finally {
|
|
1859
|
+
if (fd !== void 0) closeSync(fd);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
var MODEL_SOURCES = [
|
|
1863
|
+
{
|
|
1864
|
+
tool: "Cursor",
|
|
1865
|
+
matches: /\bcursor/i,
|
|
1866
|
+
detect: (env) => env.CURSOR_AGENT === "1" && env.CURSOR_MODEL?.trim() ? { model: env.CURSOR_MODEL.trim(), source: "Cursor's CURSOR_MODEL" } : null
|
|
1867
|
+
},
|
|
1868
|
+
{
|
|
1869
|
+
tool: "GitHub Copilot CLI",
|
|
1870
|
+
matches: /copilot/i,
|
|
1871
|
+
detect: (env, home) => {
|
|
1872
|
+
const id = env.COPILOT_AGENT_SESSION_ID;
|
|
1873
|
+
if (env.COPILOT_CLI !== "1" || !id || !SESSION_ID.test(id)) return null;
|
|
1874
|
+
const model = lastModelIn(join5(home, ".copilot", "session-state", id, "events.jsonl"));
|
|
1875
|
+
return model ? { model, source: "Copilot's session log" } : null;
|
|
1876
|
+
}
|
|
1877
|
+
},
|
|
1878
|
+
{
|
|
1879
|
+
tool: "Claude Code",
|
|
1880
|
+
matches: /claude/i,
|
|
1881
|
+
detect: (env, home) => {
|
|
1882
|
+
const id = env.CLAUDE_CODE_SESSION_ID;
|
|
1883
|
+
if (env.CLAUDECODE !== "1" || !id || !SESSION_ID.test(id)) return null;
|
|
1884
|
+
const projects = join5(home, ".claude", "projects");
|
|
1885
|
+
if (!existsSync4(projects)) return null;
|
|
1886
|
+
try {
|
|
1887
|
+
for (const dir of readdirSync3(projects)) {
|
|
1888
|
+
const p = join5(projects, dir, `${id}.jsonl`);
|
|
1889
|
+
if (existsSync4(p)) {
|
|
1890
|
+
const model = lastModelIn(p);
|
|
1891
|
+
return model ? { model, source: "Claude Code's session transcript" } : null;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
} catch {
|
|
1895
|
+
}
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
];
|
|
1900
|
+
function detectAgentModel(tool, env = process.env, home = homedir2()) {
|
|
1901
|
+
const generic = /^(?:AI|an? AI)$/i.test(tool.trim());
|
|
1902
|
+
for (const s of MODEL_SOURCES) {
|
|
1903
|
+
if (!generic && !s.matches.test(tool)) continue;
|
|
1904
|
+
const hit = s.detect(env, home);
|
|
1905
|
+
if (hit) return { tool: generic ? s.tool : tool, ...hit };
|
|
1906
|
+
}
|
|
1907
|
+
return null;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// src/commands/me.ts
|
|
1911
|
+
function parseModel(args) {
|
|
1912
|
+
const i = args.findIndex((a2) => a2 === "--model" || a2.startsWith("--model="));
|
|
1913
|
+
if (i === -1) return void 0;
|
|
1914
|
+
const a = args[i];
|
|
1915
|
+
const v = a.startsWith("--model=") ? a.slice("--model=".length) : args[i + 1];
|
|
1916
|
+
return v && !v.startsWith("-") ? v : void 0;
|
|
1917
|
+
}
|
|
1918
|
+
function aiValue(tool, model) {
|
|
1919
|
+
const line = (s) => s.replace(/\s+/g, " ").trim();
|
|
1920
|
+
const t = line(tool.replace(/:/g, " ")) || "AI";
|
|
1921
|
+
const m = model ? line(model) : "";
|
|
1922
|
+
return m ? `${t}:${m}` : t;
|
|
1923
|
+
}
|
|
1798
1924
|
function parseWithAi(args) {
|
|
1799
1925
|
const i = args.findIndex((a2) => a2 === "--with-ai" || a2.startsWith("--with-ai="));
|
|
1800
1926
|
if (i === -1) return { withAi: false, tool: "AI" };
|
|
@@ -1808,10 +1934,27 @@ function git3(root, args) {
|
|
|
1808
1934
|
}
|
|
1809
1935
|
var HUMAN_ATTEST2 = /^[ \t]*(?:Authored-by-human|Human-authored(?:-by)?)[ \t]*:/im;
|
|
1810
1936
|
var ASSIST = /^[ \t]*Assisted-by[ \t]*:/im;
|
|
1937
|
+
function recordsModel(root) {
|
|
1938
|
+
const p = join6(root, ".madeby", "policy.json");
|
|
1939
|
+
if (!existsSync5(p)) return false;
|
|
1940
|
+
try {
|
|
1941
|
+
return parseDisclosurePolicy(JSON.parse(readFileSync6(p, "utf8"))).recordModel === true;
|
|
1942
|
+
} catch {
|
|
1943
|
+
return false;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
function resolveAi(root, o) {
|
|
1947
|
+
const tool = o.tool && o.tool.trim() || "AI";
|
|
1948
|
+
const detected = recordsModel(root) ? detectAgentModel(tool, o.env ?? process.env, o.home ?? homedir3()) : null;
|
|
1949
|
+
if (detected) return { value: aiValue(detected.tool, detected.model), tool: detected.tool, model: detected.model, modelSource: detected.source };
|
|
1950
|
+
const model = o.model?.trim() || void 0;
|
|
1951
|
+
return { value: aiValue(tool, model), tool, ...model ? { model, modelSource: "as given (--model)" } : {} };
|
|
1952
|
+
}
|
|
1811
1953
|
var AI_AUTHOR = /^[ \t]*Authored-by-ai[ \t]*:/im;
|
|
1812
1954
|
function attestAiAuthorship(root, opts = {}) {
|
|
1813
|
-
const
|
|
1814
|
-
const
|
|
1955
|
+
const ai = resolveAi(root, opts);
|
|
1956
|
+
const tool = ai.value;
|
|
1957
|
+
const base = { ok: false, changed: false, withAi: false, tool, model: ai.model, modelSource: ai.modelSource, added: [], category: "ai" };
|
|
1815
1958
|
try {
|
|
1816
1959
|
execFileSync5("git", ["-C", root, "diff", "--cached", "--quiet"]);
|
|
1817
1960
|
} catch {
|
|
@@ -1832,14 +1975,33 @@ function attestAiAuthorship(root, opts = {}) {
|
|
|
1832
1975
|
added.push("Authored-by-ai");
|
|
1833
1976
|
}
|
|
1834
1977
|
if (trailers.length > 0) {
|
|
1835
|
-
execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", ...trailers]);
|
|
1978
|
+
execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", "--allow-empty", ...trailers]);
|
|
1836
1979
|
}
|
|
1837
|
-
return { ok: true, changed: trailers.length > 0,
|
|
1980
|
+
return { ...base, ok: true, changed: trailers.length > 0, subject, added };
|
|
1981
|
+
}
|
|
1982
|
+
function disclosureTrailers(root, opts) {
|
|
1983
|
+
if (opts.category === "ai") {
|
|
1984
|
+
const ai2 = resolveAi(root, opts);
|
|
1985
|
+
return { ok: true, lines: [`Authored-by-ai: ${ai2.value}`], model: ai2.model, modelSource: ai2.modelSource };
|
|
1986
|
+
}
|
|
1987
|
+
let name = "";
|
|
1988
|
+
let email = "";
|
|
1989
|
+
try {
|
|
1990
|
+
name = git3(root, ["config", "user.name"]);
|
|
1991
|
+
email = git3(root, ["config", "user.email"]);
|
|
1992
|
+
} catch {
|
|
1993
|
+
}
|
|
1994
|
+
if (!name || !email) return { ok: false, reason: "no-identity", lines: [] };
|
|
1995
|
+
const lines = [`Authored-by-human: ${name} <${email}>`];
|
|
1996
|
+
if (opts.category === "human") return { ok: true, lines };
|
|
1997
|
+
const ai = resolveAi(root, opts);
|
|
1998
|
+
return { ok: true, lines: [...lines, `Assisted-by: ${ai.value}`], model: ai.model, modelSource: ai.modelSource };
|
|
1838
1999
|
}
|
|
1839
2000
|
function affirmAuthorship(root, opts = {}) {
|
|
1840
2001
|
const withAi = opts.withAi ?? false;
|
|
1841
|
-
const
|
|
1842
|
-
const
|
|
2002
|
+
const ai = withAi ? resolveAi(root, opts) : void 0;
|
|
2003
|
+
const tool = ai?.value ?? "AI";
|
|
2004
|
+
const base = { ok: false, changed: false, withAi, tool, model: ai?.model, modelSource: ai?.modelSource, added: [], category: withAi ? "with_ai" : "human" };
|
|
1843
2005
|
let name = "";
|
|
1844
2006
|
let email = "";
|
|
1845
2007
|
try {
|
|
@@ -1874,20 +2036,36 @@ function affirmAuthorship(root, opts = {}) {
|
|
|
1874
2036
|
added.push("Assisted-by");
|
|
1875
2037
|
}
|
|
1876
2038
|
if (trailers.length > 0) {
|
|
1877
|
-
execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", ...trailers]);
|
|
2039
|
+
execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", "--allow-empty", ...trailers]);
|
|
1878
2040
|
}
|
|
1879
2041
|
const category = hasAssist || added.includes("Assisted-by") ? "with_ai" : "human";
|
|
1880
|
-
return { ok: true, changed: trailers.length > 0,
|
|
2042
|
+
return { ...base, ok: true, changed: trailers.length > 0, name, email, subject, added, category };
|
|
1881
2043
|
}
|
|
1882
2044
|
function meCommand(args) {
|
|
1883
2045
|
const { withAi, tool } = parseWithAi(args);
|
|
2046
|
+
const model = parseModel(args);
|
|
2047
|
+
if (model && !withAi) {
|
|
2048
|
+
console.error("madeby me: --model names the AI's model, so it goes with --with-ai (e.g. madeby me --with-ai Cursor --model claude-sonnet-5).");
|
|
2049
|
+
process.exit(1);
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
1884
2052
|
const root = repoRoot();
|
|
1885
2053
|
if (!root) {
|
|
1886
2054
|
console.error("madeby me: not a git repository.");
|
|
1887
2055
|
process.exit(2);
|
|
1888
2056
|
return;
|
|
1889
2057
|
}
|
|
1890
|
-
|
|
2058
|
+
if (args.includes("--print")) {
|
|
2059
|
+
const t = disclosureTrailers(root, { category: withAi ? "with_ai" : "human", tool, model });
|
|
2060
|
+
if (!t.ok) {
|
|
2061
|
+
console.error("madeby me: set your git identity first (git config user.name / user.email).");
|
|
2062
|
+
process.exit(1);
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
console.log(t.lines.join("\n"));
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
const r = affirmAuthorship(root, { withAi, tool, model });
|
|
1891
2069
|
if (!r.ok) {
|
|
1892
2070
|
if (r.reason === "no-identity") console.error("madeby me: set your git identity first (git config user.name / user.email).");
|
|
1893
2071
|
else if (r.reason === "staged-changes") console.error("madeby me: you have staged changes. Commit them first \u2014 `me` affirms HEAD, not your index.");
|
|
@@ -1902,7 +2080,8 @@ function meCommand(args) {
|
|
|
1902
2080
|
return;
|
|
1903
2081
|
}
|
|
1904
2082
|
console.log(
|
|
1905
|
-
withAi ? `madeby me: made by me, with AI \u2014 affirmed human authorship + disclosed AI assistance on HEAD \u2014 "${r.subject}" (Authored-by-human: ${r.name}; Assisted-by: ${r.tool})
|
|
2083
|
+
withAi ? `madeby me: made by me, with AI \u2014 affirmed human authorship + disclosed AI assistance on HEAD \u2014 "${r.subject}" (Authored-by-human: ${r.name}; Assisted-by: ${r.tool}).${r.modelSource && r.modelSource !== "as given (--model)" ? `
|
|
2084
|
+
model from ${r.modelSource}.` : ""}` : `madeby me: affirmed human authorship on HEAD \u2014 "${r.subject}" (Authored-by-human: ${r.name}).`
|
|
1906
2085
|
);
|
|
1907
2086
|
}
|
|
1908
2087
|
|
|
@@ -1917,7 +2096,8 @@ function aiCommand(args) {
|
|
|
1917
2096
|
}
|
|
1918
2097
|
const makeSample = args.includes("--make-parser-sample");
|
|
1919
2098
|
const witness = args.includes("--witness");
|
|
1920
|
-
const
|
|
2099
|
+
const model = parseModel(args);
|
|
2100
|
+
const positional = args.filter((a, i) => !a.startsWith("--") && !(args[i - 1] === "--model"));
|
|
1921
2101
|
if (makeSample) {
|
|
1922
2102
|
const s = writeParserSample(root, { transcriptPath: positional[0] });
|
|
1923
2103
|
if (s.error === "transcript-not-found") {
|
|
@@ -1959,7 +2139,11 @@ function aiCommand(args) {
|
|
|
1959
2139
|
return;
|
|
1960
2140
|
}
|
|
1961
2141
|
const tool = positional[0] || "AI";
|
|
1962
|
-
|
|
2142
|
+
if (args.includes("--print")) {
|
|
2143
|
+
console.log(disclosureTrailers(root, { category: "ai", tool, model }).lines.join("\n"));
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
const r = attestAiAuthorship(root, { tool, model });
|
|
1963
2147
|
if (!r.ok) {
|
|
1964
2148
|
if (r.reason === "staged-changes") {
|
|
1965
2149
|
console.error("madeby ai: you have staged changes. Commit them first \u2014 `ai` attests HEAD, not your index.");
|
|
@@ -1976,7 +2160,8 @@ function aiCommand(args) {
|
|
|
1976
2160
|
}
|
|
1977
2161
|
console.log(
|
|
1978
2162
|
`madeby ai: attested AI authorship on HEAD \u2014 "${r.subject}" (Authored-by-ai: ${r.tool}).
|
|
1979
|
-
|
|
2163
|
+
` + (r.modelSource && r.modelSource !== "as given (--model)" ? ` model from ${r.modelSource}.
|
|
2164
|
+
` : "") + " \u2192 asserted tier. For evidence-backed spans from your session log: madeby ai --witness [log]."
|
|
1980
2165
|
);
|
|
1981
2166
|
}
|
|
1982
2167
|
|
|
@@ -2028,17 +2213,18 @@ function whoCommand(args) {
|
|
|
2028
2213
|
}
|
|
2029
2214
|
|
|
2030
2215
|
// src/commands/coverage.ts
|
|
2031
|
-
import { existsSync as
|
|
2032
|
-
import { join as
|
|
2216
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
|
|
2217
|
+
import { join as join7 } from "node:path";
|
|
2033
2218
|
function policyIsRequired(root) {
|
|
2034
|
-
const p =
|
|
2035
|
-
if (!
|
|
2219
|
+
const p = join7(root, ".madeby", "policy.json");
|
|
2220
|
+
if (!existsSync6(p)) return false;
|
|
2036
2221
|
try {
|
|
2037
|
-
return parseDisclosurePolicy(JSON.parse(
|
|
2222
|
+
return parseDisclosurePolicy(JSON.parse(readFileSync7(p, "utf8"))).mode === "required";
|
|
2038
2223
|
} catch {
|
|
2039
2224
|
return false;
|
|
2040
2225
|
}
|
|
2041
2226
|
}
|
|
2227
|
+
var AGENT_ROWS = 8;
|
|
2042
2228
|
var pct = (n, d) => d > 0 ? Math.round(n / d * 100) : 0;
|
|
2043
2229
|
var bar = (p) => "\u2588".repeat(Math.round(p / 5)) + "\u2591".repeat(20 - Math.round(p / 5));
|
|
2044
2230
|
function render(r, required) {
|
|
@@ -2059,6 +2245,16 @@ function render(r, required) {
|
|
|
2059
2245
|
of the ${r.known} known: ${parts.join(" \xB7 ")}`);
|
|
2060
2246
|
console.log(` trust: asserted ${pct(r.byTier.asserted, r.known)}% \xB7 verified ${pct(r.byTier.verified, r.known)}% \u2014 self-reported; sign commits to verify`);
|
|
2061
2247
|
}
|
|
2248
|
+
const withAi = r.byCategory.with_ai + r.byCategory.ai;
|
|
2249
|
+
if (r.byAgent.length > 0) {
|
|
2250
|
+
console.log(`
|
|
2251
|
+
agents & models named in the ${withAi} AI commits:`);
|
|
2252
|
+
const width = Math.max(...r.byAgent.slice(0, AGENT_ROWS).map((a) => a.name.length));
|
|
2253
|
+
for (const a of r.byAgent.slice(0, AGENT_ROWS)) {
|
|
2254
|
+
console.log(` ${a.name.padEnd(width)} ${String(a.commits).padStart(4)} ${String(pct(a.commits, withAi)).padStart(3)}%`);
|
|
2255
|
+
}
|
|
2256
|
+
if (r.byAgent.length > AGENT_ROWS) console.log(` \u2026 and ${r.byAgent.length - AGENT_ROWS} more (--json for all)`);
|
|
2257
|
+
}
|
|
2062
2258
|
if (r.unknown > 0) {
|
|
2063
2259
|
console.log(
|
|
2064
2260
|
required ? `
|
|
@@ -2085,9 +2281,9 @@ function coverageCommand(args) {
|
|
|
2085
2281
|
}
|
|
2086
2282
|
|
|
2087
2283
|
// src/init-repo.ts
|
|
2088
|
-
import { existsSync as
|
|
2089
|
-
import { dirname as dirname2, join as
|
|
2090
|
-
var ACO_VERSION =
|
|
2284
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2285
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
2286
|
+
var ACO_VERSION = 2;
|
|
2091
2287
|
var ACO_OPEN = `<!-- madeby:aco v${ACO_VERSION} -->`;
|
|
2092
2288
|
var ACO_CLOSE = `<!-- /madeby:aco -->`;
|
|
2093
2289
|
var ACO_OPEN_RE = /<!-- madeby:aco(?: v(\d+))? -->/;
|
|
@@ -2101,16 +2297,22 @@ After you commit, run the command for the honest category (no install, no setup)
|
|
|
2101
2297
|
- **with_ai** \u2014 a human authored it with AI assistance: \`npx madeby@latest me --with-ai [tool]\`
|
|
2102
2298
|
- **human** \u2014 a human authored it: \`npx madeby@latest me\`
|
|
2103
2299
|
|
|
2300
|
+
If your platform makes the commit for you (a cloud coding agent that pushes through its own tool),
|
|
2301
|
+
amending afterwards can be lost \u2014 put the disclosure in the message instead, with \`--print\`:
|
|
2302
|
+
\`git commit -m "<message>" -m "$(npx madeby@latest ai [tool] --print)"\`.
|
|
2303
|
+
|
|
2104
2304
|
Disclose per commit, honestly. Undisclosed content stays *unknown*, never guessed.
|
|
2105
2305
|
`;
|
|
2106
|
-
var
|
|
2107
|
-
|
|
2306
|
+
var MODEL_LINE = `
|
|
2307
|
+
**This repo also records the model.** Name your tool as usual \u2014 madeby reads the model from the
|
|
2308
|
+
tool's own record of the session where it can (Cursor, Copilot CLI, Claude Code), e.g.
|
|
2309
|
+
\`npx madeby@latest me --with-ai Cursor\` writes \`Assisted-by: Cursor:<model>\`. Elsewhere add
|
|
2310
|
+
\`--model <version>\` with the exact model you are running as. If you don't know it, leave it off \u2014
|
|
2311
|
+
never guess.
|
|
2312
|
+
`;
|
|
2313
|
+
var acoBlock = (recordModel) => `${ACO_OPEN}
|
|
2314
|
+
${ACO_BODY}${recordModel ? MODEL_LINE : ""}${ACO_CLOSE}
|
|
2108
2315
|
`;
|
|
2109
|
-
var AGENTS_CONVENTION = `# AGENTS.md
|
|
2110
|
-
|
|
2111
|
-
${ACO_BLOCK}`;
|
|
2112
|
-
var ACO_SECTION = `
|
|
2113
|
-
${ACO_BLOCK}`;
|
|
2114
2316
|
function findAcoBlock(content) {
|
|
2115
2317
|
const m = ACO_OPEN_RE.exec(content);
|
|
2116
2318
|
if (!m) return null;
|
|
@@ -2133,11 +2335,11 @@ function acoStatus(content) {
|
|
|
2133
2335
|
if (loc.version > ACO_VERSION) return "newer";
|
|
2134
2336
|
return "current";
|
|
2135
2337
|
}
|
|
2136
|
-
function replaceAcoBlock(content, loc) {
|
|
2137
|
-
return content.slice(0, loc.start) +
|
|
2338
|
+
function replaceAcoBlock(content, loc, block) {
|
|
2339
|
+
return content.slice(0, loc.start) + block + content.slice(loc.end);
|
|
2138
2340
|
}
|
|
2139
2341
|
function writeIfAbsent(abs, rel, content, created, skipped) {
|
|
2140
|
-
if (
|
|
2342
|
+
if (existsSync7(abs)) {
|
|
2141
2343
|
skipped.push(rel);
|
|
2142
2344
|
return;
|
|
2143
2345
|
}
|
|
@@ -2145,47 +2347,60 @@ function writeIfAbsent(abs, rel, content, created, skipped) {
|
|
|
2145
2347
|
writeFileSync2(abs, content);
|
|
2146
2348
|
created.push(rel);
|
|
2147
2349
|
}
|
|
2148
|
-
function ensurePolicy(abs, rel,
|
|
2149
|
-
if (!
|
|
2350
|
+
function ensurePolicy(abs, rel, want, res) {
|
|
2351
|
+
if (!existsSync7(abs)) {
|
|
2150
2352
|
mkdirSync2(dirname2(abs), { recursive: true });
|
|
2151
|
-
|
|
2353
|
+
const fresh = { version: 0, mode: want.mode, ...want.recordModel ? { recordModel: true } : {} };
|
|
2354
|
+
writeFileSync2(abs, JSON.stringify(fresh, null, 2) + "\n");
|
|
2152
2355
|
res.created.push(rel);
|
|
2153
|
-
return { mode, malformed: false };
|
|
2356
|
+
return { mode: want.mode, recordModel: want.recordModel, malformed: false };
|
|
2154
2357
|
}
|
|
2155
2358
|
let raw;
|
|
2156
2359
|
try {
|
|
2157
|
-
raw = JSON.parse(
|
|
2360
|
+
raw = JSON.parse(readFileSync8(abs, "utf8"));
|
|
2158
2361
|
} catch {
|
|
2159
2362
|
res.skipped.push(rel);
|
|
2160
|
-
return { mode: "advisory", malformed: true };
|
|
2363
|
+
return { mode: "advisory", recordModel: false, malformed: true };
|
|
2161
2364
|
}
|
|
2162
2365
|
const isObject = !!raw && typeof raw === "object" && !Array.isArray(raw);
|
|
2163
|
-
const current = parseDisclosurePolicy(raw)
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2366
|
+
const current = parseDisclosurePolicy(raw);
|
|
2367
|
+
const raise = want.mode === "required" && current.mode !== "required" && isObject;
|
|
2368
|
+
const modelOn = want.recordModel && !current.recordModel && isObject;
|
|
2369
|
+
if (raise || modelOn) {
|
|
2370
|
+
const next = { ...raw, ...raise ? { mode: "required" } : {}, ...modelOn ? { recordModel: true } : {} };
|
|
2371
|
+
writeFileSync2(abs, JSON.stringify(next, null, 2) + "\n");
|
|
2372
|
+
if (raise) res.raised.push(rel);
|
|
2373
|
+
if (modelOn) res.modelOn.push(rel);
|
|
2374
|
+
} else {
|
|
2375
|
+
res.skipped.push(rel);
|
|
2168
2376
|
}
|
|
2169
|
-
|
|
2170
|
-
return { mode: current, malformed: !isObject };
|
|
2377
|
+
return { mode: raise ? "required" : current.mode, recordModel: modelOn || current.recordModel === true, malformed: !isObject };
|
|
2171
2378
|
}
|
|
2172
2379
|
function ensureAco(abs, rel, res, opts) {
|
|
2173
|
-
|
|
2380
|
+
const block = acoBlock(opts.recordModel);
|
|
2381
|
+
if (!existsSync7(abs)) {
|
|
2174
2382
|
if (!opts.createIfAbsent) return;
|
|
2175
2383
|
mkdirSync2(dirname2(abs), { recursive: true });
|
|
2176
|
-
writeFileSync2(abs,
|
|
2384
|
+
writeFileSync2(abs, `# AGENTS.md
|
|
2385
|
+
|
|
2386
|
+
${block}`);
|
|
2177
2387
|
res.created.push(rel);
|
|
2178
2388
|
return;
|
|
2179
2389
|
}
|
|
2180
|
-
const content =
|
|
2390
|
+
const content = readFileSync8(abs, "utf8");
|
|
2181
2391
|
const status = acoStatus(content);
|
|
2392
|
+
const loc = findAcoBlock(content);
|
|
2182
2393
|
if (status === "absent") {
|
|
2183
2394
|
const sep = content.endsWith("\n") ? "" : "\n";
|
|
2184
|
-
writeFileSync2(abs, content + sep +
|
|
2395
|
+
writeFileSync2(abs, content + sep + `
|
|
2396
|
+
${block}`);
|
|
2185
2397
|
res.merged.push(rel);
|
|
2398
|
+
} else if (status === "current" && opts.modelRequested && loc && content.slice(loc.start, loc.end) !== block) {
|
|
2399
|
+
writeFileSync2(abs, replaceAcoBlock(content, loc, block));
|
|
2400
|
+
res.updated.push(rel);
|
|
2186
2401
|
} else if (status === "outdated") {
|
|
2187
2402
|
if (opts.update) {
|
|
2188
|
-
writeFileSync2(abs, replaceAcoBlock(content,
|
|
2403
|
+
writeFileSync2(abs, replaceAcoBlock(content, loc, block));
|
|
2189
2404
|
res.updated.push(rel);
|
|
2190
2405
|
} else {
|
|
2191
2406
|
res.outdated.push(rel);
|
|
@@ -2204,17 +2419,23 @@ function initRepo(root, opts = {}) {
|
|
|
2204
2419
|
const updated = [];
|
|
2205
2420
|
const outdated = [];
|
|
2206
2421
|
const raised = [];
|
|
2207
|
-
const
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2422
|
+
const modelOn = [];
|
|
2423
|
+
const res = { created, skipped, merged, updated, outdated, raised, modelOn };
|
|
2424
|
+
const modelRequested = opts.recordModel ?? false;
|
|
2425
|
+
const policy = ensurePolicy(join8(root, ".madeby", "policy.json"), ".madeby/policy.json", { mode, recordModel: modelRequested }, res);
|
|
2426
|
+
const aco = { update, recordModel: policy.recordModel, modelRequested };
|
|
2427
|
+
ensureAco(join8(root, "AGENTS.md"), "AGENTS.md", res, { createIfAbsent: true, ...aco });
|
|
2428
|
+
ensureAco(join8(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false, ...aco });
|
|
2211
2429
|
if (host === "github") {
|
|
2212
|
-
writeIfAbsent(
|
|
2430
|
+
writeIfAbsent(join8(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
|
|
2213
2431
|
}
|
|
2214
2432
|
const nextSteps = ["Commit the created files."];
|
|
2215
2433
|
if (raised.length > 0) {
|
|
2216
2434
|
nextSteps.push('Raised .madeby/policy.json to "required" \u2014 undisclosed commits now fail the check. To make it block merges, also mark the check required on the protected branch (see madeby://guide/enforce).');
|
|
2217
2435
|
}
|
|
2436
|
+
if (modelOn.length > 0 || modelRequested && created.includes(".madeby/policy.json")) {
|
|
2437
|
+
nextSteps.push("This repo now asks AI disclosures to name the model version (recordModel in .madeby/policy.json; the AGENTS.md rule says how: --model <version>). `madeby coverage` lists each agent \xB7 model pairing. It never changes the gate.");
|
|
2438
|
+
}
|
|
2218
2439
|
if (policy.malformed) {
|
|
2219
2440
|
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.');
|
|
2220
2441
|
}
|
|
@@ -2240,7 +2461,7 @@ function initRepo(root, opts = {}) {
|
|
|
2240
2461
|
}
|
|
2241
2462
|
nextSteps.push("Contributors disclose origin with a Co-Authored-By / Generated-by trailer (AI), an Authored-by-human trailer ('madeby me'), an Authored-by-ai trailer ('madeby ai' \u2014 AI authored, committer accountable), a DCO Signed-off-by, or a signed commit. Evidence upgrade: 'madeby ai --witness' records your own AI session's witnessed spans.");
|
|
2242
2463
|
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.");
|
|
2243
|
-
return { created, skipped, merged, updated, outdated, raised, policyMode: policy.mode, host, nextSteps };
|
|
2464
|
+
return { created, skipped, merged, updated, outdated, raised, modelOn, policyMode: policy.mode, recordModel: policy.recordModel, host, nextSteps };
|
|
2244
2465
|
}
|
|
2245
2466
|
var GH_WORKFLOW = `name: Disclosure
|
|
2246
2467
|
on: pull_request
|
|
@@ -2264,17 +2485,19 @@ function initCommand(args) {
|
|
|
2264
2485
|
}
|
|
2265
2486
|
const mode = args.includes("--required") ? "required" : "advisory";
|
|
2266
2487
|
const update = args.includes("--update");
|
|
2488
|
+
const recordModel = args.includes("--record-model");
|
|
2267
2489
|
const hostIdx = args.indexOf("--host");
|
|
2268
2490
|
const host = hostIdx >= 0 ? args[hostIdx + 1] : void 0;
|
|
2269
|
-
const r = initRepo(root, { mode, host, update });
|
|
2491
|
+
const r = initRepo(root, { mode, host, update, recordModel });
|
|
2270
2492
|
for (const f of r.created) console.log(` + ${f}`);
|
|
2271
2493
|
for (const f of r.merged) console.log(` ~ ${f} (merged in the ACO rule)`);
|
|
2272
2494
|
for (const f of r.updated) console.log(` \u2191 ${f} (ACO convention refreshed to the current version)`);
|
|
2273
2495
|
for (const f of r.raised) console.log(` \u2191 ${f} (mode raised to required)`);
|
|
2496
|
+
for (const f of r.modelOn) console.log(` \u2191 ${f} (now records the model version)`);
|
|
2274
2497
|
for (const f of r.outdated) console.log(` ! ${f} (ACO convention OUTDATED \u2014 run 'madeby init --update')`);
|
|
2275
2498
|
for (const f of r.skipped) console.log(` \xB7 ${f} (exists \u2014 left as-is)`);
|
|
2276
2499
|
console.log(`
|
|
2277
|
-
madeby init \u2014 policy: ${r.policyMode}, host: ${r.host}`);
|
|
2500
|
+
madeby init \u2014 policy: ${r.policyMode}${r.recordModel ? " + records model" : ""}, host: ${r.host}`);
|
|
2278
2501
|
console.log("Next:");
|
|
2279
2502
|
for (const s of r.nextSteps) console.log(` - ${s}`);
|
|
2280
2503
|
}
|
|
@@ -2283,7 +2506,7 @@ madeby init \u2014 policy: ${r.policyMode}, host: ${r.host}`);
|
|
|
2283
2506
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
2284
2507
|
import { createInterface } from "node:readline";
|
|
2285
2508
|
var DEFAULT_PROTOCOL = "2024-11-05";
|
|
2286
|
-
var SERVER_INFO = { name: "madeby", version: "0.3.
|
|
2509
|
+
var SERVER_INFO = { name: "madeby", version: "0.3.10" };
|
|
2287
2510
|
function rootFor(path) {
|
|
2288
2511
|
const cwd = path && path.length ? path : process.cwd();
|
|
2289
2512
|
return execFileSync6("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
@@ -2305,10 +2528,16 @@ var TOOLS = [
|
|
|
2305
2528
|
path: { type: "string", description: "path to the repo (default: the server's working directory)" },
|
|
2306
2529
|
mode: { enum: ["advisory", "required"], description: "policy mode to seed (default: advisory)" },
|
|
2307
2530
|
host: { type: "string", description: "CI host to wire: github (default) or azure-devops" },
|
|
2308
|
-
update: { type: "boolean", description: "refresh an outdated ACO convention block in place (drift update); default false (report only)" }
|
|
2531
|
+
update: { type: "boolean", description: "refresh an outdated ACO convention block in place (drift update); default false (report only)" },
|
|
2532
|
+
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." }
|
|
2309
2533
|
}
|
|
2310
2534
|
},
|
|
2311
|
-
handler: (args) => initRepo(rootFor(args.path), {
|
|
2535
|
+
handler: (args) => initRepo(rootFor(args.path), {
|
|
2536
|
+
mode: args.mode,
|
|
2537
|
+
host: args.host,
|
|
2538
|
+
update: args.update,
|
|
2539
|
+
recordModel: args.record_model
|
|
2540
|
+
})
|
|
2312
2541
|
},
|
|
2313
2542
|
{
|
|
2314
2543
|
name: "check",
|
|
@@ -2318,7 +2547,7 @@ var TOOLS = [
|
|
|
2318
2547
|
},
|
|
2319
2548
|
{
|
|
2320
2549
|
name: "coverage",
|
|
2321
|
-
description: "Authorship coverage scoreboard for a repo \u2014 the honest estimate of how much of the code's authorship is ON THE RECORD (known) vs undisclosed (unknown \u2014 origin unknown, NEVER guessed), the human/with_ai/ai mix OF THE KNOWN (never of the whole repo),
|
|
2550
|
+
description: "Authorship coverage scoreboard for a repo \u2014 the honest estimate of how much of the code's authorship is ON THE RECORD (known) vs undisclosed (unknown \u2014 origin unknown, NEVER guessed), the human/with_ai/ai mix OF THE KNOWN (never of the whole repo), the asserted-vs-verified trust split, and byAgent: the agents and model versions named in the AI commits, exactly as disclosed (a commit naming a tool and its model counts under both). Disclosure, not detection; the report is a labeled estimate (counts only \u2014 derive percents at the edge). Use it to show a maintainer their provenance picture and motivate requiring disclosure.",
|
|
2322
2551
|
inputSchema: repoInput,
|
|
2323
2552
|
handler: (args) => repoCoverage(rootFor(args.path), { range: args.range })
|
|
2324
2553
|
},
|
|
@@ -2350,15 +2579,23 @@ var TOOLS = [
|
|
|
2350
2579
|
path: { type: "string", description: "path to the repo (default: the server's working directory)" },
|
|
2351
2580
|
category: { enum: ["human", "with_ai", "ai"], description: "the honest authorship category (see the description). Defaults from with_ai if omitted." },
|
|
2352
2581
|
with_ai: { type: "boolean", description: "legacy shorthand: true \u21D2 category with_ai, false \u21D2 human. Prefer `category`." },
|
|
2353
|
-
tool: { type: "string", description: "the AI tool to name (Assisted-by for with_ai, Authored-by-ai for ai). Default: AI." }
|
|
2582
|
+
tool: { type: "string", description: "the AI tool to name (Assisted-by for with_ai, Authored-by-ai for ai). Default: AI." },
|
|
2583
|
+
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" },
|
|
2584
|
+
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." }
|
|
2354
2585
|
}
|
|
2355
2586
|
},
|
|
2356
2587
|
handler: (args) => {
|
|
2357
2588
|
const root = rootFor(args.path);
|
|
2358
2589
|
const tool = args.tool;
|
|
2590
|
+
const model = args.model;
|
|
2359
2591
|
const category = args.category;
|
|
2360
|
-
if (
|
|
2361
|
-
|
|
2592
|
+
if (args.print === true) {
|
|
2593
|
+
const c = category === "ai" ? "ai" : category === "with_ai" || args.with_ai === true ? "with_ai" : "human";
|
|
2594
|
+
return disclosureTrailers(root, { category: c, tool, model });
|
|
2595
|
+
}
|
|
2596
|
+
if (category === "ai") return attestAiAuthorship(root, { tool, model });
|
|
2597
|
+
const withAi = category === "with_ai" || args.with_ai === true;
|
|
2598
|
+
return affirmAuthorship(root, { withAi, tool, model: withAi ? model : void 0 });
|
|
2362
2599
|
}
|
|
2363
2600
|
},
|
|
2364
2601
|
{
|
|
@@ -2580,7 +2817,8 @@ function help() {
|
|
|
2580
2817
|
Usage:
|
|
2581
2818
|
madeby init Set MadeBy up: write .madeby/policy.json + the ACO AGENTS.md convention
|
|
2582
2819
|
+ the CI disclosure check. --update refreshes an outdated ACO block in
|
|
2583
|
-
place; --required to enforce now; --host azure-devops for Azure Pipelines
|
|
2820
|
+
place; --required to enforce now; --host azure-devops for Azure Pipelines;
|
|
2821
|
+
--record-model to also ask agents for the model version (opt-in).
|
|
2584
2822
|
madeby check [<range>] The gate: do commits meet .madeby/policy.json? (a verdict \u2014 exit
|
|
2585
2823
|
code gates). <range> e.g. origin/main..HEAD; in CI it's auto-detected.
|
|
2586
2824
|
--json for a machine-readable result.
|
|
@@ -2595,6 +2833,9 @@ Usage:
|
|
|
2595
2833
|
trailer \u2014 the symmetric human claim to \`ai\`).
|
|
2596
2834
|
madeby me --with-ai [tool] Made by me, with AI: also disclose AI assistance (Assisted-by:
|
|
2597
2835
|
<tool>, default "AI") \u2014 the assisted middle state, classified with_ai.
|
|
2836
|
+
--model <version> names the model too (Assisted-by: <tool>:<version>).
|
|
2837
|
+
--print (me / ai) prints the trailer lines instead of amending HEAD,
|
|
2838
|
+
for the commit message itself: git commit -m <msg> -m "$(madeby ai <tool> --print)".
|
|
2598
2839
|
madeby mcp Run the MCP server (stdio) \u2014 the tools, agent-callable.
|
|
2599
2840
|
madeby help Show this help.
|
|
2600
2841
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "madeby",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
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",
|