aioengine 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +110 -18
  2. package/package.json +3 -2
  3. package/src/index.js +253 -14
package/README.md CHANGED
@@ -4,21 +4,49 @@ AI change control for developers using Claude Code, Cursor, Codex, Copilot, and
4
4
 
5
5
  aioengine helps you review AI-generated code before you trust it. It scans your repo for missing guardrails, checks changed files for risky edits, and flags when AI may have wandered outside the requested task.
6
6
 
7
+ ## Quick start
8
+
9
+ Run aioengine in any JavaScript or TypeScript project:
10
+
11
+ ```bash
12
+ npx aioengine check
13
+ ```
14
+
15
+ Then set up AI coding guardrails:
16
+
17
+ ```bash
18
+ npx aioengine init
19
+ ```
20
+
21
+ After your AI coding tool makes changes, review them before committing:
22
+
23
+ ```bash
24
+ npx aioengine scope "update landing page headline"
25
+ npx aioengine review
26
+ ```
27
+
28
+ In CI or pull request workflows:
29
+
30
+ ```bash
31
+ npx aioengine ci --task "update landing page headline"
32
+ ```
33
+
7
34
  ## Commands
8
35
 
9
36
  ```bash
10
- aioengine init
11
- aioengine check
12
- aioengine scope "add init command"
13
- aioengine review
14
- aioengine rules
37
+ npx aioengine init
38
+ npx aioengine check
39
+ npx aioengine scope "add init command"
40
+ npx aioengine review
41
+ npx aioengine ci --task "add init command"
42
+ npx aioengine rules
15
43
  ```
16
44
 
17
45
  ## Why aioengine exists
18
46
 
19
47
  AI coding tools can move fast, but review becomes the bottleneck.
20
48
 
21
- A simple prompt can lead to unexpected changes in sensitive files like auth, billing, database migrations, environment config, deployment settings, or dependency files.
49
+ A simple prompt can lead to unexpected changes in sensitive files like auth, billing, database migrations, environment config, deployment settings, dependency files, or CI workflows.
22
50
 
23
51
  aioengine helps answer:
24
52
 
@@ -26,12 +54,14 @@ aioengine helps answer:
26
54
  - Did AI change files outside the task?
27
55
  - Did AI add or modify dependencies?
28
56
  - Does this repo have AI coding rules?
29
- - What should I review before committing?
57
+ - What should I review before committing or merging?
30
58
 
31
59
  ## `aioengine init`
32
60
 
33
61
  Sets up aioengine in your repo.
34
62
 
63
+ Creates missing files only. aioengine will not overwrite an existing `CLAUDE.md`.
64
+
35
65
  Creates:
36
66
 
37
67
  ```txt
@@ -40,10 +70,16 @@ CLAUDE.md
40
70
  .cursor/rules/aioengine.mdc
41
71
  ```
42
72
 
73
+ If `CLAUDE.md` already exists, aioengine leaves it untouched and saves suggested rules to:
74
+
75
+ ```txt
76
+ .aioengine/suggested-claude-rules.md
77
+ ```
78
+
43
79
  Run:
44
80
 
45
81
  ```bash
46
- aioengine init
82
+ npx aioengine init
47
83
  ```
48
84
 
49
85
  ## `aioengine check`
@@ -66,7 +102,7 @@ Checks for:
66
102
  Run:
67
103
 
68
104
  ```bash
69
- aioengine check
105
+ npx aioengine check
70
106
  ```
71
107
 
72
108
  ## `aioengine scope`
@@ -76,10 +112,10 @@ Checks whether changed files match the task you gave your AI coding tool.
76
112
  Example:
77
113
 
78
114
  ```bash
79
- aioengine scope "update landing page headline"
115
+ npx aioengine scope "update landing page headline"
80
116
  ```
81
117
 
82
- If the task sounds like a UI change but AI modified billing, database, env, dependency, or deployment files, aioengine will flag possible scope drift.
118
+ If the task sounds like a UI change but AI modified billing, database, env, dependency, CLI, or deployment files, aioengine will flag possible scope drift.
83
119
 
84
120
  ## `aioengine review`
