@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.
- package/LICENSE +1 -1
- package/README.md +295 -229
- package/action.yml +154 -154
- package/dist-client/src/client/cli.js +213 -95
- package/dist-client/src/client/economics.js +31 -0
- package/dist-client/src/client/full-command.js +46 -0
- package/dist-client/src/client/mcp.js +196 -0
- package/dist-client/src/client/observe.js +40 -9
- package/dist-client/src/client/verify-savings.js +8 -2
- package/docs/agent-adoption-kit.md +96 -0
- package/docs/agent-adoption-targets.md +52 -0
- package/docs/ai-agents.md +27 -3
- package/docs/claude-code.md +3 -2
- package/docs/codex.md +3 -2
- package/docs/copilot.md +4 -4
- package/docs/cursor.md +4 -4
- package/docs/grok.md +3 -2
- package/docs/language-support.md +118 -70
- package/docs/mcp.md +21 -0
- package/docs/npm-adoption.md +6 -0
- package/llms.txt +3 -0
- package/node_modules/@babel/parser/bin/babel-parser.js +0 -0
- package/node_modules/@diffci.com/core/README.md +9 -1
- package/node_modules/@diffci.com/core/dist/cache/economics-context.d.ts +2 -0
- package/node_modules/@diffci.com/core/dist/cache/economics-context.js +21 -0
- package/node_modules/@diffci.com/core/dist/cache/economics-context.js.map +1 -0
- package/node_modules/@diffci.com/core/dist/cache/vue-analysis-cache.d.ts +5 -0
- package/node_modules/@diffci.com/core/dist/cache/vue-analysis-cache.js +100 -0
- package/node_modules/@diffci.com/core/dist/cache/vue-analysis-cache.js.map +1 -0
- package/node_modules/@diffci.com/core/dist/planner/test-command.js +32 -0
- package/node_modules/@diffci.com/core/dist/planner/test-command.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/adapters/go.d.ts +3 -0
- package/node_modules/@diffci.com/core/dist/repo/adapters/go.js +24 -6
- package/node_modules/@diffci.com/core/dist/repo/adapters/go.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/adapters/index.js +2 -1
- package/node_modules/@diffci.com/core/dist/repo/adapters/index.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/adapters/maven.d.ts +3 -0
- package/node_modules/@diffci.com/core/dist/repo/adapters/maven.js +65 -0
- package/node_modules/@diffci.com/core/dist/repo/adapters/maven.js.map +1 -0
- package/node_modules/@diffci.com/core/dist/repo/adapters/types.d.ts +11 -0
- package/node_modules/@diffci.com/core/dist/repo/adapters/types.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/adapters/vue.js +142 -14
- package/node_modules/@diffci.com/core/dist/repo/adapters/vue.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/graph.d.ts +4 -0
- package/node_modules/@diffci.com/core/dist/repo/graph.js +169 -19
- package/node_modules/@diffci.com/core/dist/repo/graph.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/impact.js +22 -5
- package/node_modules/@diffci.com/core/dist/repo/impact.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/repo-config.d.ts +15 -0
- package/node_modules/@diffci.com/core/dist/repo/repo-config.js +13 -2
- package/node_modules/@diffci.com/core/dist/repo/repo-config.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/test-discovery.js +2 -0
- package/node_modules/@diffci.com/core/dist/repo/test-discovery.js.map +1 -1
- package/node_modules/@diffci.com/core/dist/repo/types.d.ts +17 -0
- package/node_modules/@diffci.com/core/dist/repo/vue-scope.d.ts +4 -0
- package/node_modules/@diffci.com/core/dist/repo/vue-scope.js +157 -0
- package/node_modules/@diffci.com/core/dist/repo/vue-scope.js.map +1 -0
- package/node_modules/@diffci.com/core/src/cache/economics-context.ts +19 -0
- package/node_modules/@diffci.com/core/src/cache/vue-analysis-cache.ts +80 -0
- package/node_modules/@diffci.com/core/src/planner/test-command.ts +28 -0
- package/node_modules/@diffci.com/core/src/repo/adapters/go.ts +24 -5
- package/node_modules/@diffci.com/core/src/repo/adapters/index.ts +2 -1
- package/node_modules/@diffci.com/core/src/repo/adapters/maven.ts +38 -0
- package/node_modules/@diffci.com/core/src/repo/adapters/types.ts +5 -0
- package/node_modules/@diffci.com/core/src/repo/adapters/vue.ts +120 -15
- package/node_modules/@diffci.com/core/src/repo/graph.ts +146 -20
- package/node_modules/@diffci.com/core/src/repo/impact.ts +20 -4
- package/node_modules/@diffci.com/core/src/repo/repo-config.ts +20 -2
- package/node_modules/@diffci.com/core/src/repo/test-discovery.ts +2 -0
- package/node_modules/@diffci.com/core/src/repo/types.ts +11 -0
- package/node_modules/@diffci.com/core/src/repo/vue-scope.ts +122 -0
- package/node_modules/nanoid/bin/nanoid.cjs +0 -0
- package/node_modules/typescript/bin/tsc +0 -0
- package/node_modules/typescript/bin/tsserver +0 -0
- package/node_modules/yaml/bin.mjs +0 -0
- 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 {
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
|
6
|
+
The default command is:
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
9
|
npx @diffci.com/diffci@latest check
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
`check`
|
|
13
|
-
|
|
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:
|
package/docs/claude-code.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`
|
|
16
|
-
|
|
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`
|
|
16
|
-
|
|
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`
|
|
16
|
-
|
|
17
|
-
|
|
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`
|
|
16
|
-
|
|
17
|
-
default
|
|
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`
|
|
16
|
-
|
|
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.
|