madeby 0.3.11 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +23 -12
  2. package/dist/madeby.mjs +189 -37
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -67,22 +67,30 @@ sides of it, and `init`/`check` are the setup and the gate.
67
67
  ### `madeby check` — the disclosure gate
68
68
 
69
69
  Reads `.madeby/policy.json`, recognizes each commit's disclosure signals, and reports coverage.
70
- A commit **discloses its origin** if it carries any of:
70
+ A commit **discloses who wrote it** if it carries any of:
71
71
 
72
72
  - an **AI-authorship trailer** — `Co-Authored-By:` / `Generated-by:` / `Assisted-by:` naming an AI
73
- tool (Claude Code, Copilot, Cursor, aider, … already emit these);
73
+ tool (Claude Code, Copilot, Cursor, aider, … already emit these), or `Authored-by-ai:`;
74
74
  - an **affirmative human-authorship trailer** — `Authored-by-human: <you>` (see below);
75
- - a **DCO `Signed-off-by:`** line;
76
- - a **commit signature**;
77
75
  - a **`madeby ai`** attestation (your own AI session's witnessed spans).
78
76
 
79
- Exit codes: **0** = pass · **1** = a `required` policy failed on an undisclosed commit · **2** = not
80
- a git repository. Under `advisory` it only reports (always exit 0) — so it's safe to add to CI
77
+ A **DCO `Signed-off-by:`** line or a **commit signature** is recognized too, but on its own it says
78
+ who is accountable for the commit, not who wrote it, so it counts only if the policy's `accept`
79
+ lists it. `check` reports commits that carry only one of those apart from those that carry nothing.
80
+
81
+ Exit codes: **0** = pass · **1** = a `required` policy failed on an undisclosed commit, or couldn't
82
+ read the history (below) · **2** = not a git repository. Under `advisory` it only reports (always exit 0) — so it's safe to add to CI
81
83
  before you decide to enforce anything.
82
84
 
83
85
  In CI, omit `<range>` — on a pull-request build `madeby check` **auto-scopes to the PR's own
84
- commits** from the CI environment (GitHub today; other hosts via their adapter). Pass an explicit
85
- `<range>` to override.
86
+ commits** from the CI environment (GitHub, Azure DevOps and GitLab). Pass an explicit `<range>` to
87
+ override.
88
+
89
+ **The history has to be there.** On a shallow clone, when a range endpoint isn't in the clone (a
90
+ target branch that was never fetched), or on a pull-request build whose range can't be read, `check`
91
+ says so: under `required` it fails rather than score whatever happens to be present, and under
92
+ `advisory` it prints a warning. Check out the full history (`fetch-depth: 0` on GitHub,
93
+ `fetchDepth: 0` on Azure Pipelines); the files `madeby init` writes already do.
86
94
 
87
95
  ### `madeby coverage` — the authorship scoreboard
88
96
 
@@ -92,6 +100,7 @@ history's authorship is **on the record** — no policy, no config:
92
100
  ```sh
93
101
  madeby coverage # scores recent history
94
102
  madeby coverage origin/main..HEAD # or a range
103
+ madeby coverage --by author # the same, one row per commit author
95
104
  ```
96
105
 