85
121
 
@@ -88,7 +124,7 @@ Reviews current uncommitted changes for risky files.
88
124
  Run:
89
125
 
90
126
  ```bash
91
- aioengine review
127
+ npx aioengine review
92
128
  ```
93
129
 
94
130
  aioengine will flag changes to files that often deserve extra review, such as:
@@ -101,6 +137,52 @@ aioengine will flag changes to files that often deserve extra review, such as:
101
137
  - dependency files
102
138
  - GitHub workflow files
103
139
 
140
+ ## `aioengine ci`
141
+
142
+ Runs aioengine checks in CI or pull request workflows.
143
+
144
+ Run:
145
+
146
+ ```bash
147
+ npx aioengine ci --task "update landing page headline"
148
+ ```
149
+
150
+ In GitHub Actions, aioengine will try to detect changed files from the pull request context. If a task is available from the PR title, event payload, or `AIOENGINE_TASK`, it can flag possible scope drift.
151
+
152
+ By default:
153
+
154
+ - possible scope drift fails the CI check
155
+ - risky files warn but do not fail the CI check
156
+
157
+ Example GitHub Actions step:
158
+
159
+ ```yaml
160
+ - name: Run aioengine
161
+ run: npx aioengine ci
162
+ ```
163
+
164
+ For more reliable PR diffs, use checkout with full history:
165
+
166
+ ```yaml
167
+ - uses: actions/checkout@v4
168
+ with:
169
+ fetch-depth: 0
170
+
171
+ - name: Run aioengine
172
+ run: npx aioengine ci
173
+ ```
174
+
175
+ You can also pass a task manually:
176
+
177
+ ```yaml
178
+ - uses: actions/checkout@v4
179
+ with:
180
+ fetch-depth: 0
181
+
182
+ - name: Run aioengine
183
+ run: npx aioengine ci --task "update landing page headline"
184
+ ```
185
+
104
186
  ## `aioengine rules`
105
187
 
106
188
  Generates starter AI coding rules for Claude Code and Cursor.
@@ -108,26 +190,36 @@ Generates starter AI coding rules for Claude Code and Cursor.
108
190
  Run:
109
191
 
110
192
  ```bash
111
- aioengine rules
193
+ npx aioengine rules
194
+ ```
195
+
196
+ Creates missing files only. aioengine will not overwrite an existing `CLAUDE.md`.
197
+
198
+ If `CLAUDE.md` already exists, suggested Claude rules are saved to:
199
+
200
+ ```txt
201
+ .aioengine/suggested-claude-rules.md
112
202
  ```
113
203
 
114
- This creates or skips:
204
+ This command creates or skips:
115
205
 
116
206
  ```txt
117
207
  CLAUDE.md
118
208
  .cursor/rules/aioengine.mdc
209
+ .aioengine/suggested-claude-rules.md
119
210
  ```
120
211
 
121
212
  ## Example workflow
122
213
 
123
214
  ```bash
124
- aioengine init
125
- aioengine check
215
+ npx aioengine init
216
+ npx aioengine check
126
217
 
127
218
  # Ask Claude, Cursor, Codex, or another AI coding tool to make a change.
128
219
 
129
- aioengine scope "update landing page headline"
130
- aioengine review
220
+ npx aioengine scope "update landing page headline"
221
+ npx aioengine review
222
+ npx aioengine ci --task "update landing page headline"
131
223
  ```
132
224
 
133
225
  ## Current status
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aioengine",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "AI change control for developers using AI coding tools.",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -9,7 +9,8 @@
9
9
  "review": "node ./src/index.js review",
10
10
  "rules": "node ./src/index.js rules",
11
11
  "scope": "node ./src/index.js scope",
12
- "init": "node ./src/index.js init"
12
+ "init": "node ./src/index.js init",
13
+ "ci": "node ./src/index.js ci"
13
14
  },
14
15
  "keywords": [
15
16
  "ai",
package/src/index.js CHANGED
@@ -35,6 +35,12 @@ program
35
35
  runReview();
36
36
  });
37
37
 
