madeby 0.2.1 → 0.3.0

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 (2) hide show
  1. package/dist/madeby.mjs +306 -96
  2. package/package.json +1 -1
package/dist/madeby.mjs CHANGED
@@ -760,9 +760,41 @@ function hasHumanAttestation(message) {
760
760
  HUMAN_ATTEST_RE.lastIndex = 0;
761
761
  return HUMAN_ATTEST_RE.test(message);
762
762
  }
763
+ var AI_AUTHOR_RE = /^[ \t]*Authored-by-ai[ \t]*:[ \t]*(.+?)[ \t]*$/gim;
764
+ function hasAiAuthorship(message) {
765
+ AI_AUTHOR_RE.lastIndex = 0;
766
+ return AI_AUTHOR_RE.test(message);
767
+ }
768
+ var SPDX_AI_DISCLOSURE_RE = /SPDX-AI-Disclosure[ \t]*:[ \t]*(none|ai-assisted|ai-generated|autonomous)\b/gi;
769
+ var AI_DISCLOSURE_DEFAULT_RE = /^[ \t]*disclosure-default[ \t]*:[ \t]*(none|ai-assisted|ai-generated|autonomous)\b/im;
770
+ function spdxAiDisclosureCategory(value) {
771
+ switch (value.toLowerCase()) {
772
+ case "none":
773
+ return "human";
774
+ case "ai-assisted":
775
+ return "with_ai";
776
+ case "ai-generated":
777
+ case "autonomous":
778
+ return "ai";
779
+ default:
780
+ return "unknown";
781
+ }
782
+ }
783
+ function recognizeSpdxAiDisclosures(text) {
784
+ const out = [];
785
+ for (const m of text.matchAll(SPDX_AI_DISCLOSURE_RE)) {
786
+ const value = m[1].toLowerCase();
787
+ out.push({ kind: "spdx-ai-disclosure", evidence: `SPDX-AI-Disclosure: ${value}`, tier: "asserted", subjectRef: value });
788
+ }
789
+ return out;
790
+ }
791
+ function aiDisclosureDefault(md) {
792
+ const m = AI_DISCLOSURE_DEFAULT_RE.exec(md);
793
+ return m ? m[1].toLowerCase() : null;
794
+ }
763
795
  function commitDisclosureKinds(commit) {
764
796
  const kinds = [];
765
- if (hasAiTrailer(commit.message)) kinds.push("ai-trailer");
797
+ if (hasAiTrailer(commit.message) || hasAiAuthorship(commit.message)) kinds.push("ai-trailer");
766
798
  if (hasHumanAttestation(commit.message)) kinds.push("human-attestation");
767
799
  if (hasDcoSignoff(commit.message)) kinds.push("dco-signoff");
768
800
  if (commit.signed === true) kinds.push("commit-signature");
@@ -886,6 +918,8 @@ function evaluateRepoDisclosure(root, opts = {}) {
886
918
 
887
919
  // ../analyzer/src/path-provenance.ts
888
920
  import { execFileSync as execFileSync2 } from "node:child_process";
921
+ import { readFileSync as readFileSync4, existsSync as existsSync2 } from "node:fs";
922
+ import { join as join3 } from "node:path";
889
923
 
890
924
  // ../analyzer/src/provenance.ts
891
925
  import { readdirSync, readFileSync as readFileSync3 } from "node:fs";
@@ -973,6 +1007,26 @@ function provenanceOf(root, path, opts = {}) {
973
1007
  });
974
1008
  }
975
1009
  }