97
106
  ```
@@ -115,7 +124,8 @@ inferred as human — disclosure, not detection. The human/with_ai/ai mix is rep
115
124
  never of the whole repo, and the asserted-vs-verified split shows how much is signed. The agents and
116
125
  model versions are listed exactly as the AI commits name them — a commit naming a tool and its model
117
126
  (`Assisted-by: Claude Code` + `Co-Authored-By: Claude Opus 5.5`) counts under both, so those rows can
118
- sum past 100%. `--json` emits
127
+ sum past 100%. `--by author` shows one row per commit author (grouped by email): how many of their
128
+ commits disclose, and the mix — who discloses and who never does. `--json` (which includes `byAuthor`) emits
119
129
  counts only (percentages are derived at the edge, never stored). A plain merge authors nothing and
120
130
  isn't scored; a merge that changes content of its own (a conflict resolution) is, like any other
121
131
  commit — chosen by what it changes, never by whether it discloses. Use it to see your gap, then
@@ -151,8 +161,8 @@ Two modes, nothing more:
151
161
 
152
162
  - `advisory` — report coverage on each run; never fails. **The default** (and what you get with no file).
153
163
  - `required` — fail on any commit that discloses nothing your policy accepts.
154
- - optional `"accept": ["dco-signoff", "ai-trailer", …]` — restrict what satisfies the policy
155
- (omit ⇒ any recognized disclosure counts).
164
+ - optional `"accept": ["dco-signoff", "ai-trailer", …]` — set what satisfies the policy
165
+ (omit ⇒ any authorship disclosure counts; a DCO sign-off or signature alone counts only if listed).
156
166
  - optional `"recordModel": true` — written by `madeby init --record-model`: the repo asks AI
157
167
  disclosures to name the model version as well as the tool. It adds one line to the AGENTS.md rule,
158
168
  and `me --with-ai` / `ai` then read the model from the **tool's own record of the session** — Cursor's
@@ -166,7 +176,8 @@ Two modes, nothing more:
166
176
  (There's no "off" — that's just not running the check. Advisory is the floor.)
167
177
 
168
178
  Fail-safe by design: a missing or malformed policy degrades to `advisory`. `madeby` never blocks a PR
169
- on its own error, and never invents a stricter gate than the maintainer wrote.
179
+ on its own error, and never invents a stricter gate than the maintainer wrote. It never passes a
180
+ `required` check it couldn't actually run, either: missing history fails it (see `check` above).
170
181
 
171
182
  ## Use it as a PR check
172
183
 
package/dist/madeby.mjs CHANGED
@@ -16,6 +16,26 @@ var parseMeta = (rec) => {
16
16
  message: parts.slice(4).join(FIELD).trimEnd()
17
17
  };
18
18
  };
19
+ function historyProblem(repoPath, range) {
20
+ const git4 = (...args) => {
21
+ try {
22
+ return execFileSync("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
23
+ } catch {
24
+ return null;
25
+ }
26
+ };
27
+ if (git4("rev-parse", "--is-shallow-repository") === "true") {
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
+ }
30
+ if (range) {
31
+ for (const end of range.split(/\.{2,3}/).filter(Boolean)) {
32
+ if (git4("rev-parse", "--verify", "--quiet", `${end}^{commit}`) === null) {
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
+ }
35
+ }
36
+ }
37
+ return null;
38
+ }
19
39
  function readGitLog(repoPath, limit, range) {
20
40
  try {
21
41
  const args = [
@@ -873,6 +893,27 @@ var conformance_v0_default = {
873
893
  var CONFORMANCE_VERSION = conformance_v0_default.version;
874
894
 
875
895
  // ../core/src/disclosure.ts
896
+ var DISCLOSURE_KINDS = {
897
+ "ai-trailer": { label: "AI-authorship trailer", nativeness: "external", ceiling: "asserted", parser: "recognized", authorship: true },
898
+ // The AFFIRMATIVE human-authorship claim — the symmetric counterpart of the AI trailer, NOT a
899
+ // separate mechanism: same recognizer, same ladder. A bare trailer is `asserted` (identical to the
900
+ // AI trailer); sign it → verified, swear it → sworn — the signature authenticates WHO is claiming
901
+ // (invariant #10), equally for human or AI disclosure. The one genuine asymmetry is EVIDENCE vs.
902
+ // tier: AI authorship can leave a re-checkable ARTIFACT (a session log matched to the code,
903
+ // `prove`); human authorship is the author's firsthand TESTIMONY — real, but not independently
904
+ // re-checkable, so to a third party it lands as an attestation (asserted → signed → sworn testimony).
905
+ "human-attestation": { label: "human-authorship attestation", nativeness: "madeby", ceiling: "asserted", parser: "recognized", authorship: true },
906
+ "dco-signoff": { label: "DCO Signed-off-by", nativeness: "external", ceiling: "asserted", parser: "recognized", authorship: false },
907
+ "commit-signature": { label: "commit signature", nativeness: "external", ceiling: "verified", parser: "recognized", authorship: false },
908
+ "ai-tool-config": { label: "committed AI-tool config", nativeness: "external", ceiling: "asserted", parser: "recognized", authorship: false },
909
+ "spdx-license": { label: "SPDX license metadata", nativeness: "external", ceiling: "asserted", parser: "recognized", authorship: false },
910
+ "spdx-ai-disclosure": { label: "SPDX-AI-Disclosure tag", nativeness: "external", ceiling: "asserted", parser: "recognized", authorship: true },
911
+ reuse: { label: "REUSE licensing", nativeness: "external", ceiling: "asserted", parser: "planned", authorship: false },
912
+ "in-toto": { label: "in-toto attestation", nativeness: "external", ceiling: "verified", parser: "planned", authorship: false },
913
+ slsa: { label: "SLSA provenance", nativeness: "external", ceiling: "verified", parser: "planned", authorship: false },
914
+ "madeby-manifest": { label: "MadeBy witnessed manifest", nativeness: "madeby", ceiling: "asserted", parser: "recognized", authorship: true },
915
+ "madeby-declaration": { label: "MadeBy self-hosted declaration", nativeness: "madeby", ceiling: "sworn", parser: "recognized", authorship: true }
916
+ };
876
917
  var AI_TRAILER_RE = /^[ \t]*(?:Co-authored-by|Generated-by|Assisted-by)[ \t]*:[ \t]*(.+?)[ \t]*$/gim;
877
918
  var AI_MARKER_RE = /\b(?:claude|copilot|cursor(?:agent)?|devin|gemini|chatgpt|openai|codex|gpt-|codeium|windsurf|aider|codewhisperer|tabnine)\b/i;
878
919
  function hasAiTrailer(message) {
@@ -947,18 +988,22 @@ function parseDisclosurePolicy(input) {
947
988
  ...o.recordModel === true ? { recordModel: true } : {}
948
989
  };
949
990
  }
991
+ function acceptsKind(policy, kind) {
992
+ return policy.accept ? policy.accept.includes(kind) : DISCLOSURE_KINDS[kind]?.authorship === true;
993
+ }
950
994
  function evaluateDisclosurePolicy(policy, commits) {
951
995
  const accept = policy.accept;
952
- const isAccepted = (k) => !accept || accept.includes(k);
953
996
  const verdicts = commits.map((c) => {
954
- const satisfiedBy = c.kinds.filter(isAccepted);
955
- return { ref: c.ref, compliant: satisfiedBy.length > 0, satisfiedBy };
997
+ const satisfiedBy = c.kinds.filter((k) => acceptsKind(policy, k));
998
+ const notAccepted = c.kinds.filter((k) => !acceptsKind(policy, k));
999
+ return { ref: c.ref, compliant: satisfiedBy.length > 0, satisfiedBy, notAccepted };
956
1000
  });
957
1001
  const nonCompliant = verdicts.filter((v) => !v.compliant);
958
1002
  const compliant = verdicts.length - nonCompliant.length;
959
1003
  const pass = policy.mode !== "required" || nonCompliant.length === 0;
960
- const wanted = accept ? accept.join(", ") : "any recognized disclosure";
961
- const summary = `${compliant}/${verdicts.length} commits disclose origin (${policy.mode}; accepts: ${wanted}).` + (nonCompliant.length ? ` ${nonCompliant.length} undisclosed.` : "");
1004
+ const wanted = accept ? accept.join(", ") : "any authorship disclosure";
1005
+ const otherOnly = nonCompliant.filter((v) => v.notAccepted.length > 0).length;
1006
+ const summary = `${compliant}/${verdicts.length} commits disclose origin (${policy.mode}; accepts: ${wanted}).` + (nonCompliant.length ? ` ${nonCompliant.length} undisclosed` + (otherOnly ? ` (${otherOnly} carry only a sign-off or signature, which don't say who wrote the code).` : ".") : "");
962
1007
  return { mode: policy.mode, pass, total: verdicts.length, compliant, nonCompliant, summary };
963
1008
  }
