runtrim 0.1.16 → 0.1.18

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.
@@ -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
  }
@@ -635,6 +666,28 @@ function buildCategoryScope(category, hasSrc, hasApp, hasPages) {
635
666
  "Check no regression in adjacent routes"
636
667
  ]
637
668
  };
669
+ case "docs":
670
+ return {
671
+ allowedHints: [
672
+ "README.md - project documentation",
673
+ "docs/ - documentation files",
674
+ "CHANGELOG.md or CONTRIBUTING.md if task-specific"
675
+ ],
676
+ forbiddenAdditions: [
677
+ "Do not touch auth internals, session logic, or JWT handling",
678
+ "Do not touch billing, subscription, payment, or webhook logic",
679
+ "Do not touch database schema or migrations",
680
+ "Do not touch .env files or secrets"
681
+ ],
682
+ stopRules: [
683
+ "Stop if the requested change requires code-path behavior changes outside docs",
684
+ "Stop if sensitive files or secrets are referenced"
685
+ ],
686
+ verificationSteps: [
687
+ "Confirm documentation text matches the requested task",
688
+ "Check markdown formatting renders correctly"
689
+ ]
690
+ };
638
691
  default:
639
692
  return {
640
693
  allowedHints: [],
@@ -695,6 +748,28 @@ var LOOP_PATTERNS = [
695
748
  /\b(keep (trying|going|working)|iterate until|loop until|retry)\b/i,
696
749
  /\b(if it doesn.t work.{0,20}try again)\b/i
697
750
  ];
751
+ 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;
752
+ function hasNegationNear2(text, index) {
753
+ const start = Math.max(0, index - 64);
754
+ const window = text.slice(start, index + 8);
755
+ return NEGATION_PREFIX_RE2.test(window);
756
+ }
757
+ function hasPositiveKeywordMention2(taskLower, keyword) {
758
+ let idx = taskLower.indexOf(keyword.toLowerCase());
759
+ while (idx !== -1) {
760
+ if (!hasNegationNear2(taskLower, idx)) return true;
761
+ idx = taskLower.indexOf(keyword.toLowerCase(), idx + keyword.length);
762
+ }
763
+ return false;
764
+ }
765
+ function hasNegatedKeywordMention(taskLower, keyword) {
766
+ let idx = taskLower.indexOf(keyword.toLowerCase());
767
+ while (idx !== -1) {
768
+ if (hasNegationNear2(taskLower, idx)) return true;
769
+ idx = taskLower.indexOf(keyword.toLowerCase(), idx + keyword.length);
770
+ }
771
+ return false;
772
+ }
698
773
  function scoreTask(task, flags) {
699
774
  let score = 100;
700
775
  for (const flag of flags) {
@@ -755,7 +830,7 @@ function detectProjectContext(cwd = process.cwd()) {
755
830
  function detectMegaRun(taskLower, task) {
756
831
  const found = [];
757
832
  for (const [system, keywords] of Object.entries(MEGA_RUN_SYSTEMS)) {
758
- if (keywords.some((kw) => taskLower.includes(kw))) {
833
+ if (keywords.some((kw) => hasPositiveKeywordMention2(taskLower, kw))) {
759
834
  found.push(system);
760
835
  }
761
836
  }
@@ -766,17 +841,24 @@ function detectMegaRun(taskLower, task) {
766
841
  function detectAreasTouched(taskLower) {
767
842
  const forbidden = [];
768
843
  const sensitive = [];
844
+ const boundaries = [];
769
845
  for (const [area, keywords] of Object.entries(ALWAYS_FORBIDDEN_KEYWORDS)) {
770
- if (keywords.some((kw) => taskLower.includes(kw))) {
846
+ if (keywords.some((kw) => hasPositiveKeywordMention2(taskLower, kw))) {
771
847
  forbidden.push(area);
772
848
  }
849
+ if (keywords.some((kw) => hasNegatedKeywordMention(taskLower, kw))) {
850
+ boundaries.push(area);
851
+ }
773
852
  }
774
853
  for (const [area, keywords] of Object.entries(SENSITIVE_BILLING_KEYWORDS)) {
775
- if (keywords.some((kw) => taskLower.includes(kw))) {
854
+ if (keywords.some((kw) => hasPositiveKeywordMention2(taskLower, kw))) {
776
855
  sensitive.push(area);
777
856
  }
857
+ if (keywords.some((kw) => hasNegatedKeywordMention(taskLower, kw))) {
858
+ boundaries.push(area);
859
+ }
778
860
  }
779
- return { forbidden, sensitive };
861
+ return { forbidden, sensitive, boundaries: [...new Set(boundaries)] };
780
862
  }
781
863
  function auditTask(task, config, cwd = process.cwd()) {
782
864
  const flags = [];
@@ -866,7 +948,7 @@ function auditTask(task, config, cwd = process.cwd()) {
866
948
  detail: "References to full context or entire conversation force expensive context loading."
867
949
  });
868
950
  }
869
- const { forbidden: forbiddenAreasTouched, sensitive: sensitiveAreasRelevant } = detectAreasTouched(taskLower);
951
+ const { forbidden: forbiddenAreasTouched, sensitive: sensitiveAreasRelevant, boundaries } = detectAreasTouched(taskLower);
870
952
  if (forbiddenAreasTouched.length > 0) {
871
953
  flags.push({
872
954
  code: "touches_forbidden_area",
@@ -883,6 +965,14 @@ function auditTask(task, config, cwd = process.cwd()) {
883
965
  detail: `Task touches ${sensitiveAreasRelevant.join(", ")}. These are moved to SENSITIVE SCOPE: inspect allowed, editing requires explicit approval.`
884
966
  });
885
967
  }
968
+ if (boundaries.length > 0) {
969
+ flags.push({
970
+ code: "forbidden_boundaries_detected",
971
+ label: `Boundaries detected: ${boundaries.join(", ")}`,
972
+ severity: "info",
973
+ detail: "Sensitive systems in negated constraints are treated as forbidden boundaries, not active task scope."
974
+ });
975
+ }
886
976
  const isSimpleTask = task.length < 80 && flags.filter((f) => f.severity === "critical").length === 0;
887
977
  if (isSimpleTask && config.defaultModel === "opus") {
888
978
  flags.push({
@@ -960,6 +1050,10 @@ function scoreToRisk2(score) {
960
1050
  }
961
1051
  function cleanObjective(task) {
962
1052
  let t = task.trim();
1053
+ t = t.replace(
1054
+ /(?:^|[\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,
1055
+ " "
1056
+ );
963
1057
  t = t.replace(/,?\s*check everything(\s+and\b)?/gi, "");
964
1058
  t = t.replace(/,?\s*(look|search|scan)\s+everywhere(\s+and\b)?/gi, "");
965
1059
  t = t.replace(/,?\s*review everything(\s+and\b)?/gi, "");
@@ -2505,7 +2599,7 @@ function buildSyncPayload(input) {
2505
2599
  var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t;
2506
2600
  const { cwd, projectName, config, projectAudit, memoryMarkdown, runs } = input;
2507
2601
  const latest = runs[0];
2508
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2602
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
2509
2603
  const localProjectId = buildLocalProjectId(cwd);
2510
2604
  const latestPromptText = readTextFileIfExists(path6.join(cwd, ".runtrim", "latest-prompt.md"));
2511
2605
  const continuationPromptText = readTextFileIfExists(
@@ -2567,7 +2661,7 @@ function buildSyncPayload(input) {
2567
2661
  name: resolveProjectName2(cwd, projectName, projectAudit == null ? void 0 : projectAudit.projectName),
2568
2662
  stack: (_a2 = projectAudit == null ? void 0 : projectAudit.detectedStack) != null ? _a2 : config.stack ? config.stack.split(",").map((s) => s.trim()).filter(Boolean) : ["auto"],
2569
2663
  packageManager: (_c = (_b = projectAudit == null ? void 0 : projectAudit.packageManager) != null ? _b : config.packageManager) != null ? _c : null,
2570
- lastUpdated: nowIso
2664
+ lastUpdated: nowIso2
2571
2665
  },
2572
2666
  memory: {
2573
2667
  markdown: memoryMarkdown,
@@ -2649,7 +2743,8 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2649
2743
  "## How to start an AI coding task",
2650
2744
  "",
2651
2745
  "```",
2652
- 'runtrim go "<task>"',
2746
+ "runtrim start",
2747
+ 'runtrim agent "Your task" --copy',
2653
2748
  "```",
2654
2749
  "",
2655
2750
  "RunTrim creates a scoped contract, loads project memory, and generates a guarded prompt.",
@@ -2670,7 +2765,7 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2670
2765
  "",
2671
2766
  "1. Read `.runtrim/contracts/latest.md`.",
2672
2767
  " - If `Status: active` \u2014 a live task exists. Follow the contract strictly.",
2673
- ' - If `Status: none` \u2014 no active task. Ask the user to run `runtrim go "<task>"` first.',
2768
+ ' - If `Status: none` \u2014 no active task. Ask the user to run `runtrim start` then `runtrim agent "Your task" --copy`.',
2674
2769
  "2. Do not assume any prior task is still active.",
2675
2770
  "3. Stay inside the allowed scope defined in the contract.",
2676
2771
  "4. Stop and ask before touching any forbidden area.",
@@ -2695,7 +2790,8 @@ function writeRestingContract(cwd = process.cwd()) {
2695
2790
  "Start one with:",
2696
2791
  "",
2697
2792
  "```",
2698
- 'runtrim go "<your task>"',
2793
+ "runtrim start",
2794
+ 'runtrim agent "Your task" --copy',
2699
2795
  "```",
2700
2796
  "",
2701
2797
  "---",
@@ -2718,7 +2814,8 @@ function writeRestingMemory(cwd = process.cwd()) {
2718
2814
  "Start a new session with:",
2719
2815
  "",
2720
2816
  "```",
2721
- 'runtrim go "<your task>"',
2817
+ "runtrim start",
2818
+ 'runtrim agent "Your task" --copy',
2722
2819
  "```",
2723
2820
  "",
2724
2821
  "---",
@@ -2839,7 +2936,7 @@ function writeBridgeInstructions(cwd = process.cwd()) {
2839
2936
  "1. Read `RUNTRIM.md`.",
2840
2937
  "2. Read `.runtrim/contracts/latest.md`.",
2841
2938
  " - If `Status: active` \u2014 follow the contract strictly.",
2842
- ' - If `Status: none` \u2014 stop. Ask the user to run `runtrim go "<task>"` first.',
2939
+ ' - If `Status: none` \u2014 stop. Ask the user to run `runtrim start` then `runtrim agent "Your task" --copy`.',
2843
2940
  "3. If the contract is active, read `.runtrim/memory/current.md` for session context.",
2844
2941
  " If no active session, read `.runtrim/memory/baseline.md` for project baseline.",
2845
2942
  "",
@@ -2913,6 +3010,28 @@ function buildBridgePrompt(contractText, ctx) {
2913
3010
  // src/lib/run-watch.ts
2914
3011
  function normalizeScopeKeywords2(scope) {
2915
3012
  var _a2;
3013
+ const genericStopwords = /* @__PURE__ */ new Set([
3014
+ "read",
3015
+ "write",
3016
+ "reference",
3017
+ "touch",
3018
+ "modify",
3019
+ "change",
3020
+ "update",
3021
+ "allow",
3022
+ "scope",
3023
+ "paths",
3024
+ "path",
3025
+ "files",
3026
+ "file",
3027
+ "only",
3028
+ "with",
3029
+ "without",
3030
+ "before",
3031
+ "after",
3032
+ "inside",
3033
+ "outside"
3034
+ ]);
2916
3035
  const words = /* @__PURE__ */ new Set();
2917
3036
  for (const line of scope) {
2918
3037
  const lower = line.toLowerCase();
@@ -2922,7 +3041,7 @@ function normalizeScopeKeywords2(scope) {
2922
3041
  }
2923
3042
  const cleaned = lower.replace(/[^a-z0-9_./\s-]/g, " ").split(/\s+/).filter(Boolean);
2924
3043
  for (const token of cleaned) {
2925
- if (token.length >= 4) words.add(token);
3044
+ if (token.length >= 4 && !genericStopwords.has(token)) words.add(token);
2926
3045
  }
2927
3046
  }
2928
3047
  return [...words];
@@ -3019,15 +3138,7 @@ import fs8 from "fs";
3019
3138
  import os from "os";
3020
3139
  import path8 from "path";
3021
3140
  import { execa as execa2 } from "execa";
3022
- var DEFAULT_REGISTRY = {
3023
- version: 1,
3024
- plan: "free",
3025
- trackedRepos: [],
3026
- telemetry: {
3027
- enabled: false,
3028
- anonymousId: ""
3029
- }
3030
- };
3141
+ var EMPTY_TELEMETRY = { enabled: false, anonymousId: "" };
3031
3142
  function normalizeRepoPath(input) {
3032
3143
  const resolved = path8.resolve(input);
3033
3144
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
@@ -3035,41 +3146,208 @@ function normalizeRepoPath(input) {
3035
3146
  function hashValue(value) {
3036
3147
  return crypto.createHash("sha256").update(value).digest("hex").slice(0, 16);
3037
3148
  }
3149
+ function nowIso() {
3150
+ return (/* @__PURE__ */ new Date()).toISOString();
3151
+ }
3152
+ function randomId(prefix) {
3153
+ return `${prefix}_${crypto.randomBytes(12).toString("hex")}`;
3154
+ }
3038
3155
  function getGlobalRunTrimDir() {
3039
3156
  return path8.join(os.homedir(), ".runtrim");
3040
3157
  }
3041
3158
  function getGlobalRegistryPath() {
3042
3159
  return path8.join(getGlobalRunTrimDir(), "global.json");
3043
3160
  }
3044
- function loadGlobalRegistry() {
3161
+ function getInstallStatePath() {
3162
+ return path8.join(getGlobalRunTrimDir(), "install-state.json");
3163
+ }
3164
+ function buildSealInput(registry) {
3165
+ const tracked = [...registry.trackedRepos].map((r) => ({
3166
+ id: r.id,
3167
+ name: r.name,
3168
+ path: normalizeRepoPath(r.path),
3169
+ gitRemote: r.gitRemote,
3170
+ createdAt: r.createdAt,
3171
+ lastSeenAt: r.lastSeenAt
3172
+ })).sort((a, b) => `${a.id}:${a.path}`.localeCompare(`${b.id}:${b.path}`));
3173
+ const payload = {
3174
+ version: registry.version,
3175
+ stateVersion: registry.stateVersion,
3176
+ plan: registry.plan,
3177
+ machineInstallId: registry.machineInstallId,
3178
+ createdAt: registry.createdAt,
3179
+ updatedAt: registry.updatedAt,
3180
+ trackedRepos: tracked,
3181
+ lastKnownRepo: registry.lastKnownRepo ? __spreadProps(__spreadValues({}, registry.lastKnownRepo), {
3182
+ path: normalizeRepoPath(registry.lastKnownRepo.path)
3183
+ }) : null
3184
+ };
3185
+ return JSON.stringify(payload);
3186
+ }
3187
+ function computeSeal(registry) {
3188
+ return crypto.createHash("sha256").update(buildSealInput(registry)).digest("hex");
3189
+ }
3190
+ function sanitizeTrackedRepoEntry(input) {
3191
+ var _a2, _b, _c, _d, _e, _f;
3192
+ const id = String((_a2 = input.id) != null ? _a2 : "").trim();
3193
+ const rawPath = String((_b = input.path) != null ? _b : "").trim();
3194
+ if (!id || !rawPath) return null;
3195
+ return {
3196
+ id,
3197
+ name: String((_c = input.name) != null ? _c : "").trim(),
3198
+ path: normalizeRepoPath(rawPath),
3199
+ gitRemote: String((_d = input.gitRemote) != null ? _d : "").trim(),
3200
+ createdAt: String((_e = input.createdAt) != null ? _e : "").trim(),
3201
+ lastSeenAt: String((_f = input.lastSeenAt) != null ? _f : "").trim()
3202
+ };
3203
+ }
3204
+ function readInstallStateRaw() {
3205
+ var _a2, _b, _c;
3206
+ const p = getInstallStatePath();
3207
+ if (!fs8.existsSync(p)) return { exists: false, state: null };
3208
+ try {
3209
+ const parsed = JSON.parse(fs8.readFileSync(p, "utf-8"));
3210
+ const machineInstallId = String((_a2 = parsed.machineInstallId) != null ? _a2 : "").trim();
3211
+ if (!machineInstallId) return { exists: true, state: null };
3212
+ return {
3213
+ exists: true,
3214
+ state: {
3215
+ machineInstallId,
3216
+ createdAt: String((_b = parsed.createdAt) != null ? _b : "").trim() || nowIso(),
3217
+ updatedAt: String((_c = parsed.updatedAt) != null ? _c : "").trim() || nowIso()
3218
+ }
3219
+ };
3220
+ } catch (e) {
3221
+ return { exists: true, state: null };
3222
+ }
3223
+ }
3224
+ function writeInstallState(state) {
3225
+ const dir = getGlobalRunTrimDir();
3226
+ if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
3227
+ fs8.writeFileSync(getInstallStatePath(), JSON.stringify(state, null, 2), "utf-8");
3228
+ }
3229
+ function ensureInstallState() {
3230
+ const raw = readInstallStateRaw();
3231
+ if (raw.exists && raw.state) return raw.state;
3232
+ const created = {
3233
+ machineInstallId: randomId("rt_install"),
3234
+ createdAt: nowIso(),
3235
+ updatedAt: nowIso()
3236
+ };
3237
+ writeInstallState(created);
3238
+ return created;
3239
+ }
3240
+ function buildDefaultRegistry(install) {
3241
+ const base = {
3242
+ version: 2,
3243
+ stateVersion: 2,
3244
+ plan: "free",
3245
+ machineInstallId: install.machineInstallId,
3246
+ createdAt: nowIso(),
3247
+ updatedAt: nowIso(),
3248
+ trackedRepos: [],
3249
+ lastKnownRepo: null,
3250
+ telemetry: __spreadValues({}, EMPTY_TELEMETRY)
3251
+ };
3252
+ return __spreadProps(__spreadValues({}, base), {
3253
+ integrity: {
3254
+ algorithm: "sha256-local-seal-v1",
3255
+ seal: computeSeal(base)
3256
+ }
3257
+ });
3258
+ }
3259
+ function saveRegistryWithSeal(registry) {
3260
+ var _a2;
3261
+ const dir = getGlobalRunTrimDir();
3262
+ if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
3263
+ const normalizedBase = __spreadProps(__spreadValues({}, registry), {
3264
+ version: 2,
3265
+ stateVersion: 2,
3266
+ trackedRepos: registry.trackedRepos.map((r) => __spreadProps(__spreadValues({}, r), { path: normalizeRepoPath(r.path) })),
3267
+ telemetry: (_a2 = registry.telemetry) != null ? _a2 : __spreadValues({}, EMPTY_TELEMETRY)
3268
+ });
3269
+ const sealed = __spreadProps(__spreadValues({}, normalizedBase), {
3270
+ integrity: {
3271
+ algorithm: "sha256-local-seal-v1",
3272
+ seal: computeSeal(normalizedBase)
3273
+ }
3274
+ });
3275
+ fs8.writeFileSync(getGlobalRegistryPath(), JSON.stringify(sealed, null, 2), "utf-8");
3276
+ }
3277
+ function inspectGlobalRegistry() {
3278
+ var _a2, _b, _c, _d, _e, _f, _g, _h, _i;
3279
+ const installRaw = readInstallStateRaw();
3280
+ const install = (_a2 = installRaw.state) != null ? _a2 : ensureInstallState();
3045
3281
  const registryPath = getGlobalRegistryPath();
3046
- if (!fs8.existsSync(registryPath)) return __spreadValues({}, DEFAULT_REGISTRY);
3282
+ const defaultRegistry = buildDefaultRegistry(install);
3283
+ if (!fs8.existsSync(registryPath)) {
3284
+ if (installRaw.exists) {
3285
+ return {
3286
+ registry: defaultRegistry,
3287
+ needsRepair: true,
3288
+ repairReason: "missing_registry_after_initialization"
3289
+ };
3290
+ }
3291
+ return { registry: defaultRegistry, needsRepair: false, repairReason: null };
3292
+ }
3047
3293
  try {
3048
3294
  const raw = JSON.parse(fs8.readFileSync(registryPath, "utf-8"));
3049
- return {
3050
- version: 1,
3295
+ const trackedRepos = Array.isArray(raw.trackedRepos) ? raw.trackedRepos.map((item) => sanitizeTrackedRepoEntry(item)).filter((item) => Boolean(item)) : [];
3296
+ const base = {
3297
+ version: 2,
3298
+ stateVersion: 2,
3051
3299
  plan: raw.plan === "free" ? "free" : "free",
3052
- trackedRepos: Array.isArray(raw.trackedRepos) ? raw.trackedRepos.filter((item) => Boolean(item && typeof item === "object")).map((item) => ({
3053
- id: String(item.id || ""),
3054
- name: String(item.name || ""),
3055
- path: normalizeRepoPath(String(item.path || "")),
3056
- gitRemote: String(item.gitRemote || ""),
3057
- createdAt: String(item.createdAt || ""),
3058
- lastSeenAt: String(item.lastSeenAt || "")
3059
- })).filter((item) => Boolean(item.id && item.path)) : [],
3300
+ machineInstallId: String((_b = raw.machineInstallId) != null ? _b : "").trim() || install.machineInstallId,
3301
+ createdAt: String((_c = raw.createdAt) != null ? _c : "").trim() || nowIso(),
3302
+ updatedAt: String((_d = raw.updatedAt) != null ? _d : "").trim() || nowIso(),
3303
+ trackedRepos,
3304
+ lastKnownRepo: raw.lastKnownRepo && typeof raw.lastKnownRepo === "object" ? {
3305
+ id: String((_e = raw.lastKnownRepo.id) != null ? _e : "").trim(),
3306
+ name: String((_f = raw.lastKnownRepo.name) != null ? _f : "").trim(),
3307
+ path: normalizeRepoPath(String((_g = raw.lastKnownRepo.path) != null ? _g : "")),
3308
+ gitRemote: String((_h = raw.lastKnownRepo.gitRemote) != null ? _h : "").trim(),
3309
+ lastSeenAt: String((_i = raw.lastKnownRepo.lastSeenAt) != null ? _i : "").trim() || nowIso()
3310
+ } : null,
3060
3311
  telemetry: {
3061
3312
  enabled: typeof raw.telemetry === "object" && raw.telemetry !== null && Boolean(raw.telemetry.enabled),
3062
3313
  anonymousId: typeof raw.telemetry === "object" && raw.telemetry !== null && typeof raw.telemetry.anonymousId === "string" ? String(raw.telemetry.anonymousId).slice(0, 120) : ""
3063
3314
  }
3064
3315
  };
3316
+ const normalized = __spreadProps(__spreadValues({}, base), {
3317
+ integrity: {
3318
+ algorithm: "sha256-local-seal-v1",
3319
+ seal: raw.integrity && typeof raw.integrity === "object" && typeof raw.integrity.seal === "string" ? String(raw.integrity.seal) : ""
3320
+ }
3321
+ });
3322
+ if (normalized.machineInstallId !== install.machineInstallId) {
3323
+ return {
3324
+ registry: normalized,
3325
+ needsRepair: true,
3326
+ repairReason: "machine_install_id_mismatch"
3327
+ };
3328
+ }
3329
+ const expectedSeal = computeSeal(base);
3330
+ if (!normalized.integrity.seal || normalized.integrity.seal !== expectedSeal) {
3331
+ return {
3332
+ registry: normalized,
3333
+ needsRepair: true,
3334
+ repairReason: "integrity_seal_mismatch"
3335
+ };
3336
+ }
3337
+ return { registry: normalized, needsRepair: false, repairReason: null };
3065
3338
  } catch (e) {
3066
- return __spreadValues({}, DEFAULT_REGISTRY);
3339
+ return {
3340
+ registry: defaultRegistry,
3341
+ needsRepair: true,
3342
+ repairReason: "registry_corrupt"
3343
+ };
3067
3344
  }
3068
3345
  }
3346
+ function loadGlobalRegistry() {
3347
+ return inspectGlobalRegistry().registry;
3348
+ }
3069
3349
  function saveGlobalRegistry(registry) {
3070
- const dir = getGlobalRunTrimDir();
3071
- if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
3072
- fs8.writeFileSync(getGlobalRegistryPath(), JSON.stringify(registry, null, 2), "utf-8");
3350
+ saveRegistryWithSeal(registry);
3073
3351
  }
3074
3352
  async function getCurrentRepoIdentity(cwd = process.cwd()) {
3075
3353
  const normalizedPath = normalizeRepoPath(cwd);
@@ -3099,36 +3377,71 @@ function findTrackedRepo(trackedRepos, currentRepo) {
3099
3377
  return byPath != null ? byPath : null;
3100
3378
  }
3101
3379
  async function assertFreeRepoAllowed(cwd = process.cwd()) {
3102
- const registry = loadGlobalRegistry();
3380
+ const inspected = inspectGlobalRegistry();
3381
+ const registry = inspected.registry;
3103
3382
  const currentRepo = await getCurrentRepoIdentity(cwd);
3104
3383
  const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
3105
- if (registry.plan !== "free") {
3106
- return { allowed: true, plan: registry.plan, currentRepo, trackedRepo };
3384
+ const base = {
3385
+ plan: registry.plan,
3386
+ currentRepo,
3387
+ trackedRepo,
3388
+ registryPath: getGlobalRegistryPath()
3389
+ };
3390
+ if (inspected.needsRepair) {
3391
+ return __spreadProps(__spreadValues({}, base), {
3392
+ allowed: false,
3393
+ status: "blocked_repair",
3394
+ repairRequired: true,
3395
+ repairReason: inspected.repairReason
3396
+ });
3107
3397
  }
3108
- if (trackedRepo) {
3109
- return { allowed: true, plan: registry.plan, currentRepo, trackedRepo };
3398
+ if (registry.plan !== "free") {
3399
+ return __spreadProps(__spreadValues({}, base), {
3400
+ allowed: true,
3401
+ status: "allowed",
3402
+ repairRequired: false,
3403
+ repairReason: null
3404
+ });
3110
3405
  }
3111
- if (registry.trackedRepos.length === 0) {
3112
- return { allowed: true, plan: registry.plan, currentRepo, trackedRepo: null };
3406
+ if (trackedRepo || registry.trackedRepos.length === 0) {
3407
+ return __spreadProps(__spreadValues({}, base), {
3408
+ allowed: true,
3409
+ status: "allowed",
3410
+ repairRequired: false,
3411
+ repairReason: null
3412
+ });
3113
3413
  }
3114
- return {
3414
+ return __spreadProps(__spreadValues({}, base), {
3115
3415
  allowed: false,
3116
- plan: registry.plan,
3117
- currentRepo,
3416
+ status: "blocked_limit",
3417
+ repairRequired: false,
3418
+ repairReason: null,
3118
3419
  trackedRepo: registry.trackedRepos[0]
3119
- };
3420
+ });
3120
3421
  }
3121
3422
  async function registerCurrentRepo(cwd = process.cwd()) {
3423
+ const check = await assertFreeRepoAllowed(cwd);
3424
+ if (!check.allowed && check.status === "blocked_repair") {
3425
+ throw new Error("runtrim_local_state_repair_required");
3426
+ }
3122
3427
  const registry = loadGlobalRegistry();
3123
3428
  const currentRepo = await getCurrentRepoIdentity(cwd);
3124
- const now = (/* @__PURE__ */ new Date()).toISOString();
3429
+ const now = nowIso();
3125
3430
  const existing = findTrackedRepo(registry.trackedRepos, currentRepo);
3126
3431
  if (existing) {
3127
3432
  existing.lastSeenAt = now;
3128
3433
  existing.name = currentRepo.name;
3129
3434
  existing.path = currentRepo.path;
3130
3435
  existing.gitRemote = currentRepo.gitRemote;
3131
- saveGlobalRegistry(registry);
3436
+ registry.updatedAt = now;
3437
+ registry.lastKnownRepo = {
3438
+ id: currentRepo.id,
3439
+ name: currentRepo.name,
3440
+ path: currentRepo.path,
3441
+ gitRemote: currentRepo.gitRemote,
3442
+ lastSeenAt: now
3443
+ };
3444
+ saveRegistryWithSeal(registry);
3132
3445
  return existing;
3133
3446
  }
3134
3447
  const entry = {
@@ -3139,24 +3452,110 @@ async function registerCurrentRepo(cwd = process.cwd()) {
3139
3452
  createdAt: now,
3140
3453
  lastSeenAt: now
3141
3454
  };
3142
- registry.trackedRepos.push(entry);
3143
- saveGlobalRegistry(registry);
3455
+ registry.trackedRepos = [entry];
3456
+ registry.updatedAt = now;
3457
+ registry.lastKnownRepo = {
3458
+ id: entry.id,
3459
+ name: entry.name,
3460
+ path: entry.path,
3461
+ gitRemote: entry.gitRemote,
3462
+ lastSeenAt: now
3463
+ };
3464
+ saveRegistryWithSeal(registry);
3144
3465
  return entry;
3145
3466
  }
3467
+ async function repairGlobalRegistry(cwd = process.cwd(), options = {}) {
3468
+ const before = await assertFreeRepoAllowed(cwd);
3469
+ if (!before.repairRequired) {
3470
+ return { repaired: false, check: before };
3471
+ }
3472
+ const install = ensureInstallState();
3473
+ const now = nowIso();
3474
+ const repaired = buildDefaultRegistry(install);
3475
+ repaired.createdAt = now;
3476
+ repaired.updatedAt = now;
3477
+ if (options.useCurrentRepo) {
3478
+ const currentRepo = await getCurrentRepoIdentity(cwd);
3479
+ repaired.trackedRepos = [
3480
+ {
3481
+ id: currentRepo.id,
3482
+ name: currentRepo.name,
3483
+ path: currentRepo.path,
3484
+ gitRemote: currentRepo.gitRemote,
3485
+ createdAt: now,
3486
+ lastSeenAt: now
3487
+ }
3488
+ ];
3489
+ repaired.lastKnownRepo = {
3490
+ id: currentRepo.id,
3491
+ name: currentRepo.name,
3492
+ path: currentRepo.path,
3493
+ gitRemote: currentRepo.gitRemote,
3494
+ lastSeenAt: now
3495
+ };
3496
+ }
3497
+ saveRegistryWithSeal(repaired);
3498
+ const check = await assertFreeRepoAllowed(cwd);
3499
+ return { repaired: true, check };
3500
+ }
3146
3501
  async function unlinkCurrentRepo(cwd = process.cwd(), force = false) {
3147
3502
  var _a2;
3503
+ const check = await assertFreeRepoAllowed(cwd);
3504
+ if (check.status === "blocked_repair") {
3505
+ if (!force) {
3506
+ return {
3507
+ removed: false,
3508
+ forced: false,
3509
+ currentRepo: check.currentRepo,
3510
+ trackedRepo: null
3511
+ };
3512
+ }
3513
+ const install = ensureInstallState();
3514
+ const repaired = buildDefaultRegistry(install);
3515
+ repaired.updatedAt = nowIso();
3516
+ repaired.lastKnownRepo = {
3517
+ id: check.currentRepo.id,
3518
+ name: check.currentRepo.name,
3519
+ path: check.currentRepo.path,
3520
+ gitRemote: check.currentRepo.gitRemote,
3521
+ lastSeenAt: repaired.updatedAt
3522
+ };
3523
+ saveRegistryWithSeal(repaired);
3524
+ return {
3525
+ removed: true,
3526
+ forced: true,
3527
+ currentRepo: check.currentRepo,
3528
+ trackedRepo: null
3529
+ };
3530
+ }
3148
3531
  const registry = loadGlobalRegistry();
3149
3532
  const currentRepo = await getCurrentRepoIdentity(cwd);
3150
3533
  const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
3151
3534
  if (trackedRepo) {
3152
3535
  registry.trackedRepos = registry.trackedRepos.filter((repo) => repo.id !== trackedRepo.id);
3153
- saveGlobalRegistry(registry);
3536
+ registry.updatedAt = nowIso();
3537
+ registry.lastKnownRepo = {
3538
+ id: trackedRepo.id,
3539
+ name: trackedRepo.name,
3540
+ path: trackedRepo.path,
3541
+ gitRemote: trackedRepo.gitRemote,
3542
+ lastSeenAt: registry.updatedAt
3543
+ };
3544
+ saveRegistryWithSeal(registry);
3154
3545
  return { removed: true, forced: false, currentRepo, trackedRepo };
3155
3546
  }
3156
3547
  if (force && registry.trackedRepos.length > 0) {
3157
3548
  const first = registry.trackedRepos[0];
3158
3549
  registry.trackedRepos = [];
3159
- saveGlobalRegistry(registry);
3550
+ registry.updatedAt = nowIso();
3551
+ registry.lastKnownRepo = {
3552
+ id: first.id,
3553
+ name: first.name,
3554
+ path: first.path,
3555
+ gitRemote: first.gitRemote,
3556
+ lastSeenAt: registry.updatedAt
3557
+ };
3558
+ saveRegistryWithSeal(registry);
3160
3559
  return { removed: true, forced: true, currentRepo, trackedRepo: first };
3161
3560
  }
3162
3561
  return {
@@ -4640,21 +5039,21 @@ var MEDIUM_PATH_PATTERNS = [
4640
5039
  ];
4641
5040
  function classifyFileRisk(files) {
4642
5041
  if (files.length === 0) return "low";
4643
- let maxRisk = "low";
5042
+ let maxRisk2 = "low";
4644
5043
  for (const f of files) {
4645
5044
  const norm = f.replace(/\\/g, "/").toLowerCase();
4646
5045
  if (CRITICAL_PATH_PATTERNS.some((p) => norm.includes(p))) {
4647
5046
  return "critical";
4648
5047
  }
4649
- if (HIGH_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk !== "high") {
4650
- maxRisk = "high";
5048
+ if (HIGH_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk2 !== "high") {
5049
+ maxRisk2 = "high";
4651
5050
  continue;
4652
5051
  }
4653
- if (MEDIUM_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk === "low") {
4654
- maxRisk = "medium";
5052
+ if (MEDIUM_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk2 === "low") {
5053
+ maxRisk2 = "medium";
4655
5054
  }
4656
5055
  }
4657
- return maxRisk;
5056
+ return maxRisk2;
4658
5057
  }
4659
5058
  function isSensitivePath(filePath) {
4660
5059
  const norm = filePath.replace(/\\/g, "/").toLowerCase();
@@ -4997,6 +5396,24 @@ function getLearningContext(cwd, task, runs) {
4997
5396
  // src/lib/run-planner.ts
4998
5397
  var FAST_PATH_CATEGORIES = /* @__PURE__ */ new Set(["ui", "docs", "tests", "unknown"]);
4999
5398
  var ALWAYS_CONTRACT_CATEGORIES = /* @__PURE__ */ new Set(["auth", "billing", "payment", "webhook", "database", "env", "middleware"]);
5399
+ var RISK_ORDER = ["low", "medium", "high", "critical"];
5400
+ 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;
5401
+ function maxRisk(a, b) {
5402
+ return RISK_ORDER[Math.max(RISK_ORDER.indexOf(a), RISK_ORDER.indexOf(b))];
5403
+ }
5404
+ function hasNegationNear3(text, index) {
5405
+ const start = Math.max(0, index - 64);
5406
+ const window = text.slice(start, index + 8);
5407
+ return NEGATION_PREFIX_RE3.test(window);
5408
+ }
5409
+ function hasPositiveKeywordMention3(taskLower, keyword) {
5410
+ let idx = taskLower.indexOf(keyword.toLowerCase());
5411
+ while (idx !== -1) {
5412
+ if (!hasNegationNear3(taskLower, idx)) return true;
5413
+ idx = taskLower.indexOf(keyword.toLowerCase(), idx + keyword.length);
5414
+ }
5415
+ return false;
5416
+ }
5000
5417
  function isFastPathEligible(risk, category, guardMode, hasExplicitPaths) {
5001
5418
  if (guardMode === "strict") return false;
5002
5419
  if (guardMode === "off") return true;
@@ -5012,8 +5429,16 @@ function generatePlan(cwd, task, runs, config, currentChangedFiles) {
5012
5429
  const compiler = compileTask(task);
5013
5430
  const guardMode = (_a2 = config.autoGuardMode) != null ? _a2 : "smart";
5014
5431
  const adapters = detectAdapters(cwd);
5015
- const riskFiles = compiler.explicitPaths.length > 0 ? compiler.explicitPaths : currentChangedFiles.length > 0 ? currentChangedFiles : [];
5016
- const rawRisk = classifyFileRisk(riskFiles);
5432
+ const riskFiles = compiler.explicitPaths.length > 0 ? compiler.explicitPaths : [];
5433
+ let rawRisk = classifyFileRisk(riskFiles);
5434
+ if (ALWAYS_CONTRACT_CATEGORIES.has(compiler.taskCategory)) {
5435
+ rawRisk = maxRisk(rawRisk, "high");
5436
+ }
5437
+ const lowerTask = task.toLowerCase();
5438
+ const criticalSystemMentions = ["auth", "billing", "payment", "webhook", "database", "migration", "middleware"].filter((k) => hasPositiveKeywordMention3(lowerTask, k)).length;
5439
+ if (criticalSystemMentions >= 2) {
5440
+ rawRisk = maxRisk(rawRisk, "critical");
5441
+ }
5017
5442
  const catScope = buildCategoryScope(
5018
5443
  compiler.taskCategory,
5019
5444
  true,
@@ -5137,6 +5562,22 @@ var FAST_LOCAL_KEYWORDS = [
5137
5562
  "responsive",
5138
5563
  "ui polish"
5139
5564
  ];
5565
+ 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;
5566
+ function hasNegationNear4(text, index) {
5567
+ const start = Math.max(0, index - 64);
5568
+ const window = text.slice(start, index + 8);
5569
+ return NEGATION_PREFIX_RE4.test(window);
5570
+ }
5571
+ function hasPositiveKeywordMention4(task, keyword) {
5572
+ const lowerTask = task.toLowerCase();
5573
+ const lowerKeyword = keyword.toLowerCase();
5574
+ let idx = lowerTask.indexOf(lowerKeyword);
5575
+ while (idx !== -1) {
5576
+ if (!hasNegationNear4(lowerTask, idx)) return true;
5577
+ idx = lowerTask.indexOf(lowerKeyword, idx + lowerKeyword.length);
5578
+ }
5579
+ return false;
5580
+ }
5140
5581
  function normalize(s) {
5141
5582
  return (s != null ? s : "").toLowerCase();
5142
5583
  }
@@ -5195,9 +5636,9 @@ function recommendProviderRouting(ctx) {
5195
5636
  const changedFiles = (_d = ctx.changedFiles) != null ? _d : [];
5196
5637
  const learnedContext = (_e = ctx.learnedContext) != null ? _e : [];
5197
5638
  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));
5639
+ const highRiskByKeyword = HIGH_RISK_KEYWORDS.some((k) => hasPositiveKeywordMention4(task, k) || hasPositiveKeywordMention4(category, k));
5199
5640
  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;
5641
+ const multiCritical = ["auth", "billing", "database", "webhook", "payment", "migration"].filter((k) => hasPositiveKeywordMention4(task, k)).length >= 2;
5201
5642
  const broadTask = !ctx.explicitScope && task.split(/\s+/).length > 16;
5202
5643
  const hasSensitiveFiles = sensitiveAreas.length > 0 || changedFiles.some((f) => HIGH_RISK_KEYWORDS.some((k) => normalize(f).includes(k)));
5203
5644
  const noLearning = learnedContext.length === 0 && ((_f = ctx.similarRunsCount) != null ? _f : 0) === 0;
@@ -5462,6 +5903,19 @@ async function getSensitivePathStates(cwd) {
5462
5903
  return /* @__PURE__ */ new Map();
5463
5904
  }
5464
5905
  }
5906
+ function extractBoundaryLabels(forbiddenScope) {
5907
+ const labels = /* @__PURE__ */ new Set();
5908
+ for (const line of forbiddenScope) {
5909
+ const lower = line.toLowerCase();
5910
+ if (lower.includes("billing") || lower.includes("subscription") || lower.includes("payment")) labels.add("billing");
5911
+ if (lower.includes("auth") || lower.includes("session") || lower.includes("jwt")) labels.add("auth");
5912
+ if (lower.includes("middleware") || lower.includes("proxy")) labels.add("middleware");
5913
+ if (lower.includes(".env") || lower.includes("secret")) labels.add("env");
5914
+ if (lower.includes("cli")) labels.add("cli");
5915
+ if (lower.includes("mcp")) labels.add("mcp");
5916
+ }
5917
+ return [...labels];
5918
+ }
5465
5919
  function nowId() {
5466
5920
  const d = /* @__PURE__ */ new Date();
5467
5921
  const pad = (n) => String(n).padStart(2, "0");
@@ -5523,8 +5977,21 @@ function buildApprovalLevel(risk, task, category) {
5523
5977
  const text = `${task}
5524
5978
  ${category}`.toLowerCase();
5525
5979
  const highSystems = ["auth", "billing", "payment", "dodo", "stripe", "webhook", "database", "migration", "rls", "middleware", "env", "secret"];
5980
+ const hasNegationNear5 = (source, index) => {
5981
+ const start = Math.max(0, index - 64);
5982
+ const window = source.slice(start, index + 8);
5983
+ 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);
5984
+ };
5985
+ const hasPositiveKeyword = (source, keyword) => {
5986
+ let idx = source.indexOf(keyword);
5987
+ while (idx !== -1) {
5988
+ if (!hasNegationNear5(source, idx)) return true;
5989
+ idx = source.indexOf(keyword, idx + keyword.length);
5990
+ }
5991
+ return false;
5992
+ };
5526
5993
  if (risk === "high" || risk === "critical") return "required";
5527
- if (highSystems.some((k) => text.includes(k))) return "required";
5994
+ if (highSystems.some((k) => hasPositiveKeyword(text, k))) return "required";
5528
5995
  if (risk === "medium") return "recommended";
5529
5996
  return "no";
5530
5997
  }
@@ -5565,6 +6032,7 @@ function writePreviewArtifacts(cwd, preview) {
5565
6032
  "",
5566
6033
  "Forbidden:",
5567
6034
  ...preview.forbiddenScope.length > 0 ? preview.forbiddenScope.slice(0, 8).map((f) => `- ${f}`) : ["- none"],
6035
+ ...preview.boundariesDetected.length > 0 ? ["", `Boundaries detected: ${preview.boundariesDetected.join(", ")} will be forbidden, not treated as active scope.`] : [],
5568
6036
  "",
5569
6037
  "Learned context:",
5570
6038
  ...preview.learnedContext.length > 0 ? preview.learnedContext.map((x) => `- ${x}`) : ["- learning not available yet"],
@@ -5604,6 +6072,9 @@ async function runAgentPreview(task) {
5604
6072
  console.log("");
5605
6073
  console.log(GO_ACCENT.bold("Forbidden"));
5606
6074
  for (const item of preview.forbiddenScope.slice(0, 6)) console.log(DIM(" - ") + chalk.white(item));
6075
+ if (preview.boundariesDetected.length > 0) {
6076
+ console.log(DIM(" Boundaries detected: ") + chalk.white(`${preview.boundariesDetected.join(", ")} will be forbidden, not treated as active scope.`));
6077
+ }
5607
6078
  console.log("");
5608
6079
  console.log(GO_ACCENT.bold("Patch strategy"));
5609
6080
  for (let i = 0; i < preview.patchStrategy.length; i += 1) {
@@ -5668,6 +6139,7 @@ async function buildAgentPreview(task) {
5668
6139
  filesToInspect,
5669
6140
  allowedScope: contract.contract.relevantScope,
5670
6141
  forbiddenScope: contract.contract.forbiddenScope,
6142
+ boundariesDetected: extractBoundaryLabels(contract.contract.forbiddenScope),
5671
6143
  sensitiveAreas: [...contract.contract.sensitiveScope, ...plan.sensitiveAreas].slice(0, 10),
5672
6144
  stopRules: contract.contract.stopRules,
5673
6145
  successCriteria: contract.contract.successCriteria,
@@ -6984,6 +7456,24 @@ async function buildRuntrimCreateContractMcp(cwd, args) {
6984
7456
  isError: true
6985
7457
  };
6986
7458
  }
7459
+ const repoCheck = await assertFreeRepoAllowed(cwd);
7460
+ if (!repoCheck.allowed) {
7461
+ const guidance = repoCheck.status === "blocked_repair" ? "RunTrim local state needs repair. Free includes 1 tracked repo. The local repo registry changed unexpectedly. Repair the registry or upgrade to Builder for unlimited repos." : "Free includes 1 tracked repo. This repo is not currently tracked. Continue in the tracked repo, unlink the tracked repo with runtrim repo unlink --force, or upgrade to Builder for unlimited repos.";
7462
+ const blockedPayload = {
7463
+ contract_created: false,
7464
+ task: taskRaw,
7465
+ error: repoCheck.status === "blocked_repair" ? "repo_registry_repair_required" : "repo_limit_blocked",
7466
+ guidance,
7467
+ next_action: guidance,
7468
+ finish_command: "runtrim finish",
7469
+ approval_command_example: 'runtrim approve "Allow <path> for this run only"'
7470
+ };
7471
+ return {
7472
+ content: [{ type: "text", text: JSON.stringify(blockedPayload, null, 2) }],
7473
+ structuredContent: blockedPayload,
7474
+ isError: true
7475
+ };
7476
+ }
6987
7477
  const latest = loadLatestRun(cwd);
6988
7478
  if ((latest == null ? void 0 : latest.status) === "guarded") {
6989
7479
  const blockedPayload = {
@@ -7645,6 +8135,21 @@ function isInteractiveTerminal() {
7645
8135
  async function ensureRepoAllowedForFree(cwd) {
7646
8136
  var _a2, _b;
7647
8137
  const check = await assertFreeRepoAllowed(cwd);
8138
+ if (check.status === "blocked_repair") {
8139
+ console.log(chalk.yellow(" RunTrim local state needs repair."));
8140
+ console.log(chalk.yellow(" Free includes 1 tracked repo."));
8141
+ console.log(chalk.yellow(" Your local repo registry changed unexpectedly."));
8142
+ console.log("");
8143
+ console.log(DIM(" Next:"));
8144
+ console.log(chalk.white(" - runtrim repo status"));
8145
+ console.log(chalk.white(" - runtrim repo repair"));
8146
+ console.log(chalk.white(" - runtrim repo repair --use-current"));
8147
+ console.log(chalk.white(" - runtrim repo unlink --force"));
8148
+ console.log(chalk.white(" - upgrade to Builder for unlimited repos"));
8149
+ console.log(chalk.white(" - sign in to restore cloud entitlements"));
8150
+ console.log("");
8151
+ return false;
8152
+ }
7648
8153
  if (check.allowed) {
7649
8154
  await registerCurrentRepo(cwd);
7650
8155
  return true;
@@ -7658,9 +8163,12 @@ async function ensureRepoAllowedForFree(cwd) {
7658
8163
  console.log(chalk.white(` ${check.currentRepo.path}`));
7659
8164
  console.log("");
7660
8165
  console.log(DIM(" Next:"));
7661
- console.log(chalk.white(" - continue in the tracked repo"));
7662
- console.log(chalk.white(" - unlink the tracked repo with runtrim repo unlink --force"));
7663
- console.log(chalk.white(" - join Builder early access for unlimited repos"));
8166
+ console.log(
8167
+ chalk.white(
8168
+ " Free includes 1 tracked repo. This repo is not currently tracked. Continue in the tracked repo, unlink the tracked repo with runtrim repo unlink --force, or upgrade to Builder for unlimited repos."
8169
+ )
8170
+ );
8171
+ console.log(chalk.white(" Agent instructions were not installed because this repo is not tracked."));
7664
8172
  console.log("");
7665
8173
  console.log(
7666
8174
  DIM(
@@ -8027,7 +8535,8 @@ var PROTOCOL_POINTER_BLOCK = `
8027
8535
  ${PROTOCOL_BLOCK_START}
8028
8536
  This repo uses RunTrim as the guarded AI coding protocol.
8029
8537
  Before editing code, read RUNTRIM.md.
8030
- Start every task with: runtrim go "<task>"
8538
+ Start every task with: runtrim start
8539
+ Then run: runtrim agent "Your task" --copy
8031
8540
  Stay inside .runtrim/contracts/latest.md.
8032
8541
  After edits, ask the user to run: runtrim finish
8033
8542
  ${PROTOCOL_BLOCK_END}
@@ -8723,6 +9232,8 @@ program.command("execute <task>").description("Create a controlled execution pac
8723
9232
  var agentCommand = program.command("agent").description("Start a guarded AI coding run with contract, scope, memory, and handoff");
8724
9233
  agentCommand.argument("[task]").option("--copy", "Copy the handoff to clipboard").option("--bridge", "Ensure local bridge is running for this agent run").option("--preview", "Generate an execution preview instead of running any agent").option("--apply", "Generate Agent Apply handoff artifacts").option("--execute", "Create a controlled execution packet and handoff").option("--run", "Alias for --execute").option("--dry-run", "Create execution packet in pending mode without ready status").option("--confirm", "Confirm high-risk apply handoff creation").action(async (task, options) => {
8725
9234
  if (task == null ? void 0 : task.trim()) {
9235
+ const allowed = await ensureRepoAllowedForFree(process.cwd());
9236
+ if (!allowed) return;
8726
9237
  const normalizedTask = (task != null ? task : "").trim();
8727
9238
  if (options == null ? void 0 : options.bridge) {
8728
9239
  const bridge = await ensureBridgeRunningForAgent(process.cwd());
@@ -9191,12 +9702,54 @@ repoCommand.command("status").description("Show local tracked repo status").acti
9191
9702
  console.log(DIM(" Current repo ") + chalk.white(identity.path));
9192
9703
  console.log(DIM(" Tracked repo ") + chalk.white((_b = tracked == null ? void 0 : tracked.path) != null ? _b : "(none)"));
9193
9704
  console.log(DIM(" Allowed ") + chalk.white(check.allowed ? "yes" : "no"));
9705
+ console.log(DIM(" State ") + chalk.white(check.status));
9194
9706
  console.log("");
9707
+ if (check.status === "blocked_repair") {
9708
+ console.log(chalk.yellow(" RunTrim local state needs repair."));
9709
+ console.log(chalk.yellow(" Free includes 1 tracked repo."));
9710
+ console.log(chalk.yellow(" The local repo registry changed unexpectedly."));
9711
+ console.log(DIM(" Run: runtrim repo repair"));
9712
+ console.log("");
9713
+ }
9195
9714
  if (tracked) {
9196
9715
  console.log(DIM(" A tracked repo is one codebase with its own .runtrim workspace."));
9197
9716
  console.log("");
9198
9717
  }
9199
9718
  });
9719
+ repoCommand.command("repair").description("Repair local free-plan repo registry integrity").option("--use-current", "Repair and set the current repo as the single tracked Free repo").action(async (options) => {
9720
+ const cwd = process.cwd();
9721
+ const before = await assertFreeRepoAllowed(cwd);
9722
+ console.log("");
9723
+ console.log(BOLD("RunTrim") + DIM(" repo repair"));
9724
+ console.log("");
9725
+ if (!before.repairRequired) {
9726
+ console.log(DIM(" Local state is healthy. No repair required."));
9727
+ console.log("");
9728
+ return;
9729
+ }
9730
+ if (!options.useCurrent) {
9731
+ console.log(chalk.yellow(" RunTrim local state needs repair."));
9732
+ console.log(chalk.yellow(" Free includes 1 tracked repo."));
9733
+ console.log(chalk.yellow(" Your local repo registry changed unexpectedly."));
9734
+ console.log("");
9735
+ console.log(DIM(" Safe next actions:"));
9736
+ console.log(chalk.white(" - runtrim repo repair --use-current"));
9737
+ console.log(chalk.white(" - runtrim repo unlink --force"));
9738
+ console.log(chalk.white(" - upgrade to Builder for unlimited repos"));
9739
+ console.log(chalk.white(" - sign in to restore cloud entitlements"));
9740
+ console.log("");
9741
+ return;
9742
+ }
9743
+ const result = await repairGlobalRegistry(cwd, { useCurrentRepo: true });
9744
+ if (result.repaired) {
9745
+ console.log(ACCENT.bold(" Local registry repaired."));
9746
+ console.log(DIM(" Current repo is now the tracked Free repo."));
9747
+ console.log("");
9748
+ return;
9749
+ }
9750
+ console.log(DIM(" No repair changes applied."));
9751
+ console.log("");
9752
+ });
9200
9753
  repoCommand.command("unlink").description("Unlink tracked repo from local free-plan registry").option("--force", "Force unlink tracked repo even when running from another path").action(async (options) => {
9201
9754
  const cwd = process.cwd();
9202
9755
  const result = await unlinkCurrentRepo(cwd, Boolean(options.force));
@@ -10363,7 +10916,7 @@ program.command("continue").description("Create a safe continuation prompt from
10363
10916
  const audit = (_a2 = loadProjectAudit(cwd)) != null ? _a2 : performBaselineProjectAudit(cwd, null);
10364
10917
  const reason = normalizeContinuationReason(options.reason);
10365
10918
  const agent = normalizeContinuationAgent(options.agent, config.defaultAgent);
10366
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
10919
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
10367
10920
  const continuationPath = resolveContinuationPath(cwd);
10368
10921
  const latestPromptPath = resolvePromptPath(config, cwd);
10369
10922
  const latestPrompt = fs13.existsSync(latestPromptPath) ? fs13.readFileSync(latestPromptPath, "utf-8").trim() : "";
@@ -10553,14 +11106,14 @@ program.command("continue").description("Create a safe continuation prompt from
10553
11106
  fs13.writeFileSync(continuationPath, continuationPrompt, "utf-8");
10554
11107
  const copied = await copyToClipboardSafe(continuationPrompt);
10555
11108
  if (memory) {
10556
- const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso);
11109
+ const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso2);
10557
11110
  fs13.writeFileSync(path13.join(getConfigDir(cwd), "memory.md"), memoryWithContinuation, "utf-8");
10558
11111
  }
10559
11112
  if (hasConfig) {
10560
11113
  const nextConfig = __spreadProps(__spreadValues({}, config), {
10561
11114
  lastContinuationReason: reason,
10562
11115
  continuationPromptPath: continuationPath.replace(/\\/g, "/"),
10563
- continuationCreatedAt: nowIso
11116
+ continuationCreatedAt: nowIso2
10564
11117
  });
10565
11118
  saveConfig(nextConfig, cwd);
10566
11119
  }
@@ -11352,6 +11905,8 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
11352
11905
  program.command("sync").description("Sync local run history and project memory to your RunTrim dashboard").option("--dry-run", "Show what would be synced without uploading").action(async (opts) => {
11353
11906
  var _a2, _b;
11354
11907
  const cwd = process.cwd();
11908
+ const allowed = await ensureRepoAllowedForFree(cwd);
11909
+ if (!allowed) return;
11355
11910
  console.log("");
11356
11911
  console.log(BOLD("RunTrim") + DIM(" cloud sync"));
11357
11912
  console.log("");