codeep 2.20.0 → 2.22.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 (38) hide show
  1. package/dist/acp/server.js +8 -4
  2. package/dist/api/index.js +26 -19
  3. package/dist/config/index.d.ts +20 -0
  4. package/dist/config/index.js +32 -0
  5. package/dist/renderer/App.js +0 -1
  6. package/dist/renderer/Input.d.ts +0 -1
  7. package/dist/renderer/Input.js +0 -1
  8. package/dist/renderer/commands/registry.js +1 -0
  9. package/dist/renderer/commands.js +19 -3
  10. package/dist/renderer/components/Export.js +0 -2
  11. package/dist/renderer/components/Login.d.ts +0 -1
  12. package/dist/renderer/components/Login.js +0 -2
  13. package/dist/renderer/components/Logout.js +0 -2
  14. package/dist/renderer/components/Settings.d.ts +3 -0
  15. package/dist/renderer/components/Settings.js +0 -12
  16. package/dist/renderer/main.js +35 -56
  17. package/dist/utils/agent.d.ts +7 -0
  18. package/dist/utils/agent.js +102 -6
  19. package/dist/utils/auditLog.d.ts +93 -0
  20. package/dist/utils/auditLog.js +217 -0
  21. package/dist/utils/codeepCloud.d.ts +30 -4
  22. package/dist/utils/codeepCloud.js +71 -19
  23. package/dist/utils/diffPreview.js +0 -1
  24. package/dist/utils/git.js +0 -1
  25. package/dist/utils/headlessReview.d.ts +9 -1
  26. package/dist/utils/headlessReview.js +77 -3
  27. package/dist/utils/mcpStreamableHttp.d.ts +0 -1
  28. package/dist/utils/mcpStreamableHttp.js +0 -3
  29. package/dist/utils/personalities.js +0 -1
  30. package/dist/utils/reviewFix.d.ts +65 -0
  31. package/dist/utils/reviewFix.js +141 -0
  32. package/dist/utils/skillBundles.js +0 -4
  33. package/dist/utils/smartContext.js +0 -18
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.js +1 -1
  36. package/package.json +2 -2
  37. package/dist/renderer/components/Permission.d.ts +0 -24
  38. package/dist/renderer/components/Permission.js +0 -113