964
1009
 
@@ -977,6 +1022,10 @@ function detectCiRange(env = process.env) {
977
1022
  }
978
1023
  return null;
979
1024
  }
1025
+ function detectCiPullRequest(env = process.env) {
1026
+ for (const adapter of registry.values()) if (adapter.ciPullRequest?.(env)) return adapter.id;
1027
+ return null;
1028
+ }
980
1029
 
981
1030
  // ../analyzer/src/host/github.ts
982
1031
  var NOREPLY = /^(?:(\d+)\+)?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))@users\.noreply\.github\.com$/i;
@@ -996,8 +1045,11 @@ var githubAdapter = {
996
1045
  // On a `pull_request` event GitHub sets GITHUB_EVENT_NAME + GITHUB_EVENT_PATH (a JSON payload with
997
1046
  // the PR's base/head SHAs). Reading base..head here replaces the bash range computation the Action
998
1047
  // used to carry — the range logic now lives once, in the adapter. Needs the PR history present
999
- // (the quickstart's `fetch-depth: 0`). Any absence/parse failure → null (fall back to recent
1000
- // history); never throws.
1048
+ // (the quickstart's `fetch-depth: 0`). Any absence/parse failure → null (`check` reports the
1049
+ // unreadable PR build, see ciPullRequest); never throws.
1050
+ ciPullRequest(env) {
1051
+ return env.GITHUB_EVENT_NAME === "pull_request" || env.GITHUB_EVENT_NAME === "pull_request_target";
1052
+ },
1001
1053
  ciRange(env) {
1002
1054
  const event = env.GITHUB_EVENT_NAME;
1003
1055
  if (event !== "pull_request" && event !== "pull_request_target") return null;
@@ -1266,11 +1318,18 @@ function repoCoverage(root, opts = {}) {
1266
1318
  const byTier = { asserted: 0, verified: 0 };
1267
1319
  let known = 0;
1268
1320
  const agents = /* @__PURE__ */ new Map();
1321
+ const authors = /* @__PURE__ */ new Map();
1269
1322
  for (const c of commits) {
1270
1323
  const kinds = commitDisclosureKinds({ message: c.message, signed: c.signed });
1271
1324
  const { class: cls, aiContributors } = classifyCommit({ message: c.message, authorName: c.authorName, authorEmail: c.authorEmail, signed: c.signed, sha: c.sha });
1272
1325
  const category = cls === "human" && !kinds.includes("human-attestation") ? "unknown" : cls;
1326
+ const email = (c.authorEmail ?? "").toLowerCase();
1327
+ const author = authors.get(email) ?? { name: c.authorName ?? email, email, commits: 0, known: 0, byCategory: { human: 0, with_ai: 0, ai: 0, bot: 0 } };
1328
+ author.commits += 1;
1329
+ authors.set(email, author);
1273
1330
  if (category === "unknown") continue;
1331
+ author.known += 1;
1332
+ author.byCategory[category] += 1;
1274
1333
  known += 1;
1275
1334
  byCategory[category] += 1;
1276
1335
  if (c.signed) byTier.verified += 1;
@@ -1298,7 +1357,8 @@ function repoCoverage(root, opts = {}) {
1298
1357
  unknown: commits.length - known,
1299
1358
  byCategory,
1300
1359
  byTier,
1301
- byAgent: [...agents.values()].sort((a, b) => b.commits - a.commits || a.name.localeCompare(b.name))
1360
+ byAgent: [...agents.values()].sort((a, b) => b.commits - a.commits || a.name.localeCompare(b.name)),
1361
+ byAuthor: [...authors.values()].sort((a, b) => b.commits - a.commits || a.email.localeCompare(b.email))
1302
1362
  };
1303
1363
  }
1304
1364
 
@@ -1745,7 +1805,11 @@ var azureAdapter = {
1745
1805
  // these optional precisely so an adapter can omit what its host doesn't offer.
1746
1806
  // Azure Pipelines sets these predefined variables on a PR-triggered build. The target branch is
1747
1807
  // given as a ref (not a SHA), so the range references origin/<target>; the pipeline template uses
1748
- // fetchDepth: 0 so that ref + the PR history are present. Any absence → null (recent history).
1808
+ // fetchDepth: 0 so that ref + the PR history are present. Any absence → null; `check` then reports
1809
+ // the PR build whose range it couldn't read (ciPullRequest) instead of passing on recent history.
1810
+ ciPullRequest(env) {
1811
+ return env.BUILD_REASON === "PullRequest";
1812
+ },
1749
1813
  ciRange(env) {
1750
1814
  if (env.BUILD_REASON !== "PullRequest") return null;
1751
1815
  const targetRef = env.SYSTEM_PULLREQUEST_TARGETBRANCH;
@@ -1771,7 +1835,10 @@ var gitlabAdapter = {
1771
1835
  },
1772
1836
  // GitLab CI sets these predefined variables on a merge-request pipeline. CI_MERGE_REQUEST_DIFF_BASE_SHA
1773
1837
  // is the merge-base of the MR; CI_COMMIT_SHA is the head — so base..head is the MR's own commits.
1774
- // (Needs the MR pipeline / a deep-enough fetch for those SHAs.) Any absence → null (recent history).
1838
+ // (Needs the MR pipeline / a deep-enough fetch for those SHAs.) Any absence → null (reported by `check`).
1839
+ ciPullRequest(env) {
1840
+ return env.CI_PIPELINE_SOURCE === "merge_request_event";
1841
+ },
1775
1842
  ciRange(env) {
1776
1843
  if (env.CI_PIPELINE_SOURCE !== "merge_request_event") return null;
1777
1844
  const base = env.CI_MERGE_REQUEST_DIFF_BASE_SHA;
@@ -1809,9 +1876,23 @@ function checkCommand(args) {
1809
1876
  }
1810
1877
  const { range, autoHost } = resolveRange(explicit);
1811
1878
  const result = evaluateRepoDisclosure(root, { range });
1879
+ const prHost = explicit || autoHost ? null : detectCiPullRequest();
1880
+ const problem = prHost ? `this is a ${prHost} pull-request build, but its commit range couldn't be read from the CI environment. Pass the range explicitly: madeby check <base>..<head>` : historyProblem(root, range);
1881
+ const blocked = problem !== null && result.mode === "required";
1882
+ const pass = result.pass && !blocked;
1812
1883
  if (json) {
1813
- console.log(JSON.stringify({ range: range ?? null, autoHost: autoHost ?? null, ...result }, null, 2));
1814
- process.exit(result.pass ? 0 : 1);
1884
+ console.log(JSON.stringify({ range: range ?? null, autoHost: autoHost ?? null, ...result, pass, ...problem ? { historyProblem: problem } : {} }, null, 2));
1885
+ process.exit(pass ? 0 : 1);
1886
+ return;
1887
+ }
1888
+ if (problem) {
1889
+ const banner = blocked ? "FAILED \u2014 can't check this history" : "WARNING \u2014 this result may be wrong";
1890
+ console.error(`
1891
+ madeby check: ${banner}: ${problem}
1892
+ `);
1893
+ }
1894
+ if (blocked) {
1895
+ process.exit(1);
1815
1896
  return;
1816
1897
  }
1817
1898
  if (result.warning) console.warn(`madeby check: ${result.warning}`);
@@ -1826,7 +1907,7 @@ Undisclosed commits (${result.undisclosed.length}):`);
1826
1907
  if (result.undisclosed.length > 20) console.log(` \u2026 and ${result.undisclosed.length - 20} more`);
1827
1908
  console.log(
1828
1909
  `
1829
- To disclose: add a Co-Authored-By / Generated-by trailer, affirm your own authorship with 'madeby me', add a DCO Signed-off-by, sign the commit, or record your AI session with 'madeby ai'. (Disclosure, not a ban \u2014 what this project requires is set in .madeby/policy.json.)`
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.)`
1830
1911
  );
1831
1912
  }
1832
1913
  process.exit(result.pass ? 0 : 1);
@@ -2250,6 +2331,22 @@ function policyIsRequired(root) {
2250
2331
  var AGENT_ROWS = 8;
2251
2332
  var pct = (n, d) => d > 0 ? Math.round(n / d * 100) : 0;
2252
2333
  var bar = (p) => "\u2588".repeat(Math.round(p / 5)) + "\u2591".repeat(20 - Math.round(p / 5));
2334
+ function renderByAuthor(r) {
2335
+ console.log(`authorship coverage by author \u2014 ${r.scope} (estimate \xB7 disclosure, not detection)`);
2336
+ if (r.byAuthor.length === 0) {
2337
+ console.log(" (no commits to score)");
2338
+ return;
2339
+ }
2340
+ const label = (a) => a.name && a.name !== a.email ? `${a.name} <${a.email}>` : a.email;
2341
+ const width = Math.max(...r.byAuthor.map((a) => label(a).length));
2342
+ console.log(` ${"author".padEnd(width)} commits disclosed human with_ai ai`);
2343
+ for (const a of r.byAuthor) {
2344
+ const k = a.byCategory;
2345
+ console.log(
2346
+ ` ${label(a).padEnd(width)} ${String(a.commits).padStart(7)} ${`${a.known} \xB7 ${pct(a.known, a.commits)}%`.padStart(9)} ${String(k.human).padStart(6)} ${String(k.with_ai).padStart(7)} ${String(k.ai + k.bot).padStart(5)}`
2347
+ );
2348
+ }
2349
+ }
2253
2350
  function render(r, required) {
2254
2351
  const knownPct = pct(r.known, r.total);
2255
2352
  const unknownPct = 100 - knownPct;
@@ -2288,7 +2385,14 @@ function render(r, required) {
2288
2385
  }
2289
2386
  function coverageCommand(args) {
2290
2387
  const json = args.includes("--json");
2291
- const range = args.find((a) => !a.startsWith("--"));
2388
+ const byAt = args.findIndex((a) => a === "--by" || a.startsWith("--by="));
2389
+ const by = byAt < 0 ? void 0 : args[byAt].includes("=") ? args[byAt].split("=")[1] : args[byAt + 1];
2390
+ if (by !== void 0 && by !== "author") {
2391
+ console.error("madeby coverage: --by takes 'author'.");
2392
+ process.exit(2);
2393
+ return;
2394
+ }
2395
+ const range = args.find((a, i) => !a.startsWith("--") && !(byAt >= 0 && i === byAt + 1 && !args[byAt].includes("=")));
2292
2396
  const root = repoRoot();
2293
2397
  if (!root) {
2294
2398
  console.error("madeby coverage: not a git repository.");
@@ -2300,12 +2404,18 @@ function coverageCommand(args) {
2300
2404
  console.log(JSON.stringify(r, null, 2));
2301
2405
  return;
2302
2406
  }
2303
- render(r, policyIsRequired(root));
2407
+ if (by === "author") renderByAuthor(r);
2408
+ else render(r, policyIsRequired(root));
2304
2409
  }
2305
2410
 
2306
2411
  // src/init-repo.ts
2307
2412
  import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
2308
2413
  import { dirname as dirname2, join as join8 } from "node:path";
2414
+
2415
+ // src/version.ts
2416
+ var VERSION = "0.3.13";
2417
+
2418
+ // src/init-repo.ts
2309
2419
  var ACO_VERSION = 2;
2310
2420
  var ACO_OPEN = `<!-- madeby:aco v${ACO_VERSION} -->`;
2311
2421
  var ACO_CLOSE = `<!-- /madeby:aco -->`;
@@ -2451,6 +2561,8 @@ function initRepo(root, opts = {}) {
2451
2561
  ensureAco(join8(root, "CLAUDE.md"), "CLAUDE.md", res, { createIfAbsent: false, ...aco });
2452
2562
  if (host === "github") {
2453
2563
  writeIfAbsent(join8(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
2564
+ } else if (host === "azure-devops") {
2565
+ writeIfAbsent(join8(root, ".madeby", "azure-pipelines.yml"), ".madeby/azure-pipelines.yml", AZURE_PIPELINE, created, skipped);
2454
2566
  }
2455
2567
  const nextSteps = ["Commit the created files."];
2456
2568
  if (raised.length > 0) {
@@ -2475,19 +2587,21 @@ function initRepo(root, opts = {}) {
2475
2587
  nextSteps.push(`${outdated.join(" + ")} carries an OUTDATED ACO convention \u2014 run 'madeby init --update' to refresh it in place (only the ACO block changes).`);
2476
2588
  }
2477
2589
  if (host === "azure-devops") {
2478
- nextSteps.push("Add the Azure Pipelines template (packages/action/azure-pipelines-disclosure.yml) and wire it as a PR build-validation branch policy.");
2590
+ nextSteps.push("In Azure DevOps, create a pipeline from .madeby/azure-pipelines.yml (a PR-only pipeline: no credentials kept, madeby pinned) and add it to the protected branch as a build-validation policy.");
2479
2591
  } else if (host !== "github") {
2480
2592
  nextSteps.push(`Add a CI step that runs 'npx madeby check' on pull requests for ${host}.`);
2481
2593
  }
2482
2594
  if (policy.mode !== "required") {
2483
2595
  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.`);
2484
2596
  }
2485
- 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.");
2597
+ 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.");
2486
2598
  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.");
2487
2599
  return { created, skipped, merged, updated, outdated, raised, modelOn, policyMode: policy.mode, recordModel: policy.recordModel, host, nextSteps };
2488
2600
  }
2489
2601
  var GH_WORKFLOW = `name: Disclosure
2490
2602
  on: pull_request
2603
+ permissions:
2604
+ contents: read
2491
2605
  jobs:
2492
2606
  madeby:
2493
2607
  runs-on: ubuntu-latest
@@ -2495,7 +2609,27 @@ jobs:
2495
2609
  - uses: actions/checkout@v4
2496
2610
  with:
2497
2611
  fetch-depth: 0
2612
+ persist-credentials: false
2498
2613
  - uses: MadeByFYI/MadeBy/packages/action@v0.2.0
2614
+ with:
2615
+ version: "${VERSION}"
2616
+ `;
2617
+ var AZURE_PIPELINE = `# MadeBy disclosure check. Run it as a build-validation policy on the protected branch: Azure
2618
+ # DevOps runs it for each pull request. It reads the git history only, so it keeps no credentials.
2619
+ trigger: none
2620
+
2621
+ pool:
2622
+ vmImage: ubuntu-latest
2623
+
2624
+ steps:
2625
+ - checkout: self
2626
+ fetchDepth: 0 # the PR's commits and the target branch, so the range can be read
2627
+ persistCredentials: false
2628
+ - task: NodeTool@0
2629
+ inputs:
2630
+ versionSpec: "20.x"
2631
+ - script: npx --yes madeby@${VERSION} check
2632
+ displayName: MadeBy disclosure check
2499
2633
  `;
2500
2634
 
2501
2635
  // src/commands/init.ts
@@ -2529,7 +2663,7 @@ madeby init \u2014 policy: ${r.policyMode}${r.recordModel ? " + records model" :
2529
2663
  import { execFileSync as execFileSync6 } from "node:child_process";
2530
2664
  import { createInterface } from "node:readline";
2531
2665
  var DEFAULT_PROTOCOL = "2024-11-05";
2532
- var SERVER_INFO = { name: "madeby", version: "0.3.11" };
2666
+ var SERVER_INFO = { name: "madeby", version: VERSION };
2533
2667
  function rootFor(path) {
2534
2668
  const cwd = path && path.length ? path : process.cwd();
2535
2669
  return execFileSync6("git", ["-C", cwd, "-c", "core.quotePath=false", "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
@@ -2570,7 +2704,7 @@ var TOOLS = [
2570
2704
  },
2571
2705
  {
2572
2706
  name: "coverage",
2573
- 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.",
2707
+ 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, 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), and byAuthor: the same picture per commit author (by email), to see who discloses and who doesn't. 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.",
2574
2708
  inputSchema: repoInput,
2575
2709
  handler: (args) => repoCoverage(rootFor(args.path), { range: args.range })
2576
2710
  },
@@ -2640,14 +2774,20 @@ var ALIGN_GUIDE = `# Aligning a repository with MadeBy
2640
2774
  MadeBy is **disclosure, not detection** \u2014 it records what a commit *states* about its origin, never
2641
2775
  guesses whether code is AI. To align a repo:
2642
2776
 
2643
- 1. **Assess** \u2014 call \`check\` (or \`who\` with no path for a repo-wide map) to see current coverage.
2644
- 2. **Set up** \u2014 call \`init\`: it writes \`.madeby/policy.json\` (advisory) and the CI disclosure check.
2645
- Idempotent; it never clobbers an existing policy or workflow (\`mode: "required"\` only ever raises an
2646
- advisory policy to required). Then commit the created files.
2647
- 3. **Disclose going forward** \u2014 contributors disclose origin with a Co-Authored-By / Generated-by
2648
- trailer (AI), an \`Authored-by-human\` trailer (work they wrote themselves \u2014 the symmetric human
2649
- claim; \`madeby me\` adds it), a DCO Signed-off-by, a signed commit, or \`ai\` (record your own AI
2650
- session's witnessed spans, structurally verified and local). Human and AI disclosure ride the same ladder.
2777
+ 1. **Assess** \u2014 call \`coverage\` for the scoreboard (known vs unknown, the human / with_ai / ai mix, and
2778
+ the agents and model versions named), \`check\` for the gate's verdict, or \`who\` with no path for a
2779
+ repo-wide map.
2780
+ 2. **Set up** \u2014 call \`init\`: it writes \`.madeby/policy.json\` (advisory), the AGENTS.md disclosure rule,
2781
+ and the CI disclosure check (\`host: "azure-devops"\` for Azure Pipelines). Idempotent; it never
2782
+ clobbers an existing policy or workflow (\`mode: "required"\` only ever raises an advisory policy to
2783
+ required). Add \`record_model: true\` if the repo should also record which model version did the work
2784
+ (opt-in; a project's choice). Then commit the created files.
2785
+ 3. **Disclose going forward** \u2014 contributors disclose origin with an \`Authored-by-ai\` trailer (an AI
2786
+ wrote it; \`madeby ai\` adds it), \`Authored-by-human\` plus \`Assisted-by\` (a human wrote it with AI;
2787
+ \`madeby me --with-ai\`), \`Authored-by-human\` alone (\`madeby me\`), a Co-Authored-By / Generated-by AI
2788
+ trailer, or witnessed spans from an AI session log (\`madeby ai --witness\`). Human and AI disclosure
2789
+ ride the same ladder. A DCO Signed-off-by or a signed commit alone says who is accountable, not who
2790
+ wrote the code, so it satisfies the policy only if the policy's \`accept\` lists it.
2651
2791
  4. **Account for existing history honestly** \u2014 do NOT fabricate disclosure for past commits. Read the
2652
2792
  \`madeby://guide/backfill\` resource: recognize what's already disclosed, attach recoverable
2653
2793
  evidence, and set an adoption boundary \u2014 pre-boundary history is labeled *unknown*, never invented.
@@ -2685,8 +2825,8 @@ from a PR.
2685
2825
  ## Azure DevOps (build validation branch policy)
2686
2826
 
2687
2827
  Project Settings -> Repositories -> (repo) -> Policies -> Branch (main) -> Build Validation -> Add:
2688
- point it at the pipeline that includes azure-pipelines-disclosure.yml; set trigger automatic and
2689
- required. CLI equivalent: az repos policy build create ... --branch main --blocking true.
2828
+ point it at the pipeline created from .madeby/azure-pipelines.yml (written by init with host
2829
+ azure-devops); set trigger automatic and required. CLI equivalent: az repos policy build create ... --branch main --blocking true.
2690
2830
  `;
2691
2831
  var BACKFILL_GUIDE = `# Honest backfill for an existing repo
2692
2832
 
@@ -2695,8 +2835,9 @@ refuses it. But you CAN make an honest, verifiable statement about history. Back
2695
2835
  never raise a commit's origin above its evidence \u2014 unknown stays unknown, just explicitly so.
2696
2836
 
2697
2837
  1. **Recognize what's already there** (free, pure evidence). Run \`check\` over FULL history:
2698
- existing Co-Authored-By AI trailers, DCO Signed-off-by, signed commits, and bot authorship are
2699
- recognized now even though they predate adoption. Much of your history may already be disclosed.
2838
+ existing Co-Authored-By AI trailers and bot authorship are recognized now even though they predate
2839
+ adoption. Much of your history may already be disclosed. (DCO sign-offs and signatures are
2840
+ recognized too, but say who is accountable, not who wrote the code.)
2700
2841
 
2701
2842
  2. **Attach recoverable evidence** (opt-in, evidence-grade):
2702
2843
  - Old AI-tool session logs -> \`ai\` attaches structurally-verified spans to their commits (it
@@ -2727,7 +2868,8 @@ var POLICY_SCHEMA = {
2727
2868
  properties: {
2728
2869
  version: { const: 0 },
2729
2870
  mode: { enum: ["advisory", "required"], description: "advisory: report only (the default, and the safe degrade); required: fail on any undisclosed commit" },
2730
- accept: { type: "array", items: { type: "string" }, description: "restrict what satisfies the policy (e.g. dco-signoff, ai-trailer, commit-signature); omit \u21D2 any recognized disclosure counts" }
2871
+ accept: { type: "array", items: { type: "string" }, description: "restrict what satisfies the policy (e.g. dco-signoff, ai-trailer, commit-signature); omit \u21D2 any authorship disclosure counts (a DCO sign-off or signature alone counts only if listed)" },
2872
+ recordModel: { type: "boolean", description: "ask AI disclosures to name the model version too (tool:model, e.g. Assisted-by: Cursor:gpt-5.3-codex); madeby reads it from the tool's own session record where it can. Set by init --record-model. A request, not a gate" }
2731
2873
  }
2732
2874
  };
2733
2875
  var ACO_GUIDE = `# ACO \u2014 the Authorship Certificate of Origin
@@ -2743,12 +2885,22 @@ https://github.com/MadeByFYI/MadeBy/blob/main/ACO.md
2743
2885
  - **ai** \u2014 an AI agent authored it (a human remains accountable).
2744
2886
 
2745
2887
  ## How an agent discloses its OWN work (asserted tier, no keys)
2746
- - Collaborative / human-directed work you just did -> call **disclose** (with_ai: true, tool: "<your name>").
2747
- It affirms human authorship + discloses AI assistance on HEAD, classifying the commit with_ai.
2888
+ - After you commit, call **disclose** with the honest \`category\` and your \`tool\` name (e.g. "Claude Code"):
2889
+ - \`ai\`: you wrote it; the committer stays accountable. Adds \`Authored-by-ai: <tool>\`.
2890
+ - \`with_ai\`: a human wrote it with your help. Adds \`Authored-by-human\` + \`Assisted-by: <tool>\`.
2891
+ - \`human\`: a human wrote it alone. Adds \`Authored-by-human\`.
2892
+ If you wrote the code and a human is only committing it, that is \`ai\`, not \`with_ai\`.
2893
+ - **The model version.** If the repo's \`.madeby/policy.json\` has \`"recordModel": true\`, madeby reads your
2894
+ model from the tool's own record of the session (Cursor, Copilot CLI, Claude Code) and writes
2895
+ \`<tool>:<model>\`. Elsewhere pass \`model\` only if you know your exact model version. Never guess.
2896
+ - **If your platform makes or rebuilds the commit** (a cloud coding agent that pushes through its own
2897
+ tool), a trailer amended in afterwards can be lost. Call disclose with \`print: true\` to get the trailer
2898
+ lines instead, and put them in the commit message's last paragraph.
2748
2899
  - Fully AI-authored spans recoverable from your session log -> call **ai** (records structurally-verified
2749
2900
  spans, local-only).
2750
- - Read what's already disclosed -> **who**. Align a whole repo -> **init** (also scaffolds an AGENTS.md
2751
- ACO convention so any agent working there discloses).
2901
+ - Read what's already disclosed -> **who**; measure a repo -> **coverage** (also lists the agents and
2902
+ models named). Align a whole repo -> **init** (also scaffolds an AGENTS.md ACO convention so any agent
2903
+ working there discloses).
2752
2904
 
2753
2905
  Disclose per commit, honestly. Undisclosed content stays *unknown*, never guessed.
2754
2906
  `;
@@ -2849,7 +3001,7 @@ Usage:
2849
3001
  (alias: whom) file's origin, or the whole repo with no path. --lines=A-B to scope.
2850
3002
  madeby coverage [<range>] The scoreboard: how much of your repo's authorship is disclosed (known)
2851
3003
  vs unknown, the human/with_ai/ai mix, and the asserted/verified split.
2852
- --json for a machine-readable result.
3004
+ --by author for one row per commit author. --json for a machine-readable result.
2853
3005
  madeby ai [<log>] [<ref>] Made by AI: record your AI session's witnessed spans into .madeby/spans.
2854
3006
  <log> defaults to the auto-discovered Claude Code transcript.
2855
3007
  madeby me Made by me: affirm you authored HEAD yourself (an Authored-by-human
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "madeby",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
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",