aioengine 0.1.3 → 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 +74 -3
  2. package/package.json +3 -2
  3. package/src/index.js +204 -0
package/README.md CHANGED
@@ -25,6 +25,12 @@ npx aioengine scope "update landing page headline"
25
25
  npx aioengine review
26
26
  ```
27
27
 
28
+ In CI or pull request workflows:
29
+
30
+ ```bash
31
+ npx aioengine ci --task "update landing page headline"
32
+ ```
33
+
28
34
  ## Commands
29
35
 
30
36
  ```bash
@@ -32,6 +38,7 @@ npx aioengine init
32
38
  npx aioengine check
33
39
  npx aioengine scope "add init command"
34
40
  npx aioengine review
41
+ npx aioengine ci --task "add init command"
35
42
  npx aioengine rules
36
43
  ```
37
44
 
@@ -39,7 +46,7 @@ npx aioengine rules
39
46
 
40
47
  AI coding tools can move fast, but review becomes the bottleneck.
41
48
 
42
- 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.
43
50
 
44
51
  aioengine helps answer:
45
52
 
@@ -47,12 +54,14 @@ aioengine helps answer:
47
54
  - Did AI change files outside the task?
48
55
  - Did AI add or modify dependencies?
49
56
  - Does this repo have AI coding rules?
50
- - What should I review before committing?
57
+ - What should I review before committing or merging?
51
58
 
52
59
  ## `aioengine init`
53
60
 
54
61
  Sets up aioengine in your repo.
55
62
 
63
+ Creates missing files only. aioengine will not overwrite an existing `CLAUDE.md`.
64
+
56
65
  Creates:
57
66
 
58
67
  ```txt
@@ -61,6 +70,12 @@ CLAUDE.md
61
70
  .cursor/rules/aioengine.mdc
62
71
  ```
63
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
+
64
79
  Run:
65
80
 
66
81
  ```bash
@@ -122,6 +137,52 @@ aioengine will flag changes to files that often deserve extra review, such as:
122
137
  - dependency files
123
138
  - GitHub workflow files
124
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
+
125
186
  ## `aioengine rules`
126
187
 
127
188
  Generates starter AI coding rules for Claude Code and Cursor.
@@ -132,11 +193,20 @@ Run:
132
193
  npx aioengine rules
133
194
  ```
134
195
 
135
- This creates or skips:
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
202
+ ```
203
+
204
+ This command creates or skips:
136
205
 
137
206
  ```txt
138
207
  CLAUDE.md
139
208
  .cursor/rules/aioengine.mdc
209
+ .aioengine/suggested-claude-rules.md
140
210
  ```
141
211
 
142
212
  ## Example workflow
@@ -149,6 +219,7 @@ npx aioengine check
149
219
 
150
220
  npx aioengine scope "update landing page headline"
151
221
  npx aioengine review
222
+ npx aioengine ci --task "update landing page headline"
152
223
  ```
153
224
 
154
225
  ## Current status
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aioengine",
3
- "version": "0.1.3",
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.")
@@ -275,6 +281,112 @@ function runReview() {
275
281
  }
276
282
  }
277
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
+
278
390
  function runScope(task) {
279
391
  printHeader("aioengine Scope");
280
392
 
@@ -932,6 +1044,98 @@ For UI-only tasks, avoid backend, API, database, and config changes.
932
1044
  `;
933
1045
  }
934
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
+
935
1139
  function getCliVersion() {
936
1140
  try {
937
1141
  const currentFile = fileURLToPath(import.meta.url);