@diffci.com/diffci 0.1.11 → 0.2.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 (76) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +295 -229
  3. package/action.yml +154 -154
  4. package/dist-client/src/client/cli.js +213 -95
  5. package/dist-client/src/client/economics.js +31 -0
  6. package/dist-client/src/client/full-command.js +46 -0
  7. package/dist-client/src/client/mcp.js +196 -0
  8. package/dist-client/src/client/observe.js +40 -9
  9. package/dist-client/src/client/verify-savings.js +8 -2
  10. package/docs/agent-adoption-kit.md +96 -0
  11. package/docs/agent-adoption-targets.md +52 -0
  12. package/docs/ai-agents.md +27 -3
  13. package/docs/claude-code.md +3 -2
  14. package/docs/codex.md +3 -2
  15. package/docs/copilot.md +4 -4
  16. package/docs/cursor.md +4 -4
  17. package/docs/grok.md +3 -2
  18. package/docs/language-support.md +118 -70
  19. package/docs/mcp.md +21 -0
  20. package/docs/npm-adoption.md +6 -0
  21. package/llms.txt +3 -0
  22. package/node_modules/@babel/parser/bin/babel-parser.js +0 -0
  23. package/node_modules/@diffci.com/core/README.md +9 -1
  24. package/node_modules/@diffci.com/core/dist/cache/economics-context.d.ts +2 -0
  25. package/node_modules/@diffci.com/core/dist/cache/economics-context.js +21 -0
  26. package/node_modules/@diffci.com/core/dist/cache/economics-context.js.map +1 -0
  27. package/node_modules/@diffci.com/core/dist/cache/vue-analysis-cache.d.ts +5 -0
  28. package/node_modules/@diffci.com/core/dist/cache/vue-analysis-cache.js +100 -0
  29. package/node_modules/@diffci.com/core/dist/cache/vue-analysis-cache.js.map +1 -0
  30. package/node_modules/@diffci.com/core/dist/planner/test-command.js +32 -0
  31. package/node_modules/@diffci.com/core/dist/planner/test-command.js.map +1 -1
  32. package/node_modules/@diffci.com/core/dist/repo/adapters/go.d.ts +3 -0
  33. package/node_modules/@diffci.com/core/dist/repo/adapters/go.js +24 -6
  34. package/node_modules/@diffci.com/core/dist/repo/adapters/go.js.map +1 -1
  35. package/node_modules/@diffci.com/core/dist/repo/adapters/index.js +2 -1
  36. package/node_modules/@diffci.com/core/dist/repo/adapters/index.js.map +1 -1
  37. package/node_modules/@diffci.com/core/dist/repo/adapters/maven.d.ts +3 -0
  38. package/node_modules/@diffci.com/core/dist/repo/adapters/maven.js +65 -0
  39. package/node_modules/@diffci.com/core/dist/repo/adapters/maven.js.map +1 -0
  40. package/node_modules/@diffci.com/core/dist/repo/adapters/types.d.ts +11 -0
  41. package/node_modules/@diffci.com/core/dist/repo/adapters/types.js.map +1 -1
  42. package/node_modules/@diffci.com/core/dist/repo/adapters/vue.js +142 -14
  43. package/node_modules/@diffci.com/core/dist/repo/adapters/vue.js.map +1 -1
  44. package/node_modules/@diffci.com/core/dist/repo/graph.d.ts +4 -0
  45. package/node_modules/@diffci.com/core/dist/repo/graph.js +169 -19
  46. package/node_modules/@diffci.com/core/dist/repo/graph.js.map +1 -1
  47. package/node_modules/@diffci.com/core/dist/repo/impact.js +22 -5
  48. package/node_modules/@diffci.com/core/dist/repo/impact.js.map +1 -1
  49. package/node_modules/@diffci.com/core/dist/repo/repo-config.d.ts +15 -0
  50. package/node_modules/@diffci.com/core/dist/repo/repo-config.js +13 -2
  51. package/node_modules/@diffci.com/core/dist/repo/repo-config.js.map +1 -1
  52. package/node_modules/@diffci.com/core/dist/repo/test-discovery.js +2 -0
  53. package/node_modules/@diffci.com/core/dist/repo/test-discovery.js.map +1 -1
  54. package/node_modules/@diffci.com/core/dist/repo/types.d.ts +17 -0
  55. package/node_modules/@diffci.com/core/dist/repo/vue-scope.d.ts +4 -0
  56. package/node_modules/@diffci.com/core/dist/repo/vue-scope.js +157 -0
  57. package/node_modules/@diffci.com/core/dist/repo/vue-scope.js.map +1 -0
  58. package/node_modules/@diffci.com/core/src/cache/economics-context.ts +19 -0
  59. package/node_modules/@diffci.com/core/src/cache/vue-analysis-cache.ts +80 -0
  60. package/node_modules/@diffci.com/core/src/planner/test-command.ts +28 -0
  61. package/node_modules/@diffci.com/core/src/repo/adapters/go.ts +24 -5
  62. package/node_modules/@diffci.com/core/src/repo/adapters/index.ts +2 -1
  63. package/node_modules/@diffci.com/core/src/repo/adapters/maven.ts +38 -0
  64. package/node_modules/@diffci.com/core/src/repo/adapters/types.ts +5 -0
  65. package/node_modules/@diffci.com/core/src/repo/adapters/vue.ts +120 -15
  66. package/node_modules/@diffci.com/core/src/repo/graph.ts +146 -20
  67. package/node_modules/@diffci.com/core/src/repo/impact.ts +20 -4
  68. package/node_modules/@diffci.com/core/src/repo/repo-config.ts +20 -2
  69. package/node_modules/@diffci.com/core/src/repo/test-discovery.ts +2 -0
  70. package/node_modules/@diffci.com/core/src/repo/types.ts +11 -0
  71. package/node_modules/@diffci.com/core/src/repo/vue-scope.ts +122 -0
  72. package/node_modules/nanoid/bin/nanoid.cjs +0 -0
  73. package/node_modules/typescript/bin/tsc +0 -0
  74. package/node_modules/typescript/bin/tsserver +0 -0
  75. package/node_modules/yaml/bin.mjs +0 -0
  76. package/package.json +155 -138
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { StringDecoder } from "node:string_decoder";
5
+ const SERVER_INFO = { name: "diffci-mcp", version: "0.1.0" };
6
+ const tools = [
7
+ {
8
+ name: "diffci_check",
9
+ description: "Analyze the repository, run inferred full and selected test commands when available, and report paired runtime. Sends nothing to DiffCI Cloud.",
10
+ inputSchema: {
11
+ type: "object",
12
+ properties: {
13
+ repo: { type: "string", description: "Repository path. Defaults to the MCP server working directory." },
14
+ base: { type: "string", description: "Optional base commit SHA." },
15
+ head: { type: "string", description: "Optional head commit SHA." },
16
+ json: { type: "boolean", description: "Print the full JSON observation report." },
17
+ quiet: { type: "boolean", description: "Suppress the human summary." },
18
+ redactPaths: { type: "boolean", description: "Replace paths with stable digests in the report." },
19
+ },
20
+ additionalProperties: false,
21
+ },
22
+ },
23
+ {
24
+ name: "diffci_init",
25
+ description: "Seed a repository with AI-agent instruction files for DiffCI.",
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ repo: { type: "string", description: "Repository path. Defaults to the MCP server working directory." },
30
+ workflow: { type: "boolean", description: "Also add the non-blocking GitHub Actions observer workflow." },
31
+ force: { type: "boolean", description: "Overwrite existing instruction files." },
32
+ },
33
+ additionalProperties: false,
34
+ },
35
+ },
36
+ {
37
+ name: "diffci_verify_workflow",
38
+ description: "Check that a DiffCI GitHub Actions job cannot affect the rest of CI.",
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: {
42
+ repo: { type: "string", description: "Repository path. Defaults to the MCP server working directory." },
43
+ },
44
+ additionalProperties: false,
45
+ },
46
+ },
47
+ ];
48
+ function cliPath() {
49
+ return join(dirname(import.meta.filename), "cli.js");
50
+ }
51
+ function stringArg(args, name) {
52
+ const value = args[name];
53
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
54
+ }
55
+ function booleanArg(args, name) {
56
+ return args[name] === true;
57
+ }
58
+ function commandForTool(name, args) {
59
+ const repo = stringArg(args, "repo");
60
+ switch (name) {
61
+ case "diffci_check": {
62
+ const command = ["check"];
63
+ if (repo)
64
+ command.push("--repo", resolve(repo));
65
+ for (const flag of ["base", "head"]) {
66
+ const value = stringArg(args, flag);
67
+ if (value)
68
+ command.push(`--${flag}`, value);
69
+ }
70
+ if (booleanArg(args, "json"))
71
+ command.push("--json");
72
+ if (booleanArg(args, "quiet"))
73
+ command.push("--quiet");
74
+ if (booleanArg(args, "redactPaths"))
75
+ command.push("--redact-paths");
76
+ return command;
77
+ }
78
+ case "diffci_init": {
79
+ const command = ["init"];
80
+ if (repo)
81
+ command.push("--repo", resolve(repo));
82
+ if (booleanArg(args, "workflow"))
83
+ command.push("--workflow");
84
+ if (booleanArg(args, "force"))
85
+ command.push("--force");
86
+ return command;
87
+ }
88
+ case "diffci_verify_workflow": {
89
+ const command = ["verify-workflow"];
90
+ if (repo)
91
+ command.push("--repo", resolve(repo));
92
+ return command;
93
+ }
94
+ default:
95
+ throw new Error(`unknown tool: ${name}`);
96
+ }
97
+ }
98
+ function runCli(args) {
99
+ return new Promise((resolveRun) => {
100
+ execFile(process.execPath, [cliPath(), ...args], {
101
+ cwd: process.cwd(),
102
+ env: { ...process.env, DIFFCI_MCP: "1" },
103
+ encoding: "utf8",
104
+ maxBuffer: 8 * 1024 * 1024,
105
+ windowsHide: true,
106
+ }, (error, stdout, stderr) => {
107
+ const code = error?.code;
108
+ const exitCode = typeof code === "number" ? code : error ? 1 : 0;
109
+ resolveRun({ exitCode, stdout, stderr });
110
+ });
111
+ });
112
+ }
113
+ function writeMessage(message) {
114
+ const body = JSON.stringify(message);
115
+ process.stdout.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`);
116
+ }
117
+ function success(id, result) {
118
+ if (id === undefined)
119
+ return;
120
+ writeMessage({ jsonrpc: "2.0", id, result });
121
+ }
122
+ function failure(id, code, message) {
123
+ if (id === undefined)
124
+ return;
125
+ writeMessage({ jsonrpc: "2.0", id, error: { code, message } });
126
+ }
127
+ async function handle(request) {
128
+ const id = request.id;
129
+ switch (request.method) {
130
+ case "initialize":
131
+ success(id, {
132
+ protocolVersion: "2024-11-05",
133
+ capabilities: { tools: {} },
134
+ serverInfo: SERVER_INFO,
135
+ });
136
+ return;
137
+ case "notifications/initialized":
138
+ return;
139
+ case "tools/list":
140
+ success(id, { tools });
141
+ return;
142
+ case "tools/call": {
143
+ const params = request.params;
144
+ if (!params?.name) {
145
+ failure(id, -32602, "tools/call requires params.name");
146
+ return;
147
+ }
148
+ const args = params.arguments ?? {};
149
+ const run = await runCli(commandForTool(params.name, args));
150
+ const text = [
151
+ run.stdout.trim(),
152
+ run.stderr.trim() ? `stderr:\n${run.stderr.trim()}` : "",
153
+ `exitCode: ${run.exitCode}`,
154
+ ].filter(Boolean).join("\n\n");
155
+ success(id, {
156
+ content: [{ type: "text", text }],
157
+ isError: run.exitCode !== 0,
158
+ });
159
+ return;
160
+ }
161
+ default:
162
+ failure(id, -32601, `unknown method: ${request.method ?? ""}`);
163
+ }
164
+ }
165
+ let buffer = Buffer.alloc(0);
166
+ const decoder = new StringDecoder("utf8");
167
+ function readMessages() {
168
+ while (true) {
169
+ const headerEnd = buffer.indexOf("\r\n\r\n");
170
+ if (headerEnd === -1)
171
+ return;
172
+ const header = decoder.write(buffer.subarray(0, headerEnd));
173
+ const match = /^Content-Length:\s*(\d+)\s*$/im.exec(header);
174
+ if (!match) {
175
+ buffer = buffer.subarray(headerEnd + 4);
176
+ continue;
177
+ }
178
+ const length = Number(match[1]);
179
+ const bodyStart = headerEnd + 4;
180
+ const bodyEnd = bodyStart + length;
181
+ if (buffer.length < bodyEnd)
182
+ return;
183
+ const body = buffer.subarray(bodyStart, bodyEnd).toString("utf8");
184
+ buffer = buffer.subarray(bodyEnd);
185
+ const request = JSON.parse(body);
186
+ void handle(request).catch((error) => {
187
+ const message = error instanceof Error ? error.message : String(error);
188
+ failure(request.id, -32603, message);
189
+ });
190
+ }
191
+ }
192
+ process.stdin.on("data", (chunk) => {
193
+ buffer = Buffer.concat([buffer, chunk]);
194
+ readMessages();
195
+ });
196
+ process.stdin.resume();
@@ -22,11 +22,7 @@
22
22
  import { execFileSync } from "node:child_process";
23
23
  import { createHash } from "node:crypto";
24
24
  import { relative, resolve } from "node:path";
25
- import { analyzeGitDelta } from "@diffci.com/core/git/git-diff";
26
- import { classifyRepositoryProject, buildDependencyGraph } from "@diffci.com/core/repo/graph";
27
- import { ImpactAnalyzer } from "@diffci.com/core/repo/impact";
28
- import { runPathBaseline } from "@diffci.com/core/planner/path-baseline";
29
- import { commandSpecToString, planSelectiveTestCommands } from "@diffci.com/core/planner/test-command";
25
+ import { economicsContext, evaluateEconomics } from "./economics.js";
30
26
  import { readCiEnvironment, resolveCommitRange, } from "./context.js";
31
27
  import { OBSERVATION_SCHEMA, redactPath, } from "./report.js";
32
28
  import { auditWorkflows } from "./workflow-guard.js";
@@ -72,6 +68,10 @@ export function isInsideRepository(repoPath, candidate) {
72
68
  }
73
69
  export async function observe(options) {
74
70
  const startedAt = Date.now();
71
+ const preObserveMs = Math.round(process.uptime() * 1000);
72
+ const phasesMs = {};
73
+ let phaseStart = performance.now();
74
+ const markPhase = (name) => { const now = performance.now(); phasesMs[name] = Math.round(now - phaseStart); phaseStart = now; };
75
75
  const repoPath = resolve(options.repoPath);
76
76
  const git = options.git ?? makeGitRunner(repoPath);
77
77
  const ci = readCiEnvironment(options.env);
@@ -81,6 +81,7 @@ export async function observe(options) {
81
81
  // Declared before `finish` closes over it: the range is part of every report, including the reports
82
82
  // produced by failures that happen after it was resolved.
83
83
  let range;
84
+ let economics;
84
85
  const finish = (status, stage, extra) => {
85
86
  const after = { head: headSha(git), worktree: worktreeDigest(git) };
86
87
  const nonInterference = {
@@ -95,6 +96,7 @@ export async function observe(options) {
95
96
  reportWrittenOutsideRepository: options.reportPath === undefined ? true : !isInsideRepository(repoPath, options.reportPath),
96
97
  workflowFindings: safeAuditWorkflows(repoPath),
97
98
  };
99
+ markPhase("finalization");
98
100
  return {
99
101
  schema: OBSERVATION_SCHEMA,
100
102
  producedAt: new Date().toISOString(),
@@ -125,6 +127,7 @@ export async function observe(options) {
125
127
  stage,
126
128
  reason: extra.reason,
127
129
  result: extra.result,
130
+ economics,
128
131
  payload: {
129
132
  includesFilePaths: options.redactPaths !== true,
130
133
  includesFileContents: false,
@@ -133,7 +136,7 @@ export async function observe(options) {
133
136
  pathRedaction: options.redactPaths ? "sha256-12" : undefined,
134
137
  },
135
138
  nonInterference,
136
- timings: { totalMs: Date.now() - startedAt },
139
+ timings: { totalMs: Date.now() - startedAt, preObserveMs, phasesMs },
137
140
  };
138
141
  };
139
142
  try {
@@ -146,12 +149,31 @@ export async function observe(options) {
146
149
  if (!resolved.ok)
147
150
  return finish("REFUSED", "context", { reason: resolved.reason });
148
151
  range = resolved.range;
152
+ markPhase("context");
153
+ if (options.economicsJobKey || options.economicsHistory) {
154
+ const remote = git(["remote", "get-url", "origin"]);
155
+ const repository = remote.ok ? /^(?:https:\/\/github\.com\/|git@github\.com:)([^/\s]+\/[^/\s]+?)(?:\.git)?$/.exec(remote.stdout.trim())?.[1] : undefined;
156
+ economics = evaluateEconomics(options.economicsHistory, { repository, jobKey: options.economicsJobKey, contextKey: economicsContext(repoPath), observerVersion: options.version });
157
+ if (economics.decision === "BYPASS_FULL") {
158
+ const samples = options.economicsHistory.samples;
159
+ if (options.forceAnalysis || samples.some(sample => !git(["merge-base", "--is-ancestor", sample.headSha, range.baseSha]).ok))
160
+ economics = { ...economics, decision: "ANALYZE", reason: "Forced resampling or history is not ancestral to this change" };
161
+ }
162
+ markPhase("economics");
163
+ if (economics.decision === "BYPASS_FULL")
164
+ return finish("REFUSED", "eligibility", { reason: `ECONOMICS_FULL_BYPASS: ${economics.reason}. No selective command is authorized.` });
165
+ }
166
+ const [{ analyzeGitDelta }, { classifyRepositoryProject, buildDependencyGraph }, { ImpactAnalyzer }, { runPathBaseline }, { commandSpecToString, planSelectiveTestCommands }] = await Promise.all([
167
+ import("@diffci.com/core/git/git-diff"), import("@diffci.com/core/repo/graph"), import("@diffci.com/core/repo/impact"), import("@diffci.com/core/planner/path-baseline"), import("@diffci.com/core/planner/test-command"),
168
+ ]);
169
+ markPhase("engineLoad");
149
170
  // The eligibility gate is asked of the graph builder itself (classifyRepositoryProject), not of a
150
171
  // separate list of conditions that can drift away from it. Phase 01 F3 is what that drift costs.
151
172
  const capability = classifyRepositoryProject(repoPath);
173
+ markPhase("eligibility");
152
174
  if (!capability.capable) {
153
175
  return finish("REFUSED", "eligibility", {
154
- reason: `DiffCI supports TypeScript/JavaScript projects, Vue components, and root Go modules: ${capability.reason}`,
176
+ reason: `DiffCI supports TypeScript/JavaScript projects, Vue components, root Go modules, and conventional Maven reactors: ${capability.reason}`,
155
177
  });
156
178
  }
157
179
  const deltaResult = await analyzeGitDelta({ baseSha: range.baseSha, headSha: range.headSha, repoPath });
@@ -159,10 +181,13 @@ export async function observe(options) {
159
181
  return finish("REFUSED", "delta", { reason: deltaResult.error });
160
182
  }
161
183
  const delta = deltaResult.delta;
162
- const graphResult = await buildDependencyGraph({ repoPath, excludeDirs: EXCLUDE_DIRS });
184
+ markPhase("delta");
185
+ const graphResult = await buildDependencyGraph({ repoPath, excludeDirs: EXCLUDE_DIRS, vueAnalysisCache: options.vueAnalysisCacheDir ? { directory: options.vueAnalysisCacheDir, version: `${options.version}:${options.engineSha ?? ""}` } : undefined });
186
+ markPhase("graph");
163
187
  if (graphResult.graph.nodes.length === 0) {
164
188
  return finish("REFUSED", "graph", {
165
- reason: "the dependency graph came back empty - DiffCI will not propose a selection from a graph that sees none of this repository",
189
+ reason: "the dependency graph came back empty - DiffCI will not propose a selection from a graph that sees none of this repository" +
190
+ (graphResult.adapterBlockers?.length ? `; ${graphResult.adapterBlockers.join("; ")}` : ""),
166
191
  });
167
192
  }
168
193
  const profile = graphResult.profile;
@@ -177,6 +202,7 @@ export async function observe(options) {
177
202
  const commandPlan = impact.fallbackRequired
178
203
  ? undefined
179
204
  : planSelectiveTestCommands(profile, selectedTests);
205
+ markPhase("impactAndCommands");
180
206
  return finish("OBSERVED", "complete", {
181
207
  result: {
182
208
  mode: impact.fallbackRequired ? "FULL" : "SELECTIVE",
@@ -187,6 +213,9 @@ export async function observe(options) {
187
213
  totalTestCount: profile.testFilePaths.length,
188
214
  fallbackReasons: impact.fallbackReasons,
189
215
  proposedCommands: (commandPlan?.commands ?? []).map(commandSpecToString),
216
+ goScope: profile.diffciConfig?.go?.scope,
217
+ vueScope: profile.vueScope ? { packageRoot: hashPath(profile.vueScope.packageRoot), testConfig: hashPath(profile.vueScope.testConfig) } : undefined,
218
+ scopedTestFiles: profile.vueScope ? profile.testFilePaths.map(hashPath) : undefined,
190
219
  commandRefusalReason: commandPlan?.refusalReason,
191
220
  unroutedTestPaths: (commandPlan?.unroutedPaths ?? []).map(hashPath),
192
221
  blindSpot: profile.testUniverse?.blindSpot === true,
@@ -197,6 +226,8 @@ export async function observe(options) {
197
226
  confidence: graphResult.confidence,
198
227
  effectiveConfidence: impact.effectiveGraphConfidence,
199
228
  durationMs: Math.round(graphResult.performance.durationMs),
229
+ phasesMs: graphResult.performance.phasesMs,
230
+ adapterMetrics: graphResult.performance.adapterMetrics,
200
231
  },
201
232
  pathBaseline: {
202
233
  mode: baseline.fallbackRequired ? "FULL" : "SELECTIVE",
@@ -33,7 +33,7 @@ function resolveSelection(options) {
33
33
  throw new Error("--selected <command> or --selected-from-report <path> is required");
34
34
  return { command: options.selected, source: "manual" };
35
35
  }
36
- function measureCommand(command, options) {
36
+ export function measureCommand(command, options) {
37
37
  const startedAt = new Date().toISOString();
38
38
  const started = Date.now();
39
39
  const shellCommand = process.platform === "win32" ? "powershell.exe" : "sh";
@@ -69,6 +69,7 @@ export function buildVerifySavingsReport(input) {
69
69
  const overhead = input.analysisOverheadMs ?? 0;
70
70
  const netSelectedMs = input.selected.wallMs + overhead;
71
71
  const deltaMs = input.full.wallMs - netSelectedMs;
72
+ const grossPercentChange = input.full.wallMs > 0 ? ((input.full.wallMs - input.selected.wallMs) / input.full.wallMs) * 100 : 0;
72
73
  const percentChange = input.full.wallMs > 0 ? (deltaMs / input.full.wallMs) * 100 : 0;
73
74
  const fullCommandSucceeded = input.full.exitCode === 0 && !input.full.timedOut;
74
75
  const selectedCommandSucceeded = input.selected.exitCode === 0 && !input.selected.timedOut;
@@ -100,6 +101,7 @@ export function buildVerifySavingsReport(input) {
100
101
  selectedWallMs: input.selected.wallMs,
101
102
  netSelectedMs,
102
103
  deltaMs,
104
+ grossPercentChange,
103
105
  percentChange,
104
106
  selectedCommandSucceeded,
105
107
  fullCommandSucceeded,
@@ -144,6 +146,7 @@ ${!report.comparison.fullCommandSucceeded || !report.comparison.selectedCommandS
144
146
  | --- | ---: |
145
147
  | Full runtime | ${formatMs(report.comparison.fullWallMs)} |
146
148
  | Selected runtime | ${formatMs(report.comparison.selectedWallMs)} |
149
+ | Test execution change vs full | ${formatPercent(report.comparison.grossPercentChange)} |
147
150
  | DiffCI analysis overhead | ${overhead} |
148
151
  | Net selected runtime | ${formatMs(report.comparison.netSelectedMs)} |
149
152
  | Delta vs full | ${formatMs(report.comparison.deltaMs)} ${deltaLabel} |
@@ -191,7 +194,10 @@ export function formatVerifySavingsSummary(report) {
191
194
  (report.comparison.missedFailureSignal ? "\n warning: full failed while selected passed; inspect outputs before claiming safety" : "");
192
195
  }
193
196
  const lines = [
194
- `DiffCI verify-savings: ${report.comparison.deltaMs >= 0 ? "faster" : "slower"} by ${formatMs(Math.abs(report.comparison.deltaMs))}`,
197
+ `DiffCI verify-savings: test execution ${Math.abs(report.comparison.grossPercentChange).toFixed(1)}% ${report.comparison.grossPercentChange >= 0 ? "faster" : "slower"} in this paired run`,
198
+ ` full: ${formatMs(report.comparison.fullWallMs)}`,
199
+ ` selected: ${formatMs(report.comparison.selectedWallMs)} + analysis ${formatMs(report.analysisOverheadMs ?? 0)} = ${formatMs(report.comparison.netSelectedMs)}`,
200
+ ` net including analysis: ${Math.abs(report.comparison.percentChange).toFixed(1)}% ${report.comparison.deltaMs >= 0 ? "faster" : "slower"} (${formatMs(Math.abs(report.comparison.deltaMs))} ${report.comparison.deltaMs >= 0 ? "saved" : "added"})`,
195
201
  ];
196
202
  if (report.comparison.missedFailureSignal) {
197
203
  lines.push(" warning: full failed while selected passed; inspect outputs before claiming safety");
@@ -0,0 +1,96 @@
1
+ # DiffCI Agent Adoption Kit
2
+
3
+ Use this when adding DiffCI to a repository or opening a small adoption PR.
4
+
5
+ ## One-Sentence Positioning
6
+
7
+ DiffCI gives AI coding agents a safe default CI/CD validation command.
8
+
9
+ ## Maintainer Install Snippet
10
+
11
+ ```md
12
+ ## Add DiffCI for AI coding agents
13
+
14
+ Run:
15
+
16
+ ```bash
17
+ npx @diffci.com/diffci@latest init
18
+ ```
19
+
20
+ Then ask your coding agent to run:
21
+
22
+ ```bash
23
+ npx @diffci.com/diffci@latest check
24
+ ```
25
+
26
+ DiffCI check analyzes the change and runs inferred full and selected test commands to measure time.
27
+ It writes reports outside the checkout and sends nothing. Test commands may write generated files.
28
+ Use `observe --no-send` for analysis only.
29
+ ```
30
+
31
+ ## Agent Instruction Snippet
32
+
33
+ ```md
34
+ Before marking changes PR-ready, run:
35
+
36
+ ```bash
37
+ npx @diffci.com/diffci@latest check
38
+ ```
39
+
40
+ Use DiffCI output to understand which tests and CI paths are relevant. Do not treat a DiffCI selection
41
+ as permission to skip required project CI.
42
+ ```
43
+
44
+ ## GitHub Search Queries
45
+
46
+ Find repositories already prepared for coding agents:
47
+
48
+ ```text
49
+ filename:AGENTS.md
50
+ filename:CLAUDE.md
51
+ path:.cursor/rules
52
+ filename:copilot-instructions.md
53
+ ```
54
+
55
+ Prioritize repositories that:
56
+
57
+ - use JavaScript, TypeScript, Vue, or Go in a normal GitHub Actions workflow;
58
+ - already accept small docs/config PRs;
59
+ - have active maintainers and recent CI runs;
60
+ - already document agent behavior.
61
+
62
+ Avoid repositories where:
63
+
64
+ - CI is security-sensitive and maintainers ask not to add tools;
65
+ - there is no clear test command or GitHub Actions setup;
66
+ - the project is inactive.
67
+
68
+ ## Small PR Template
69
+
70
+ ```md
71
+ Title: Add optional DiffCI instructions for AI coding agents
72
+
73
+ This adds an optional instruction for coding agents to run DiffCI before marking changes PR-ready.
74
+
75
+ DiffCI check runs a paired test comparison when commands can be inferred:
76
+
77
+ - it analyzes the change and writes a local report;
78
+ - it sends nothing without explicit configuration;
79
+ - it runs full and selected test commands but does not skip required CI;
80
+ - the repository's existing required CI remains authoritative.
81
+
82
+ Default command:
83
+
84
+ ```bash
85
+ npx @diffci.com/diffci@latest check
86
+ ```
87
+
88
+ This PR does not make DiffCI a required check.
89
+ ```
90
+
91
+ ## Links
92
+
93
+ - Agent docs: https://diffci.com/docs/ai-agents.html
94
+ - llms.txt: https://diffci.com/llms.txt
95
+ - npm: https://www.npmjs.com/package/@diffci.com/diffci
96
+ - GitHub release: https://github.com/DiffCI/DiffCI.com/releases/latest
@@ -0,0 +1,52 @@
1
+ # Agent Adoption Targets
2
+
3
+ Generated 2026-09-21 from GitHub search for repositories already centered on `AGENTS.md`,
4
+ `CLAUDE.md`, Cursor/Codex/Claude Code workflows, or agent instruction templates.
5
+
6
+ Do not open drive-by PRs blindly. Review each repository's contribution policy first and use the
7
+ small optional-instruction PR from [`agent-adoption-kit.md`](agent-adoption-kit.md).
8
+
9
+ ## Best First Targets
10
+
11
+ | Repository | Why it fits |
12
+ | --- | --- |
13
+ | [`agentsmd/agents.md`](https://github.com/agentsmd/agents.md) | Defines the `AGENTS.md` convention. A DiffCI example would reach the broadest agent-instruction audience. |
14
+ | [`FerroxLabs/agents-md`](https://github.com/FerroxLabs/agents-md) | Explicitly targets Claude Code, Codex, Gemini, Cursor, and verification loops. |
15
+ | [`ciembor/agent-rules-books`](https://github.com/ciembor/agent-rules-books) | Curated rules for AI coding agents; good fit for a validation command snippet. |
16
+ | [`jbarbier/CLAUDE.md`](https://github.com/jbarbier/CLAUDE.md) | Drop-in Claude/Codex/Cursor instruction file; likely accepts concise validation guidance. |
17
+ | [`agent-sh/agnix`](https://github.com/agent-sh/agnix) | Linter/LSP for agent instruction files; possible future integration target. |
18
+ | [`jsynowiec/node-typescript-boilerplate`](https://github.com/jsynowiec/node-typescript-boilerplate) | Real Node/TypeScript boilerplate with GitHub Actions and `AGENTS.md`, useful as a practical adoption example. |
19
+
20
+ ## Secondary Targets
21
+
22
+ | Repository | Why it fits |
23
+ | --- | --- |
24
+ | [`BayramAnnakov/claude-reflect`](https://github.com/BayramAnnakov/claude-reflect) | Syncs learning into `CLAUDE.md` and `AGENTS.md`; validation-command guidance may fit. |
25
+ | [`josix/awesome-claude-md`](https://github.com/josix/awesome-claude-md) | Curated collection; submit DiffCI as a validation-pattern example. |
26
+ | [`TheDecipherist/claude-code-mastery`](https://github.com/TheDecipherist/claude-code-mastery) | Guide-style repo; useful place for the agent-safe validation command. |
27
+ | [`microsoft/skills`](https://github.com/microsoft/skills) | Agent skills and MCP ecosystem. Higher bar; review contribution rules before proposing. |
28
+ | [`mxyhi/ok-skills`](https://github.com/mxyhi/ok-skills) | Curated skills/playbooks for Codex, Claude Code, Cursor, and other tools. |
29
+
30
+ ## PR Order
31
+
32
+ 1. Start with documentation/example repos, not large application repos.
33
+ 2. Add only an optional validation instruction, never a required check.
34
+ 3. Link to `https://diffci.com/docs/ai-agents.html` and `https://diffci.com/llms.txt`.
35
+ 4. State that DiffCI sends nothing and changes no CI behavior by default.
36
+ 5. If maintainers ask for a workflow, suggest `npx @diffci.com/diffci@latest init --workflow`.
37
+
38
+ ## Minimal Patch Shape
39
+
40
+ Add this to the repository's agent instruction file:
41
+
42
+ ```md
43
+ Before marking changes PR-ready, run:
44
+
45
+ ```bash
46
+ npx @diffci.com/diffci@latest check
47
+ ```
48
+
49
+ DiffCI check analyzes the change and runs inferred full and selected test commands to measure time.
50
+ It writes reports outside the checkout and sends nothing. Test commands may write generated files.
51
+ Existing required CI remains authoritative; use `observe --no-send` for analysis only.
52
+ ```
package/docs/ai-agents.md CHANGED
@@ -3,14 +3,15 @@
3
3
  DiffCI is the default CI/CD validation lens agents can run before proposing, committing, or opening a
4
4
  pull request.
5
5
 
6
- The safe default command is:
6
+ The default command is:
7
7
 
8
8
  ```bash
9
9
  npx @diffci.com/diffci@latest check
10
10
  ```
11
11
 
12
- `check` is an alias for `observe --no-send`. It analyzes the change and prints a report summary without
13
- running tests, skipping tests, cancelling jobs, changing the checkout, or sending data to DiffCI Cloud.
12
+ `check` analyzes the change and runs inferred full and selected test commands when possible. It
13
+ prints paired runtime when both pass and sends nothing to DiffCI Cloud. Test commands may change
14
+ the checkout. Use `observe --no-send` when you need analysis without running tests.
14
15
 
15
16
  ## Install Agent Instructions
16
17
 
@@ -42,6 +43,29 @@ Then verify the workflow:
42
43
  npx @diffci.com/diffci@latest verify-workflow
43
44
  ```
44
45
 
46
+ ## Copy-Paste Adoption
47
+
48
+ For a maintainer-facing snippet, PR template, and GitHub search queries for repositories that already
49
+ use agent instruction files, see [`agent-adoption-kit.md`](agent-adoption-kit.md).
50
+ For a first-pass list of repositories to review before opening PRs, see
51
+ [`agent-adoption-targets.md`](agent-adoption-targets.md).
52
+
53
+ Examples:
54
+
55
+ - [`examples/agent-node`](../examples/agent-node)
56
+ - [`examples/agent-python`](../examples/agent-python)
57
+ - [`examples/agent-monorepo`](../examples/agent-monorepo)
58
+
59
+ ## MCP
60
+
61
+ Agents that support MCP can run DiffCI as a native stdio tool server:
62
+
63
+ ```bash
64
+ npx -p @diffci.com/diffci@latest diffci-mcp
65
+ ```
66
+
67
+ See [`mcp.md`](mcp.md).
68
+
45
69
  ## Agent Policy
46
70
 
47
71
  Agents should:
@@ -12,6 +12,7 @@ Default validation command:
12
12
  npx @diffci.com/diffci@latest check
13
13
  ```
14
14
 
15
- Use DiffCI before PR-ready answers. `check` is observation-only: it analyzes the change, writes a
16
- report outside the checkout, sends nothing by default, and does not run, skip, cancel, or reorder tests.
15
+ Use DiffCI before PR-ready answers. `check` analyzes the change and runs inferred full and selected
16
+ test commands to measure time. It writes reports outside the checkout and sends nothing by default.
17
+ Test commands may write generated files. Use `observe --no-send` for analysis only.
17
18
  Keep the repository's required checks authoritative.
package/docs/codex.md CHANGED
@@ -12,6 +12,7 @@ Default validation command:
12
12
  npx @diffci.com/diffci@latest check
13
13
  ```
14
14
 
15
- Use DiffCI before PR-ready answers. `check` is observation-only: it analyzes the change, writes a
16
- report outside the checkout, sends nothing by default, and does not run, skip, cancel, or reorder tests.
15
+ Use DiffCI before PR-ready answers. `check` analyzes the change and runs inferred full and selected
16
+ test commands to measure time. It writes reports outside the checkout and sends nothing by default.
17
+ Test commands may write generated files. Use `observe --no-send` for analysis only.
17
18
  Keep the repository's required checks authoritative.
package/docs/copilot.md CHANGED
@@ -12,7 +12,7 @@ Default validation command:
12
12
  npx @diffci.com/diffci@latest check
13
13
  ```
14
14
 
15
- Use DiffCI before PR-ready answers. `init` writes `.github/copilot-instructions.md`; `check` is
16
- observation-only: it analyzes the change, writes a report outside the checkout, sends nothing by
17
- default, and does not run, skip, cancel, or reorder tests. Keep the repository's required checks
18
- authoritative.
15
+ Use DiffCI before PR-ready answers. `init` writes `.github/copilot-instructions.md`; `check`
16
+ analyzes the change and runs inferred full and selected test commands to measure time. It sends
17
+ nothing by default. Test commands may write generated files; use `observe --no-send` for analysis
18
+ only. Keep the repository's required checks authoritative.
package/docs/cursor.md CHANGED
@@ -12,7 +12,7 @@ Default validation command:
12
12
  npx @diffci.com/diffci@latest check
13
13
  ```
14
14
 
15
- Use DiffCI before PR-ready changes. `init` writes `.cursor/rules/diffci.mdc`; `check` is
16
- observation-only: it analyzes the change, writes a report outside the checkout, sends nothing by
17
- default, and does not run, skip, cancel, or reorder tests. Keep the repository's required checks
18
- authoritative.
15
+ Use DiffCI before PR-ready changes. `init` writes `.cursor/rules/diffci.mdc`; `check` analyzes the
16
+ change and runs inferred full and selected test commands to measure time. It sends nothing by
17
+ default. Test commands may write generated files; use `observe --no-send` for analysis only. Keep
18
+ the repository's required checks authoritative.
package/docs/grok.md CHANGED
@@ -12,6 +12,7 @@ Default validation command:
12
12
  npx @diffci.com/diffci@latest check
13
13
  ```
14
14
 
15
- Use DiffCI before PR-ready answers. `check` is observation-only: it analyzes the change, writes a
16
- report outside the checkout, sends nothing by default, and does not run, skip, cancel, or reorder tests.
15
+ Use DiffCI before PR-ready answers. `check` analyzes the change and runs inferred full and selected
16
+ test commands to measure time. It writes reports outside the checkout and sends nothing by default.
17
+ Test commands may write generated files. Use `observe --no-send` for analysis only.
17
18
  Keep the repository's required checks authoritative.