@@ -301,6 +301,52 @@ function writeFileBundle(kind, items) {
301
301
  * file so web edits actually take effect, but every divergent local body is
302
302
  * first copied to ~/.codeep/backups/personalities/. */
303
303
  let lastPersonalityPullBackupCount = 0;
304
+ /** Copy an about-to-be-replaced-or-removed personality into the backup dir.
305
+ * Shared so a deletion is backed up by exactly the same rules as an overwrite
306
+ * — nothing local is ever lost without a copy first. */
307
+ function backupLocalPersonality(name, body) {
308
+ const backupDir = join(homedir(), '.codeep', 'backups', 'personalities');
309
+ if (!existsSync(backupDir))
310
+ mkdirSync(backupDir, { recursive: true });
311
+ const suffix = new Date().toISOString().replace(/[:.]/g, '-');
312
+ let backupPath = join(backupDir, `${name}-${suffix}.md`);
313
+ let collision = 1;
314
+ while (existsSync(backupPath)) {
315
+ backupPath = join(backupDir, `${name}-${suffix}-${collision++}.md`);
316
+ }
317
+ writeFileSync(backupPath, body);
318
+ lastPersonalityPullBackupCount++;
319
+ }
320
+ /** Apply the server's explicit deletion list.
321
+ *
322
+ * Only names the server named. Absence from `items` is deliberately NOT a
323
+ * deletion signal: an expired session, the wrong account, or a truncated
324
+ * response all yield an empty `items`, and deleting on absence would wipe
325
+ * every local agent. Project-scoped agents in `.codeep/personalities/` are
326
+ * not cloud-owned and are never touched — only the global directory is.
327
+ * Every removal is backed up first, and a failed backup cancels the delete. */
328
+ function applyPersonalityTombstones(deleted) {
329
+ const dir = globalDir('personalities');
330
+ if (!existsSync(dir))
331
+ return 0;
332
+ let removed = 0;
333
+ for (const name of deleted) {
334
+ if (typeof name !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(name) || name.length > 64)
335
+ continue;
336
+ const filePath = join(dir, `${name}.md`);
337
+ if (!existsSync(filePath))
338
+ continue;
339
+ try {
340
+ backupLocalPersonality(name, readFileSync(filePath, 'utf8'));
341
+ unlinkSync(filePath);
342
+ removed++;
343
+ }
344
+ catch {
345
+ // A failed backup must not become a deletion — leave the file alone.
346
+ }
347
+ }
348
+ return removed;
349
+ }
304
350
  function writePulledPersonalityBundle(items) {
305
351
  lastPersonalityPullBackupCount = 0;
306
352
  const dir = globalDir('personalities');
@@ -317,17 +363,7 @@ function writePulledPersonalityBundle(items) {
317
363
  const local = readFileSync(filePath, 'utf8');
318
364
  if (local === body)
319
365
  continue;
320
- const backupDir = join(homedir(), '.codeep', 'backups', 'personalities');
321
- if (!existsSync(backupDir))
322
- mkdirSync(backupDir, { recursive: true });
323
- let suffix = new Date().toISOString().replace(/[:.]/g, '-');
324
- let backupPath = join(backupDir, `${name}-${suffix}.md`);
325
- let collision = 1;
326
- while (existsSync(backupPath)) {
327
- backupPath = join(backupDir, `${name}-${suffix}-${collision++}.md`);
328
- }
329
- writeFileSync(backupPath, local);
330
- lastPersonalityPullBackupCount++;
366
+ backupLocalPersonality(name, local);
331
367
  }
332
368
  // Same-directory rename is atomic on supported local filesystems: a
333
369
  // crash cannot leave a half-written active personality.
@@ -348,39 +384,54 @@ function writePulledPersonalityBundle(items) {
348
384
  }
349
385
  return written;
350
386
  }
387
+ export function describeSyncFailure(reason) {
388
+ switch (reason) {
389
+ case 'not-linked': return 'not linked to codeep.dev — run: codeep account';
390
+ case 'unreachable': return "couldn't reach codeep.dev";
391
+ case 'rejected': return 'codeep.dev refused the request — try signing in again';
392
+ case 'malformed': return 'codeep.dev sent a response this version cannot read';
393
+ }
394
+ }
351
395
  async function pullBundle(kind) {
352
396
  const syncToken = getSyncToken();
353
397
  if (!syncToken)
354
- return null;
398
+ return { ok: false, reason: 'not-linked' };
355
399
  const res = await fetchWithRetry(`${API_BASE}/api/${kind}`, { headers: { 'x-sync-token': syncToken } });
356
400
  if (!res?.ok)
357
- return null;
401
+ return { ok: false, reason: 'unreachable' };
358
402
  try {
359
403
  const data = await res.json();
360
404
  if (!data.ok)
361
- return null;
362
- return kind === 'personalities'
405
+ return { ok: false, reason: 'rejected' };
406
+ const count = kind === 'personalities'
363
407
  ? writePulledPersonalityBundle(data.items ?? {})
364
408
  : writeFileBundle(kind, data.items ?? {});
409
+ // A missing `deleted` field means "no deletions" — never "delete
410
+ // everything". Older servers simply omit it, and a client that treated the
411
+ // omission as a full tombstone list would empty the user's agent folder.
412
+ const removed = kind === 'personalities' && Array.isArray(data.deleted)
413
+ ? applyPersonalityTombstones(data.deleted)
414
+ : 0;
415
+ return { ok: true, count, removed };
365
416
  }
366
417
  catch {
367
- return null;
418
+ return { ok: false, reason: 'malformed' };
368
419
  }
369
420
  }
370
421
  async function pushBundle(kind) {
371
422
  const syncToken = getSyncToken();
372
423
  if (!syncToken)
373
- return null;
424
+ return { ok: false, reason: 'not-linked' };
374
425
  const items = readFileBundle(kind);
375
426
  const count = Object.keys(items).length;
376
427
  if (count === 0)
377
- return 0;
428
+ return { ok: true, count: 0, removed: 0 };
378
429
  const res = await fetchWithRetry(`${API_BASE}/api/${kind}`, {
379
430
  method: 'POST',
380
431
  headers: { 'Content-Type': 'application/json', 'x-sync-token': syncToken },
381
432
  body: JSON.stringify({ items }),
382
433
  });
383
- return res?.ok ? count : null;
434
+ return res?.ok ? { ok: true, count, removed: 0 } : { ok: false, reason: 'unreachable' };
384
435
  }
385
436
  export const pullPersonalities = () => pullBundle('personalities');
386
437
  export const getLastPersonalityPullBackupCount = () => lastPersonalityPullBackupCount;
@@ -699,3 +750,4 @@ export const _globalDirForTest = globalDir;
699
750
  export const _readFileBundleForTest = readFileBundle;
700
751
  export const _writeFileBundleForTest = writeFileBundle;
701
752
  export const _writePulledPersonalityBundleForTest = writePulledPersonalityBundle;
753
+ export const _applyPersonalityTombstonesForTest = applyPersonalityTombstones;
@@ -15,7 +15,6 @@ export function generateDiff(oldContent, newContent, contextLines = 3) {
15
15
  let oldIdx = 0;
16
16
  let newIdx = 0;
17
17
  let currentHunk = null;
18
- let pendingContext = [];
19
18
  for (const [oldMatch, newMatch] of lcs) {
20
19
  // Handle deletions
21
20
  while (oldIdx < oldMatch) {
package/dist/utils/git.js CHANGED
@@ -286,7 +286,6 @@ export function generateCommitMessage(prompt, actions) {
286
286
  const hasWrites = actions.some(a => a.type === 'write');
287
287
  const hasEdits = actions.some(a => a.type === 'edit');
288
288
  const hasDeletes = actions.some(a => a.type === 'delete');
289
- const hasCommands = actions.some(a => a.type === 'command');
290
289
  // Determine prefix
291
290
  let prefix = 'chore';
292
291
  // Check prompt for common patterns
@@ -1,3 +1,4 @@
1
+ import { type FixPlan } from './reviewFix.js';
1
2
  import { ReviewResult } from './codeReview.js';
2
3
  export type FailOn = 'error' | 'warning' | 'info' | 'none';
3
4
  export interface ReviewArgs {
@@ -7,8 +8,12 @@ export interface ReviewArgs {
7
8
  rules: boolean;
8
9
  ai: boolean;
9
10
  help: boolean;
11
+ /** Hand the findings to an agent and let it edit the working tree. */
12
+ fix: boolean;
13
+ /** Lowest severity the fix run may act on. Suggestions are never eligible. */
14
+ fixMinSeverity: 'error' | 'warning';
10
15
  }
11
- export declare const REVIEW_HELP = "Usage: codeep review [options] [files...]\n\nRun a deterministic, offline code review (no API key required). With no files,\nreviews your unstaged git changes, falling back to a src/ scan when the tree is\nclean. Pass files (or let your CI pass the PR's changed files) to scope it.\n\nCustom/disabled rules come from .codeep/review.yml (or .json) in the repo.\n\nOptions:\n --json Print the result as JSON instead of the markdown report\n --fail-on <level> Exit non-zero when an issue at or above <level> is found:\n error | warning | info | none (default: error)\n --rules List the built-in rule ids (for \"disable\" in .codeep/review.*) and exit\n --ai After the offline pass, ask your configured provider for a\n contextual second opinion on the working-tree diff\n (advisory; needs an API key; never affects the exit code)\n -h, --help Show this help\n\nExit code: 0 when nothing at/above --fail-on is found, 1 otherwise.";
16
+ export declare const REVIEW_HELP = "Usage: codeep review [options] [files...]\n\nRun a deterministic, offline code review (no API key required). With no files,\nreviews your unstaged git changes, falling back to a src/ scan when the tree is\nclean. Pass files (or let your CI pass the PR's changed files) to scope it.\n\nCustom/disabled rules come from .codeep/review.yml (or .json) in the repo.\n\nOptions:\n --json Print the result as JSON instead of the markdown report\n --fail-on <level> Exit non-zero when an issue at or above <level> is found:\n error | warning | info | none (default: error)\n --fix After the review, let an agent fix what it found. Edits the\n working tree and stops there \u2014 it never commits, branches\n or pushes. Runs under a files+tests boundary: no shell, no\n network, no git. Needs an API key.\n --fix-min-severity Lowest severity --fix may act on: error | warning\n (default: warning). Suggestions are never eligible.\n --rules List the built-in rule ids (for \"disable\" in .codeep/review.*) and exit\n --ai After the offline pass, ask your configured provider for a\n contextual second opinion on the working-tree diff\n (advisory; needs an API key; never affects the exit code)\n -h, --help Show this help\n\nExit code: 0 when nothing at/above --fail-on is found, 1 otherwise.";
12
17
  /** Parse `codeep review` argv (everything after the subcommand). Pure. */
13
18
  export declare function parseReviewArgs(argv: string[]): ReviewArgs;
14
19
  /** Exit code for a result under a fail-on threshold. Pure. */
@@ -29,6 +34,9 @@ export interface ReviewDeps {
29
34
  provider?: string;
30
35
  model?: string;
31
36
  };
37
+ /** Run an agent over a fix plan. Returns a human summary, or null when it
38
+ * could not run at all (no API key, provider unreachable). */
39
+ applyFixes: (plan: FixPlan) => Promise<string | null>;
32
40
  }
33
41
  /**
34
42
  * Orchestrate a headless review and return the process exit code. Side effects
@@ -1,3 +1,4 @@
1
+ import { buildFixPlan, summariseFixPlan } from './reviewFix.js';
1
2
  // Headless `codeep review` — a non-interactive entry point around the
2
3
  // deterministic reviewer in codeReview.ts. No API key, no TUI: it scans, prints
3
4
  // a report (markdown or JSON), and exits non-zero when issues at/above a chosen
@@ -24,6 +25,12 @@ Options:
24
25
  --json Print the result as JSON instead of the markdown report
25
26
  --fail-on <level> Exit non-zero when an issue at or above <level> is found:
26
27
  error | warning | info | none (default: error)
28
+ --fix After the review, let an agent fix what it found. Edits the
29
+ working tree and stops there — it never commits, branches
30
+ or pushes. Runs under a files+tests boundary: no shell, no
31
+ network, no git. Needs an API key.
32
+ --fix-min-severity Lowest severity --fix may act on: error | warning
33
+ (default: warning). Suggestions are never eligible.
27
34
  --rules List the built-in rule ids (for "disable" in .codeep/review.*) and exit
28
35
  --ai After the offline pass, ask your configured provider for a
29
36
  contextual second opinion on the working-tree diff
@@ -33,7 +40,10 @@ Options:
33
40
  Exit code: 0 when nothing at/above --fail-on is found, 1 otherwise.`;
34
41
  /** Parse `codeep review` argv (everything after the subcommand). Pure. */
35
42
  export function parseReviewArgs(argv) {
36
- const out = { files: [], json: false, failOn: 'error', rules: false, ai: false, help: false };
43
+ const out = {
44
+ files: [], json: false, failOn: 'error', rules: false, ai: false, help: false,
45
+ fix: false, fixMinSeverity: 'warning',
46
+ };
37
47
  for (let i = 0; i < argv.length; i++) {
38
48
  const arg = argv[i];
39
49
  if (arg === '--json') {
@@ -45,6 +55,19 @@ export function parseReviewArgs(argv) {
45
55
  else if (arg === '--ai') {
46
56
  out.ai = true;
47
57
  }
58
+ else if (arg === '--fix') {
59
+ out.fix = true;
60
+ }
61
+ else if (arg === '--fix-min-severity') {
62
+ const v = argv[++i];
63
+ if (v === 'error' || v === 'warning')
64
+ out.fixMinSeverity = v;
65
+ }
66
+ else if (arg.startsWith('--fix-min-severity=')) {
67
+ const v = arg.slice('--fix-min-severity='.length);
68
+ if (v === 'error' || v === 'warning')
69
+ out.fixMinSeverity = v;
70
+ }
48
71
  else if (arg === '-h' || arg === '--help') {
49
72
  out.help = true;
50
73
  }
@@ -104,12 +127,26 @@ export async function runHeadlessReview(argv, deps = defaultDeps()) {
104
127
  }
105
128
  const result = deps.review(args.files.length ? args.files : undefined);
106
129
  const aiText = args.ai ? await deps.aiReview(result) : null;
130
+ // Fixing happens after reporting, and never changes the exit code. CI decides
131
+ // pass or fail from what the reviewer found; whether an agent then managed to
132
+ // repair some of it is a separate question, and letting a successful fix turn
133
+ // a red check green would hide the finding rather than resolve it.
134
+ let fixSummary = null;
135
+ if (args.fix) {
136
+ const plan = buildFixPlan(result.issues, { minSeverity: args.fixMinSeverity });
137
+ fixSummary = plan.skipped ? summariseFixPlan(plan) : await deps.applyFixes(plan);
138
+ }
107
139
  if (args.json) {
108
- deps.write(JSON.stringify(args.ai ? { ...result, aiReview: aiText } : result, null, 2));
140
+ deps.write(JSON.stringify({
141
+ ...result,
142
+ ...(args.ai ? { aiReview: aiText } : {}),
143
+ ...(args.fix ? { fix: fixSummary } : {}),
144
+ }, null, 2));
109
145
  }
110
146
  else {
111
147
  const md = formatReviewResult(result);
112
- deps.write(args.ai ? appendAiSection(md, aiText, deps.aiMeta()) : md);
148
+ const withAi = args.ai ? appendAiSection(md, aiText, deps.aiMeta()) : md;
149
+ deps.write(fixSummary ? `${withAi}\n\n## Fix run\n\n${fixSummary}\n` : withAi);
113
150
  }
114
151
  return exitCodeForResult(result, args.failOn);
115
152
  }
@@ -132,6 +169,7 @@ function defaultDeps() {
132
169
  review: (files) => performCodeReview(minimalContext(cwd), files),
133
170
  write: (text) => process.stdout.write(text + '\n'),
134
171
  listRules: () => formatBuiltinRules(),
172
+ applyFixes: (plan) => runFixPlan(plan, minimalContext(cwd)),
135
173
  aiMeta: () => {
136
174
  try {
137
175
  return { provider: getCurrentProvider().name, model: String(config.get('model') || '') };
@@ -165,3 +203,39 @@ function defaultDeps() {
165
203
  },
166
204
  };
167
205
  }
206
+ /**
207
+ * Run a fix plan through the agent.
208
+ *
209
+ * The plan's personality is passed as the active one, so the same enforcement
210
+ * any custom bot gets applies here: the model is offered `files` and `tests`
211
+ * and nothing else. It edits the working tree and stops — branching, committing
212
+ * and opening a pull request belong to whatever called this, which in CI is the
213
+ * action that holds the token.
214
+ */
215
+ async function runFixPlan(plan, context) {
216
+ try {
217
+ const { runAgent } = await import('./agent.js');
218
+ // No cast here. `as never` on this call once hid the fact that
219
+ // personalityOverride did not exist, which would have run the CI fix with
220
+ // no boundary at all while the tests happily asserted otherwise.
221
+ const result = await runAgent(plan.prompt, context, {
222
+ personalityOverride: plan.personality,
223
+ maxIterations: 12,
224
+ });
225
+ const edited = new Set(result.actions
226
+ .filter(a => a.type === 'write' || a.type === 'edit')
227
+ .map(a => a.target));
228
+ if (!result.success) {
229
+ return `${summariseFixPlan(plan)} The run did not finish: ${result.error ?? 'unknown error'}.`;
230
+ }
231
+ if (edited.size === 0) {
232
+ return `${summariseFixPlan(plan)} Nothing was changed — the agent judged the findings not mechanically fixable.`;
233
+ }
234
+ return `${summariseFixPlan(plan)} Edited ${edited.size} file${edited.size === 1 ? '' : 's'}: ${[...edited].join(', ')}.`;
235
+ }
236
+ catch (error) {
237
+ // A missing key or an unreachable provider must not fail the review. The
238
+ // findings are already reported and the exit code already decided.
239
+ return `Could not run the fix: ${error.message}`;
240
+ }
241
+ }
@@ -32,7 +32,6 @@ export declare class StreamableHttpClient {
32
32
  private notificationAbort;
33
33
  private stopped;
34
34
  /** True after the server has set a session id (i.e. it tracks state). */
35
- private get hasServerSession();
36
35
  constructor(opts: StreamableHttpOptions);
37
36
  /**
38
37
  * Issue a JSON-RPC frame as POST. Reply may be a single JSON response
@@ -28,9 +28,6 @@ export class StreamableHttpClient {
28
28
  notificationAbort = null;
29
29
  stopped = false;
30
30
  /** True after the server has set a session id (i.e. it tracks state). */
31
- get hasServerSession() {
32
- return this.sessionId !== null;
33
- }
34
31
  constructor(opts) {
35
32
  this.opts = opts;
36
33
  }
@@ -648,7 +648,6 @@ export function isPersonalityToolCallAllowed(personality, toolCall, registeredMc
648
648
  if (tool === 'execute_command') {
649
649
  if (personality.tools?.includes('terminal'))
650
650
  return true;
651
- const command = commandName(toolCall);
652
651
  if (personality.tools?.includes('git') && isRestrictedGitCommandAllowed(toolCall))
653
652
  return true;
654
653
  if (personality.tools?.includes('tests') && isTestCommand(toolCall))
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Deciding what a CI agent may attempt to fix, and under what boundary.
3
+ *
4
+ * The reviewer already finds problems. This decides which of them are worth
5
+ * handing to an agent, caps how much it may take on, and pins the capabilities
6
+ * it runs with. It deliberately stops there: it produces a plan, never a
7
+ * commit. Staging, branching and opening a pull request belong to the action,
8
+ * which has the token — and keeping git out of the agent's reach is half the
9
+ * reason this is safe to run in CI at all.
10
+ *
11
+ * The boundary is the point. A fix run gets `files` (it must edit) and `tests`
12
+ * (it must check its own work) and nothing else. No shell, no git, no network.
13
+ * Enforced by the same machinery as any other custom bot, so an agent that
14
+ * decides it would like to curl something simply has no tool to do it with.
15
+ */
16
+ import type { ReviewIssue } from './codeReview.js';
17
+ import type { Personality } from './personalities.js';
18
+ export interface FixPlanOptions {
19
+ /** Lowest severity to attempt. Defaults to `warning`. */
20
+ minSeverity?: 'error' | 'warning';
21
+ /** Most issues to hand over in one run. */
22
+ maxIssues?: number;
23
+ /** Most files to touch. A fix that rewrites half the repo is not a fix. */
24
+ maxFiles?: number;
25
+ }
26
+ export interface FixPlan {
27
+ /** The issues the agent is being asked to address, in file order. */
28
+ issues: ReviewIssue[];
29
+ /** Files it is allowed to be working in. */
30
+ files: string[];
31
+ /** Why nothing is being attempted, when that is the case. */
32
+ skipped?: 'no-issues' | 'nothing-fixable';
33
+ /** The instruction handed to the agent. */
34
+ prompt: string;
35
+ /** The capability boundary the run executes under. */
36
+ personality: Personality;
37
+ }
38
+ /**
39
+ * `suggestion` and `info` are opinion — style preferences, "consider extracting
40
+ * this". Acting on them unasked produces churn in someone else's pull request
41
+ * and buries the findings that matter. Only what the reviewer states as a
42
+ * defect is eligible.
43
+ */
44
+ export declare function isFixable(issue: ReviewIssue, minSeverity: 'error' | 'warning'): boolean;
45
+ /**
46
+ * The capability set a CI fix runs under.
47
+ *
48
+ * Not a suggestion in the prompt — a real `custom-bot/v1` personality, enforced
49
+ * by `isPersonalityToolCallAllowed` and by the tool registry filter, exactly as
50
+ * a bot built in Agent Studio would be. An agent running unattended against
51
+ * someone else's repository is precisely where a boundary has to be real.
52
+ */
53
+ export declare function ciFixPersonality(): Personality;
54
+ /**
55
+ * Turn a review into a bounded instruction, or decline.
56
+ *
57
+ * Caps matter more than they look. An agent handed sixty findings across forty
58
+ * files will produce a pull request nobody reviews, which is the same as no
59
+ * pull request — except it also burned tokens and someone's afternoon.
60
+ */
61
+ export declare function buildFixPlan(issues: ReviewIssue[], options?: FixPlanOptions): FixPlan;
62
+ /** The instruction the agent receives: the findings, grouped by file. */
63
+ export declare function formatFixPrompt(issues: ReviewIssue[]): string;
64
+ /** A one-line summary for the pull request body the action opens. */
65
+ export declare function summariseFixPlan(plan: FixPlan): string;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Deciding what a CI agent may attempt to fix, and under what boundary.
3
+ *
4
+ * The reviewer already finds problems. This decides which of them are worth
5
+ * handing to an agent, caps how much it may take on, and pins the capabilities
6
+ * it runs with. It deliberately stops there: it produces a plan, never a
7
+ * commit. Staging, branching and opening a pull request belong to the action,
8
+ * which has the token — and keeping git out of the agent's reach is half the
9
+ * reason this is safe to run in CI at all.
10
+ *
11
+ * The boundary is the point. A fix run gets `files` (it must edit) and `tests`
12
+ * (it must check its own work) and nothing else. No shell, no git, no network.
13
+ * Enforced by the same machinery as any other custom bot, so an agent that
14
+ * decides it would like to curl something simply has no tool to do it with.
15
+ */
16
+ /** Severities an agent may act on, in descending confidence. */
17
+ const FIXABLE_SEVERITIES = ['error', 'warning'];
18
+ const DEFAULTS = { minSeverity: 'warning', maxIssues: 20, maxFiles: 10 };
19
+ /**
20
+ * `suggestion` and `info` are opinion — style preferences, "consider extracting
21
+ * this". Acting on them unasked produces churn in someone else's pull request
22
+ * and buries the findings that matter. Only what the reviewer states as a
23
+ * defect is eligible.
24
+ */
25
+ export function isFixable(issue, minSeverity) {
26
+ if (!FIXABLE_SEVERITIES.includes(issue.severity))
27
+ return false;
28
+ return minSeverity === 'warning' ? true : issue.severity === 'error';
29
+ }
30
+ /**
31
+ * The capability set a CI fix runs under.
32
+ *
33
+ * Not a suggestion in the prompt — a real `custom-bot/v1` personality, enforced
34
+ * by `isPersonalityToolCallAllowed` and by the tool registry filter, exactly as
35
+ * a bot built in Agent Studio would be. An agent running unattended against
36
+ * someone else's repository is precisely where a boundary has to be real.
37
+ */
38
+ export function ciFixPersonality() {
39
+ return {
40
+ name: 'ci-fix',
41
+ displayName: 'CI Fix',
42
+ description: 'Applies review findings in CI. Files and tests only.',
43
+ prompt: [
44
+ 'You are fixing problems a reviewer already found in a pull request.',
45
+ '',
46
+ 'Rules:',
47
+ '- Fix only the issues listed. Do not refactor around them.',
48
+ '- Do not reformat untouched lines; the diff should read as a fix, not a rewrite.',
49
+ '- Run the project tests when you are done and fix what you broke.',
50
+ '- If an issue needs a judgement call you cannot make from the code, leave it and say so.',
51
+ ].join('\n'),
52
+ scope: 'project',
53
+ structured: true,
54
+ schemaValid: true,
55
+ modelPreference: 'automatic',
56
+ restrictTools: true,
57
+ tools: ['files', 'tests'],
58
+ declaredTools: ['files', 'tests'],
59
+ projectScope: 'all',
60
+ };
61
+ }
62
+ /**
63
+ * Turn a review into a bounded instruction, or decline.
64
+ *
65
+ * Caps matter more than they look. An agent handed sixty findings across forty
66
+ * files will produce a pull request nobody reviews, which is the same as no
67
+ * pull request — except it also burned tokens and someone's afternoon.
68
+ */
69
+ export function buildFixPlan(issues, options = {}) {
70
+ const { minSeverity, maxIssues, maxFiles } = { ...DEFAULTS, ...options };
71
+ const personality = ciFixPersonality();
72
+ if (issues.length === 0) {
73
+ return { issues: [], files: [], skipped: 'no-issues', prompt: '', personality };
74
+ }
75
+ const eligible = issues
76
+ .filter(issue => isFixable(issue, minSeverity))
77
+ // Errors before warnings, then by file so one file's issues arrive together.
78
+ .sort((a, b) => {
79
+ if (a.severity !== b.severity)
80
+ return a.severity === 'error' ? -1 : 1;
81
+ return a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0);
82
+ });
83
+ if (eligible.length === 0) {
84
+ return { issues: [], files: [], skipped: 'nothing-fixable', prompt: '', personality };
85
+ }
86
+ // Take whole files rather than cutting a file's issues in half — a partial
87
+ // fix to one file is the worst outcome available, since it looks addressed.
88
+ const files = [];
89
+ const taken = [];
90
+ for (const issue of eligible) {
91
+ const knownFile = files.includes(issue.file);
92
+ if (!knownFile && files.length >= maxFiles)
93
+ continue;
94
+ if (taken.length >= maxIssues && !knownFile)
95
+ continue;
96
+ if (!knownFile)
97
+ files.push(issue.file);
98
+ taken.push(issue);
99
+ }
100
+ return { issues: taken, files, prompt: formatFixPrompt(taken), personality };
101
+ }
102
+ /** The instruction the agent receives: the findings, grouped by file. */
103
+ export function formatFixPrompt(issues) {
104
+ const byFile = new Map();
105
+ for (const issue of issues) {
106
+ const list = byFile.get(issue.file) ?? [];
107
+ list.push(issue);
108
+ byFile.set(issue.file, list);
109
+ }
110
+ const lines = [
111
+ `Fix the following ${issues.length} review finding${issues.length === 1 ? '' : 's'}.`,
112
+ '',
113
+ ];
114
+ for (const [file, found] of byFile) {
115
+ lines.push(`## ${file}`);
116
+ for (const issue of found) {
117
+ const where = issue.line ? `line ${issue.line}` : 'file';
118
+ lines.push(`- [${issue.severity}] ${where}: ${issue.message}`);
119
+ if (issue.suggestion)
120
+ lines.push(` suggested: ${issue.suggestion}`);
121
+ }
122
+ lines.push('');
123
+ }
124
+ lines.push('Change nothing outside these files.');
125
+ return lines.join('\n');
126
+ }
127
+ /** A one-line summary for the pull request body the action opens. */
128
+ export function summariseFixPlan(plan) {
129
+ if (plan.skipped === 'no-issues')
130
+ return 'The review found nothing.';
131
+ if (plan.skipped === 'nothing-fixable') {
132
+ return 'The review found only suggestions, which are left for a human to weigh.';
133
+ }
134
+ const errors = plan.issues.filter(i => i.severity === 'error').length;
135
+ const warnings = plan.issues.length - errors;
136
+ const parts = [
137
+ errors ? `${errors} error${errors === 1 ? '' : 's'}` : '',
138
+ warnings ? `${warnings} warning${warnings === 1 ? '' : 's'}` : '',
139
+ ].filter(Boolean);
140
+ return `Attempting ${parts.join(' and ')} across ${plan.files.length} file${plan.files.length === 1 ? '' : 's'}.`;
141
+ }
@@ -52,12 +52,10 @@ export function parseFrontmatter(raw) {
52
52
  return { meta: {}, body: normalised };
53
53
  const meta = {};
54
54
  const lines = match[1].split('\n');
55
- let currentKey = null;
56
55
  let currentList = null;
57
56
  for (const rawLine of lines) {
58
57
  const line = rawLine.replace(/\s+$/, '');
59
58
  if (!line.trim()) {
60
- currentKey = null;
61
59
  currentList = null;
62
60
  continue;
63
61
  }
@@ -75,7 +73,6 @@ export function parseFrontmatter(raw) {
75
73
  let value = kv[2];
76
74
  if (value === '') {
77
75
  // Empty → expecting a block list below
78
- currentKey = key;
79
76
  currentList = [];
80
77
  meta[key] = currentList;
81
78
  continue;
@@ -89,7 +86,6 @@ export function parseFrontmatter(raw) {
89
86
  value = stripQuotes(value);
90
87
  }
91
88
  meta[key] = value;
92
- currentKey = null;
93
89
  currentList = null;
94
90
  }
95
91
  return { meta, body: match[2].trimStart() };
@@ -9,24 +9,6 @@ import { logger } from './logger.js';
9
9
  const MAX_CONTEXT_SIZE = 50000;
10
10
  const MAX_FILES = 15;
11
11
  // File extensions we care about
12
- const CODE_EXTENSIONS = new Set([
13
- '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
14
- '.py', '.pyw',
15
- '.go',
16
- '.rs',
17
- '.php', '.phtml',
18
- '.java', '.kt', '.scala',
19
- '.cs', '.fs',
20
- '.rb',
21
- '.swift',
22
- '.c', '.cpp', '.h', '.hpp',
23
- '.vue', '.svelte',
24
- '.css', '.scss', '.less',
25
- '.html', '.htm',
26
- '.json', '.yaml', '.yml', '.toml',
27
- '.sql',
28
- '.md',
29
- ]);
30
12
  /**
31
13
  * Extract imports/requires from file content
32
14
  */
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "2.20.0";
1
+ export declare const VERSION = "2.22.0";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED by scripts/gen-version.js — do not edit by hand.
2
2
  // Baked from package.json at build time so the bun-compiled binary reports
3
3
  // the right version (it has no package.json on disk to read at runtime).
4
- export const VERSION = '2.20.0';
4
+ export const VERSION = '2.22.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "2.20.0",
3
+ "version": "2.22.0",
4
4
  "description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,7 +42,7 @@
42
42
  "@napi-rs/keyring": "^1.3.0",
43
43
  "clipboardy": "^4.0.0",
44
44
  "conf": "^13.1.0",
45
- "js-yaml": "^4.1.0",
45
+ "js-yaml": "^4.3.1",
46
46
  "open": "^10.0.0"
47
47
  },
48
48
  "devDependencies": {
@@ -1,24 +0,0 @@
1
- /**
2
- * Permission screen for granting folder access
3
- */
4
- import { Screen } from '../Screen';
5
- export type PermissionLevel = 'none' | 'read' | 'write';
6
- export interface PermissionOptions {
7
- projectPath: string;
8
- isProject: boolean;
9
- currentPermission: PermissionLevel;
10
- onSelect: (permission: PermissionLevel) => void;
11
- onCancel: () => void;
12
- }
13
- /**
14
- * Render permission screen
15
- */
16
- export declare function renderPermissionScreen(screen: Screen, options: PermissionOptions, selectedIndex: number): void;
17
- /**
18
- * Get permission options array for easy indexing
19
- */
20
- export declare function getPermissionOptions(): PermissionLevel[];
21
- /**
22
- * Truncate path for display
23
- */
24
- export declare function truncatePath(path: string, maxLen: number): string;