madeby 0.3.7 → 0.3.9
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 -3
- package/dist/madeby.mjs +317 -83
- 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,10 @@ 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.
|
|
55
57
|
madeby mcp Run the MCP server (stdio) — the tools, agent-callable.
|
|
56
58
|
madeby help
|
|
57
59
|
```
|
|
@@ -97,13 +99,23 @@ authorship coverage — last 500 commits (estimate · disclosure, not detection
|
|
|
97
99
|
of the 310 known: human 41% · with_ai 38% · ai 21%
|
|
98
100
|
trust: asserted 88% · verified 12% — self-reported; sign commits to verify
|
|
99
101
|
|
|
102
|
+
agents & models named in the 183 AI commits:
|
|
103
|
+
Claude Code 121 66%
|
|
104
|
+
Claude Opus 5.5 98 54%
|
|
105
|
+
Cursor 62 34%
|
|
106
|
+
|
|
100
107
|
→ 38% unknown. Require disclosure going forward: madeby init --required
|
|
101
108
|
```
|
|
102
109
|
|
|
103
110
|
It is a **labeled estimate**, honest by construction: an undisclosed commit is **unknown**, never
|
|
104
111
|
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.
|
|
106
|
-
|
|
112
|
+
never of the whole repo, and the asserted-vs-verified split shows how much is signed. The agents and
|
|
113
|
+
model versions are listed exactly as the AI commits name them — a commit naming a tool and its model
|
|
114
|
+
(`Assisted-by: Claude Code` + `Co-Authored-By: Claude Opus 5.5`) counts under both, so those rows can
|
|
115
|
+
sum past 100%. `--json` emits
|
|
116
|
+
counts only (percentages are derived at the edge, never stored). A plain merge authors nothing and
|
|
117
|
+
isn't scored; a merge that changes content of its own (a conflict resolution) is, like any other
|
|
118
|
+
commit — chosen by what it changes, never by whether it discloses. Use it to see your gap, then
|
|
107
119
|
`madeby init --required` to close it going forward.
|
|
108
120
|
|
|
109
121
|
### `madeby me` — affirm human authorship
|
|
@@ -138,6 +150,15 @@ Two modes, nothing more:
|
|
|
138
150
|
- `required` — fail on any commit that discloses nothing your policy accepts.
|
|
139
151
|
- optional `"accept": ["dco-signoff", "ai-trailer", …]` — restrict what satisfies the policy
|
|
140
152
|
(omit ⇒ any recognized disclosure counts).
|
|
153
|
+
- optional `"recordModel": true` — written by `madeby init --record-model`: the repo asks AI
|
|
154
|
+
disclosures to name the model version as well as the tool. It adds one line to the AGENTS.md rule,
|
|
155
|
+
and `me --with-ai` / `ai` then read the model from the **tool's own record of the session** — Cursor's
|
|
156
|
+
`CURSOR_MODEL`, the Copilot CLI and Claude Code session logs (locally; only the model id is read) —
|
|
157
|
+
which wins over an agent's `--model`, since an agent routed by "Auto" may not know which model served
|
|
158
|
+
it. Elsewhere `--model <version>` is used, never guessed. So `coverage` can list each **agent · model** pairing
|
|
159
|
+
(`Cursor · gpt-5.3-codex` vs `Claude Code · claude-sonnet-5`). A request, not a gate — an AI
|
|
160
|
+
disclosure without a model still satisfies the policy. Opt-in because naming models is a project's
|
|
161
|
+
call: the Linux kernel, for one, asks only that an LLM was used, not which.
|
|
141
162
|
|
|
142
163
|
(There's no "off" — that's just not running the check. Advisory is the floor.)
|
|
143
164
|
|
package/dist/madeby.mjs
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
var FIELD = "";
|
|
6
6
|
var RECORD = "";
|
|
7
|
+
var GROUP = "";
|
|
8
|
+
var parseMeta = (rec) => {
|
|
9
|
+
const parts = rec.split(FIELD);
|
|
10
|
+
const sig = parts[3] ?? "N";
|
|
11
|
+
return {
|
|
12
|
+
sha: parts[0] ?? "",
|
|
13
|
+
authorName: parts[1] ?? "",
|
|
14
|
+
authorEmail: parts[2] ?? "",
|
|
15
|
+
signed: sig !== "N" && sig !== "",
|
|
16
|
+
message: parts.slice(4).join(FIELD).trimEnd()
|
|
17
|
+
};
|
|
18
|
+
};
|
|
7
19
|
function readGitLog(repoPath, limit, range) {
|
|
8
20
|
try {
|
|
9
21
|
const args = [
|
|
@@ -11,7 +23,7 @@ function readGitLog(repoPath, limit, range) {
|
|
|
11
23
|
repoPath,
|
|
12
24
|
"log",
|
|
13
25
|
"--no-merges",
|
|
14
|
-
// merge
|
|
26
|
+
// a merge is not authored content by default; readAuthoredMerges finds those that are
|
|
15
27
|
...limit ? ["-n", String(limit)] : [],
|
|
16
28
|
// %G? is a signature-PRESENCE probe for the Disclosure Score: 'N' = unsigned; any other code
|
|
17
29
|
// (G/U/E/…) = a signature is present (E = present but unverifiable here — no keyring). We only
|
|
@@ -22,19 +34,38 @@ function readGitLog(repoPath, limit, range) {
|
|
|
22
34
|
...range ? [range] : []
|
|
23
35
|
];
|
|
24
36
|
const out = execFileSync("git", args, { encoding: "utf8", maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
|
|
25
|
-
return out.split(RECORD).map((r) => r.replace(/^\s+/, "")).filter((r) => r.length > 0).map(
|
|
26
|
-
const parts = rec.split(FIELD);
|
|
27
|
-
const sha = parts[0] ?? "";
|
|
28
|
-
const authorName = parts[1] ?? "";
|
|
29
|
-
const authorEmail = parts[2] ?? "";
|
|
30
|
-
const sig = parts[3] ?? "N";
|
|
31
|
-
const message = parts.slice(4).join(FIELD).trimEnd();
|
|
32
|
-
return { sha, authorName, authorEmail, signed: sig !== "N" && sig !== "", message };
|
|
33
|
-
});
|
|
37
|
+
return out.split(RECORD).map((r) => r.replace(/^\s+/, "")).filter((r) => r.length > 0).map(parseMeta);
|
|
34
38
|
} catch {
|
|
35
39
|
return [];
|
|
36
40
|
}
|
|
37
41
|
}
|
|
42
|
+
function readAuthoredMerges(repoPath, opts = {}) {
|
|
43
|
+
try {
|
|
44
|
+
const args = [
|
|
45
|
+
"-C",
|
|
46
|
+
repoPath,
|
|
47
|
+
"log",
|
|
48
|
+
"--merges",
|
|
49
|
+
"--cc",
|
|
50
|
+
"--name-only",
|
|
51
|
+
`--format=${RECORD}%H${FIELD}%an${FIELD}%ae${FIELD}%G?${FIELD}%B${GROUP}`,
|
|
52
|
+
...opts.since !== void 0 && !opts.range ? [`--since=@${opts.since}`] : [],
|
|
53
|
+
...opts.range ? [opts.range] : []
|
|
54
|
+
];
|
|
55
|
+
const out = execFileSync("git", args, { encoding: "utf8", maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
|
|
56
|
+
return out.split(RECORD).filter((r) => r.includes(GROUP)).filter((r) => r.slice(r.lastIndexOf(GROUP) + 1).trim().length > 0).map((r) => parseMeta(r.slice(0, r.lastIndexOf(GROUP))));
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function commitTime(repoPath, sha) {
|
|
62
|
+
try {
|
|
63
|
+
const t = Number(execFileSync("git", ["-C", repoPath, "log", "-1", "--format=%ct", sha], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
|
|
64
|
+
return Number.isFinite(t) && t > 0 ? t : void 0;
|
|
65
|
+
} catch {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
38
69
|
|
|
39
70
|
// ../classify/src/classify.ts
|
|
40
71
|
var AI_PATTERNS = [
|
|
@@ -55,6 +86,20 @@ function matchAi(text) {
|
|
|
55
86
|
}
|
|
56
87
|
return null;
|
|
57
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
|
+
}
|
|
58
103
|
var GENERIC_AI_TRAILER = /^(?:AI|an? AI|AI (?:assistant|tool|pair)|artificial intelligence)$/i;
|
|
59
104
|
function genericAiDisclosure(who) {
|
|
60
105
|
const w = who.trim();
|
|
@@ -94,13 +139,13 @@ function classifyCommit(commit) {
|
|
|
94
139
|
const aiAuthored = [...commit.message.matchAll(AI_AUTHOR_TRAILER)].map((m) => m[1].trim());
|
|
95
140
|
if (aiAuthored.length > 0 && !HUMAN_ATTEST.test(commit.message)) {
|
|
96
141
|
const who = aiAuthored[0];
|
|
97
|
-
const c =
|
|
142
|
+
const c = matchAiTrailer(who) ?? { provider: "unspecified", raw: who };
|
|
98
143
|
return { class: "ai", confidence: CONF_AI, signals: [`authored-by-ai:${c.provider}`], aiContributors: [c] };
|
|
99
144
|
}
|
|
100
145
|
const trailers = [...commit.message.matchAll(TRAILER_RE)].map((m) => m[1].trim());
|
|
101
146
|
let humanCoAuthors = 0;
|
|
102
147
|
for (const who of trailers) {
|
|
103
|
-
const ai =
|
|
148
|
+
const ai = matchAiTrailer(who);
|
|
104
149
|
if (ai) {
|
|
105
150
|
aiContributors.push(ai);
|
|
106
151
|
signals.push(`co-authored-by:${ai.provider}`);
|
|
@@ -109,7 +154,7 @@ function classifyCommit(commit) {
|
|
|
109
154
|
}
|
|
110
155
|
}
|
|
111
156
|
for (const who of aiAuthored) {
|
|
112
|
-
const ai =
|
|
157
|
+
const ai = matchAiTrailer(who) ?? { provider: "unspecified", raw: who };
|
|
113
158
|
aiContributors.push(ai);
|
|
114
159
|
signals.push(`authored-by-ai:${ai.provider}`);
|
|
115
160
|
}
|
|
@@ -894,7 +939,12 @@ function parseDisclosurePolicy(input) {
|
|
|
894
939
|
const mode = MODES.includes(o.mode) ? o.mode : "advisory";
|
|
895
940
|
const version = typeof o.version === "number" ? o.version : 0;
|
|
896
941
|
const accept = Array.isArray(o.accept) ? o.accept.filter((k) => typeof k === "string") : void 0;
|
|
897
|
-
return
|
|
942
|
+
return {
|
|
943
|
+
version,
|
|
944
|
+
mode,
|
|
945
|
+
...accept && accept.length > 0 ? { accept } : {},
|
|
946
|
+
...o.recordModel === true ? { recordModel: true } : {}
|
|
947
|
+
};
|
|
898
948
|
}
|
|
899
949
|
function evaluateDisclosurePolicy(policy, commits) {
|
|
900
950
|
const accept = policy.accept;
|
|
@@ -1200,30 +1250,54 @@ function provenanceMap(root, opts = {}) {
|
|
|
1200
1250
|
}
|
|
1201
1251
|
|
|
1202
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
|
+
}
|
|
1203
1257
|
function repoCoverage(root, opts = {}) {
|
|
1204
|
-
const
|
|
1258
|
+
const limit = opts.range ? void 0 : opts.limit ?? 500;
|
|
1259
|
+
const authored = readGitLog(root, limit, opts.range);
|
|
1260
|
+
const oldest = authored.at(-1)?.sha;
|
|
1261
|
+
const since = limit && oldest && authored.length >= limit ? commitTime(root, oldest) : void 0;
|
|
1262
|
+
const merges = authored.length > 0 && (!limit || authored.length < limit || since !== void 0) ? readAuthoredMerges(root, { range: opts.range, since }) : [];
|
|
1263
|
+
const commits = [...authored, ...merges];
|
|
1205
1264
|
const byCategory = { human: 0, with_ai: 0, ai: 0, bot: 0 };
|
|
1206
1265
|
const byTier = { asserted: 0, verified: 0 };
|
|
1207
1266
|
let known = 0;
|
|
1267
|
+
const agents = /* @__PURE__ */ new Map();
|
|
1208
1268
|
for (const c of commits) {
|
|
1209
1269
|
const kinds = commitDisclosureKinds({ message: c.message, signed: c.signed });
|
|
1210
|
-
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 });
|
|
1211
1271
|
const category = cls === "human" && !kinds.includes("human-attestation") ? "unknown" : cls;
|
|
1212
1272
|
if (category === "unknown") continue;
|
|
1213
1273
|
known += 1;
|
|
1214
1274
|
byCategory[category] += 1;
|
|
1215
1275
|
if (c.signed) byTier.verified += 1;
|
|
1216
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
|
+
}
|
|
1217
1290
|
}
|
|
1218
1291
|
return {
|
|
1219
1292
|
isEstimate: true,
|
|
1220
|
-
methodology: "commit-level authorship disclosure (estimate); undisclosed = unknown, never inferred",
|
|
1221
|
-
scope: opts.range ? `range ${opts.range}` : `last ${commits.length}
|
|
1293
|
+
methodology: "commit-level authorship disclosure (estimate); undisclosed = unknown, never inferred; merges scored only when they change content of their own",
|
|
1294
|
+
scope: (opts.range ? `range ${opts.range}` : `last ${authored.length} commits`) + (merges.length ? ` + ${merges.length} merges with changes of their own` : ""),
|
|
1222
1295
|
total: commits.length,
|
|
1223
1296
|
known,
|
|
1224
1297
|
unknown: commits.length - known,
|
|
1225
1298
|
byCategory,
|
|
1226
|
-
byTier
|
|
1299
|
+
byTier,
|
|
1300
|
+
byAgent: [...agents.values()].sort((a, b) => b.commits - a.commits || a.name.localeCompare(b.name))
|
|
1227
1301
|
};
|
|
1228
1302
|
}
|
|
1229
1303
|
|
|
@@ -1759,6 +1833,94 @@ To disclose: add a Co-Authored-By / Generated-by trailer, affirm your own author
|
|
|
1759
1833
|
|
|
1760
1834
|
// src/commands/me.ts
|
|
1761
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 CLI'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
|
+
}
|
|
1762
1924
|
function parseWithAi(args) {
|
|
1763
1925
|
const i = args.findIndex((a2) => a2 === "--with-ai" || a2.startsWith("--with-ai="));
|
|
1764
1926
|
if (i === -1) return { withAi: false, tool: "AI" };
|
|
@@ -1772,10 +1934,27 @@ function git3(root, args) {
|
|
|
1772
1934
|
}
|
|
1773
1935
|
var HUMAN_ATTEST2 = /^[ \t]*(?:Authored-by-human|Human-authored(?:-by)?)[ \t]*:/im;
|
|
1774
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
|
+
}
|
|
1775
1953
|
var AI_AUTHOR = /^[ \t]*Authored-by-ai[ \t]*:/im;
|
|
1776
1954
|
function attestAiAuthorship(root, opts = {}) {
|
|
1777
|
-
const
|
|
1778
|
-
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" };
|
|
1779
1958
|
try {
|
|
1780
1959
|
execFileSync5("git", ["-C", root, "diff", "--cached", "--quiet"]);
|
|
1781
1960
|
} catch {
|
|
@@ -1796,14 +1975,15 @@ function attestAiAuthorship(root, opts = {}) {
|
|
|
1796
1975
|
added.push("Authored-by-ai");
|
|
1797
1976
|
}
|
|
1798
1977
|
if (trailers.length > 0) {
|
|
1799
|
-
execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", ...trailers]);
|
|
1978
|
+
execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", "--allow-empty", ...trailers]);
|
|
1800
1979
|
}
|
|
1801
|
-
return { ok: true, changed: trailers.length > 0,
|
|
1980
|
+
return { ...base, ok: true, changed: trailers.length > 0, subject, added };
|
|
1802
1981
|
}
|
|
1803
1982
|
function affirmAuthorship(root, opts = {}) {
|
|
1804
1983
|
const withAi = opts.withAi ?? false;
|
|
1805
|
-
const
|
|
1806
|
-
const
|
|
1984
|
+
const ai = withAi ? resolveAi(root, opts) : void 0;
|
|
1985
|
+
const tool = ai?.value ?? "AI";
|
|
1986
|
+
const base = { ok: false, changed: false, withAi, tool, model: ai?.model, modelSource: ai?.modelSource, added: [], category: withAi ? "with_ai" : "human" };
|
|
1807
1987
|
let name = "";
|
|
1808
1988
|
let email = "";
|
|
1809
1989
|
try {
|
|
@@ -1838,20 +2018,26 @@ function affirmAuthorship(root, opts = {}) {
|
|
|
1838
2018
|
added.push("Assisted-by");
|
|
1839
2019
|
}
|
|
1840
2020
|
if (trailers.length > 0) {
|
|
1841
|
-
execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", ...trailers]);
|
|
2021
|
+
execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", "--allow-empty", ...trailers]);
|
|
1842
2022
|
}
|
|
1843
2023
|
const category = hasAssist || added.includes("Assisted-by") ? "with_ai" : "human";
|
|
1844
|
-
return { ok: true, changed: trailers.length > 0,
|
|
2024
|
+
return { ...base, ok: true, changed: trailers.length > 0, name, email, subject, added, category };
|
|
1845
2025
|
}
|
|
1846
2026
|
function meCommand(args) {
|
|
1847
2027
|
const { withAi, tool } = parseWithAi(args);
|
|
2028
|
+
const model = parseModel(args);
|
|
2029
|
+
if (model && !withAi) {
|
|
2030
|
+
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).");
|
|
2031
|
+
process.exit(1);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
1848
2034
|
const root = repoRoot();
|
|
1849
2035
|
if (!root) {
|
|
1850
2036
|
console.error("madeby me: not a git repository.");
|
|
1851
2037
|
process.exit(2);
|
|
1852
2038
|
return;
|
|
1853
2039
|
}
|
|
1854
|
-
const r = affirmAuthorship(root, { withAi, tool });
|
|
2040
|
+
const r = affirmAuthorship(root, { withAi, tool, model });
|
|
1855
2041
|
if (!r.ok) {
|
|
1856
2042
|
if (r.reason === "no-identity") console.error("madeby me: set your git identity first (git config user.name / user.email).");
|
|
1857
2043
|
else if (r.reason === "staged-changes") console.error("madeby me: you have staged changes. Commit them first \u2014 `me` affirms HEAD, not your index.");
|
|
@@ -1866,7 +2052,8 @@ function meCommand(args) {
|
|
|
1866
2052
|
return;
|
|
1867
2053
|
}
|
|
1868
2054
|
console.log(
|
|
1869
|
-
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})
|
|
2055
|
+
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)" ? `
|
|
2056
|
+
model from ${r.modelSource}.` : ""}` : `madeby me: affirmed human authorship on HEAD \u2014 "${r.subject}" (Authored-by-human: ${r.name}).`
|
|
1870
2057
|
);
|
|
1871
2058
|
}
|
|
1872
2059
|
|
|
@@ -1881,7 +2068,8 @@ function aiCommand(args) {
|
|
|
1881
2068
|
}
|
|
1882
2069
|
const makeSample = args.includes("--make-parser-sample");
|
|
1883
2070
|
const witness = args.includes("--witness");
|
|
1884
|
-
const
|
|
2071
|
+
const model = parseModel(args);
|
|
2072
|
+
const positional = args.filter((a, i) => !a.startsWith("--") && !(args[i - 1] === "--model"));
|
|
1885
2073
|
if (makeSample) {
|
|
1886
2074
|
const s = writeParserSample(root, { transcriptPath: positional[0] });
|
|
1887
2075
|
if (s.error === "transcript-not-found") {
|
|
@@ -1923,7 +2111,7 @@ function aiCommand(args) {
|
|
|
1923
2111
|
return;
|
|
1924
2112
|
}
|
|
1925
2113
|
const tool = positional[0] || "AI";
|
|
1926
|
-
const r = attestAiAuthorship(root, { tool });
|
|
2114
|
+
const r = attestAiAuthorship(root, { tool, model });
|
|
1927
2115
|
if (!r.ok) {
|
|
1928
2116
|
if (r.reason === "staged-changes") {
|
|
1929
2117
|
console.error("madeby ai: you have staged changes. Commit them first \u2014 `ai` attests HEAD, not your index.");
|
|
@@ -1940,7 +2128,8 @@ function aiCommand(args) {
|
|
|
1940
2128
|
}
|
|
1941
2129
|
console.log(
|
|
1942
2130
|
`madeby ai: attested AI authorship on HEAD \u2014 "${r.subject}" (Authored-by-ai: ${r.tool}).
|
|
1943
|
-
|
|
2131
|
+
` + (r.modelSource && r.modelSource !== "as given (--model)" ? ` model from ${r.modelSource}.
|
|
2132
|
+
` : "") + " \u2192 asserted tier. For evidence-backed spans from your session log: madeby ai --witness [log]."
|
|
1944
2133
|
);
|
|
1945
2134
|
}
|
|
1946
2135
|
|
|
@@ -1992,17 +2181,18 @@ function whoCommand(args) {
|
|
|
1992
2181
|
}
|
|
1993
2182
|
|
|
1994
2183
|
// src/commands/coverage.ts
|
|
1995
|
-
import { existsSync as
|
|
1996
|
-
import { join as
|
|
2184
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
|
|
2185
|
+
import { join as join7 } from "node:path";
|
|
1997
2186
|
function policyIsRequired(root) {
|
|
1998
|
-
const p =
|
|
1999
|
-
if (!
|
|
2187
|
+
const p = join7(root, ".madeby", "policy.json");
|
|
2188
|
+
if (!existsSync6(p)) return false;
|
|
2000
2189
|
try {
|
|
2001
|
-
return parseDisclosurePolicy(JSON.parse(
|
|
2190
|
+
return parseDisclosurePolicy(JSON.parse(readFileSync7(p, "utf8"))).mode === "required";
|
|
2002
2191
|
} catch {
|
|
2003
2192
|
return false;
|
|
2004
2193
|
}
|
|
2005
2194
|
}
|
|
2195
|
+
var AGENT_ROWS = 8;
|
|
2006
2196
|
var pct = (n, d) => d > 0 ? Math.round(n / d * 100) : 0;
|
|
2007
2197
|
var bar = (p) => "\u2588".repeat(Math.round(p / 5)) + "\u2591".repeat(20 - Math.round(p / 5));
|
|
2008
2198
|
function render(r, required) {
|
|
@@ -2023,6 +2213,16 @@ function render(r, required) {
|
|
|
2023
2213
|
of the ${r.known} known: ${parts.join(" \xB7 ")}`);
|
|
2024
2214
|
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`);
|
|
2025
2215
|
}
|
|
2216
|
+
const withAi = r.byCategory.with_ai + r.byCategory.ai;
|
|
2217
|
+
if (r.byAgent.length > 0) {
|
|
2218
|
+
console.log(`
|
|
2219
|
+
agents & models named in the ${withAi} AI commits:`);
|
|
2220
|
+
const width = Math.max(...r.byAgent.slice(0, AGENT_ROWS).map((a) => a.name.length));
|
|
2221
|
+
for (const a of r.byAgent.slice(0, AGENT_ROWS)) {
|
|
2222
|
+
console.log(` ${a.name.padEnd(width)} ${String(a.commits).padStart(4)} ${String(pct(a.commits, withAi)).padStart(3)}%`);
|
|
2223
|
+
}
|
|
2224
|
+
if (r.byAgent.length > AGENT_ROWS) console.log(` \u2026 and ${r.byAgent.length - AGENT_ROWS} more (--json for all)`);
|
|
2225
|
+
}
|
|
2026
2226
|
if (r.unknown > 0) {
|
|
2027
2227
|
console.log(
|
|
2028
2228
|
required ? `
|
|
@@ -2049,8 +2249,8 @@ function coverageCommand(args) {
|
|
|
2049
2249
|
}
|
|
2050
2250
|
|
|
2051
2251
|
// src/init-repo.ts
|
|
2052
|
-
import { existsSync as
|
|
2053
|
-
import { dirname as dirname2, join as
|
|
2252
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2253
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
2054
2254
|
var ACO_VERSION = 1;
|
|
2055
2255
|
var ACO_OPEN = `<!-- madeby:aco v${ACO_VERSION} -->`;
|
|
2056
2256
|
var ACO_CLOSE = `<!-- /madeby:aco -->`;
|
|
@@ -2067,14 +2267,16 @@ After you commit, run the command for the honest category (no install, no setup)
|
|
|
2067
2267
|
|
|
2068
2268
|
Disclose per commit, honestly. Undisclosed content stays *unknown*, never guessed.
|
|
2069
2269
|
`;
|
|
2070
|
-
var
|
|
2071
|
-
|
|
2270
|
+
var MODEL_LINE = `
|
|
2271
|
+
**This repo also records the model.** Name your tool as usual \u2014 madeby reads the model from the
|
|
2272
|
+
tool's own record of the session where it can (Cursor, Copilot CLI, Claude Code), e.g.
|
|
2273
|
+
\`npx madeby@latest me --with-ai Cursor\` writes \`Assisted-by: Cursor:<model>\`. Elsewhere add
|
|
2274
|
+
\`--model <version>\` with the exact model you are running as. If you don't know it, leave it off \u2014
|
|
2275
|
+
never guess.
|
|
2276
|
+
`;
|
|
2277
|
+
var acoBlock = (recordModel) => `${ACO_OPEN}
|
|
2278
|
+
${ACO_BODY}${recordModel ? MODEL_LINE : ""}${ACO_CLOSE}
|
|
2072
2279
|
`;
|
|
2073
|
-
var AGENTS_CONVENTION = `# AGENTS.md
|
|
2074
|
-
|
|
2075
|
-
${ACO_BLOCK}`;
|
|
2076
|
-
var ACO_SECTION = `
|
|
2077
|
-
${ACO_BLOCK}`;
|
|
2078
2280
|
function findAcoBlock(content) {
|
|
2079
2281
|
const m = ACO_OPEN_RE.exec(content);
|
|
2080
2282
|
if (!m) return null;
|
|
@@ -2097,11 +2299,11 @@ function acoStatus(content) {
|
|
|
2097
2299
|
if (loc.version > ACO_VERSION) return "newer";
|
|
2098
2300
|
return "current";
|
|
2099
2301
|
}
|
|
2100
|
-
function replaceAcoBlock(content, loc) {
|
|
2101
|
-
return content.slice(0, loc.start) +
|
|
2302
|
+
function replaceAcoBlock(content, loc, block) {
|
|
2303
|
+
return content.slice(0, loc.start) + block + content.slice(loc.end);
|
|
2102
2304
|
}
|
|
2103
2305
|
function writeIfAbsent(abs, rel, content, created, skipped) {
|
|
2104
|
-
if (
|
|
2306
|
+
if (existsSync7(abs)) {
|
|
2105
2307
|
skipped.push(rel);
|
|
2106
2308
|
return;
|
|
2107
2309
|
}
|
|
@@ -2109,47 +2311,60 @@ function writeIfAbsent(abs, rel, content, created, skipped) {
|
|
|
2109
2311
|
writeFileSync2(abs, content);
|
|
2110
2312
|
created.push(rel);
|
|
2111
2313
|
}
|
|
2112
|
-
function ensurePolicy(abs, rel,
|
|
2113
|
-
if (!
|
|
2314
|
+
function ensurePolicy(abs, rel, want, res) {
|
|
2315
|
+
if (!existsSync7(abs)) {
|
|
2114
2316
|
mkdirSync2(dirname2(abs), { recursive: true });
|
|
2115
|
-
|
|
2317
|
+
const fresh = { version: 0, mode: want.mode, ...want.recordModel ? { recordModel: true } : {} };
|
|
2318
|
+
writeFileSync2(abs, JSON.stringify(fresh, null, 2) + "\n");
|
|
2116
2319
|
res.created.push(rel);
|
|
2117
|
-
return { mode, malformed: false };
|
|
2320
|
+
return { mode: want.mode, recordModel: want.recordModel, malformed: false };
|
|
2118
2321
|
}
|
|
2119
2322
|
let raw;
|
|
2120
2323
|
try {
|
|
2121
|
-
raw = JSON.parse(
|
|
2324
|
+
raw = JSON.parse(readFileSync8(abs, "utf8"));
|
|
2122
2325
|
} catch {
|
|
2123
2326
|
res.skipped.push(rel);
|
|
2124
|
-
return { mode: "advisory", malformed: true };
|
|
2327
|
+
return { mode: "advisory", recordModel: false, malformed: true };
|
|
2125
2328
|
}
|
|
2126
2329
|
const isObject = !!raw && typeof raw === "object" && !Array.isArray(raw);
|
|
2127
|
-
const current = parseDisclosurePolicy(raw)
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2330
|
+
const current = parseDisclosurePolicy(raw);
|
|
2331
|
+
const raise = want.mode === "required" && current.mode !== "required" && isObject;
|
|
2332
|
+
const modelOn = want.recordModel && !current.recordModel && isObject;
|
|
2333
|
+
if (raise || modelOn) {
|
|
2334
|
+
const next = { ...raw, ...raise ? { mode: "required" } : {}, ...modelOn ? { recordModel: true } : {} };
|
|
2335
|
+
writeFileSync2(abs, JSON.stringify(next, null, 2) + "\n");
|
|
2336
|
+
if (raise) res.raised.push(rel);
|
|
2337
|
+
if (modelOn) res.modelOn.push(rel);
|
|
2338
|
+
} else {
|
|
2339
|
+
res.skipped.push(rel);
|
|
2132
2340
|
}
|
|
2133
|
-
|
|
2134
|
-
return { mode: current, malformed: !isObject };
|
|
2341
|
+
return { mode: raise ? "required" : current.mode, recordModel: modelOn || current.recordModel === true, malformed: !isObject };
|
|
2135
2342
|
}
|
|
2136
2343
|
function ensureAco(abs, rel, res, opts) {
|
|
2137
|
-
|
|
2344
|
+
const block = acoBlock(opts.recordModel);
|
|
2345
|
+
if (!existsSync7(abs)) {
|
|
2138
2346
|
if (!opts.createIfAbsent) return;
|
|
2139
2347
|
mkdirSync2(dirname2(abs), { recursive: true });
|
|
2140
|
-
writeFileSync2(abs,
|
|
2348
|
+
writeFileSync2(abs, `# AGENTS.md
|
|
2349
|
+
|
|
2350
|
+
${block}`);
|
|
2141
2351
|
res.created.push(rel);
|
|
2142
2352
|
return;
|
|
2143
2353
|
}
|
|
2144
|
-
const content =
|
|
2354
|
+
const content = readFileSync8(abs, "utf8");
|
|
2145
2355
|
const status = acoStatus(content);
|
|
2356
|
+
const loc = findAcoBlock(content);
|
|
2146
2357
|
if (status === "absent") {
|
|
2147
2358
|
const sep = content.endsWith("\n") ? "" : "\n";
|
|
2148
|
-
writeFileSync2(abs, content + sep +
|
|
2359
|
+
writeFileSync2(abs, content + sep + `
|
|
2360
|
+
${block}`);
|
|
2149
2361
|
res.merged.push(rel);
|
|
2362
|
+
} else if (status === "current" && opts.modelRequested && loc && content.slice(loc.start, loc.end) !== block) {
|
|
2363
|
+
writeFileSync2(abs, replaceAcoBlock(content, loc, block));
|
|
2364
|
+
res.updated.push(rel);
|
|
2150
2365
|
} else if (status === "outdated") {
|
|
2151
2366
|
if (opts.update) {
|
|
2152
|
-
writeFileSync2(abs, replaceAcoBlock(content,
|
|
2367
|
+
writeFileSync2(abs, replaceAcoBlock(content, loc, block));
|
|
2153
2368
|
res.updated.push(rel);
|
|
2154
2369
|
} else {
|
|
2155
2370
|
res.outdated.push(rel);
|
|
@@ -2168,17 +2383,23 @@ function initRepo(root, opts = {}) {
|
|
|
2168
2383
|
const updated = [];
|
|
2169
2384
|
const outdated = [];
|
|
2170
2385
|
const raised = [];
|
|
2171
|
-
const
|
|
2172
|
-
const
|
|
2173
|
-
|
|
2174
|
-
|
|
2386
|
+
const modelOn = [];
|
|
2387
|
+
const res = { created, skipped, merged, updated, outdated, raised, modelOn };
|
|
2388
|
+
const modelRequested = opts.recordModel ?? false;
|
|
2389
|
+
const policy = ensurePolicy(join8(root, ".madeby", "policy.json"), ".madeby/policy.json", { mode, recordModel: modelRequested }, res);
|
|
2390
|
+
const aco = { update, recordModel: policy.recordModel, modelRequested };
|
|
2391
|
+
ensureAco(join8(root, "AGENTS.md"), "AGENTS.md", res, { createIfAbsent: true, ...aco });
|
|
2392
|
+
ensureAco(join8(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false, ...aco });
|
|
2175
2393
|
if (host === "github") {
|
|
2176
|
-
writeIfAbsent(
|
|
2394
|
+
writeIfAbsent(join8(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
|
|
2177
2395
|
}
|
|
2178
2396
|
const nextSteps = ["Commit the created files."];
|
|
2179
2397
|
if (raised.length > 0) {
|
|
2180
2398
|
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).');
|
|
2181
2399
|
}
|
|
2400
|
+
if (modelOn.length > 0 || modelRequested && created.includes(".madeby/policy.json")) {
|
|
2401
|
+
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.");
|
|
2402
|
+
}
|
|
2182
2403
|
if (policy.malformed) {
|
|
2183
2404
|
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.');
|
|
2184
2405
|
}
|
|
@@ -2204,7 +2425,7 @@ function initRepo(root, opts = {}) {
|
|
|
2204
2425
|
}
|
|
2205
2426
|
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.");
|
|
2206
2427
|
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.");
|
|
2207
|
-
return { created, skipped, merged, updated, outdated, raised, policyMode: policy.mode, host, nextSteps };
|
|
2428
|
+
return { created, skipped, merged, updated, outdated, raised, modelOn, policyMode: policy.mode, recordModel: policy.recordModel, host, nextSteps };
|
|
2208
2429
|
}
|
|
2209
2430
|
var GH_WORKFLOW = `name: Disclosure
|
|
2210
2431
|
on: pull_request
|
|
@@ -2228,17 +2449,19 @@ function initCommand(args) {
|
|
|
2228
2449
|
}
|
|
2229
2450
|
const mode = args.includes("--required") ? "required" : "advisory";
|
|
2230
2451
|
const update = args.includes("--update");
|
|
2452
|
+
const recordModel = args.includes("--record-model");
|
|
2231
2453
|
const hostIdx = args.indexOf("--host");
|
|
2232
2454
|
const host = hostIdx >= 0 ? args[hostIdx + 1] : void 0;
|
|
2233
|
-
const r = initRepo(root, { mode, host, update });
|
|
2455
|
+
const r = initRepo(root, { mode, host, update, recordModel });
|
|
2234
2456
|
for (const f of r.created) console.log(` + ${f}`);
|
|
2235
2457
|
for (const f of r.merged) console.log(` ~ ${f} (merged in the ACO rule)`);
|
|
2236
2458
|
for (const f of r.updated) console.log(` \u2191 ${f} (ACO convention refreshed to the current version)`);
|
|
2237
2459
|
for (const f of r.raised) console.log(` \u2191 ${f} (mode raised to required)`);
|
|
2460
|
+
for (const f of r.modelOn) console.log(` \u2191 ${f} (now records the model version)`);
|
|
2238
2461
|
for (const f of r.outdated) console.log(` ! ${f} (ACO convention OUTDATED \u2014 run 'madeby init --update')`);
|
|
2239
2462
|
for (const f of r.skipped) console.log(` \xB7 ${f} (exists \u2014 left as-is)`);
|
|
2240
2463
|
console.log(`
|
|
2241
|
-
madeby init \u2014 policy: ${r.policyMode}, host: ${r.host}`);
|
|
2464
|
+
madeby init \u2014 policy: ${r.policyMode}${r.recordModel ? " + records model" : ""}, host: ${r.host}`);
|
|
2242
2465
|
console.log("Next:");
|
|
2243
2466
|
for (const s of r.nextSteps) console.log(` - ${s}`);
|
|
2244
2467
|
}
|
|
@@ -2247,7 +2470,7 @@ madeby init \u2014 policy: ${r.policyMode}, host: ${r.host}`);
|
|
|
2247
2470
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
2248
2471
|
import { createInterface } from "node:readline";
|
|
2249
2472
|
var DEFAULT_PROTOCOL = "2024-11-05";
|
|
2250
|
-
var SERVER_INFO = { name: "madeby", version: "0.3.
|
|
2473
|
+
var SERVER_INFO = { name: "madeby", version: "0.3.9" };
|
|
2251
2474
|
function rootFor(path) {
|
|
2252
2475
|
const cwd = path && path.length ? path : process.cwd();
|
|
2253
2476
|
return execFileSync6("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
@@ -2269,10 +2492,16 @@ var TOOLS = [
|
|
|
2269
2492
|
path: { type: "string", description: "path to the repo (default: the server's working directory)" },
|
|
2270
2493
|
mode: { enum: ["advisory", "required"], description: "policy mode to seed (default: advisory)" },
|
|
2271
2494
|
host: { type: "string", description: "CI host to wire: github (default) or azure-devops" },
|
|
2272
|
-
update: { type: "boolean", description: "refresh an outdated ACO convention block in place (drift update); default false (report only)" }
|
|
2495
|
+
update: { type: "boolean", description: "refresh an outdated ACO convention block in place (drift update); default false (report only)" },
|
|
2496
|
+
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." }
|
|
2273
2497
|
}
|
|
2274
2498
|
},
|
|
2275
|
-
handler: (args) => initRepo(rootFor(args.path), {
|
|
2499
|
+
handler: (args) => initRepo(rootFor(args.path), {
|
|
2500
|
+
mode: args.mode,
|
|
2501
|
+
host: args.host,
|
|
2502
|
+
update: args.update,
|
|
2503
|
+
recordModel: args.record_model
|
|
2504
|
+
})
|
|
2276
2505
|
},
|
|
2277
2506
|
{
|
|
2278
2507
|
name: "check",
|
|
@@ -2282,7 +2511,7 @@ var TOOLS = [
|
|
|
2282
2511
|
},
|
|
2283
2512
|
{
|
|
2284
2513
|
name: "coverage",
|
|
2285
|
-
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),
|
|
2514
|
+
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.",
|
|
2286
2515
|
inputSchema: repoInput,
|
|
2287
2516
|
handler: (args) => repoCoverage(rootFor(args.path), { range: args.range })
|
|
2288
2517
|
},
|
|
@@ -2314,15 +2543,18 @@ var TOOLS = [
|
|
|
2314
2543
|
path: { type: "string", description: "path to the repo (default: the server's working directory)" },
|
|
2315
2544
|
category: { enum: ["human", "with_ai", "ai"], description: "the honest authorship category (see the description). Defaults from with_ai if omitted." },
|
|
2316
2545
|
with_ai: { type: "boolean", description: "legacy shorthand: true \u21D2 category with_ai, false \u21D2 human. Prefer `category`." },
|
|
2317
|
-
tool: { type: "string", description: "the AI tool to name (Assisted-by for with_ai, Authored-by-ai for ai). Default: AI." }
|
|
2546
|
+
tool: { type: "string", description: "the AI tool to name (Assisted-by for with_ai, Authored-by-ai for ai). Default: AI." },
|
|
2547
|
+
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." }
|
|
2318
2548
|
}
|
|
2319
2549
|
},
|
|
2320
2550
|
handler: (args) => {
|
|
2321
2551
|
const root = rootFor(args.path);
|
|
2322
2552
|
const tool = args.tool;
|
|
2553
|
+
const model = args.model;
|
|
2323
2554
|
const category = args.category;
|
|
2324
|
-
if (category === "ai") return attestAiAuthorship(root, { tool });
|
|
2325
|
-
|
|
2555
|
+
if (category === "ai") return attestAiAuthorship(root, { tool, model });
|
|
2556
|
+
const withAi = category === "with_ai" || args.with_ai === true;
|
|
2557
|
+
return affirmAuthorship(root, { withAi, tool, model: withAi ? model : void 0 });
|
|
2326
2558
|
}
|
|
2327
2559
|
},
|
|
2328
2560
|
{
|
|
@@ -2544,7 +2776,8 @@ function help() {
|
|
|
2544
2776
|
Usage:
|
|
2545
2777
|
madeby init Set MadeBy up: write .madeby/policy.json + the ACO AGENTS.md convention
|
|
2546
2778
|
+ the CI disclosure check. --update refreshes an outdated ACO block in
|
|
2547
|
-
place; --required to enforce now; --host azure-devops for Azure Pipelines
|
|
2779
|
+
place; --required to enforce now; --host azure-devops for Azure Pipelines;
|
|
2780
|
+
--record-model to also ask agents for the model version (opt-in).
|
|
2548
2781
|
madeby check [<range>] The gate: do commits meet .madeby/policy.json? (a verdict \u2014 exit
|
|
2549
2782
|
code gates). <range> e.g. origin/main..HEAD; in CI it's auto-detected.
|
|
2550
2783
|
--json for a machine-readable result.
|
|
@@ -2559,6 +2792,7 @@ Usage:
|
|
|
2559
2792
|
trailer \u2014 the symmetric human claim to \`ai\`).
|
|
2560
2793
|
madeby me --with-ai [tool] Made by me, with AI: also disclose AI assistance (Assisted-by:
|
|
2561
2794
|
<tool>, default "AI") \u2014 the assisted middle state, classified with_ai.
|
|
2795
|
+
--model <version> names the model too (Assisted-by: <tool>:<version>).
|
|
2562
2796
|
madeby mcp Run the MCP server (stdio) \u2014 the tools, agent-callable.
|
|
2563
2797
|
madeby help Show this help.
|
|
2564
2798
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "madeby",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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",
|