1010
+ try {
1011
+ const head = readFileSync4(join3(root, path), "utf8").slice(0, 4096);
1012
+ const sig = recognizeSpdxAiDisclosures(head)[0];
1013
+ if (sig) {
1014
+ const value = sig.subjectRef ?? "";
1015
+ const cat = spdxAiDisclosureCategory(value);
1016
+ const model = /SPDX-AI-Model[ \t]*:[ \t]*(.+)/i.exec(head)?.[1]?.trim();
1017
+ records.push({
1018
+ source: "header",
1019
+ granularity: "file",
1020
+ ai: cat !== "human",
1021
+ bot: false,
1022
+ model,
1023
+ disclosures: ["spdx-ai-disclosure"],
1024
+ confidence: "disclosed",
1025
+ evidence: `SPDX-AI-Disclosure: ${value} (${cat})${model ? ` \u2014 ${model}` : ""}`
1026
+ });
1027
+ }
1028
+ } catch {
1029
+ }
976
1030
  if (opts.useBlame !== false) {
977
1031
  try {
978
1032
  const range = opts.startLine !== void 0 ? [`-L`, `${opts.startLine},${opts.endLine ?? opts.startLine}`] : [];
@@ -1040,12 +1094,22 @@ function provenanceMap(root, opts = {}) {
1040
1094
  const pct = commits.length ? Math.round(disclosed / commits.length * 100) : 0;
1041
1095
  const scope = prefix ?? "repo";
1042
1096
  const allModels = [...new Set(witnessed.flatMap((w) => w.models))];
1097
+ let aiDefault;
1098
+ try {
1099
+ const p = join3(root, "AI_DISCLOSURE.md");
1100
+ if (existsSync2(p)) {
1101
+ const v = aiDisclosureDefault(readFileSync4(p, "utf8"));
1102
+ if (v) aiDefault = { value: v, category: spdxAiDisclosureCategory(v) };
1103
+ }
1104
+ } catch {
1105
+ }
1043
1106
  return {
1044
1107
  scope,
1045
1108
  trackedFiles,
1046
1109
  witnessedFiles: witnessed.length,
1047
1110
  witnessed,
1048
1111
  commitCoverage: { disclosed, total: commits.length },
1112
+ aiDisclosureDefault: aiDefault,
1049
1113
  summary: `${scope}: ${trackedFiles} tracked file${trackedFiles === 1 ? "" : "s"}; ${witnessed.length} with witnessed AI provenance${allModels.length ? ` (${allModels.join(", ")})` : ""}; ${pct}% of the last ${commits.length} commits disclose origin.`,
1050
1114
  note: "Witnessed spans are precise (region-level, model-named); commit coverage is commit-level, not per-file authorship. Files without a witnessed span have unknown origin \u2014 disclosure, not detection: we report what's disclosed and never infer origin we can't see. Resolution is a configurable convention (ARCH \xA712)."
1051
1115
  };
@@ -1053,9 +1117,9 @@ function provenanceMap(root, opts = {}) {
1053
1117
 
1054
1118
  // ../analyzer/src/record-spans.ts
1055
1119
  import { execFileSync as execFileSync3 } from "node:child_process";
1056
- import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync, writeFileSync } from "node:fs";
1120
+ import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync, writeFileSync } from "node:fs";
1057
1121
  import { homedir } from "node:os";
1058
- import { dirname, join as join3 } from "node:path";
1122
+ import { dirname, join as join4 } from "node:path";
1059
1123
 
1060
1124
  // ../analyzer/src/capture-local.ts
1061
1125
  var MATCH_THRESHOLD = 0.52;
@@ -1391,19 +1455,19 @@ function git2(root, ...a) {
1391
1455
  function autodiscover(root) {
1392
1456
  const enc3 = root.replace(/[/_]/g, "-");
1393
1457
  try {
1394
- const dir = join3(homedir(), ".claude", "projects", enc3);
1395
- return readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join3(dir, f)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0] ?? null;
1458
+ const dir = join4(homedir(), ".claude", "projects", enc3);
1459
+ return readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join4(dir, f)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0] ?? null;
1396
1460
  } catch {
1397
1461
  return null;
1398
1462
  }
1399
1463
  }
1400
1464
  function recordAiSpans(root, opts = {}) {
1401
1465
  const transcriptPath = opts.transcriptPath ?? autodiscover(root);
1402
- if (!transcriptPath || !existsSync2(transcriptPath)) {
1466
+ if (!transcriptPath || !existsSync3(transcriptPath)) {
1403
1467
  return { written: false, matched: 0, discarded: 0, message: "transcript not found \u2014 pass a transcript path", error: "transcript-not-found" };
1404
1468
  }
1405
1469
  const commit = git2(root, "rev-parse", opts.ref ?? "HEAD");
1406
- const transcriptText = readFileSync4(transcriptPath, "utf8");
1470
+ const transcriptText = readFileSync5(transcriptPath, "utf8");
1407
1471
  const parser = detectParser(transcriptText);
1408
1472
  if (!parser) {
1409
1473
  return { written: false, matched: 0, discarded: 0, message: "unrecognized log format (Claude Code is supported today)", error: "unrecognized-format" };
@@ -1414,7 +1478,7 @@ function recordAiSpans(root, opts = {}) {
1414
1478
  repoRoot: root,
1415
1479
  readFile: (rel) => {
1416
1480
  try {
1417
- return readFileSync4(join3(root, rel), "utf8");
1481
+ return readFileSync5(join4(root, rel), "utf8");
1418
1482
  } catch {
1419
1483
  return null;
1420
1484
  }
@@ -1432,12 +1496,12 @@ function recordAiSpans(root, opts = {}) {
1432
1496
  message: `no AI-authored content from the session is structurally present (matched 0, discarded ${result.discarded.length})`
1433
1497
  };
1434
1498
  }
1435
- const out = join3(root, ".madeby", "spans", `${commit}.json`);
1499
+ const out = join4(root, ".madeby", "spans", `${commit}.json`);
1436
1500
  let kept = [];
1437
- if (existsSync2(out)) {
1501
+ if (existsSync3(out)) {
1438
1502
  const thisSource = `${result.tool}-session-log`;
1439
1503
  try {
1440
- kept = parseSpanManifest(readFileSync4(out, "utf8")).attestations.filter((a) => a.attribution.source !== thisSource);
1504
+ kept = parseSpanManifest(readFileSync5(out, "utf8")).attestations.filter((a) => a.attribution.source !== thisSource);
1441
1505
  } catch {
1442
1506
  }
1443
1507
  }
@@ -1455,16 +1519,16 @@ function recordAiSpans(root, opts = {}) {
1455
1519
  }
1456
1520
  function writeParserSample(root, opts = {}) {
1457
1521
  const transcriptPath = opts.transcriptPath ?? autodiscover(root);
1458
- if (!transcriptPath || !existsSync2(transcriptPath)) {
1522
+ if (!transcriptPath || !existsSync3(transcriptPath)) {
1459
1523
  return { written: false, message: "session log not found \u2014 pass its path", error: "transcript-not-found" };
1460
1524
  }
1461
- const text = readFileSync4(transcriptPath, "utf8");
1525
+ const text = readFileSync5(transcriptPath, "utf8");
1462
1526
  const tool = detectParser(text)?.id;
1463
1527
  const skeleton = redactSample(text);
1464
- const dir = join3(root, ".madeby", "parser-samples");
1528
+ const dir = join4(root, ".madeby", "parser-samples");
1465
1529
  mkdirSync(dir, { recursive: true });
1466
1530
  const stamp = (opts.now ?? (/* @__PURE__ */ new Date()).toISOString()).replace(/[:.]/g, "-");
1467
- const out = join3(dir, `${tool ?? "unknown-tool"}-${stamp}.txt`);
1531
+ const out = join4(dir, `${tool ?? "unknown-tool"}-${stamp}.txt`);
1468
1532
  writeFileSync(out, skeleton);
1469
1533
  return { written: true, path: out, tool, message: `wrote a content-free format sample to ${out}` };
1470
1534
  }
@@ -1581,56 +1645,6 @@ To disclose: add a Co-Authored-By / Generated-by trailer, affirm your own author
1581
1645
  process.exit(result.pass ? 0 : 1);
1582
1646
  }
1583
1647
 
1584
- // src/commands/ai.ts
1585
- var ISSUE_URL = "https://github.com/MadeByFYI/MadeBy/issues/new";
1586
- function aiCommand(args) {
1587
- const root = repoRoot();
1588
- if (!root) {
1589
- console.error("madeby ai: not a git repository.");
1590
- process.exit(2);
1591
- return;
1592
- }
1593
- const makeSample = args.includes("--make-parser-sample");
1594
- const positional = args.filter((a) => !a.startsWith("--"));
1595
- if (makeSample) {
1596
- const s = writeParserSample(root, { transcriptPath: positional[0] });
1597
- if (s.error === "transcript-not-found") {
1598
- console.error("madeby ai: session log not found \u2014 pass it: madeby ai --make-parser-sample <log>");
1599
- process.exit(1);
1600
- return;
1601
- }
1602
- console.log(
1603
- `madeby ai: ${s.message}.
1604
- \u2192 It contains only the log's structure + model ids \u2014 all code, prompts, paths, and secrets
1605
- were redacted locally. Review it, then attach it to a new issue so we can add a parser:
1606
- ${ISSUE_URL}`
1607
- );
1608
- process.exit(0);
1609
- return;
1610
- }
1611
- const res = recordAiSpans(root, { transcriptPath: positional[0], ref: positional[1] });
1612
- if (res.error === "transcript-not-found") {
1613
- console.error("madeby ai: session log not found \u2014 pass it: madeby ai <transcript.jsonl> [ref]");
1614
- process.exit(1);
1615
- return;
1616
- }
1617
- if (res.error === "unrecognized-format") {
1618
- console.error(
1619
- "madeby ai: unrecognized log format \u2014 no registered tool parser matched (Claude Code, aider, and\n SpecStory are supported today). Help add your tool WITHOUT sharing any code or prompts:\n madeby ai --make-parser-sample <log> # writes a content-free format sample to review + share"
1620
- );
1621
- process.exit(1);
1622
- return;
1623
- }
1624
- if (!res.written) {
1625
- console.log(`madeby ai: ${res.message}. Nothing recorded.`);
1626
- process.exit(0);
1627
- return;
1628
- }
1629
- console.log(
1630
- `madeby ai: wrote ${res.path} \u2014 [${res.tool}] ${res.matched} witnessed file(s) matched, ${res.discarded} discarded (not present in the checkout).`
1631
- );
1632
- }
1633
-
1634
1648
  // src/commands/me.ts
1635
1649
  import { execFileSync as execFileSync5 } from "node:child_process";
1636
1650
  function parseWithAi(args) {
@@ -1645,14 +1659,39 @@ function git3(root, args) {
1645
1659
  return execFileSync5("git", ["-C", root, ...args], { encoding: "utf8" }).trim();
1646
1660
  }
1647
1661
  var HUMAN_ATTEST = /^[ \t]*(?:Authored-by-human|Human-authored(?:-by)?)[ \t]*:/im;
1648
- function meCommand(args) {
1649
- const { withAi, tool } = parseWithAi(args);
1650
- const root = repoRoot();
1651
- if (!root) {
1652
- console.error("madeby me: not a git repository.");
1653
- process.exit(2);
1654
- return;
1662
+ var ASSIST = /^[ \t]*Assisted-by[ \t]*:/im;
1663
+ var AI_AUTHOR = /^[ \t]*Authored-by-ai[ \t]*:/im;
1664
+ function attestAiAuthorship(root, opts = {}) {
1665
+ const tool = opts.tool && opts.tool.trim() || "AI";
1666
+ const base = { ok: false, changed: false, withAi: false, tool, added: [], category: "ai" };
1667
+ try {
1668
+ execFileSync5("git", ["-C", root, "diff", "--cached", "--quiet"]);
1669
+ } catch {
1670
+ return { ...base, reason: "staged-changes" };
1671
+ }
1672
+ let subject = "";
1673
+ let message = "";
1674
+ try {
1675
+ subject = git3(root, ["show", "-s", "--format=%s", "HEAD"]);
1676
+ message = git3(root, ["show", "-s", "--format=%B", "HEAD"]);
1677
+ } catch {
1678
+ return { ...base, reason: "no-commit" };
1679
+ }
1680
+ const added = [];
1681
+ const trailers = [];
1682
+ if (!AI_AUTHOR.test(message)) {
1683
+ trailers.push("--trailer", `Authored-by-ai: ${tool}`);
1684
+ added.push("Authored-by-ai");
1685
+ }
1686
+ if (trailers.length > 0) {
1687
+ execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", ...trailers]);
1655
1688
  }
1689
+ return { ok: true, changed: trailers.length > 0, withAi: false, tool, subject, added, category: "ai" };
1690
+ }
1691
+ function affirmAuthorship(root, opts = {}) {
1692
+ const withAi = opts.withAi ?? false;
1693
+ const tool = opts.tool && opts.tool.trim() || "AI";
1694
+ const base = { ok: false, changed: false, withAi, tool, added: [], category: withAi ? "with_ai" : "human" };
1656
1695
  let name = "";
1657
1696
  let email = "";
1658
1697
  try {
@@ -1660,17 +1699,11 @@ function meCommand(args) {
1660
1699
  email = git3(root, ["config", "user.email"]);
1661
1700
  } catch {
1662
1701
  }
1663
- if (!name || !email) {
1664
- console.error("madeby me: set your git identity first (git config user.name / user.email).");
1665
- process.exit(1);
1666
- return;
1667
- }
1702
+ if (!name || !email) return { ...base, reason: "no-identity" };
1668
1703
  try {
1669
1704
  execFileSync5("git", ["-C", root, "diff", "--cached", "--quiet"]);
1670
1705
  } catch {
1671
- console.error("madeby me: you have staged changes. Commit them first \u2014 `me` affirms HEAD, not your index.");
1672
- process.exit(1);
1673
- return;
1706
+ return { ...base, reason: "staged-changes", name, email };
1674
1707
  }
1675
1708
  let subject = "";
1676
1709
  let message = "";
@@ -1678,24 +1711,124 @@ function meCommand(args) {
1678
1711
  subject = git3(root, ["show", "-s", "--format=%s", "HEAD"]);
1679
1712
  message = git3(root, ["show", "-s", "--format=%B", "HEAD"]);
1680
1713
  } catch {
1681
- console.error("madeby me: no commit to affirm \u2014 make a commit first.");
1682
- process.exit(1);
1683
- return;
1714
+ return { ...base, reason: "no-commit" };
1684
1715
  }
1685
1716
  const hasHuman = HUMAN_ATTEST.test(message);
1686
- const hasAssist = /^[ \t]*Assisted-by[ \t]*:/im.test(message);
1717
+ const hasAssist = ASSIST.test(message);
1687
1718
  const trailers = [];
1688
- if (!hasHuman) trailers.push("--trailer", `Authored-by-human: ${name} <${email}>`);
1689
- if (withAi && !hasAssist) trailers.push("--trailer", `Assisted-by: ${tool}`);
1690
- if (trailers.length === 0) {
1719
+ const added = [];
1720
+ if (!hasHuman) {
1721
+ trailers.push("--trailer", `Authored-by-human: ${name} <${email}>`);
1722
+ added.push("Authored-by-human");
1723
+ }
1724
+ if (withAi && !hasAssist) {
1725
+ trailers.push("--trailer", `Assisted-by: ${tool}`);
1726
+ added.push("Assisted-by");
1727
+ }
1728
+ if (trailers.length > 0) {
1729
+ execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", ...trailers]);
1730
+ }
1731
+ const category = hasAssist || added.includes("Assisted-by") ? "with_ai" : "human";
1732
+ return { ok: true, changed: trailers.length > 0, withAi, tool, name, email, subject, added, category };
1733
+ }
1734
+ function meCommand(args) {
1735
+ const { withAi, tool } = parseWithAi(args);
1736
+ const root = repoRoot();
1737
+ if (!root) {
1738
+ console.error("madeby me: not a git repository.");
1739
+ process.exit(2);
1740
+ return;
1741
+ }
1742
+ const r = affirmAuthorship(root, { withAi, tool });
1743
+ if (!r.ok) {
1744
+ if (r.reason === "no-identity") console.error("madeby me: set your git identity first (git config user.name / user.email).");
1745
+ else if (r.reason === "staged-changes") console.error("madeby me: you have staged changes. Commit them first \u2014 `me` affirms HEAD, not your index.");
1746
+ else console.error("madeby me: no commit to affirm \u2014 make a commit first.");
1747
+ process.exit(1);
1748
+ return;
1749
+ }
1750
+ if (!r.changed) {
1691
1751
  const already = withAi ? "already affirms human authorship and discloses AI assistance" : "already affirms human authorship";
1692
1752
  console.log(`madeby me: HEAD ${already}. Nothing to do.`);
1693
1753
  process.exit(0);
1694
1754
  return;
1695
1755
  }
1696
- execFileSync5("git", ["-C", root, "commit", "--amend", "--no-edit", ...trailers]);
1697
1756
  console.log(
1698
- withAi ? `madeby me: made by me, with AI \u2014 affirmed human authorship + disclosed AI assistance on HEAD \u2014 "${subject}" (Authored-by-human: ${name}; Assisted-by: ${tool}).` : `madeby me: affirmed human authorship on HEAD \u2014 "${subject}" (Authored-by-human: ${name}).`
1757
+ 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}).` : `madeby me: affirmed human authorship on HEAD \u2014 "${r.subject}" (Authored-by-human: ${r.name}).`
1758
+ );
1759
+ }
1760
+
1761
+ // src/commands/ai.ts
1762
+ var ISSUE_URL = "https://github.com/MadeByFYI/MadeBy/issues/new";
1763
+ function aiCommand(args) {
1764
+ const root = repoRoot();
1765
+ if (!root) {
1766
+ console.error("madeby ai: not a git repository.");
1767
+ process.exit(2);
1768
+ return;
1769
+ }
1770
+ const makeSample = args.includes("--make-parser-sample");
1771
+ const witness = args.includes("--witness");
1772
+ const positional = args.filter((a) => !a.startsWith("--"));
1773
+ if (makeSample) {
1774
+ const s = writeParserSample(root, { transcriptPath: positional[0] });
1775
+ if (s.error === "transcript-not-found") {
1776
+ console.error("madeby ai: session log not found \u2014 pass it: madeby ai --make-parser-sample <log>");
1777
+ process.exit(1);
1778
+ return;
1779
+ }
1780
+ console.log(
1781
+ `madeby ai: ${s.message}.
1782
+ \u2192 It contains only the log's structure + model ids \u2014 all code, prompts, paths, and secrets
1783
+ were redacted locally. Review it, then attach it to a new issue so we can add a parser:
1784
+ ${ISSUE_URL}`
1785
+ );
1786
+ process.exit(0);
1787
+ return;
1788
+ }
1789
+ if (witness) {
1790
+ const res = recordAiSpans(root, { transcriptPath: positional[0], ref: positional[1] });
1791
+ if (res.error === "transcript-not-found") {
1792
+ console.error("madeby ai --witness: session log not found \u2014 pass it: madeby ai --witness <transcript.jsonl> [ref]");
1793
+ process.exit(1);
1794
+ return;
1795
+ }
1796
+ if (res.error === "unrecognized-format") {
1797
+ console.error(
1798
+ "madeby ai --witness: unrecognized log format \u2014 no registered tool parser matched (Claude Code,\n aider, and SpecStory today). The asserted disclosure still works \u2014 run plain `madeby ai` \u2014 or\n help add your tool: madeby ai --make-parser-sample <log>."
1799
+ );
1800
+ process.exit(1);
1801
+ return;
1802
+ }
1803
+ if (!res.written) {
1804
+ console.log(`madeby ai --witness: ${res.message}. Nothing recorded.`);
1805
+ process.exit(0);
1806
+ return;
1807
+ }
1808
+ console.log(
1809
+ `madeby ai --witness: wrote ${res.path} \u2014 [${res.tool}] ${res.matched} witnessed file(s) matched, ${res.discarded} discarded (not present in the checkout).`
1810
+ );
1811
+ return;
1812
+ }
1813
+ const tool = positional[0] || "AI";
1814
+ const r = attestAiAuthorship(root, { tool });
1815
+ if (!r.ok) {
1816
+ if (r.reason === "staged-changes") {
1817
+ console.error("madeby ai: you have staged changes. Commit them first \u2014 `ai` attests HEAD, not your index.");
1818
+ } else {
1819
+ console.error("madeby ai: no commit to attest \u2014 make a commit first.");
1820
+ }
1821
+ process.exit(1);
1822
+ return;
1823
+ }
1824
+ if (!r.changed) {
1825
+ console.log("madeby ai: HEAD already attests AI authorship. Nothing to do.");
1826
+ process.exit(0);
1827
+ return;
1828
+ }
1829
+ console.log(
1830
+ `madeby ai: attested AI authorship on HEAD \u2014 "${r.subject}" (Authored-by-ai: ${r.tool}).
1831
+ \u2192 asserted tier. For evidence-backed spans from your session log: madeby ai --witness [log].`
1699
1832
  );
1700
1833
  }
1701
1834
 
@@ -1747,8 +1880,8 @@ function whoCommand(args) {
1747
1880
  }
1748
1881
 
1749
1882
  // src/init-repo.ts
1750
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
1751
- import { dirname as dirname2, join as join4 } from "node:path";
1883
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
1884
+ import { dirname as dirname2, join as join5 } from "node:path";
1752
1885
  var GH_WORKFLOW = `name: Disclosure
1753
1886
  on: pull_request
1754
1887
  jobs:
@@ -1760,8 +1893,36 @@ jobs:
1760
1893
  fetch-depth: 0
1761
1894
  - uses: MadeByFYI/MadeBy/packages/action@v0.2.0
1762
1895
  `;
1896
+ var AGENTS_CONVENTION = `# AGENTS.md \u2014 Authorship disclosure convention (ACO)
1897
+
1898
+ This repository asks any AI agent \u2014 or a human working with one \u2014 to **disclose who authored the
1899
+ changes they make**, per **ACO, the Authorship Certificate of Origin**
1900
+ (https://github.com/MadeByFYI/MadeBy/blob/main/ACO.md).
1901
+
1902
+ ## The rule
1903
+
1904
+ When you author or modify content here, disclose the authorship **category** on your commit \u2014 one of:
1905
+
1906
+ - \`human\` \u2014 a human authored it, no AI involvement.
1907
+ - \`with_ai\` \u2014 a human authored it with disclosed AI assistance.
1908
+ - \`ai\` \u2014 an AI agent authored it (a human remains accountable).
1909
+
1910
+ Disclose **per commit, honestly** \u2014 the category describes what actually happened for that change.
1911
+
1912
+ ## The one-line way (asserted tier \u2014 no keys, no setup)
1913
+
1914
+ After you commit, run the matching command (works with no install via \`npx\`):
1915
+
1916
+ | Category | Command |
1917
+ |-----------|----------------------------------|
1918
+ | \`human\` | \`npx madeby me\` |
1919
+ | \`with_ai\` | \`npx madeby me --with-ai <tool>\` |
1920
+ | \`ai\` | \`npx madeby ai [tool]\` |
1921
+
1922
+ Undisclosed content stays *unknown*, never guessed. See ACO for the standard.
1923
+ `;
1763
1924
  function writeIfAbsent(abs, rel, content, created, skipped) {
1764
- if (existsSync3(abs)) {
1925
+ if (existsSync4(abs)) {
1765
1926
  skipped.push(rel);
1766
1927
  return;
1767
1928
  }
@@ -1774,11 +1935,17 @@ function initRepo(root, opts = {}) {
1774
1935
  const host = opts.host ?? "github";
1775
1936
  const created = [];
1776
1937
  const skipped = [];
1777
- writeIfAbsent(join4(root, ".madeby", "policy.json"), ".madeby/policy.json", JSON.stringify({ version: 0, mode }, null, 2) + "\n", created, skipped);
1938
+ writeIfAbsent(join5(root, ".madeby", "policy.json"), ".madeby/policy.json", JSON.stringify({ version: 0, mode }, null, 2) + "\n", created, skipped);
1939
+ writeIfAbsent(join5(root, "AGENTS.md"), "AGENTS.md", AGENTS_CONVENTION, created, skipped);
1778
1940
  if (host === "github") {
1779
- writeIfAbsent(join4(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
1941
+ writeIfAbsent(join5(root, ".github", "workflows", "disclosure.yml"), ".github/workflows/disclosure.yml", GH_WORKFLOW, created, skipped);
1780
1942
  }
1781
1943
  const nextSteps = ["Commit the created files."];
1944
+ if (created.includes("AGENTS.md")) {
1945
+ nextSteps.push("An AGENTS.md ACO convention was added \u2014 agents that read it disclose authorship (e.g. 'npx madeby me --with-ai <tool>'). Tune it to your repo's tools.");
1946
+ } else if (skipped.includes("AGENTS.md")) {
1947
+ nextSteps.push("You already have an AGENTS.md \u2014 add the ACO disclosure rule to it (reference: https://github.com/MadeByFYI/MadeBy/blob/main/AGENTS.md).");
1948
+ }
1782
1949
  if (host === "azure-devops") {
1783
1950
  nextSteps.push("Add the Azure Pipelines template (packages/action/azure-pipelines-disclosure.yml) and wire it as a PR build-validation branch policy.");
1784
1951
  } else if (host !== "github") {
@@ -1787,7 +1954,7 @@ function initRepo(root, opts = {}) {
1787
1954
  if (mode !== "required") {
1788
1955
  nextSteps.push(`When ready to enforce: set mode to "required", 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.`);
1789
1956
  }
1790
- nextSteps.push("Contributors disclose origin with a Co-Authored-By / Generated-by trailer (AI), an Authored-by-human trailer ('madeby me'), a DCO Signed-off-by, a signed commit, or 'madeby ai' (record your own AI session's witnessed spans).");
1957
+ 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.");
1791
1958
  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.");
1792
1959
  return { created, skipped, policyMode: mode, host, nextSteps };
1793
1960
  }
@@ -1867,6 +2034,26 @@ var TOOLS = [
1867
2034
  endLine: args.endLine
1868
2035
  }) : provenanceMap(rootFor(args.repo), { prefix: args.prefix, commitLimit: args.commitLimit })
1869
2036
  },
2037
+ {
2038
+ name: "disclose",
2039
+ description: "Disclose authorship on HEAD per ACO \u2014 the agent-native disclosure of work you just did. Pick the honest category: 'ai' (an AI authored it; you, the committer, are accountable) adds Authored-by-ai; 'with_ai' (a human authored it WITH AI assistance) adds Authored-by-human + Assisted-by; 'human' adds Authored-by-human. IMPORTANT: if the AI wrote the code and a human is merely committing it, that is 'ai' \u2014 do NOT claim 'with_ai'/human authorship the human didn't do. Amends HEAD's MESSAGE only (a testimony, never a content change) and refuses if there are staged changes. Evidence upgrade for the ai category (witnessed spans from a session log) is the CLI `madeby ai --witness`. Read the standard: madeby://guide/aco.",
2040
+ inputSchema: {
2041
+ type: "object",
2042
+ properties: {
2043
+ path: { type: "string", description: "path to the repo (default: the server's working directory)" },
2044
+ category: { enum: ["human", "with_ai", "ai"], description: "the honest authorship category (see the description). Defaults from with_ai if omitted." },
2045
+ with_ai: { type: "boolean", description: "legacy shorthand: true \u21D2 category with_ai, false \u21D2 human. Prefer `category`." },
2046
+ tool: { type: "string", description: "the AI tool to name (Assisted-by for with_ai, Authored-by-ai for ai). Default: AI." }
2047
+ }
2048
+ },
2049
+ handler: (args) => {
2050
+ const root = rootFor(args.path);
2051
+ const tool = args.tool;
2052
+ const category = args.category;
2053
+ if (category === "ai") return attestAiAuthorship(root, { tool });
2054
+ return affirmAuthorship(root, { withAi: category === "with_ai" || args.with_ai === true, tool });
2055
+ }
2056
+ },
1870
2057
  {
1871
2058
  name: "ai",
1872
2059
  description: "Made by AI (WRITE): record witnessed AI-authorship spans from YOUR OWN session log into the repo's .madeby/spans (asserted tier, self-reported) \u2014 how an agent DISCLOSES its own contribution. Honest by construction: it records only spans whose AI-authored content is STRUCTURALLY PRESENT in the checkout (survives reformatting); anything not present is discarded. Writes only derived attribution, locally \u2014 the log/content never leave the machine. It cannot claim a higher tier or attribute work to anyone else.",
@@ -1975,7 +2162,30 @@ var POLICY_SCHEMA = {
1975
2162
  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" }
1976
2163
  }
1977
2164
  };
2165
+ var ACO_GUIDE = `# ACO \u2014 the Authorship Certificate of Origin
2166
+
2167
+ ACO is the open standard MadeBy implements and verifies: a signed, content-bound, tiered statement of
2168
+ which of {human, with_ai, ai} authored a unit of work. **Disclosure, not detection** \u2014 it records what a
2169
+ party PUT ON THE RECORD, never inferring authorship from source. Full spec:
2170
+ https://github.com/MadeByFYI/MadeBy/blob/main/ACO.md
2171
+
2172
+ ## The three categories
2173
+ - **human** \u2014 a human authored it, no AI involvement.
2174
+ - **with_ai** \u2014 a human authored it with disclosed AI assistance.
2175
+ - **ai** \u2014 an AI agent authored it (a human remains accountable).
2176
+
2177
+ ## How an agent discloses its OWN work (asserted tier, no keys)
2178
+ - Collaborative / human-directed work you just did -> call **disclose** (with_ai: true, tool: "<your name>").
2179
+ It affirms human authorship + discloses AI assistance on HEAD, classifying the commit with_ai.
2180
+ - Fully AI-authored spans recoverable from your session log -> call **ai** (records structurally-verified
2181
+ spans, local-only).
2182
+ - Read what's already disclosed -> **who**. Align a whole repo -> **init** (also scaffolds an AGENTS.md
2183
+ ACO convention so any agent working there discloses).
2184
+
2185
+ Disclose per commit, honestly. Undisclosed content stays *unknown*, never guessed.
2186
+ `;
1978
2187
  var RESOURCES = [
2188
+ { uri: "madeby://guide/aco", name: "ACO \u2014 the Authorship Certificate of Origin", description: "The open standard MadeBy implements + verifies: verifiable authorship disclosure (human/with_ai/ai), and how an agent discloses its own work.", mimeType: "text/markdown", text: ACO_GUIDE },
1979
2189
  { uri: "madeby://guide/align", name: "Aligning a repo with MadeBy", description: "The step-by-step workflow to align a repository (for humans and agents).", mimeType: "text/markdown", text: ALIGN_GUIDE },
1980
2190
  { uri: "madeby://guide/enforce", name: "Enforcing the disclosure check", description: "How to make the check required on a protected branch (GitHub / Azure DevOps) \u2014 the host's control plane, exact commands.", mimeType: "text/markdown", text: ENFORCE_GUIDE },
1981
2191
  { uri: "madeby://guide/backfill", name: "Honest backfill for existing repos", description: "How to account for pre-adoption history honestly \u2014 recognize, attach evidence, set a boundary; never fabricate.", mimeType: "text/markdown", text: BACKFILL_GUIDE },
@@ -2052,7 +2262,7 @@ function startMcpServer() {
2052
2262
  }
2053
2263
  handle(msg);
2054
2264
  });
2055
- process.stderr.write("madeby mcp: ready (stdio; tools: init, check, who, ai; resources: align/enforce/backfill guides, policy schema)\n");
2265
+ process.stderr.write("madeby mcp: ready (stdio; tools: init, check, who, disclose, ai; resources: aco/align/enforce/backfill guides, policy schema)\n");
2056
2266
  }
2057
2267
 
2058
2268
  // src/madeby.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "madeby",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "MadeBy — verifiable content provenance. Disclose your AI use (madeby ai) and gate PRs on disclosure (madeby check).",
5
5
  "license": "MIT",
6
6
  "type": "module",