38
+ program
39
+ .command("ci")
40
+ .description("Run aioengine review checks in CI and pull request workflows.")
41
+ .option("--task <task>", "Task description to compare changed files against")
42
+ .action((options) => runCi(options));
43
+
38
44
  program
39
45
  .command("scope")
40
46
  .description("Check whether changed files match the requested task.")
@@ -62,7 +68,6 @@ function runInit() {
62
68
 
63
69
  const aioengineDir = ".aioengine";
64
70
  const configPath = path.join(aioengineDir, "config.json");
65
- const claudePath = "CLAUDE.md";
66
71
  const cursorDir = ".cursor/rules";
67
72
  const cursorPath = path.join(cursorDir, "aioengine.mdc");
68
73
 
@@ -92,12 +97,7 @@ function runInit() {
92
97
  skipped.push(configPath);
93
98
  }
94
99
 
95
- if (!exists(claudePath, root)) {
96
- fs.writeFileSync(path.join(root, claudePath), getClaudeRules());
97
- created.push(claudePath);
98
- } else {
99
- skipped.push(claudePath);
100
- }
100
+ createClaudeRulesSafely(root, created, skipped);
101
101
 
102
102
  if (!exists(cursorDir, root)) {
103
103
  fs.mkdirSync(path.join(root, cursorDir), { recursive: true });
@@ -113,6 +113,14 @@ function runInit() {
113
113
  printSection("Created", created, "green");
114
114
  printSection("Skipped", skipped, "yellow");
115
115
 
116
+ if (skipped.includes("CLAUDE.md already exists")) {
117
+ console.log(
118
+ `\n${pc.dim(
119
+ "aioengine did not modify your existing CLAUDE.md. Suggested Claude rules were saved to .aioengine/suggested-claude-rules.md if that file did not already exist."
120
+ )}`
121
+ );
122
+ }
123
+
116
124
  console.log(pc.bold("Next steps:"));
117
125
  console.log(` 1. Run ${pc.cyan("aioengine check")}`);
118
126
  console.log(` 2. Make or review AI-generated changes`);
@@ -273,6 +281,112 @@ function runReview() {
273
281
  }
274
282
  }
275
283
 
284
+ function runCi(options = {}) {
285
+ printHeader("aioengine CI");
286
+
287
+ const root = getProjectRoot();
288
+
289
+ if (!isInsideGitRepo()) {
290
+ console.log(
291
+ `${pc.red("✗")} No Git repo detected. aioengine ci must run inside a Git repository.`
292
+ );
293
+ process.exitCode = 1;
294
+ return;
295
+ }
296
+
297
+ const task = options.task || getCiTask();
298
+ const files = getCiChangedFiles(root);
299
+
300
+ console.log(`${pc.dim("Project:")} ${root}`);
301
+
302
+ if (isGitHubActions()) {
303
+ console.log(`${pc.dim("Environment:")} GitHub Actions`);
304
+ } else {
305
+ console.log(`${pc.dim("Environment:")} Local / unknown CI`);
306
+ }
307
+
308
+ if (task) {
309
+ console.log(`${pc.dim("Task:")} ${task}`);
310
+ } else {
311
+ console.log(
312
+ `${pc.yellow("!")} No task detected. Scope checks will be less precise.`
313
+ );
314
+ }
315
+
316
+ if (files.length === 0) {
317
+ console.log(pc.green("\nNo changed files found."));
318
+ return;
319
+ }
320
+
321
+ const profile = task ? inferTaskProfile(task) : null;
322
+ const riskyFiles = files.filter(isRiskyFile);
323
+ const outOfScopeFiles = profile
324
+ ? files.filter((file) => isProbablyOutOfScope(file, profile))
325
+ : [];
326
+
327
+ if (profile) {
328
+ console.log(`${pc.dim("Detected task type:")} ${profile.label}`);
329
+ }
330
+
331
+ console.log(`${pc.dim("Changed files:")} ${files.length}\n`);
332
+
333
+ for (const file of files) {
334
+ const risky = riskyFiles.includes(file);
335
+ const outOfScope = outOfScopeFiles.includes(file);
336
+
337
+ if (outOfScope) {
338
+ console.log(` ${pc.red("✗")} ${file} ${pc.red("— possible scope drift")}`);
339
+ } else if (risky) {
340
+ console.log(` ${pc.yellow("!")} ${file} ${pc.yellow("— review carefully")}`);
341
+ } else {
342
+ console.log(` ${pc.green("✓")} ${file}`);
343
+ }
344
+ }
345
+
346
+ const hasScopeDrift = outOfScopeFiles.length > 0;
347
+ const hasRiskyFiles = riskyFiles.length > 0;
348
+
349
+ if (hasScopeDrift) {
350
+ console.log(pc.yellow("\nCI review recommended"));
351
+
352
+ printSection(
353
+ "Possible scope drift",
354
+ outOfScopeFiles.map(
355
+ (file) => `Possible out-of-scope file changed: ${file}`
356
+ ),
357
+ "warning"
358
+ );
359
+
360
+ if (hasRiskyFiles) {
361
+ printSection(
362
+ "Risky files",
363
+ riskyFiles.map((file) => `High-risk file changed: ${file}`),
364
+ "warning"
365
+ );
366
+ }
367
+
368
+ console.log(
369
+ pc.dim(
370
+ "\nRecommendation: Review these changes before merging. aioengine is failing this CI check because possible scope drift was detected."
371
+ )
372
+ );
373
+
374
+ process.exitCode = 1;
375
+ return;
376
+ }
377
+
378
+ if (hasRiskyFiles) {
379
+ console.log(
380
+ pc.yellow(
381
+ "\nRisky files were changed. aioengine is allowing this check to pass, but these files should receive extra human review."
382
+ )
383
+ );
384
+ return;
385
+ }
386
+
387
+ console.log(pc.green("\nNo obvious AI change-control issues detected."));
388
+ }
389
+
276
390
  function runScope(task) {
277
391
  printHeader("aioengine Scope");
278
392
 
@@ -382,19 +496,13 @@ function runRules() {
382
496
 
383
497
  const root = getProjectRoot();
384
498
 
385
- const claudePath = "CLAUDE.md";
386
499
  const cursorDir = ".cursor/rules";
387
500
  const cursorPath = path.join(cursorDir, "aioengine.mdc");
388
501
 
389
502
  const created = [];
390
503
  const skipped = [];
391
504
 
392
- if (!exists(claudePath, root)) {
393
- fs.writeFileSync(path.join(root, claudePath), getClaudeRules());
394
- created.push(claudePath);
395
- } else {
396
- skipped.push(claudePath);
397
- }
505
+ createClaudeRulesSafely(root, created, skipped);
398
506
 
399
507
  if (!exists(cursorDir, root)) {
400
508
  fs.mkdirSync(path.join(root, cursorDir), { recursive: true });
@@ -410,6 +518,14 @@ function runRules() {
410
518
  printSection("Created", created, "green");
411
519
  printSection("Skipped", skipped, "yellow");
412
520
 
521
+ if (skipped.includes("CLAUDE.md already exists")) {
522
+ console.log(
523
+ `\n${pc.dim(
524
+ "aioengine did not modify your existing CLAUDE.md. Suggested Claude rules were saved to .aioengine/suggested-claude-rules.md if that file did not already exist."
525
+ )}`
526
+ );
527
+ }
528
+
413
529
  console.log(pc.bold("Next step:"));
414
530
  console.log(` Run ${pc.cyan("aioengine check")} again.`);
415
531
  }
@@ -839,6 +955,37 @@ function getDefaultConfig() {
839
955
  };
840
956
  }
841
957
 
958
+ function createClaudeRulesSafely(root, created, skipped) {
959
+ const claudePath = path.join(root, "CLAUDE.md");
960
+ const aioengineDir = path.join(root, ".aioengine");
961
+ const suggestedPath = path.join(
962
+ root,
963
+ ".aioengine",
964
+ "suggested-claude-rules.md"
965
+ );
966
+
967
+ if (!fs.existsSync(aioengineDir)) {
968
+ fs.mkdirSync(aioengineDir, { recursive: true });
969
+ created.push(".aioengine");
970
+ }
971
+
972
+ if (!fs.existsSync(claudePath)) {
973
+ fs.writeFileSync(claudePath, getClaudeRules(), "utf8");
974
+ created.push("CLAUDE.md");
975
+ return;
976
+ }
977
+
978
+ skipped.push("CLAUDE.md already exists");
979
+
980
+ if (!fs.existsSync(suggestedPath)) {
981
+ fs.writeFileSync(suggestedPath, getClaudeRules(), "utf8");
982
+ created.push(".aioengine/suggested-claude-rules.md");
983
+ return;
984
+ }
985
+
986
+ skipped.push(".aioengine/suggested-claude-rules.md already exists");
987
+ }
988
+
842
989
  function getClaudeRules() {
843
990
  return `# AI Coding Rules
844
991
 
@@ -897,6 +1044,98 @@ For UI-only tasks, avoid backend, API, database, and config changes.
897
1044
  `;
898
1045
  }
899
1046
 
1047
+ function isGitHubActions() {
1048
+ return process.env.GITHUB_ACTIONS === "true";
1049
+ }
1050
+
1051
+ function getCiTask() {
1052
+ const explicitTask = process.env.AIOENGINE_TASK;
1053
+
1054
+ if (explicitTask) {
1055
+ return explicitTask;
1056
+ }
1057
+
1058
+ const eventPath = process.env.GITHUB_EVENT_PATH;
1059
+
1060
+ if (!eventPath || !fs.existsSync(eventPath)) {
1061
+ return "";
1062
+ }
1063
+
1064
+ try {
1065
+ const event = JSON.parse(fs.readFileSync(eventPath, "utf8"));
1066
+
1067
+ return (
1068
+ event.pull_request?.title ||
1069
+ event.issue?.title ||
1070
+ event.head_commit?.message ||
1071
+ ""
1072
+ );
1073
+ } catch {
1074
+ return "";
1075
+ }
1076
+ }
1077
+
1078
+ function getCiChangedFiles(root) {
1079
+ if (isGitHubActions()) {
1080
+ const files = getGitHubActionsChangedFiles(root);
1081
+
1082
+ if (files.length > 0) {
1083
+ return files;
1084
+ }
1085
+ }
1086
+
1087
+ return getChangedFiles(root);
1088
+ }
1089
+
1090
+ function getGitHubActionsChangedFiles(root) {
1091
+ const baseRef = process.env.GITHUB_BASE_REF;
1092
+ const beforeSha = process.env.GITHUB_EVENT_BEFORE;
1093
+ const currentSha = process.env.GITHUB_SHA || "HEAD";
1094
+
1095
+ try {
1096
+ if (baseRef) {
1097
+ try {
1098
+ execSync(`git fetch origin ${baseRef} --depth=1`, {
1099
+ cwd: root,
1100
+ stdio: "ignore",
1101
+ });
1102
+ } catch {
1103
+ // The workflow may already have enough history.
1104
+ }
1105
+
1106
+ return uniqueFiles(
1107
+ execSync(`git diff --name-only origin/${baseRef}...HEAD`, {
1108
+ cwd: root,
1109
+ encoding: "utf8",
1110
+ })
1111
+ .split("\n")
1112
+ .map((file) => file.trim())
1113
+ .filter(Boolean)
1114
+ );
1115
+ }
1116
+
1117
+ if (beforeSha && currentSha) {
1118
+ return uniqueFiles(
1119
+ execSync(`git diff --name-only ${beforeSha} ${currentSha}`, {
1120
+ cwd: root,
1121
+ encoding: "utf8",
1122
+ })
1123
+ .split("\n")
1124
+ .map((file) => file.trim())
1125
+ .filter(Boolean)
1126
+ );
1127
+ }
1128
+ } catch {
1129
+ return [];
1130
+ }
1131
+
1132
+ return [];
1133
+ }
1134
+
1135
+ function uniqueFiles(files) {
1136
+ return [...new Set(files)].sort();
1137
+ }
1138
+
900
1139
  function getCliVersion() {
901
1140
  try {
902
1141
  const currentFile = fileURLToPath(import.meta.url);