runtrim 0.1.15 → 0.1.17
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/README.md +38 -205
- package/dist-cli/runtrim.cjs +175 -30
- package/dist-cli/runtrim.js +175 -30
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -1,243 +1,76 @@
|
|
|
1
1
|
# RunTrim
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
RunTrim is the control layer for AI coding agents.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It gives Claude, Codex, Cursor, ChatGPT, and other agents project memory, scoped contracts, MCP guidance, approval flow, and finish verification before changes are accepted.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
RunTrim is a CLI for developers using tools like Claude Code, Codex, Cursor, and ChatGPT. It sits in front of your coding run and helps you avoid risky, oversized, or context-wasting prompts.
|
|
10
|
-
|
|
11
|
-
## Why RunTrim exists
|
|
12
|
-
|
|
13
|
-
AI coding runs often fail for operational reasons, not model quality:
|
|
14
|
-
- tasks are too broad
|
|
15
|
-
- sensitive surfaces are touched by accident
|
|
16
|
-
- context is lost between sessions
|
|
17
|
-
- teams repeat failed attempts without a reliable run log
|
|
18
|
-
|
|
19
|
-
RunTrim adds structure before the run and continuity after the run.
|
|
20
|
-
|
|
21
|
-
## Daily loop
|
|
22
|
-
|
|
23
|
-
Recommended daily flow:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
runtrim go "your task"
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
RunTrim prepares a guarded prompt, copies it for your agent, records the run locally, and prints the next steps.
|
|
30
|
-
Paste the guarded prompt into your agent.
|
|
31
|
-
Keep the local panel open:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
runtrim panel --monitor
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
After edits:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
runtrim check
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
If context or usage runs out:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
runtrim continue --reason usage_limit
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Guided menu when unsure:
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
runtrim start
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
`runtrim start` checks repo state and tells you the next safe command.
|
|
56
|
-
Free includes 1 tracked local repo.
|
|
57
|
-
|
|
58
|
-
Direct operator flow:
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
runtrim go "fix checkout redirect"
|
|
62
|
-
runtrim panel --monitor
|
|
63
|
-
runtrim check
|
|
64
|
-
runtrim continue --reason usage_limit
|
|
65
|
-
runtrim memory
|
|
66
|
-
```
|
|
7
|
+
Website: https://www.runtrim.com
|
|
67
8
|
|
|
68
9
|
## Install
|
|
69
10
|
|
|
70
|
-
Global install for end users:
|
|
71
|
-
|
|
72
11
|
```bash
|
|
73
12
|
npm install -g runtrim
|
|
74
|
-
runtrim go "your task"
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Local preview for repository development:
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
git clone https://github.com/michelpronkk-oss/rtrim
|
|
81
|
-
cd rtrim
|
|
82
|
-
npm install
|
|
83
|
-
npm run runtrim -- init
|
|
84
|
-
npm run runtrim -- start
|
|
85
13
|
```
|
|
86
14
|
|
|
87
|
-
##
|
|
88
|
-
|
|
89
|
-
Before publishing to npm:
|
|
90
|
-
|
|
91
|
-
1. `npm run build`
|
|
92
|
-
2. `npm run verify:cli`
|
|
93
|
-
3. `npm run verify:package`
|
|
94
|
-
4. `npm link`
|
|
95
|
-
5. `runtrim agent --help`
|
|
96
|
-
6. `runtrim go "test task" --no-sync`
|
|
97
|
-
7. `npm version patch`
|
|
98
|
-
8. `npm publish`
|
|
99
|
-
|
|
100
|
-
## What RunTrim does
|
|
101
|
-
|
|
102
|
-
- audits task scope before execution
|
|
103
|
-
- blocks unsafe mega-runs and suggests split-safe follow-ups
|
|
104
|
-
- generates guarded prompts with explicit stop rules
|
|
105
|
-
- monitors changed files during execution
|
|
106
|
-
- reviews changed files, risk flags, verification debt, and next safe action after edits
|
|
107
|
-
- shows current project memory, latest run state, and continuation guidance in `.runtrim/`
|
|
108
|
-
|
|
109
|
-
## Core commands
|
|
15
|
+
## Quickstart
|
|
110
16
|
|
|
111
17
|
```bash
|
|
112
|
-
|
|
113
|
-
runtrim go "<task>"
|
|
18
|
+
cd your-project
|
|
114
19
|
runtrim start
|
|
115
|
-
runtrim
|
|
116
|
-
runtrim
|
|
117
|
-
runtrim panel --monitor
|
|
118
|
-
runtrim check
|
|
119
|
-
runtrim continue --reason usage_limit
|
|
120
|
-
runtrim memory
|
|
121
|
-
runtrim sync
|
|
20
|
+
runtrim agent "Fix the homepage copy" --copy
|
|
21
|
+
runtrim finish
|
|
122
22
|
```
|
|
123
23
|
|
|
124
|
-
|
|
24
|
+
## Primary flow
|
|
125
25
|
|
|
126
|
-
|
|
127
|
-
runtrim
|
|
128
|
-
|
|
129
|
-
runtrim
|
|
130
|
-
runtrim panel --monitor
|
|
131
|
-
runtrim check
|
|
132
|
-
runtrim continue --reason usage_limit
|
|
133
|
-
```
|
|
26
|
+
1. `runtrim start` analyzes the project and prepares local RunTrim memory and instructions.
|
|
27
|
+
2. `runtrim agent "task" --copy` creates a guarded run and handoff prompt for your coding agent.
|
|
28
|
+
3. Agent completes the task inside contract scope.
|
|
29
|
+
4. `runtrim finish` verifies scope and sensitive-file safety with a clear verdict: `PASS`, `WARN`, or `BLOCKED`.
|
|
134
30
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
Guarded prepare flow:
|
|
31
|
+
If scope needs to expand safely:
|
|
138
32
|
|
|
139
33
|
```bash
|
|
140
|
-
runtrim
|
|
34
|
+
runtrim approve "Allow <path or scope> for this run only"
|
|
141
35
|
```
|
|
142
36
|
|
|
143
|
-
|
|
37
|
+
## MCP (optional)
|
|
144
38
|
|
|
145
39
|
```bash
|
|
146
|
-
runtrim
|
|
40
|
+
runtrim mcp instructions
|
|
41
|
+
runtrim mcp config --print
|
|
42
|
+
runtrim mcp start
|
|
147
43
|
```
|
|
148
44
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Use `runtrim panel` to open a local browser panel on localhost.
|
|
152
|
-
Use `runtrim panel --monitor` to open the same local panel with live git change monitoring.
|
|
45
|
+
MCP lets compatible agents use RunTrim tools like contract creation, path checks, approval suggestions, and finish guidance.
|
|
153
46
|
|
|
154
|
-
|
|
155
|
-
Quick keys in panel:
|
|
156
|
-
- `p` prepare
|
|
157
|
-
- `g` guard
|
|
158
|
-
- `c` check
|
|
159
|
-
- `m` memory
|
|
160
|
-
- `r` report
|
|
161
|
-
- `s` sync
|
|
162
|
-
- `q` quit
|
|
47
|
+
## Local-first trust model
|
|
163
48
|
|
|
164
|
-
|
|
49
|
+
- Free CLI runs locally.
|
|
50
|
+
- Source code stays local by default.
|
|
51
|
+
- RunTrim does not read env file contents.
|
|
52
|
+
- Ignored `.env.local` is warned and reported, not read.
|
|
53
|
+
- Sensitive tracked/changed or unignored sensitive files still block finish.
|
|
165
54
|
|
|
166
|
-
|
|
55
|
+
## Plans and sync
|
|
167
56
|
|
|
168
|
-
|
|
57
|
+
- Free: local control flow and local history.
|
|
58
|
+
- Pro+: cloud sync and hosted dashboard history.
|
|
169
59
|
|
|
170
|
-
##
|
|
171
|
-
|
|
172
|
-
When a run stops due to usage or context limits:
|
|
60
|
+
## Core commands
|
|
173
61
|
|
|
174
62
|
```bash
|
|
175
|
-
runtrim
|
|
63
|
+
runtrim start
|
|
64
|
+
runtrim agent "Your task" --copy
|
|
65
|
+
runtrim finish
|
|
66
|
+
runtrim approve "Allow <scope> for this run only"
|
|
67
|
+
runtrim status
|
|
68
|
+
runtrim mcp instructions
|
|
69
|
+
runtrim bridge status
|
|
176
70
|
```
|
|
177
71
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
## Project memory
|
|
181
|
-
|
|
182
|
-
`runtrim memory` shows where the project currently stands:
|
|
183
|
-
- latest task and run status
|
|
184
|
-
- changed files
|
|
185
|
-
- missing proof
|
|
186
|
-
- protected areas
|
|
187
|
-
- next safe action
|
|
188
|
-
|
|
189
|
-
## Sync V0 private beta
|
|
190
|
-
|
|
191
|
-
Cloud sync is private beta and metadata-only.
|
|
192
|
-
|
|
193
|
-
Sync can upload:
|
|
194
|
-
- project name and status
|
|
195
|
-
- run status and risk metadata
|
|
196
|
-
- RunTrim-generated prompts
|
|
197
|
-
- changed file paths
|
|
198
|
-
- project memory summaries
|
|
199
|
-
- timestamps and estimated savings
|
|
200
|
-
|
|
201
|
-
Sync does not intentionally upload:
|
|
202
|
-
- source code
|
|
203
|
-
- `.env` values
|
|
204
|
-
- secret file contents
|
|
72
|
+
Advanced/lower-level command (still supported):
|
|
205
73
|
|
|
206
74
|
```bash
|
|
207
|
-
runtrim
|
|
208
|
-
runtrim config set dashboard-url https://www.runtrim.com/app
|
|
209
|
-
runtrim sync
|
|
75
|
+
runtrim go "Your task"
|
|
210
76
|
```
|
|
211
|
-
|
|
212
|
-
## Privacy model
|
|
213
|
-
|
|
214
|
-
RunTrim Free runs locally and stores state in `.runtrim`.
|
|
215
|
-
Free includes 1 tracked local repo. Builder early access supports unlimited tracked repos.
|
|
216
|
-
A tracked repo is one codebase with its own `.runtrim` workspace.
|
|
217
|
-
|
|
218
|
-
V1 is designed so source code is not uploaded. Cloud sync stores metadata only when enabled.
|
|
219
|
-
|
|
220
|
-
See:
|
|
221
|
-
- https://www.runtrim.com/privacy
|
|
222
|
-
- https://www.runtrim.com/security
|
|
223
|
-
|
|
224
|
-
## Status
|
|
225
|
-
|
|
226
|
-
RunTrim is in early V1.
|
|
227
|
-
|
|
228
|
-
Free local CLI is available. Cloud sync and hosted dashboard access are private beta.
|
|
229
|
-
|
|
230
|
-
## Roadmap
|
|
231
|
-
|
|
232
|
-
- npm package launch hardening
|
|
233
|
-
- stronger local policy presets
|
|
234
|
-
- richer post-run verification workflows
|
|
235
|
-
- expanded cloud memory rollout
|
|
236
|
-
|
|
237
|
-
## Packaging
|
|
238
|
-
|
|
239
|
-
```bash
|
|
240
|
-
npm run build
|
|
241
|
-
npm run build:cli
|
|
242
|
-
npm pack --dry-run
|
|
243
|
-
```
|
package/dist-cli/runtrim.cjs
CHANGED
|
@@ -169,6 +169,22 @@ var ENV_FILE_RE = /(?:^|[\s"'`,(])(\.[.]?env(?:\.[a-zA-Z\d]+)?)\b/g;
|
|
|
169
169
|
var ONLY_EDIT_RE = /\bonly\s+(?:edit|touch|modify|change|update|fix)\b/i;
|
|
170
170
|
var MUST_INCLUDE_RE = /\ballowed\s+scope\s+(?:must\s+)?include\b|\bmust\s+(?:include|contain)\b/i;
|
|
171
171
|
var CLI_SCOPE_RE = /\b(cli|command routing|runtrim command|run compiler|contract generation|scope inference|preview command|agent preview command|agent apply|adapters?|auto-guard|bridge helpers?|daemon|local server|localhost|\.runtrim(?:\s+artifacts?)?)\b/i;
|
|
172
|
+
var NEGATION_PREFIX_RE = /\b(do not|don't|dont|never|avoid|must not|should not|without changing|without touching|no changes to|keep .* untouched|leave .* untouched|keep .* unchanged)\b/i;
|
|
173
|
+
function hasNegationNear(text, index) {
|
|
174
|
+
const start = Math.max(0, index - 64);
|
|
175
|
+
const window = text.slice(start, index + 8);
|
|
176
|
+
return NEGATION_PREFIX_RE.test(window);
|
|
177
|
+
}
|
|
178
|
+
function hasPositiveKeywordMention(task, keyword) {
|
|
179
|
+
const lowerTask = task.toLowerCase();
|
|
180
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
181
|
+
let idx = lowerTask.indexOf(lowerKeyword);
|
|
182
|
+
while (idx !== -1) {
|
|
183
|
+
if (!hasNegationNear(lowerTask, idx)) return true;
|
|
184
|
+
idx = lowerTask.indexOf(lowerKeyword, idx + lowerKeyword.length);
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
172
188
|
function extractScopePhrase(task, re) {
|
|
173
189
|
var _a2, _b;
|
|
174
190
|
const m = task.match(re);
|
|
@@ -226,18 +242,34 @@ function buildExplicitAllowedScope(task, explicitPaths) {
|
|
|
226
242
|
}
|
|
227
243
|
function buildExplicitForbiddenScope(task) {
|
|
228
244
|
const out = [];
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
245
|
+
const addBoundaryList = (raw) => {
|
|
246
|
+
if (!raw) return;
|
|
247
|
+
const normalized = raw.replace(/\b(logic|internals?|behavior|files?|systems?)\b/gi, "").replace(/\s+/g, " ").trim();
|
|
248
|
+
const parts = normalized.split(/\s*(?:,|;|\band\b|\bor\b)\s*/i).map((p) => p.trim().replace(/[.]+$/, "")).filter(Boolean).slice(0, 12);
|
|
249
|
+
for (const p of parts) {
|
|
250
|
+
if (p.length < 2) continue;
|
|
251
|
+
out.push(`Do not touch ${p}`);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const explicitPhrases = [
|
|
255
|
+
/\bforbidden\s+scope\s+must\s+include\s+([^\n.]+)/i,
|
|
256
|
+
/\bdo\s+not\s+touch\s+([^\n.]+)/i,
|
|
257
|
+
/\bdo\s+not\s+edit\s+([^\n.]+)/i,
|
|
258
|
+
/\bdo\s+not\s+change\s+([^\n.]+)/i,
|
|
259
|
+
/\bmust\s+not\s+touch\s+([^\n.]+)/i,
|
|
260
|
+
/\bshould\s+not\s+touch\s+([^\n.]+)/i,
|
|
261
|
+
/\bwithout\s+changing\s+([^\n.]+)/i,
|
|
262
|
+
/\bwithout\s+touching\s+([^\n.]+)/i,
|
|
263
|
+
/\bno\s+changes\s+to\s+([^\n.]+)/i,
|
|
264
|
+
/\bkeep\s+([^\n.]+?)\s+(?:untouched|unchanged)\b/i,
|
|
265
|
+
/\bleave\s+([^\n.]+?)\s+untouched\b/i,
|
|
266
|
+
/\bavoid\s+changing\s+([^\n.]+)/i,
|
|
267
|
+
/\bexclude\s+([^\n.]+)/i,
|
|
268
|
+
/\bforbidden\s+([^\n.]+)/i
|
|
269
|
+
];
|
|
270
|
+
for (const re of explicitPhrases) {
|
|
271
|
+
addBoundaryList(extractScopePhrase(task, re));
|
|
272
|
+
}
|
|
241
273
|
return [...new Set(out)];
|
|
242
274
|
}
|
|
243
275
|
function extractExplicitPaths(task) {
|
|
@@ -469,11 +501,10 @@ var CATEGORY_KEYWORDS = [
|
|
|
469
501
|
];
|
|
470
502
|
function classifyTaskCategory(task, explicitPaths) {
|
|
471
503
|
const lower = task.toLowerCase();
|
|
472
|
-
if (CLI_SCOPE_RE.test(task)) return "cli";
|
|
473
504
|
const pathHints = explicitPaths.join(" ").toLowerCase();
|
|
474
505
|
for (const [category, keywords] of CATEGORY_KEYWORDS) {
|
|
475
506
|
const combined = lower + " " + pathHints;
|
|
476
|
-
if (keywords.some((kw) => combined
|
|
507
|
+
if (keywords.some((kw) => hasPositiveKeywordMention(combined, kw))) {
|
|
477
508
|
return category;
|
|
478
509
|
}
|
|
479
510
|
}
|
|
@@ -716,6 +747,28 @@ var LOOP_PATTERNS = [
|
|
|
716
747
|
/\b(keep (trying|going|working)|iterate until|loop until|retry)\b/i,
|
|
717
748
|
/\b(if it doesn.t work.{0,20}try again)\b/i
|
|
718
749
|
];
|
|
750
|
+
var NEGATION_PREFIX_RE2 = /\b(do not|don't|dont|never|avoid|must not|should not|without changing|without touching|no changes to|keep .* untouched|leave .* untouched|keep .* unchanged)\b/i;
|
|
751
|
+
function hasNegationNear2(text, index) {
|
|
752
|
+
const start = Math.max(0, index - 64);
|
|
753
|
+
const window = text.slice(start, index + 8);
|
|
754
|
+
return NEGATION_PREFIX_RE2.test(window);
|
|
755
|
+
}
|
|
756
|
+
function hasPositiveKeywordMention2(taskLower, keyword) {
|
|
757
|
+
let idx = taskLower.indexOf(keyword.toLowerCase());
|
|
758
|
+
while (idx !== -1) {
|
|
759
|
+
if (!hasNegationNear2(taskLower, idx)) return true;
|
|
760
|
+
idx = taskLower.indexOf(keyword.toLowerCase(), idx + keyword.length);
|
|
761
|
+
}
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
function hasNegatedKeywordMention(taskLower, keyword) {
|
|
765
|
+
let idx = taskLower.indexOf(keyword.toLowerCase());
|
|
766
|
+
while (idx !== -1) {
|
|
767
|
+
if (hasNegationNear2(taskLower, idx)) return true;
|
|
768
|
+
idx = taskLower.indexOf(keyword.toLowerCase(), idx + keyword.length);
|
|
769
|
+
}
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
719
772
|
function scoreTask(task, flags) {
|
|
720
773
|
let score = 100;
|
|
721
774
|
for (const flag of flags) {
|
|
@@ -776,7 +829,7 @@ function detectProjectContext(cwd = process.cwd()) {
|
|
|
776
829
|
function detectMegaRun(taskLower, task) {
|
|
777
830
|
const found = [];
|
|
778
831
|
for (const [system, keywords] of Object.entries(MEGA_RUN_SYSTEMS)) {
|
|
779
|
-
if (keywords.some((kw) => taskLower
|
|
832
|
+
if (keywords.some((kw) => hasPositiveKeywordMention2(taskLower, kw))) {
|
|
780
833
|
found.push(system);
|
|
781
834
|
}
|
|
782
835
|
}
|
|
@@ -787,17 +840,24 @@ function detectMegaRun(taskLower, task) {
|
|
|
787
840
|
function detectAreasTouched(taskLower) {
|
|
788
841
|
const forbidden = [];
|
|
789
842
|
const sensitive = [];
|
|
843
|
+
const boundaries = [];
|
|
790
844
|
for (const [area, keywords] of Object.entries(ALWAYS_FORBIDDEN_KEYWORDS)) {
|
|
791
|
-
if (keywords.some((kw) => taskLower
|
|
845
|
+
if (keywords.some((kw) => hasPositiveKeywordMention2(taskLower, kw))) {
|
|
792
846
|
forbidden.push(area);
|
|
793
847
|
}
|
|
848
|
+
if (keywords.some((kw) => hasNegatedKeywordMention(taskLower, kw))) {
|
|
849
|
+
boundaries.push(area);
|
|
850
|
+
}
|
|
794
851
|
}
|
|
795
852
|
for (const [area, keywords] of Object.entries(SENSITIVE_BILLING_KEYWORDS)) {
|
|
796
|
-
if (keywords.some((kw) => taskLower
|
|
853
|
+
if (keywords.some((kw) => hasPositiveKeywordMention2(taskLower, kw))) {
|
|
797
854
|
sensitive.push(area);
|
|
798
855
|
}
|
|
856
|
+
if (keywords.some((kw) => hasNegatedKeywordMention(taskLower, kw))) {
|
|
857
|
+
boundaries.push(area);
|
|
858
|
+
}
|
|
799
859
|
}
|
|
800
|
-
return { forbidden, sensitive };
|
|
860
|
+
return { forbidden, sensitive, boundaries: [...new Set(boundaries)] };
|
|
801
861
|
}
|
|
802
862
|
function auditTask(task, config, cwd = process.cwd()) {
|
|
803
863
|
const flags = [];
|
|
@@ -887,7 +947,7 @@ function auditTask(task, config, cwd = process.cwd()) {
|
|
|
887
947
|
detail: "References to full context or entire conversation force expensive context loading."
|
|
888
948
|
});
|
|
889
949
|
}
|
|
890
|
-
const { forbidden: forbiddenAreasTouched, sensitive: sensitiveAreasRelevant } = detectAreasTouched(taskLower);
|
|
950
|
+
const { forbidden: forbiddenAreasTouched, sensitive: sensitiveAreasRelevant, boundaries } = detectAreasTouched(taskLower);
|
|
891
951
|
if (forbiddenAreasTouched.length > 0) {
|
|
892
952
|
flags.push({
|
|
893
953
|
code: "touches_forbidden_area",
|
|
@@ -904,6 +964,14 @@ function auditTask(task, config, cwd = process.cwd()) {
|
|
|
904
964
|
detail: `Task touches ${sensitiveAreasRelevant.join(", ")}. These are moved to SENSITIVE SCOPE: inspect allowed, editing requires explicit approval.`
|
|
905
965
|
});
|
|
906
966
|
}
|
|
967
|
+
if (boundaries.length > 0) {
|
|
968
|
+
flags.push({
|
|
969
|
+
code: "forbidden_boundaries_detected",
|
|
970
|
+
label: `Boundaries detected: ${boundaries.join(", ")}`,
|
|
971
|
+
severity: "info",
|
|
972
|
+
detail: "Sensitive systems in negated constraints are treated as forbidden boundaries, not active task scope."
|
|
973
|
+
});
|
|
974
|
+
}
|
|
907
975
|
const isSimpleTask = task.length < 80 && flags.filter((f) => f.severity === "critical").length === 0;
|
|
908
976
|
if (isSimpleTask && config.defaultModel === "opus") {
|
|
909
977
|
flags.push({
|
|
@@ -981,6 +1049,10 @@ function scoreToRisk2(score) {
|
|
|
981
1049
|
}
|
|
982
1050
|
function cleanObjective(task) {
|
|
983
1051
|
let t = task.trim();
|
|
1052
|
+
t = t.replace(
|
|
1053
|
+
/(?:^|[\s,.])(do not|don't|dont|must not|should not|without changing|without touching|no changes to|keep .*? unchanged|keep .*? untouched|leave .*? untouched|avoid changing)\b[^.]*\.?/gi,
|
|
1054
|
+
" "
|
|
1055
|
+
);
|
|
984
1056
|
t = t.replace(/,?\s*check everything(\s+and\b)?/gi, "");
|
|
985
1057
|
t = t.replace(/,?\s*(look|search|scan)\s+everywhere(\s+and\b)?/gi, "");
|
|
986
1058
|
t = t.replace(/,?\s*review everything(\s+and\b)?/gi, "");
|
|
@@ -4661,21 +4733,21 @@ var MEDIUM_PATH_PATTERNS = [
|
|
|
4661
4733
|
];
|
|
4662
4734
|
function classifyFileRisk(files) {
|
|
4663
4735
|
if (files.length === 0) return "low";
|
|
4664
|
-
let
|
|
4736
|
+
let maxRisk2 = "low";
|
|
4665
4737
|
for (const f of files) {
|
|
4666
4738
|
const norm = f.replace(/\\/g, "/").toLowerCase();
|
|
4667
4739
|
if (CRITICAL_PATH_PATTERNS.some((p) => norm.includes(p))) {
|
|
4668
4740
|
return "critical";
|
|
4669
4741
|
}
|
|
4670
|
-
if (HIGH_PATH_PATTERNS.some((p) => norm.includes(p)) &&
|
|
4671
|
-
|
|
4742
|
+
if (HIGH_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk2 !== "high") {
|
|
4743
|
+
maxRisk2 = "high";
|
|
4672
4744
|
continue;
|
|
4673
4745
|
}
|
|
4674
|
-
if (MEDIUM_PATH_PATTERNS.some((p) => norm.includes(p)) &&
|
|
4675
|
-
|
|
4746
|
+
if (MEDIUM_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk2 === "low") {
|
|
4747
|
+
maxRisk2 = "medium";
|
|
4676
4748
|
}
|
|
4677
4749
|
}
|
|
4678
|
-
return
|
|
4750
|
+
return maxRisk2;
|
|
4679
4751
|
}
|
|
4680
4752
|
function isSensitivePath(filePath) {
|
|
4681
4753
|
const norm = filePath.replace(/\\/g, "/").toLowerCase();
|
|
@@ -5018,6 +5090,24 @@ function getLearningContext(cwd, task, runs) {
|
|
|
5018
5090
|
// src/lib/run-planner.ts
|
|
5019
5091
|
var FAST_PATH_CATEGORIES = /* @__PURE__ */ new Set(["ui", "docs", "tests", "unknown"]);
|
|
5020
5092
|
var ALWAYS_CONTRACT_CATEGORIES = /* @__PURE__ */ new Set(["auth", "billing", "payment", "webhook", "database", "env", "middleware"]);
|
|
5093
|
+
var RISK_ORDER = ["low", "medium", "high", "critical"];
|
|
5094
|
+
var NEGATION_PREFIX_RE3 = /\b(do not|don't|dont|never|avoid|must not|should not|without changing|without touching|no changes to|keep .* untouched|leave .* untouched|keep .* unchanged)\b/i;
|
|
5095
|
+
function maxRisk(a, b) {
|
|
5096
|
+
return RISK_ORDER[Math.max(RISK_ORDER.indexOf(a), RISK_ORDER.indexOf(b))];
|
|
5097
|
+
}
|
|
5098
|
+
function hasNegationNear3(text, index) {
|
|
5099
|
+
const start = Math.max(0, index - 64);
|
|
5100
|
+
const window = text.slice(start, index + 8);
|
|
5101
|
+
return NEGATION_PREFIX_RE3.test(window);
|
|
5102
|
+
}
|
|
5103
|
+
function hasPositiveKeywordMention3(taskLower, keyword) {
|
|
5104
|
+
let idx = taskLower.indexOf(keyword.toLowerCase());
|
|
5105
|
+
while (idx !== -1) {
|
|
5106
|
+
if (!hasNegationNear3(taskLower, idx)) return true;
|
|
5107
|
+
idx = taskLower.indexOf(keyword.toLowerCase(), idx + keyword.length);
|
|
5108
|
+
}
|
|
5109
|
+
return false;
|
|
5110
|
+
}
|
|
5021
5111
|
function isFastPathEligible(risk, category, guardMode, hasExplicitPaths) {
|
|
5022
5112
|
if (guardMode === "strict") return false;
|
|
5023
5113
|
if (guardMode === "off") return true;
|
|
@@ -5033,8 +5123,16 @@ function generatePlan(cwd, task, runs, config, currentChangedFiles) {
|
|
|
5033
5123
|
const compiler = compileTask(task);
|
|
5034
5124
|
const guardMode = (_a2 = config.autoGuardMode) != null ? _a2 : "smart";
|
|
5035
5125
|
const adapters = detectAdapters(cwd);
|
|
5036
|
-
const riskFiles = compiler.explicitPaths.length > 0 ? compiler.explicitPaths :
|
|
5037
|
-
|
|
5126
|
+
const riskFiles = compiler.explicitPaths.length > 0 ? compiler.explicitPaths : [];
|
|
5127
|
+
let rawRisk = classifyFileRisk(riskFiles);
|
|
5128
|
+
if (ALWAYS_CONTRACT_CATEGORIES.has(compiler.taskCategory)) {
|
|
5129
|
+
rawRisk = maxRisk(rawRisk, "high");
|
|
5130
|
+
}
|
|
5131
|
+
const lowerTask = task.toLowerCase();
|
|
5132
|
+
const criticalSystemMentions = ["auth", "billing", "payment", "webhook", "database", "migration", "middleware"].filter((k) => hasPositiveKeywordMention3(lowerTask, k)).length;
|
|
5133
|
+
if (criticalSystemMentions >= 2) {
|
|
5134
|
+
rawRisk = maxRisk(rawRisk, "critical");
|
|
5135
|
+
}
|
|
5038
5136
|
const catScope = buildCategoryScope(
|
|
5039
5137
|
compiler.taskCategory,
|
|
5040
5138
|
true,
|
|
@@ -5158,6 +5256,22 @@ var FAST_LOCAL_KEYWORDS = [
|
|
|
5158
5256
|
"responsive",
|
|
5159
5257
|
"ui polish"
|
|
5160
5258
|
];
|
|
5259
|
+
var NEGATION_PREFIX_RE4 = /\b(do not|don't|dont|never|avoid|must not|should not|without changing|without touching|no changes to|keep .* untouched|leave .* untouched|keep .* unchanged)\b/i;
|
|
5260
|
+
function hasNegationNear4(text, index) {
|
|
5261
|
+
const start = Math.max(0, index - 64);
|
|
5262
|
+
const window = text.slice(start, index + 8);
|
|
5263
|
+
return NEGATION_PREFIX_RE4.test(window);
|
|
5264
|
+
}
|
|
5265
|
+
function hasPositiveKeywordMention4(task, keyword) {
|
|
5266
|
+
const lowerTask = task.toLowerCase();
|
|
5267
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
5268
|
+
let idx = lowerTask.indexOf(lowerKeyword);
|
|
5269
|
+
while (idx !== -1) {
|
|
5270
|
+
if (!hasNegationNear4(lowerTask, idx)) return true;
|
|
5271
|
+
idx = lowerTask.indexOf(lowerKeyword, idx + lowerKeyword.length);
|
|
5272
|
+
}
|
|
5273
|
+
return false;
|
|
5274
|
+
}
|
|
5161
5275
|
function normalize(s) {
|
|
5162
5276
|
return (s != null ? s : "").toLowerCase();
|
|
5163
5277
|
}
|
|
@@ -5216,9 +5330,9 @@ function recommendProviderRouting(ctx) {
|
|
|
5216
5330
|
const changedFiles = (_d = ctx.changedFiles) != null ? _d : [];
|
|
5217
5331
|
const learnedContext = (_e = ctx.learnedContext) != null ? _e : [];
|
|
5218
5332
|
const hasProofGapSignals = proofRequired.some((p) => /proof gap|missing|vercel log|manual verification/i.test(p));
|
|
5219
|
-
const highRiskByKeyword = HIGH_RISK_KEYWORDS.some((k) => task
|
|
5333
|
+
const highRiskByKeyword = HIGH_RISK_KEYWORDS.some((k) => hasPositiveKeywordMention4(task, k) || hasPositiveKeywordMention4(category, k));
|
|
5220
5334
|
const fastKeyword = FAST_LOCAL_KEYWORDS.some((k) => task.includes(k));
|
|
5221
|
-
const multiCritical = ["auth", "billing", "database", "webhook", "payment", "migration"].filter((k) => task
|
|
5335
|
+
const multiCritical = ["auth", "billing", "database", "webhook", "payment", "migration"].filter((k) => hasPositiveKeywordMention4(task, k)).length >= 2;
|
|
5222
5336
|
const broadTask = !ctx.explicitScope && task.split(/\s+/).length > 16;
|
|
5223
5337
|
const hasSensitiveFiles = sensitiveAreas.length > 0 || changedFiles.some((f) => HIGH_RISK_KEYWORDS.some((k) => normalize(f).includes(k)));
|
|
5224
5338
|
const noLearning = learnedContext.length === 0 && ((_f = ctx.similarRunsCount) != null ? _f : 0) === 0;
|
|
@@ -5483,6 +5597,19 @@ async function getSensitivePathStates(cwd) {
|
|
|
5483
5597
|
return /* @__PURE__ */ new Map();
|
|
5484
5598
|
}
|
|
5485
5599
|
}
|
|
5600
|
+
function extractBoundaryLabels(forbiddenScope) {
|
|
5601
|
+
const labels = /* @__PURE__ */ new Set();
|
|
5602
|
+
for (const line of forbiddenScope) {
|
|
5603
|
+
const lower = line.toLowerCase();
|
|
5604
|
+
if (lower.includes("billing") || lower.includes("subscription") || lower.includes("payment")) labels.add("billing");
|
|
5605
|
+
if (lower.includes("auth") || lower.includes("session") || lower.includes("jwt")) labels.add("auth");
|
|
5606
|
+
if (lower.includes("middleware") || lower.includes("proxy")) labels.add("middleware");
|
|
5607
|
+
if (lower.includes(".env") || lower.includes("secret")) labels.add("env");
|
|
5608
|
+
if (lower.includes("cli")) labels.add("cli");
|
|
5609
|
+
if (lower.includes("mcp")) labels.add("mcp");
|
|
5610
|
+
}
|
|
5611
|
+
return [...labels];
|
|
5612
|
+
}
|
|
5486
5613
|
function nowId() {
|
|
5487
5614
|
const d = /* @__PURE__ */ new Date();
|
|
5488
5615
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -5544,8 +5671,21 @@ function buildApprovalLevel(risk, task, category) {
|
|
|
5544
5671
|
const text = `${task}
|
|
5545
5672
|
${category}`.toLowerCase();
|
|
5546
5673
|
const highSystems = ["auth", "billing", "payment", "dodo", "stripe", "webhook", "database", "migration", "rls", "middleware", "env", "secret"];
|
|
5674
|
+
const hasNegationNear5 = (source, index) => {
|
|
5675
|
+
const start = Math.max(0, index - 64);
|
|
5676
|
+
const window = source.slice(start, index + 8);
|
|
5677
|
+
return /\b(do not|don't|dont|never|avoid|must not|should not|without changing|without touching|no changes to|keep .* untouched|leave .* untouched|keep .* unchanged)\b/i.test(window);
|
|
5678
|
+
};
|
|
5679
|
+
const hasPositiveKeyword = (source, keyword) => {
|
|
5680
|
+
let idx = source.indexOf(keyword);
|
|
5681
|
+
while (idx !== -1) {
|
|
5682
|
+
if (!hasNegationNear5(source, idx)) return true;
|
|
5683
|
+
idx = source.indexOf(keyword, idx + keyword.length);
|
|
5684
|
+
}
|
|
5685
|
+
return false;
|
|
5686
|
+
};
|
|
5547
5687
|
if (risk === "high" || risk === "critical") return "required";
|
|
5548
|
-
if (highSystems.some((k) => text
|
|
5688
|
+
if (highSystems.some((k) => hasPositiveKeyword(text, k))) return "required";
|
|
5549
5689
|
if (risk === "medium") return "recommended";
|
|
5550
5690
|
return "no";
|
|
5551
5691
|
}
|
|
@@ -5586,6 +5726,7 @@ function writePreviewArtifacts(cwd, preview) {
|
|
|
5586
5726
|
"",
|
|
5587
5727
|
"Forbidden:",
|
|
5588
5728
|
...preview.forbiddenScope.length > 0 ? preview.forbiddenScope.slice(0, 8).map((f) => `- ${f}`) : ["- none"],
|
|
5729
|
+
...preview.boundariesDetected.length > 0 ? ["", `Boundaries detected: ${preview.boundariesDetected.join(", ")} will be forbidden, not treated as active scope.`] : [],
|
|
5589
5730
|
"",
|
|
5590
5731
|
"Learned context:",
|
|
5591
5732
|
...preview.learnedContext.length > 0 ? preview.learnedContext.map((x) => `- ${x}`) : ["- learning not available yet"],
|
|
@@ -5625,6 +5766,9 @@ async function runAgentPreview(task) {
|
|
|
5625
5766
|
console.log("");
|
|
5626
5767
|
console.log(GO_ACCENT.bold("Forbidden"));
|
|
5627
5768
|
for (const item of preview.forbiddenScope.slice(0, 6)) console.log(DIM(" - ") + chalk.white(item));
|
|
5769
|
+
if (preview.boundariesDetected.length > 0) {
|
|
5770
|
+
console.log(DIM(" Boundaries detected: ") + chalk.white(`${preview.boundariesDetected.join(", ")} will be forbidden, not treated as active scope.`));
|
|
5771
|
+
}
|
|
5628
5772
|
console.log("");
|
|
5629
5773
|
console.log(GO_ACCENT.bold("Patch strategy"));
|
|
5630
5774
|
for (let i = 0; i < preview.patchStrategy.length; i += 1) {
|
|
@@ -5689,6 +5833,7 @@ async function buildAgentPreview(task) {
|
|
|
5689
5833
|
filesToInspect,
|
|
5690
5834
|
allowedScope: contract.contract.relevantScope,
|
|
5691
5835
|
forbiddenScope: contract.contract.forbiddenScope,
|
|
5836
|
+
boundariesDetected: extractBoundaryLabels(contract.contract.forbiddenScope),
|
|
5692
5837
|
sensitiveAreas: [...contract.contract.sensitiveScope, ...plan.sensitiveAreas].slice(0, 10),
|
|
5693
5838
|
stopRules: contract.contract.stopRules,
|
|
5694
5839
|
successCriteria: contract.contract.successCriteria,
|
package/dist-cli/runtrim.js
CHANGED
|
@@ -148,6 +148,22 @@ var ENV_FILE_RE = /(?:^|[\s"'`,(])(\.[.]?env(?:\.[a-zA-Z\d]+)?)\b/g;
|
|
|
148
148
|
var ONLY_EDIT_RE = /\bonly\s+(?:edit|touch|modify|change|update|fix)\b/i;
|
|
149
149
|
var MUST_INCLUDE_RE = /\ballowed\s+scope\s+(?:must\s+)?include\b|\bmust\s+(?:include|contain)\b/i;
|
|
150
150
|
var CLI_SCOPE_RE = /\b(cli|command routing|runtrim command|run compiler|contract generation|scope inference|preview command|agent preview command|agent apply|adapters?|auto-guard|bridge helpers?|daemon|local server|localhost|\.runtrim(?:\s+artifacts?)?)\b/i;
|
|
151
|
+
var NEGATION_PREFIX_RE = /\b(do not|don't|dont|never|avoid|must not|should not|without changing|without touching|no changes to|keep .* untouched|leave .* untouched|keep .* unchanged)\b/i;
|
|
152
|
+
function hasNegationNear(text, index) {
|
|
153
|
+
const start = Math.max(0, index - 64);
|
|
154
|
+
const window = text.slice(start, index + 8);
|
|
155
|
+
return NEGATION_PREFIX_RE.test(window);
|
|
156
|
+
}
|
|
157
|
+
function hasPositiveKeywordMention(task, keyword) {
|
|
158
|
+
const lowerTask = task.toLowerCase();
|
|
159
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
160
|
+
let idx = lowerTask.indexOf(lowerKeyword);
|
|
161
|
+
while (idx !== -1) {
|
|
162
|
+
if (!hasNegationNear(lowerTask, idx)) return true;
|
|
163
|
+
idx = lowerTask.indexOf(lowerKeyword, idx + lowerKeyword.length);
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
151
167
|
function extractScopePhrase(task, re) {
|
|
152
168
|
var _a2, _b;
|
|
153
169
|
const m = task.match(re);
|
|
@@ -205,18 +221,34 @@ function buildExplicitAllowedScope(task, explicitPaths) {
|
|
|
205
221
|
}
|
|
206
222
|
function buildExplicitForbiddenScope(task) {
|
|
207
223
|
const out = [];
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
224
|
+
const addBoundaryList = (raw) => {
|
|
225
|
+
if (!raw) return;
|
|
226
|
+
const normalized = raw.replace(/\b(logic|internals?|behavior|files?|systems?)\b/gi, "").replace(/\s+/g, " ").trim();
|
|
227
|
+
const parts = normalized.split(/\s*(?:,|;|\band\b|\bor\b)\s*/i).map((p) => p.trim().replace(/[.]+$/, "")).filter(Boolean).slice(0, 12);
|
|
228
|
+
for (const p of parts) {
|
|
229
|
+
if (p.length < 2) continue;
|
|
230
|
+
out.push(`Do not touch ${p}`);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const explicitPhrases = [
|
|
234
|
+
/\bforbidden\s+scope\s+must\s+include\s+([^\n.]+)/i,
|
|
235
|
+
/\bdo\s+not\s+touch\s+([^\n.]+)/i,
|
|
236
|
+
/\bdo\s+not\s+edit\s+([^\n.]+)/i,
|
|
237
|
+
/\bdo\s+not\s+change\s+([^\n.]+)/i,
|
|
238
|
+
/\bmust\s+not\s+touch\s+([^\n.]+)/i,
|
|
239
|
+
/\bshould\s+not\s+touch\s+([^\n.]+)/i,
|
|
240
|
+
/\bwithout\s+changing\s+([^\n.]+)/i,
|
|
241
|
+
/\bwithout\s+touching\s+([^\n.]+)/i,
|
|
242
|
+
/\bno\s+changes\s+to\s+([^\n.]+)/i,
|
|
243
|
+
/\bkeep\s+([^\n.]+?)\s+(?:untouched|unchanged)\b/i,
|
|
244
|
+
/\bleave\s+([^\n.]+?)\s+untouched\b/i,
|
|
245
|
+
/\bavoid\s+changing\s+([^\n.]+)/i,
|
|
246
|
+
/\bexclude\s+([^\n.]+)/i,
|
|
247
|
+
/\bforbidden\s+([^\n.]+)/i
|
|
248
|
+
];
|
|
249
|
+
for (const re of explicitPhrases) {
|
|
250
|
+
addBoundaryList(extractScopePhrase(task, re));
|
|
251
|
+
}
|
|
220
252
|
return [...new Set(out)];
|
|
221
253
|
}
|
|
222
254
|
function extractExplicitPaths(task) {
|
|
@@ -448,11 +480,10 @@ var CATEGORY_KEYWORDS = [
|
|
|
448
480
|
];
|
|
449
481
|
function classifyTaskCategory(task, explicitPaths) {
|
|
450
482
|
const lower = task.toLowerCase();
|
|
451
|
-
if (CLI_SCOPE_RE.test(task)) return "cli";
|
|
452
483
|
const pathHints = explicitPaths.join(" ").toLowerCase();
|
|
453
484
|
for (const [category, keywords] of CATEGORY_KEYWORDS) {
|
|
454
485
|
const combined = lower + " " + pathHints;
|
|
455
|
-
if (keywords.some((kw) => combined
|
|
486
|
+
if (keywords.some((kw) => hasPositiveKeywordMention(combined, kw))) {
|
|
456
487
|
return category;
|
|
457
488
|
}
|
|
458
489
|
}
|
|
@@ -695,6 +726,28 @@ var LOOP_PATTERNS = [
|
|
|
695
726
|
/\b(keep (trying|going|working)|iterate until|loop until|retry)\b/i,
|
|
696
727
|
/\b(if it doesn.t work.{0,20}try again)\b/i
|
|
697
728
|
];
|
|
729
|
+
var NEGATION_PREFIX_RE2 = /\b(do not|don't|dont|never|avoid|must not|should not|without changing|without touching|no changes to|keep .* untouched|leave .* untouched|keep .* unchanged)\b/i;
|
|
730
|
+
function hasNegationNear2(text, index) {
|
|
731
|
+
const start = Math.max(0, index - 64);
|
|
732
|
+
const window = text.slice(start, index + 8);
|
|
733
|
+
return NEGATION_PREFIX_RE2.test(window);
|
|
734
|
+
}
|
|
735
|
+
function hasPositiveKeywordMention2(taskLower, keyword) {
|
|
736
|
+
let idx = taskLower.indexOf(keyword.toLowerCase());
|
|
737
|
+
while (idx !== -1) {
|
|
738
|
+
if (!hasNegationNear2(taskLower, idx)) return true;
|
|
739
|
+
idx = taskLower.indexOf(keyword.toLowerCase(), idx + keyword.length);
|
|
740
|
+
}
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
function hasNegatedKeywordMention(taskLower, keyword) {
|
|
744
|
+
let idx = taskLower.indexOf(keyword.toLowerCase());
|
|
745
|
+
while (idx !== -1) {
|
|
746
|
+
if (hasNegationNear2(taskLower, idx)) return true;
|
|
747
|
+
idx = taskLower.indexOf(keyword.toLowerCase(), idx + keyword.length);
|
|
748
|
+
}
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
698
751
|
function scoreTask(task, flags) {
|
|
699
752
|
let score = 100;
|
|
700
753
|
for (const flag of flags) {
|
|
@@ -755,7 +808,7 @@ function detectProjectContext(cwd = process.cwd()) {
|
|
|
755
808
|
function detectMegaRun(taskLower, task) {
|
|
756
809
|
const found = [];
|
|
757
810
|
for (const [system, keywords] of Object.entries(MEGA_RUN_SYSTEMS)) {
|
|
758
|
-
if (keywords.some((kw) => taskLower
|
|
811
|
+
if (keywords.some((kw) => hasPositiveKeywordMention2(taskLower, kw))) {
|
|
759
812
|
found.push(system);
|
|
760
813
|
}
|
|
761
814
|
}
|
|
@@ -766,17 +819,24 @@ function detectMegaRun(taskLower, task) {
|
|
|
766
819
|
function detectAreasTouched(taskLower) {
|
|
767
820
|
const forbidden = [];
|
|
768
821
|
const sensitive = [];
|
|
822
|
+
const boundaries = [];
|
|
769
823
|
for (const [area, keywords] of Object.entries(ALWAYS_FORBIDDEN_KEYWORDS)) {
|
|
770
|
-
if (keywords.some((kw) => taskLower
|
|
824
|
+
if (keywords.some((kw) => hasPositiveKeywordMention2(taskLower, kw))) {
|
|
771
825
|
forbidden.push(area);
|
|
772
826
|
}
|
|
827
|
+
if (keywords.some((kw) => hasNegatedKeywordMention(taskLower, kw))) {
|
|
828
|
+
boundaries.push(area);
|
|
829
|
+
}
|
|
773
830
|
}
|
|
774
831
|
for (const [area, keywords] of Object.entries(SENSITIVE_BILLING_KEYWORDS)) {
|
|
775
|
-
if (keywords.some((kw) => taskLower
|
|
832
|
+
if (keywords.some((kw) => hasPositiveKeywordMention2(taskLower, kw))) {
|
|
776
833
|
sensitive.push(area);
|
|
777
834
|
}
|
|
835
|
+
if (keywords.some((kw) => hasNegatedKeywordMention(taskLower, kw))) {
|
|
836
|
+
boundaries.push(area);
|
|
837
|
+
}
|
|
778
838
|
}
|
|
779
|
-
return { forbidden, sensitive };
|
|
839
|
+
return { forbidden, sensitive, boundaries: [...new Set(boundaries)] };
|
|
780
840
|
}
|
|
781
841
|
function auditTask(task, config, cwd = process.cwd()) {
|
|
782
842
|
const flags = [];
|
|
@@ -866,7 +926,7 @@ function auditTask(task, config, cwd = process.cwd()) {
|
|
|
866
926
|
detail: "References to full context or entire conversation force expensive context loading."
|
|
867
927
|
});
|
|
868
928
|
}
|
|
869
|
-
const { forbidden: forbiddenAreasTouched, sensitive: sensitiveAreasRelevant } = detectAreasTouched(taskLower);
|
|
929
|
+
const { forbidden: forbiddenAreasTouched, sensitive: sensitiveAreasRelevant, boundaries } = detectAreasTouched(taskLower);
|
|
870
930
|
if (forbiddenAreasTouched.length > 0) {
|
|
871
931
|
flags.push({
|
|
872
932
|
code: "touches_forbidden_area",
|
|
@@ -883,6 +943,14 @@ function auditTask(task, config, cwd = process.cwd()) {
|
|
|
883
943
|
detail: `Task touches ${sensitiveAreasRelevant.join(", ")}. These are moved to SENSITIVE SCOPE: inspect allowed, editing requires explicit approval.`
|
|
884
944
|
});
|
|
885
945
|
}
|
|
946
|
+
if (boundaries.length > 0) {
|
|
947
|
+
flags.push({
|
|
948
|
+
code: "forbidden_boundaries_detected",
|
|
949
|
+
label: `Boundaries detected: ${boundaries.join(", ")}`,
|
|
950
|
+
severity: "info",
|
|
951
|
+
detail: "Sensitive systems in negated constraints are treated as forbidden boundaries, not active task scope."
|
|
952
|
+
});
|
|
953
|
+
}
|
|
886
954
|
const isSimpleTask = task.length < 80 && flags.filter((f) => f.severity === "critical").length === 0;
|
|
887
955
|
if (isSimpleTask && config.defaultModel === "opus") {
|
|
888
956
|
flags.push({
|
|
@@ -960,6 +1028,10 @@ function scoreToRisk2(score) {
|
|
|
960
1028
|
}
|
|
961
1029
|
function cleanObjective(task) {
|
|
962
1030
|
let t = task.trim();
|
|
1031
|
+
t = t.replace(
|
|
1032
|
+
/(?:^|[\s,.])(do not|don't|dont|must not|should not|without changing|without touching|no changes to|keep .*? unchanged|keep .*? untouched|leave .*? untouched|avoid changing)\b[^.]*\.?/gi,
|
|
1033
|
+
" "
|
|
1034
|
+
);
|
|
963
1035
|
t = t.replace(/,?\s*check everything(\s+and\b)?/gi, "");
|
|
964
1036
|
t = t.replace(/,?\s*(look|search|scan)\s+everywhere(\s+and\b)?/gi, "");
|
|
965
1037
|
t = t.replace(/,?\s*review everything(\s+and\b)?/gi, "");
|
|
@@ -4640,21 +4712,21 @@ var MEDIUM_PATH_PATTERNS = [
|
|
|
4640
4712
|
];
|
|
4641
4713
|
function classifyFileRisk(files) {
|
|
4642
4714
|
if (files.length === 0) return "low";
|
|
4643
|
-
let
|
|
4715
|
+
let maxRisk2 = "low";
|
|
4644
4716
|
for (const f of files) {
|
|
4645
4717
|
const norm = f.replace(/\\/g, "/").toLowerCase();
|
|
4646
4718
|
if (CRITICAL_PATH_PATTERNS.some((p) => norm.includes(p))) {
|
|
4647
4719
|
return "critical";
|
|
4648
4720
|
}
|
|
4649
|
-
if (HIGH_PATH_PATTERNS.some((p) => norm.includes(p)) &&
|
|
4650
|
-
|
|
4721
|
+
if (HIGH_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk2 !== "high") {
|
|
4722
|
+
maxRisk2 = "high";
|
|
4651
4723
|
continue;
|
|
4652
4724
|
}
|
|
4653
|
-
if (MEDIUM_PATH_PATTERNS.some((p) => norm.includes(p)) &&
|
|
4654
|
-
|
|
4725
|
+
if (MEDIUM_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk2 === "low") {
|
|
4726
|
+
maxRisk2 = "medium";
|
|
4655
4727
|
}
|
|
4656
4728
|
}
|
|
4657
|
-
return
|
|
4729
|
+
return maxRisk2;
|
|
4658
4730
|
}
|
|
4659
4731
|
function isSensitivePath(filePath) {
|
|
4660
4732
|
const norm = filePath.replace(/\\/g, "/").toLowerCase();
|
|
@@ -4997,6 +5069,24 @@ function getLearningContext(cwd, task, runs) {
|
|
|
4997
5069
|
// src/lib/run-planner.ts
|
|
4998
5070
|
var FAST_PATH_CATEGORIES = /* @__PURE__ */ new Set(["ui", "docs", "tests", "unknown"]);
|
|
4999
5071
|
var ALWAYS_CONTRACT_CATEGORIES = /* @__PURE__ */ new Set(["auth", "billing", "payment", "webhook", "database", "env", "middleware"]);
|
|
5072
|
+
var RISK_ORDER = ["low", "medium", "high", "critical"];
|
|
5073
|
+
var NEGATION_PREFIX_RE3 = /\b(do not|don't|dont|never|avoid|must not|should not|without changing|without touching|no changes to|keep .* untouched|leave .* untouched|keep .* unchanged)\b/i;
|
|
5074
|
+
function maxRisk(a, b) {
|
|
5075
|
+
return RISK_ORDER[Math.max(RISK_ORDER.indexOf(a), RISK_ORDER.indexOf(b))];
|
|
5076
|
+
}
|
|
5077
|
+
function hasNegationNear3(text, index) {
|
|
5078
|
+
const start = Math.max(0, index - 64);
|
|
5079
|
+
const window = text.slice(start, index + 8);
|
|
5080
|
+
return NEGATION_PREFIX_RE3.test(window);
|
|
5081
|
+
}
|
|
5082
|
+
function hasPositiveKeywordMention3(taskLower, keyword) {
|
|
5083
|
+
let idx = taskLower.indexOf(keyword.toLowerCase());
|
|
5084
|
+
while (idx !== -1) {
|
|
5085
|
+
if (!hasNegationNear3(taskLower, idx)) return true;
|
|
5086
|
+
idx = taskLower.indexOf(keyword.toLowerCase(), idx + keyword.length);
|
|
5087
|
+
}
|
|
5088
|
+
return false;
|
|
5089
|
+
}
|
|
5000
5090
|
function isFastPathEligible(risk, category, guardMode, hasExplicitPaths) {
|
|
5001
5091
|
if (guardMode === "strict") return false;
|
|
5002
5092
|
if (guardMode === "off") return true;
|
|
@@ -5012,8 +5102,16 @@ function generatePlan(cwd, task, runs, config, currentChangedFiles) {
|
|
|
5012
5102
|
const compiler = compileTask(task);
|
|
5013
5103
|
const guardMode = (_a2 = config.autoGuardMode) != null ? _a2 : "smart";
|
|
5014
5104
|
const adapters = detectAdapters(cwd);
|
|
5015
|
-
const riskFiles = compiler.explicitPaths.length > 0 ? compiler.explicitPaths :
|
|
5016
|
-
|
|
5105
|
+
const riskFiles = compiler.explicitPaths.length > 0 ? compiler.explicitPaths : [];
|
|
5106
|
+
let rawRisk = classifyFileRisk(riskFiles);
|
|
5107
|
+
if (ALWAYS_CONTRACT_CATEGORIES.has(compiler.taskCategory)) {
|
|
5108
|
+
rawRisk = maxRisk(rawRisk, "high");
|
|
5109
|
+
}
|
|
5110
|
+
const lowerTask = task.toLowerCase();
|
|
5111
|
+
const criticalSystemMentions = ["auth", "billing", "payment", "webhook", "database", "migration", "middleware"].filter((k) => hasPositiveKeywordMention3(lowerTask, k)).length;
|
|
5112
|
+
if (criticalSystemMentions >= 2) {
|
|
5113
|
+
rawRisk = maxRisk(rawRisk, "critical");
|
|
5114
|
+
}
|
|
5017
5115
|
const catScope = buildCategoryScope(
|
|
5018
5116
|
compiler.taskCategory,
|
|
5019
5117
|
true,
|
|
@@ -5137,6 +5235,22 @@ var FAST_LOCAL_KEYWORDS = [
|
|
|
5137
5235
|
"responsive",
|
|
5138
5236
|
"ui polish"
|
|
5139
5237
|
];
|
|
5238
|
+
var NEGATION_PREFIX_RE4 = /\b(do not|don't|dont|never|avoid|must not|should not|without changing|without touching|no changes to|keep .* untouched|leave .* untouched|keep .* unchanged)\b/i;
|
|
5239
|
+
function hasNegationNear4(text, index) {
|
|
5240
|
+
const start = Math.max(0, index - 64);
|
|
5241
|
+
const window = text.slice(start, index + 8);
|
|
5242
|
+
return NEGATION_PREFIX_RE4.test(window);
|
|
5243
|
+
}
|
|
5244
|
+
function hasPositiveKeywordMention4(task, keyword) {
|
|
5245
|
+
const lowerTask = task.toLowerCase();
|
|
5246
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
5247
|
+
let idx = lowerTask.indexOf(lowerKeyword);
|
|
5248
|
+
while (idx !== -1) {
|
|
5249
|
+
if (!hasNegationNear4(lowerTask, idx)) return true;
|
|
5250
|
+
idx = lowerTask.indexOf(lowerKeyword, idx + lowerKeyword.length);
|
|
5251
|
+
}
|
|
5252
|
+
return false;
|
|
5253
|
+
}
|
|
5140
5254
|
function normalize(s) {
|
|
5141
5255
|
return (s != null ? s : "").toLowerCase();
|
|
5142
5256
|
}
|
|
@@ -5195,9 +5309,9 @@ function recommendProviderRouting(ctx) {
|
|
|
5195
5309
|
const changedFiles = (_d = ctx.changedFiles) != null ? _d : [];
|
|
5196
5310
|
const learnedContext = (_e = ctx.learnedContext) != null ? _e : [];
|
|
5197
5311
|
const hasProofGapSignals = proofRequired.some((p) => /proof gap|missing|vercel log|manual verification/i.test(p));
|
|
5198
|
-
const highRiskByKeyword = HIGH_RISK_KEYWORDS.some((k) => task
|
|
5312
|
+
const highRiskByKeyword = HIGH_RISK_KEYWORDS.some((k) => hasPositiveKeywordMention4(task, k) || hasPositiveKeywordMention4(category, k));
|
|
5199
5313
|
const fastKeyword = FAST_LOCAL_KEYWORDS.some((k) => task.includes(k));
|
|
5200
|
-
const multiCritical = ["auth", "billing", "database", "webhook", "payment", "migration"].filter((k) => task
|
|
5314
|
+
const multiCritical = ["auth", "billing", "database", "webhook", "payment", "migration"].filter((k) => hasPositiveKeywordMention4(task, k)).length >= 2;
|
|
5201
5315
|
const broadTask = !ctx.explicitScope && task.split(/\s+/).length > 16;
|
|
5202
5316
|
const hasSensitiveFiles = sensitiveAreas.length > 0 || changedFiles.some((f) => HIGH_RISK_KEYWORDS.some((k) => normalize(f).includes(k)));
|
|
5203
5317
|
const noLearning = learnedContext.length === 0 && ((_f = ctx.similarRunsCount) != null ? _f : 0) === 0;
|
|
@@ -5462,6 +5576,19 @@ async function getSensitivePathStates(cwd) {
|
|
|
5462
5576
|
return /* @__PURE__ */ new Map();
|
|
5463
5577
|
}
|
|
5464
5578
|
}
|
|
5579
|
+
function extractBoundaryLabels(forbiddenScope) {
|
|
5580
|
+
const labels = /* @__PURE__ */ new Set();
|
|
5581
|
+
for (const line of forbiddenScope) {
|
|
5582
|
+
const lower = line.toLowerCase();
|
|
5583
|
+
if (lower.includes("billing") || lower.includes("subscription") || lower.includes("payment")) labels.add("billing");
|
|
5584
|
+
if (lower.includes("auth") || lower.includes("session") || lower.includes("jwt")) labels.add("auth");
|
|
5585
|
+
if (lower.includes("middleware") || lower.includes("proxy")) labels.add("middleware");
|
|
5586
|
+
if (lower.includes(".env") || lower.includes("secret")) labels.add("env");
|
|
5587
|
+
if (lower.includes("cli")) labels.add("cli");
|
|
5588
|
+
if (lower.includes("mcp")) labels.add("mcp");
|
|
5589
|
+
}
|
|
5590
|
+
return [...labels];
|
|
5591
|
+
}
|
|
5465
5592
|
function nowId() {
|
|
5466
5593
|
const d = /* @__PURE__ */ new Date();
|
|
5467
5594
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -5523,8 +5650,21 @@ function buildApprovalLevel(risk, task, category) {
|
|
|
5523
5650
|
const text = `${task}
|
|
5524
5651
|
${category}`.toLowerCase();
|
|
5525
5652
|
const highSystems = ["auth", "billing", "payment", "dodo", "stripe", "webhook", "database", "migration", "rls", "middleware", "env", "secret"];
|
|
5653
|
+
const hasNegationNear5 = (source, index) => {
|
|
5654
|
+
const start = Math.max(0, index - 64);
|
|
5655
|
+
const window = source.slice(start, index + 8);
|
|
5656
|
+
return /\b(do not|don't|dont|never|avoid|must not|should not|without changing|without touching|no changes to|keep .* untouched|leave .* untouched|keep .* unchanged)\b/i.test(window);
|
|
5657
|
+
};
|
|
5658
|
+
const hasPositiveKeyword = (source, keyword) => {
|
|
5659
|
+
let idx = source.indexOf(keyword);
|
|
5660
|
+
while (idx !== -1) {
|
|
5661
|
+
if (!hasNegationNear5(source, idx)) return true;
|
|
5662
|
+
idx = source.indexOf(keyword, idx + keyword.length);
|
|
5663
|
+
}
|
|
5664
|
+
return false;
|
|
5665
|
+
};
|
|
5526
5666
|
if (risk === "high" || risk === "critical") return "required";
|
|
5527
|
-
if (highSystems.some((k) => text
|
|
5667
|
+
if (highSystems.some((k) => hasPositiveKeyword(text, k))) return "required";
|
|
5528
5668
|
if (risk === "medium") return "recommended";
|
|
5529
5669
|
return "no";
|
|
5530
5670
|
}
|
|
@@ -5565,6 +5705,7 @@ function writePreviewArtifacts(cwd, preview) {
|
|
|
5565
5705
|
"",
|
|
5566
5706
|
"Forbidden:",
|
|
5567
5707
|
...preview.forbiddenScope.length > 0 ? preview.forbiddenScope.slice(0, 8).map((f) => `- ${f}`) : ["- none"],
|
|
5708
|
+
...preview.boundariesDetected.length > 0 ? ["", `Boundaries detected: ${preview.boundariesDetected.join(", ")} will be forbidden, not treated as active scope.`] : [],
|
|
5568
5709
|
"",
|
|
5569
5710
|
"Learned context:",
|
|
5570
5711
|
...preview.learnedContext.length > 0 ? preview.learnedContext.map((x) => `- ${x}`) : ["- learning not available yet"],
|
|
@@ -5604,6 +5745,9 @@ async function runAgentPreview(task) {
|
|
|
5604
5745
|
console.log("");
|
|
5605
5746
|
console.log(GO_ACCENT.bold("Forbidden"));
|
|
5606
5747
|
for (const item of preview.forbiddenScope.slice(0, 6)) console.log(DIM(" - ") + chalk.white(item));
|
|
5748
|
+
if (preview.boundariesDetected.length > 0) {
|
|
5749
|
+
console.log(DIM(" Boundaries detected: ") + chalk.white(`${preview.boundariesDetected.join(", ")} will be forbidden, not treated as active scope.`));
|
|
5750
|
+
}
|
|
5607
5751
|
console.log("");
|
|
5608
5752
|
console.log(GO_ACCENT.bold("Patch strategy"));
|
|
5609
5753
|
for (let i = 0; i < preview.patchStrategy.length; i += 1) {
|
|
@@ -5668,6 +5812,7 @@ async function buildAgentPreview(task) {
|
|
|
5668
5812
|
filesToInspect,
|
|
5669
5813
|
allowedScope: contract.contract.relevantScope,
|
|
5670
5814
|
forbiddenScope: contract.contract.forbiddenScope,
|
|
5815
|
+
boundariesDetected: extractBoundaryLabels(contract.contract.forbiddenScope),
|
|
5671
5816
|
sensitiveAreas: [...contract.contract.sensitiveScope, ...plan.sensitiveAreas].slice(0, 10),
|
|
5672
5817
|
stopRules: contract.contract.stopRules,
|
|
5673
5818
|
successCriteria: contract.contract.successCriteria,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runtrim",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.17",
|
|
4
|
+
"description": "The control layer for AI coding agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "RunTrim",
|
|
7
|
-
"homepage": "https://runtrim.
|
|
7
|
+
"homepage": "https://www.runtrim.com",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/michelpronkk-oss/rtrim.git"
|
|
@@ -15,11 +15,15 @@
|
|
|
15
15
|
"keywords": [
|
|
16
16
|
"cli",
|
|
17
17
|
"ai-coding",
|
|
18
|
+
"coding-agents",
|
|
18
19
|
"codex",
|
|
19
20
|
"claude",
|
|
20
21
|
"cursor",
|
|
22
|
+
"mcp",
|
|
21
23
|
"developer-tools",
|
|
22
|
-
"
|
|
24
|
+
"agent-control",
|
|
25
|
+
"code-safety",
|
|
26
|
+
"ai-agents"
|
|
23
27
|
],
|
|
24
28
|
"bin": {
|
|
25
29
|
"runtrim": "dist-cli/runtrim.cjs"
|