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.
- package/dist/madeby.mjs +306 -96
- 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
|
|
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
|
|
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 =
|
|
1395
|
-
return readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).map((f) =>
|
|
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 || !
|
|
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 =
|
|
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
|
|
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 =
|
|
1499
|
+
const out = join4(root, ".madeby", "spans", `${commit}.json`);
|
|
1436
1500
|
let kept = [];
|
|
1437
|
-
if (
|
|
1501
|
+
if (existsSync3(out)) {
|
|
1438
1502
|
const thisSource = `${result.tool}-session-log`;
|
|
1439
1503
|
try {
|
|
1440
|
-
kept = parseSpanManifest(
|
|
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 || !
|
|
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 =
|
|
1525
|
+
const text = readFileSync5(transcriptPath, "utf8");
|
|
1462
1526
|
const tool = detectParser(text)?.id;
|
|
1463
1527
|
const skeleton = redactSample(text);
|
|
1464
|
-
const dir =
|
|
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 =
|
|
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
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1717
|
+
const hasAssist = ASSIST.test(message);
|
|
1687
1718
|
const trailers = [];
|
|
1688
|
-
|
|
1689
|
-
if (
|
|
1690
|
-
|
|
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
|
|
1751
|
-
import { dirname as dirname2, join as
|
|
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 (
|
|
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(
|
|
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(
|
|
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
|
|
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