runtrim 0.1.16 → 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.
@@ -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 forbiddenPhrase = extractScopePhrase(task, /\bforbidden\s+scope\s+must\s+include\s+([^\n.]+)/i);
230
- if (forbiddenPhrase) out.push(forbiddenPhrase);
231
- const doNotTouch = extractScopePhrase(task, /\bdo\s+not\s+touch\s+([^\n.]+)/i);
232
- if (doNotTouch) out.push(`Do not touch ${doNotTouch}`);
233
- const doNotEdit = extractScopePhrase(task, /\bdo\s+not\s+edit\s+([^\n.]+)/i);
234
- if (doNotEdit) out.push(`Do not edit ${doNotEdit}`);
235
- const withoutTouching = extractScopePhrase(task, /\bwithout\s+touching\s+([^\n.]+)/i);
236
- if (withoutTouching) out.push(`Without touching ${withoutTouching}`);
237
- const exclude = extractScopePhrase(task, /\bexclude\s+([^\n.]+)/i);
238
- if (exclude) out.push(`Exclude ${exclude}`);
239
- const forbidden = extractScopePhrase(task, /\bforbidden\s+([^\n.]+)/i);
240
- if (forbidden) out.push(`Forbidden ${forbidden}`);
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.includes(kw))) {
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.includes(kw))) {
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.includes(kw))) {
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.includes(kw))) {
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 maxRisk = "low";
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)) && maxRisk !== "high") {
4671
- maxRisk = "high";
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)) && maxRisk === "low") {
4675
- maxRisk = "medium";
4746
+ if (MEDIUM_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk2 === "low") {
4747
+ maxRisk2 = "medium";
4676
4748
  }
4677
4749
  }
4678
- return maxRisk;
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 : currentChangedFiles.length > 0 ? currentChangedFiles : [];
5037
- const rawRisk = classifyFileRisk(riskFiles);
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.includes(k) || category.includes(k));
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.includes(k)).length >= 2;
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.includes(k))) return "required";
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,
@@ -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 forbiddenPhrase = extractScopePhrase(task, /\bforbidden\s+scope\s+must\s+include\s+([^\n.]+)/i);
209
- if (forbiddenPhrase) out.push(forbiddenPhrase);
210
- const doNotTouch = extractScopePhrase(task, /\bdo\s+not\s+touch\s+([^\n.]+)/i);
211
- if (doNotTouch) out.push(`Do not touch ${doNotTouch}`);
212
- const doNotEdit = extractScopePhrase(task, /\bdo\s+not\s+edit\s+([^\n.]+)/i);
213
- if (doNotEdit) out.push(`Do not edit ${doNotEdit}`);
214
- const withoutTouching = extractScopePhrase(task, /\bwithout\s+touching\s+([^\n.]+)/i);
215
- if (withoutTouching) out.push(`Without touching ${withoutTouching}`);
216
- const exclude = extractScopePhrase(task, /\bexclude\s+([^\n.]+)/i);
217
- if (exclude) out.push(`Exclude ${exclude}`);
218
- const forbidden = extractScopePhrase(task, /\bforbidden\s+([^\n.]+)/i);
219
- if (forbidden) out.push(`Forbidden ${forbidden}`);
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.includes(kw))) {
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.includes(kw))) {
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.includes(kw))) {
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.includes(kw))) {
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 maxRisk = "low";
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)) && maxRisk !== "high") {
4650
- maxRisk = "high";
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)) && maxRisk === "low") {
4654
- maxRisk = "medium";
4725
+ if (MEDIUM_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk2 === "low") {
4726
+ maxRisk2 = "medium";
4655
4727
  }
4656
4728
  }
4657
- return maxRisk;
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 : currentChangedFiles.length > 0 ? currentChangedFiles : [];
5016
- const rawRisk = classifyFileRisk(riskFiles);
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.includes(k) || category.includes(k));
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.includes(k)).length >= 2;
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.includes(k))) return "required";
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,6 +1,6 @@
1
1
  {
2
2
  "name": "runtrim",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "The control layer for AI coding agents.",
5
5
  "license": "MIT",
6
6
  "author": "RunTrim",