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.
@@ -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
  }
@@ -656,6 +687,28 @@ function buildCategoryScope(category, hasSrc, hasApp, hasPages) {
656
687
  "Check no regression in adjacent routes"
657
688
  ]
658
689
  };
690
+ case "docs":
691
+ return {
692
+ allowedHints: [
693
+ "README.md - project documentation",
694
+ "docs/ - documentation files",
695
+ "CHANGELOG.md or CONTRIBUTING.md if task-specific"
696
+ ],
697
+ forbiddenAdditions: [
698
+ "Do not touch auth internals, session logic, or JWT handling",
699
+ "Do not touch billing, subscription, payment, or webhook logic",
700
+ "Do not touch database schema or migrations",
701
+ "Do not touch .env files or secrets"
702
+ ],
703
+ stopRules: [
704
+ "Stop if the requested change requires code-path behavior changes outside docs",
705
+ "Stop if sensitive files or secrets are referenced"
706
+ ],
707
+ verificationSteps: [
708
+ "Confirm documentation text matches the requested task",
709
+ "Check markdown formatting renders correctly"
710
+ ]
711
+ };
659
712
  default:
660
713
  return {
661
714
  allowedHints: [],
@@ -716,6 +769,28 @@ var LOOP_PATTERNS = [
716
769
  /\b(keep (trying|going|working)|iterate until|loop until|retry)\b/i,
717
770
  /\b(if it doesn.t work.{0,20}try again)\b/i
718
771
  ];
772
+ 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;
773
+ function hasNegationNear2(text, index) {
774
+ const start = Math.max(0, index - 64);
775
+ const window = text.slice(start, index + 8);
776
+ return NEGATION_PREFIX_RE2.test(window);
777
+ }
778
+ function hasPositiveKeywordMention2(taskLower, keyword) {
779
+ let idx = taskLower.indexOf(keyword.toLowerCase());
780
+ while (idx !== -1) {
781
+ if (!hasNegationNear2(taskLower, idx)) return true;
782
+ idx = taskLower.indexOf(keyword.toLowerCase(), idx + keyword.length);
783
+ }
784
+ return false;
785
+ }
786
+ function hasNegatedKeywordMention(taskLower, keyword) {
787
+ let idx = taskLower.indexOf(keyword.toLowerCase());
788
+ while (idx !== -1) {
789
+ if (hasNegationNear2(taskLower, idx)) return true;
790
+ idx = taskLower.indexOf(keyword.toLowerCase(), idx + keyword.length);
791
+ }
792
+ return false;
793
+ }
719
794
  function scoreTask(task, flags) {
720
795
  let score = 100;
721
796
  for (const flag of flags) {
@@ -776,7 +851,7 @@ function detectProjectContext(cwd = process.cwd()) {
776
851
  function detectMegaRun(taskLower, task) {
777
852
  const found = [];
778
853
  for (const [system, keywords] of Object.entries(MEGA_RUN_SYSTEMS)) {
779
- if (keywords.some((kw) => taskLower.includes(kw))) {
854
+ if (keywords.some((kw) => hasPositiveKeywordMention2(taskLower, kw))) {
780
855
  found.push(system);
781
856
  }
782
857
  }
@@ -787,17 +862,24 @@ function detectMegaRun(taskLower, task) {
787
862
  function detectAreasTouched(taskLower) {
788
863
  const forbidden = [];
789
864
  const sensitive = [];
865
+ const boundaries = [];
790
866
  for (const [area, keywords] of Object.entries(ALWAYS_FORBIDDEN_KEYWORDS)) {
791
- if (keywords.some((kw) => taskLower.includes(kw))) {
867
+ if (keywords.some((kw) => hasPositiveKeywordMention2(taskLower, kw))) {
792
868
  forbidden.push(area);
793
869
  }
870
+ if (keywords.some((kw) => hasNegatedKeywordMention(taskLower, kw))) {
871
+ boundaries.push(area);
872
+ }
794
873
  }
795
874
  for (const [area, keywords] of Object.entries(SENSITIVE_BILLING_KEYWORDS)) {
796
- if (keywords.some((kw) => taskLower.includes(kw))) {
875
+ if (keywords.some((kw) => hasPositiveKeywordMention2(taskLower, kw))) {
797
876
  sensitive.push(area);
798
877
  }
878
+ if (keywords.some((kw) => hasNegatedKeywordMention(taskLower, kw))) {
879
+ boundaries.push(area);
880
+ }
799
881
  }
800
- return { forbidden, sensitive };
882
+ return { forbidden, sensitive, boundaries: [...new Set(boundaries)] };
801
883
  }
802
884
  function auditTask(task, config, cwd = process.cwd()) {
803
885
  const flags = [];
@@ -887,7 +969,7 @@ function auditTask(task, config, cwd = process.cwd()) {
887
969
  detail: "References to full context or entire conversation force expensive context loading."
888
970
  });
889
971
  }
890
- const { forbidden: forbiddenAreasTouched, sensitive: sensitiveAreasRelevant } = detectAreasTouched(taskLower);
972
+ const { forbidden: forbiddenAreasTouched, sensitive: sensitiveAreasRelevant, boundaries } = detectAreasTouched(taskLower);
891
973
  if (forbiddenAreasTouched.length > 0) {
892
974
  flags.push({
893
975
  code: "touches_forbidden_area",
@@ -904,6 +986,14 @@ function auditTask(task, config, cwd = process.cwd()) {
904
986
  detail: `Task touches ${sensitiveAreasRelevant.join(", ")}. These are moved to SENSITIVE SCOPE: inspect allowed, editing requires explicit approval.`
905
987
  });
906
988
  }
989
+ if (boundaries.length > 0) {
990
+ flags.push({
991
+ code: "forbidden_boundaries_detected",
992
+ label: `Boundaries detected: ${boundaries.join(", ")}`,
993
+ severity: "info",
994
+ detail: "Sensitive systems in negated constraints are treated as forbidden boundaries, not active task scope."
995
+ });
996
+ }
907
997
  const isSimpleTask = task.length < 80 && flags.filter((f) => f.severity === "critical").length === 0;
908
998
  if (isSimpleTask && config.defaultModel === "opus") {
909
999
  flags.push({
@@ -981,6 +1071,10 @@ function scoreToRisk2(score) {
981
1071
  }
982
1072
  function cleanObjective(task) {
983
1073
  let t = task.trim();
1074
+ t = t.replace(
1075
+ /(?:^|[\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,
1076
+ " "
1077
+ );
984
1078
  t = t.replace(/,?\s*check everything(\s+and\b)?/gi, "");
985
1079
  t = t.replace(/,?\s*(look|search|scan)\s+everywhere(\s+and\b)?/gi, "");
986
1080
  t = t.replace(/,?\s*review everything(\s+and\b)?/gi, "");
@@ -2526,7 +2620,7 @@ function buildSyncPayload(input) {
2526
2620
  var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t;
2527
2621
  const { cwd, projectName, config, projectAudit, memoryMarkdown, runs } = input;
2528
2622
  const latest = runs[0];
2529
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2623
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
2530
2624
  const localProjectId = buildLocalProjectId(cwd);
2531
2625
  const latestPromptText = readTextFileIfExists(import_path6.default.join(cwd, ".runtrim", "latest-prompt.md"));
2532
2626
  const continuationPromptText = readTextFileIfExists(
@@ -2588,7 +2682,7 @@ function buildSyncPayload(input) {
2588
2682
  name: resolveProjectName2(cwd, projectName, projectAudit == null ? void 0 : projectAudit.projectName),
2589
2683
  stack: (_a2 = projectAudit == null ? void 0 : projectAudit.detectedStack) != null ? _a2 : config.stack ? config.stack.split(",").map((s) => s.trim()).filter(Boolean) : ["auto"],
2590
2684
  packageManager: (_c = (_b = projectAudit == null ? void 0 : projectAudit.packageManager) != null ? _b : config.packageManager) != null ? _c : null,
2591
- lastUpdated: nowIso
2685
+ lastUpdated: nowIso2
2592
2686
  },
2593
2687
  memory: {
2594
2688
  markdown: memoryMarkdown,
@@ -2670,7 +2764,8 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2670
2764
  "## How to start an AI coding task",
2671
2765
  "",
2672
2766
  "```",
2673
- 'runtrim go "<task>"',
2767
+ "runtrim start",
2768
+ 'runtrim agent "Your task" --copy',
2674
2769
  "```",
2675
2770
  "",
2676
2771
  "RunTrim creates a scoped contract, loads project memory, and generates a guarded prompt.",
@@ -2691,7 +2786,7 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2691
2786
  "",
2692
2787
  "1. Read `.runtrim/contracts/latest.md`.",
2693
2788
  " - If `Status: active` \u2014 a live task exists. Follow the contract strictly.",
2694
- ' - If `Status: none` \u2014 no active task. Ask the user to run `runtrim go "<task>"` first.',
2789
+ ' - If `Status: none` \u2014 no active task. Ask the user to run `runtrim start` then `runtrim agent "Your task" --copy`.',
2695
2790
  "2. Do not assume any prior task is still active.",
2696
2791
  "3. Stay inside the allowed scope defined in the contract.",
2697
2792
  "4. Stop and ask before touching any forbidden area.",
@@ -2716,7 +2811,8 @@ function writeRestingContract(cwd = process.cwd()) {
2716
2811
  "Start one with:",
2717
2812
  "",
2718
2813
  "```",
2719
- 'runtrim go "<your task>"',
2814
+ "runtrim start",
2815
+ 'runtrim agent "Your task" --copy',
2720
2816
  "```",
2721
2817
  "",
2722
2818
  "---",
@@ -2739,7 +2835,8 @@ function writeRestingMemory(cwd = process.cwd()) {
2739
2835
  "Start a new session with:",
2740
2836
  "",
2741
2837
  "```",
2742
- 'runtrim go "<your task>"',
2838
+ "runtrim start",
2839
+ 'runtrim agent "Your task" --copy',
2743
2840
  "```",
2744
2841
  "",
2745
2842
  "---",
@@ -2860,7 +2957,7 @@ function writeBridgeInstructions(cwd = process.cwd()) {
2860
2957
  "1. Read `RUNTRIM.md`.",
2861
2958
  "2. Read `.runtrim/contracts/latest.md`.",
2862
2959
  " - If `Status: active` \u2014 follow the contract strictly.",
2863
- ' - If `Status: none` \u2014 stop. Ask the user to run `runtrim go "<task>"` first.',
2960
+ ' - If `Status: none` \u2014 stop. Ask the user to run `runtrim start` then `runtrim agent "Your task" --copy`.',
2864
2961
  "3. If the contract is active, read `.runtrim/memory/current.md` for session context.",
2865
2962
  " If no active session, read `.runtrim/memory/baseline.md` for project baseline.",
2866
2963
  "",
@@ -2934,6 +3031,28 @@ function buildBridgePrompt(contractText, ctx) {
2934
3031
  // src/lib/run-watch.ts
2935
3032
  function normalizeScopeKeywords2(scope) {
2936
3033
  var _a2;
3034
+ const genericStopwords = /* @__PURE__ */ new Set([
3035
+ "read",
3036
+ "write",
3037
+ "reference",
3038
+ "touch",
3039
+ "modify",
3040
+ "change",
3041
+ "update",
3042
+ "allow",
3043
+ "scope",
3044
+ "paths",
3045
+ "path",
3046
+ "files",
3047
+ "file",
3048
+ "only",
3049
+ "with",
3050
+ "without",
3051
+ "before",
3052
+ "after",
3053
+ "inside",
3054
+ "outside"
3055
+ ]);
2937
3056
  const words = /* @__PURE__ */ new Set();
2938
3057
  for (const line of scope) {
2939
3058
  const lower = line.toLowerCase();
@@ -2943,7 +3062,7 @@ function normalizeScopeKeywords2(scope) {
2943
3062
  }
2944
3063
  const cleaned = lower.replace(/[^a-z0-9_./\s-]/g, " ").split(/\s+/).filter(Boolean);
2945
3064
  for (const token of cleaned) {
2946
- if (token.length >= 4) words.add(token);
3065
+ if (token.length >= 4 && !genericStopwords.has(token)) words.add(token);
2947
3066
  }
2948
3067
  }
2949
3068
  return [...words];
@@ -3040,15 +3159,7 @@ var import_fs8 = __toESM(require("fs"), 1);
3040
3159
  var import_os = __toESM(require("os"), 1);
3041
3160
  var import_path8 = __toESM(require("path"), 1);
3042
3161
  var import_execa2 = require("execa");
3043
- var DEFAULT_REGISTRY = {
3044
- version: 1,
3045
- plan: "free",
3046
- trackedRepos: [],
3047
- telemetry: {
3048
- enabled: false,
3049
- anonymousId: ""
3050
- }
3051
- };
3162
+ var EMPTY_TELEMETRY = { enabled: false, anonymousId: "" };
3052
3163
  function normalizeRepoPath(input) {
3053
3164
  const resolved = import_path8.default.resolve(input);
3054
3165
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
@@ -3056,41 +3167,208 @@ function normalizeRepoPath(input) {
3056
3167
  function hashValue(value) {
3057
3168
  return import_crypto2.default.createHash("sha256").update(value).digest("hex").slice(0, 16);
3058
3169
  }
3170
+ function nowIso() {
3171
+ return (/* @__PURE__ */ new Date()).toISOString();
3172
+ }
3173
+ function randomId(prefix) {
3174
+ return `${prefix}_${import_crypto2.default.randomBytes(12).toString("hex")}`;
3175
+ }
3059
3176
  function getGlobalRunTrimDir() {
3060
3177
  return import_path8.default.join(import_os.default.homedir(), ".runtrim");
3061
3178
  }
3062
3179
  function getGlobalRegistryPath() {
3063
3180
  return import_path8.default.join(getGlobalRunTrimDir(), "global.json");
3064
3181
  }
3065
- function loadGlobalRegistry() {
3182
+ function getInstallStatePath() {
3183
+ return import_path8.default.join(getGlobalRunTrimDir(), "install-state.json");
3184
+ }
3185
+ function buildSealInput(registry) {
3186
+ const tracked = [...registry.trackedRepos].map((r) => ({
3187
+ id: r.id,
3188
+ name: r.name,
3189
+ path: normalizeRepoPath(r.path),
3190
+ gitRemote: r.gitRemote,
3191
+ createdAt: r.createdAt,
3192
+ lastSeenAt: r.lastSeenAt
3193
+ })).sort((a, b) => `${a.id}:${a.path}`.localeCompare(`${b.id}:${b.path}`));
3194
+ const payload = {
3195
+ version: registry.version,
3196
+ stateVersion: registry.stateVersion,
3197
+ plan: registry.plan,
3198
+ machineInstallId: registry.machineInstallId,
3199
+ createdAt: registry.createdAt,
3200
+ updatedAt: registry.updatedAt,
3201
+ trackedRepos: tracked,
3202
+ lastKnownRepo: registry.lastKnownRepo ? __spreadProps(__spreadValues({}, registry.lastKnownRepo), {
3203
+ path: normalizeRepoPath(registry.lastKnownRepo.path)
3204
+ }) : null
3205
+ };
3206
+ return JSON.stringify(payload);
3207
+ }
3208
+ function computeSeal(registry) {
3209
+ return import_crypto2.default.createHash("sha256").update(buildSealInput(registry)).digest("hex");
3210
+ }
3211
+ function sanitizeTrackedRepoEntry(input) {
3212
+ var _a2, _b, _c, _d, _e, _f;
3213
+ const id = String((_a2 = input.id) != null ? _a2 : "").trim();
3214
+ const rawPath = String((_b = input.path) != null ? _b : "").trim();
3215
+ if (!id || !rawPath) return null;
3216
+ return {
3217
+ id,
3218
+ name: String((_c = input.name) != null ? _c : "").trim(),
3219
+ path: normalizeRepoPath(rawPath),
3220
+ gitRemote: String((_d = input.gitRemote) != null ? _d : "").trim(),
3221
+ createdAt: String((_e = input.createdAt) != null ? _e : "").trim(),
3222
+ lastSeenAt: String((_f = input.lastSeenAt) != null ? _f : "").trim()
3223
+ };
3224
+ }
3225
+ function readInstallStateRaw() {
3226
+ var _a2, _b, _c;
3227
+ const p = getInstallStatePath();
3228
+ if (!import_fs8.default.existsSync(p)) return { exists: false, state: null };
3229
+ try {
3230
+ const parsed = JSON.parse(import_fs8.default.readFileSync(p, "utf-8"));
3231
+ const machineInstallId = String((_a2 = parsed.machineInstallId) != null ? _a2 : "").trim();
3232
+ if (!machineInstallId) return { exists: true, state: null };
3233
+ return {
3234
+ exists: true,
3235
+ state: {
3236
+ machineInstallId,
3237
+ createdAt: String((_b = parsed.createdAt) != null ? _b : "").trim() || nowIso(),
3238
+ updatedAt: String((_c = parsed.updatedAt) != null ? _c : "").trim() || nowIso()
3239
+ }
3240
+ };
3241
+ } catch (e) {
3242
+ return { exists: true, state: null };
3243
+ }
3244
+ }
3245
+ function writeInstallState(state) {
3246
+ const dir = getGlobalRunTrimDir();
3247
+ if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
3248
+ import_fs8.default.writeFileSync(getInstallStatePath(), JSON.stringify(state, null, 2), "utf-8");
3249
+ }
3250
+ function ensureInstallState() {
3251
+ const raw = readInstallStateRaw();
3252
+ if (raw.exists && raw.state) return raw.state;
3253
+ const created = {
3254
+ machineInstallId: randomId("rt_install"),
3255
+ createdAt: nowIso(),
3256
+ updatedAt: nowIso()
3257
+ };
3258
+ writeInstallState(created);
3259
+ return created;
3260
+ }
3261
+ function buildDefaultRegistry(install) {
3262
+ const base = {
3263
+ version: 2,
3264
+ stateVersion: 2,
3265
+ plan: "free",
3266
+ machineInstallId: install.machineInstallId,
3267
+ createdAt: nowIso(),
3268
+ updatedAt: nowIso(),
3269
+ trackedRepos: [],
3270
+ lastKnownRepo: null,
3271
+ telemetry: __spreadValues({}, EMPTY_TELEMETRY)
3272
+ };
3273
+ return __spreadProps(__spreadValues({}, base), {
3274
+ integrity: {
3275
+ algorithm: "sha256-local-seal-v1",
3276
+ seal: computeSeal(base)
3277
+ }
3278
+ });
3279
+ }
3280
+ function saveRegistryWithSeal(registry) {
3281
+ var _a2;
3282
+ const dir = getGlobalRunTrimDir();
3283
+ if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
3284
+ const normalizedBase = __spreadProps(__spreadValues({}, registry), {
3285
+ version: 2,
3286
+ stateVersion: 2,
3287
+ trackedRepos: registry.trackedRepos.map((r) => __spreadProps(__spreadValues({}, r), { path: normalizeRepoPath(r.path) })),
3288
+ telemetry: (_a2 = registry.telemetry) != null ? _a2 : __spreadValues({}, EMPTY_TELEMETRY)
3289
+ });
3290
+ const sealed = __spreadProps(__spreadValues({}, normalizedBase), {
3291
+ integrity: {
3292
+ algorithm: "sha256-local-seal-v1",
3293
+ seal: computeSeal(normalizedBase)
3294
+ }
3295
+ });
3296
+ import_fs8.default.writeFileSync(getGlobalRegistryPath(), JSON.stringify(sealed, null, 2), "utf-8");
3297
+ }
3298
+ function inspectGlobalRegistry() {
3299
+ var _a2, _b, _c, _d, _e, _f, _g, _h, _i;
3300
+ const installRaw = readInstallStateRaw();
3301
+ const install = (_a2 = installRaw.state) != null ? _a2 : ensureInstallState();
3066
3302
  const registryPath = getGlobalRegistryPath();
3067
- if (!import_fs8.default.existsSync(registryPath)) return __spreadValues({}, DEFAULT_REGISTRY);
3303
+ const defaultRegistry = buildDefaultRegistry(install);
3304
+ if (!import_fs8.default.existsSync(registryPath)) {
3305
+ if (installRaw.exists) {
3306
+ return {
3307
+ registry: defaultRegistry,
3308
+ needsRepair: true,
3309
+ repairReason: "missing_registry_after_initialization"
3310
+ };
3311
+ }
3312
+ return { registry: defaultRegistry, needsRepair: false, repairReason: null };
3313
+ }
3068
3314
  try {
3069
3315
  const raw = JSON.parse(import_fs8.default.readFileSync(registryPath, "utf-8"));
3070
- return {
3071
- version: 1,
3316
+ const trackedRepos = Array.isArray(raw.trackedRepos) ? raw.trackedRepos.map((item) => sanitizeTrackedRepoEntry(item)).filter((item) => Boolean(item)) : [];
3317
+ const base = {
3318
+ version: 2,
3319
+ stateVersion: 2,
3072
3320
  plan: raw.plan === "free" ? "free" : "free",
3073
- trackedRepos: Array.isArray(raw.trackedRepos) ? raw.trackedRepos.filter((item) => Boolean(item && typeof item === "object")).map((item) => ({
3074
- id: String(item.id || ""),
3075
- name: String(item.name || ""),
3076
- path: normalizeRepoPath(String(item.path || "")),
3077
- gitRemote: String(item.gitRemote || ""),
3078
- createdAt: String(item.createdAt || ""),
3079
- lastSeenAt: String(item.lastSeenAt || "")
3080
- })).filter((item) => Boolean(item.id && item.path)) : [],
3321
+ machineInstallId: String((_b = raw.machineInstallId) != null ? _b : "").trim() || install.machineInstallId,
3322
+ createdAt: String((_c = raw.createdAt) != null ? _c : "").trim() || nowIso(),
3323
+ updatedAt: String((_d = raw.updatedAt) != null ? _d : "").trim() || nowIso(),
3324
+ trackedRepos,
3325
+ lastKnownRepo: raw.lastKnownRepo && typeof raw.lastKnownRepo === "object" ? {
3326
+ id: String((_e = raw.lastKnownRepo.id) != null ? _e : "").trim(),
3327
+ name: String((_f = raw.lastKnownRepo.name) != null ? _f : "").trim(),
3328
+ path: normalizeRepoPath(String((_g = raw.lastKnownRepo.path) != null ? _g : "")),
3329
+ gitRemote: String((_h = raw.lastKnownRepo.gitRemote) != null ? _h : "").trim(),
3330
+ lastSeenAt: String((_i = raw.lastKnownRepo.lastSeenAt) != null ? _i : "").trim() || nowIso()
3331
+ } : null,
3081
3332
  telemetry: {
3082
3333
  enabled: typeof raw.telemetry === "object" && raw.telemetry !== null && Boolean(raw.telemetry.enabled),
3083
3334
  anonymousId: typeof raw.telemetry === "object" && raw.telemetry !== null && typeof raw.telemetry.anonymousId === "string" ? String(raw.telemetry.anonymousId).slice(0, 120) : ""
3084
3335
  }
3085
3336
  };
3337
+ const normalized = __spreadProps(__spreadValues({}, base), {
3338
+ integrity: {
3339
+ algorithm: "sha256-local-seal-v1",
3340
+ seal: raw.integrity && typeof raw.integrity === "object" && typeof raw.integrity.seal === "string" ? String(raw.integrity.seal) : ""
3341
+ }
3342
+ });
3343
+ if (normalized.machineInstallId !== install.machineInstallId) {
3344
+ return {
3345
+ registry: normalized,
3346
+ needsRepair: true,
3347
+ repairReason: "machine_install_id_mismatch"
3348
+ };
3349
+ }
3350
+ const expectedSeal = computeSeal(base);
3351
+ if (!normalized.integrity.seal || normalized.integrity.seal !== expectedSeal) {
3352
+ return {
3353
+ registry: normalized,
3354
+ needsRepair: true,
3355
+ repairReason: "integrity_seal_mismatch"
3356
+ };
3357
+ }
3358
+ return { registry: normalized, needsRepair: false, repairReason: null };
3086
3359
  } catch (e) {
3087
- return __spreadValues({}, DEFAULT_REGISTRY);
3360
+ return {
3361
+ registry: defaultRegistry,
3362
+ needsRepair: true,
3363
+ repairReason: "registry_corrupt"
3364
+ };
3088
3365
  }
3089
3366
  }
3367
+ function loadGlobalRegistry() {
3368
+ return inspectGlobalRegistry().registry;
3369
+ }
3090
3370
  function saveGlobalRegistry(registry) {
3091
- const dir = getGlobalRunTrimDir();
3092
- if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
3093
- import_fs8.default.writeFileSync(getGlobalRegistryPath(), JSON.stringify(registry, null, 2), "utf-8");
3371
+ saveRegistryWithSeal(registry);
3094
3372
  }
3095
3373
  async function getCurrentRepoIdentity(cwd = process.cwd()) {
3096
3374
  const normalizedPath = normalizeRepoPath(cwd);
@@ -3120,36 +3398,71 @@ function findTrackedRepo(trackedRepos, currentRepo) {
3120
3398
  return byPath != null ? byPath : null;
3121
3399
  }
3122
3400
  async function assertFreeRepoAllowed(cwd = process.cwd()) {
3123
- const registry = loadGlobalRegistry();
3401
+ const inspected = inspectGlobalRegistry();
3402
+ const registry = inspected.registry;
3124
3403
  const currentRepo = await getCurrentRepoIdentity(cwd);
3125
3404
  const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
3126
- if (registry.plan !== "free") {
3127
- return { allowed: true, plan: registry.plan, currentRepo, trackedRepo };
3405
+ const base = {
3406
+ plan: registry.plan,
3407
+ currentRepo,
3408
+ trackedRepo,
3409
+ registryPath: getGlobalRegistryPath()
3410
+ };
3411
+ if (inspected.needsRepair) {
3412
+ return __spreadProps(__spreadValues({}, base), {
3413
+ allowed: false,
3414
+ status: "blocked_repair",
3415
+ repairRequired: true,
3416
+ repairReason: inspected.repairReason
3417
+ });
3128
3418
  }
3129
- if (trackedRepo) {
3130
- return { allowed: true, plan: registry.plan, currentRepo, trackedRepo };
3419
+ if (registry.plan !== "free") {
3420
+ return __spreadProps(__spreadValues({}, base), {
3421
+ allowed: true,
3422
+ status: "allowed",
3423
+ repairRequired: false,
3424
+ repairReason: null
3425
+ });
3131
3426
  }
3132
- if (registry.trackedRepos.length === 0) {
3133
- return { allowed: true, plan: registry.plan, currentRepo, trackedRepo: null };
3427
+ if (trackedRepo || registry.trackedRepos.length === 0) {
3428
+ return __spreadProps(__spreadValues({}, base), {
3429
+ allowed: true,
3430
+ status: "allowed",
3431
+ repairRequired: false,
3432
+ repairReason: null
3433
+ });
3134
3434
  }
3135
- return {
3435
+ return __spreadProps(__spreadValues({}, base), {
3136
3436
  allowed: false,
3137
- plan: registry.plan,
3138
- currentRepo,
3437
+ status: "blocked_limit",
3438
+ repairRequired: false,
3439
+ repairReason: null,
3139
3440
  trackedRepo: registry.trackedRepos[0]
3140
- };
3441
+ });
3141
3442
  }
3142
3443
  async function registerCurrentRepo(cwd = process.cwd()) {
3444
+ const check = await assertFreeRepoAllowed(cwd);
3445
+ if (!check.allowed && check.status === "blocked_repair") {
3446
+ throw new Error("runtrim_local_state_repair_required");
3447
+ }
3143
3448
  const registry = loadGlobalRegistry();
3144
3449
  const currentRepo = await getCurrentRepoIdentity(cwd);
3145
- const now = (/* @__PURE__ */ new Date()).toISOString();
3450
+ const now = nowIso();
3146
3451
  const existing = findTrackedRepo(registry.trackedRepos, currentRepo);
3147
3452
  if (existing) {
3148
3453
  existing.lastSeenAt = now;
3149
3454
  existing.name = currentRepo.name;
3150
3455
  existing.path = currentRepo.path;
3151
3456
  existing.gitRemote = currentRepo.gitRemote;
3152
- saveGlobalRegistry(registry);
3457
+ registry.updatedAt = now;
3458
+ registry.lastKnownRepo = {
3459
+ id: currentRepo.id,
3460
+ name: currentRepo.name,
3461
+ path: currentRepo.path,
3462
+ gitRemote: currentRepo.gitRemote,
3463
+ lastSeenAt: now
3464
+ };
3465
+ saveRegistryWithSeal(registry);
3153
3466
  return existing;
3154
3467
  }
3155
3468
  const entry = {
@@ -3160,24 +3473,110 @@ async function registerCurrentRepo(cwd = process.cwd()) {
3160
3473
  createdAt: now,
3161
3474
  lastSeenAt: now
3162
3475
  };
3163
- registry.trackedRepos.push(entry);
3164
- saveGlobalRegistry(registry);
3476
+ registry.trackedRepos = [entry];
3477
+ registry.updatedAt = now;
3478
+ registry.lastKnownRepo = {
3479
+ id: entry.id,
3480
+ name: entry.name,
3481
+ path: entry.path,
3482
+ gitRemote: entry.gitRemote,
3483
+ lastSeenAt: now
3484
+ };
3485
+ saveRegistryWithSeal(registry);
3165
3486
  return entry;
3166
3487
  }
3488
+ async function repairGlobalRegistry(cwd = process.cwd(), options = {}) {
3489
+ const before = await assertFreeRepoAllowed(cwd);
3490
+ if (!before.repairRequired) {
3491
+ return { repaired: false, check: before };
3492
+ }
3493
+ const install = ensureInstallState();
3494
+ const now = nowIso();
3495
+ const repaired = buildDefaultRegistry(install);
3496
+ repaired.createdAt = now;
3497
+ repaired.updatedAt = now;
3498
+ if (options.useCurrentRepo) {
3499
+ const currentRepo = await getCurrentRepoIdentity(cwd);
3500
+ repaired.trackedRepos = [
3501
+ {
3502
+ id: currentRepo.id,
3503
+ name: currentRepo.name,
3504
+ path: currentRepo.path,
3505
+ gitRemote: currentRepo.gitRemote,
3506
+ createdAt: now,
3507
+ lastSeenAt: now
3508
+ }
3509
+ ];
3510
+ repaired.lastKnownRepo = {
3511
+ id: currentRepo.id,
3512
+ name: currentRepo.name,
3513
+ path: currentRepo.path,
3514
+ gitRemote: currentRepo.gitRemote,
3515
+ lastSeenAt: now
3516
+ };
3517
+ }
3518
+ saveRegistryWithSeal(repaired);
3519
+ const check = await assertFreeRepoAllowed(cwd);
3520
+ return { repaired: true, check };
3521
+ }
3167
3522
  async function unlinkCurrentRepo(cwd = process.cwd(), force = false) {
3168
3523
  var _a2;
3524
+ const check = await assertFreeRepoAllowed(cwd);
3525
+ if (check.status === "blocked_repair") {
3526
+ if (!force) {
3527
+ return {
3528
+ removed: false,
3529
+ forced: false,
3530
+ currentRepo: check.currentRepo,
3531
+ trackedRepo: null
3532
+ };
3533
+ }
3534
+ const install = ensureInstallState();
3535
+ const repaired = buildDefaultRegistry(install);
3536
+ repaired.updatedAt = nowIso();
3537
+ repaired.lastKnownRepo = {
3538
+ id: check.currentRepo.id,
3539
+ name: check.currentRepo.name,
3540
+ path: check.currentRepo.path,
3541
+ gitRemote: check.currentRepo.gitRemote,
3542
+ lastSeenAt: repaired.updatedAt
3543
+ };
3544
+ saveRegistryWithSeal(repaired);
3545
+ return {
3546
+ removed: true,
3547
+ forced: true,
3548
+ currentRepo: check.currentRepo,
3549
+ trackedRepo: null
3550
+ };
3551
+ }
3169
3552
  const registry = loadGlobalRegistry();
3170
3553
  const currentRepo = await getCurrentRepoIdentity(cwd);
3171
3554
  const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
3172
3555
  if (trackedRepo) {
3173
3556
  registry.trackedRepos = registry.trackedRepos.filter((repo) => repo.id !== trackedRepo.id);
3174
- saveGlobalRegistry(registry);
3557
+ registry.updatedAt = nowIso();
3558
+ registry.lastKnownRepo = {
3559
+ id: trackedRepo.id,
3560
+ name: trackedRepo.name,
3561
+ path: trackedRepo.path,
3562
+ gitRemote: trackedRepo.gitRemote,
3563
+ lastSeenAt: registry.updatedAt
3564
+ };
3565
+ saveRegistryWithSeal(registry);
3175
3566
  return { removed: true, forced: false, currentRepo, trackedRepo };
3176
3567
  }
3177
3568
  if (force && registry.trackedRepos.length > 0) {
3178
3569
  const first = registry.trackedRepos[0];
3179
3570
  registry.trackedRepos = [];
3180
- saveGlobalRegistry(registry);
3571
+ registry.updatedAt = nowIso();
3572
+ registry.lastKnownRepo = {
3573
+ id: first.id,
3574
+ name: first.name,
3575
+ path: first.path,
3576
+ gitRemote: first.gitRemote,
3577
+ lastSeenAt: registry.updatedAt
3578
+ };
3579
+ saveRegistryWithSeal(registry);
3181
3580
  return { removed: true, forced: true, currentRepo, trackedRepo: first };
3182
3581
  }
3183
3582
  return {
@@ -4661,21 +5060,21 @@ var MEDIUM_PATH_PATTERNS = [
4661
5060
  ];
4662
5061
  function classifyFileRisk(files) {
4663
5062
  if (files.length === 0) return "low";
4664
- let maxRisk = "low";
5063
+ let maxRisk2 = "low";
4665
5064
  for (const f of files) {
4666
5065
  const norm = f.replace(/\\/g, "/").toLowerCase();
4667
5066
  if (CRITICAL_PATH_PATTERNS.some((p) => norm.includes(p))) {
4668
5067
  return "critical";
4669
5068
  }
4670
- if (HIGH_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk !== "high") {
4671
- maxRisk = "high";
5069
+ if (HIGH_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk2 !== "high") {
5070
+ maxRisk2 = "high";
4672
5071
  continue;
4673
5072
  }
4674
- if (MEDIUM_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk === "low") {
4675
- maxRisk = "medium";
5073
+ if (MEDIUM_PATH_PATTERNS.some((p) => norm.includes(p)) && maxRisk2 === "low") {
5074
+ maxRisk2 = "medium";
4676
5075
  }
4677
5076
  }
4678
- return maxRisk;
5077
+ return maxRisk2;
4679
5078
  }
4680
5079
  function isSensitivePath(filePath) {
4681
5080
  const norm = filePath.replace(/\\/g, "/").toLowerCase();
@@ -5018,6 +5417,24 @@ function getLearningContext(cwd, task, runs) {
5018
5417
  // src/lib/run-planner.ts
5019
5418
  var FAST_PATH_CATEGORIES = /* @__PURE__ */ new Set(["ui", "docs", "tests", "unknown"]);
5020
5419
  var ALWAYS_CONTRACT_CATEGORIES = /* @__PURE__ */ new Set(["auth", "billing", "payment", "webhook", "database", "env", "middleware"]);
5420
+ var RISK_ORDER = ["low", "medium", "high", "critical"];
5421
+ 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;
5422
+ function maxRisk(a, b) {
5423
+ return RISK_ORDER[Math.max(RISK_ORDER.indexOf(a), RISK_ORDER.indexOf(b))];
5424
+ }
5425
+ function hasNegationNear3(text, index) {
5426
+ const start = Math.max(0, index - 64);
5427
+ const window = text.slice(start, index + 8);
5428
+ return NEGATION_PREFIX_RE3.test(window);
5429
+ }
5430
+ function hasPositiveKeywordMention3(taskLower, keyword) {
5431
+ let idx = taskLower.indexOf(keyword.toLowerCase());
5432
+ while (idx !== -1) {
5433
+ if (!hasNegationNear3(taskLower, idx)) return true;
5434
+ idx = taskLower.indexOf(keyword.toLowerCase(), idx + keyword.length);
5435
+ }
5436
+ return false;
5437
+ }
5021
5438
  function isFastPathEligible(risk, category, guardMode, hasExplicitPaths) {
5022
5439
  if (guardMode === "strict") return false;
5023
5440
  if (guardMode === "off") return true;
@@ -5033,8 +5450,16 @@ function generatePlan(cwd, task, runs, config, currentChangedFiles) {
5033
5450
  const compiler = compileTask(task);
5034
5451
  const guardMode = (_a2 = config.autoGuardMode) != null ? _a2 : "smart";
5035
5452
  const adapters = detectAdapters(cwd);
5036
- const riskFiles = compiler.explicitPaths.length > 0 ? compiler.explicitPaths : currentChangedFiles.length > 0 ? currentChangedFiles : [];
5037
- const rawRisk = classifyFileRisk(riskFiles);
5453
+ const riskFiles = compiler.explicitPaths.length > 0 ? compiler.explicitPaths : [];
5454
+ let rawRisk = classifyFileRisk(riskFiles);
5455
+ if (ALWAYS_CONTRACT_CATEGORIES.has(compiler.taskCategory)) {
5456
+ rawRisk = maxRisk(rawRisk, "high");
5457
+ }
5458
+ const lowerTask = task.toLowerCase();
5459
+ const criticalSystemMentions = ["auth", "billing", "payment", "webhook", "database", "migration", "middleware"].filter((k) => hasPositiveKeywordMention3(lowerTask, k)).length;
5460
+ if (criticalSystemMentions >= 2) {
5461
+ rawRisk = maxRisk(rawRisk, "critical");
5462
+ }
5038
5463
  const catScope = buildCategoryScope(
5039
5464
  compiler.taskCategory,
5040
5465
  true,
@@ -5158,6 +5583,22 @@ var FAST_LOCAL_KEYWORDS = [
5158
5583
  "responsive",
5159
5584
  "ui polish"
5160
5585
  ];
5586
+ 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;
5587
+ function hasNegationNear4(text, index) {
5588
+ const start = Math.max(0, index - 64);
5589
+ const window = text.slice(start, index + 8);
5590
+ return NEGATION_PREFIX_RE4.test(window);
5591
+ }
5592
+ function hasPositiveKeywordMention4(task, keyword) {
5593
+ const lowerTask = task.toLowerCase();
5594
+ const lowerKeyword = keyword.toLowerCase();
5595
+ let idx = lowerTask.indexOf(lowerKeyword);
5596
+ while (idx !== -1) {
5597
+ if (!hasNegationNear4(lowerTask, idx)) return true;
5598
+ idx = lowerTask.indexOf(lowerKeyword, idx + lowerKeyword.length);
5599
+ }
5600
+ return false;
5601
+ }
5161
5602
  function normalize(s) {
5162
5603
  return (s != null ? s : "").toLowerCase();
5163
5604
  }
@@ -5216,9 +5657,9 @@ function recommendProviderRouting(ctx) {
5216
5657
  const changedFiles = (_d = ctx.changedFiles) != null ? _d : [];
5217
5658
  const learnedContext = (_e = ctx.learnedContext) != null ? _e : [];
5218
5659
  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));
5660
+ const highRiskByKeyword = HIGH_RISK_KEYWORDS.some((k) => hasPositiveKeywordMention4(task, k) || hasPositiveKeywordMention4(category, k));
5220
5661
  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;
5662
+ const multiCritical = ["auth", "billing", "database", "webhook", "payment", "migration"].filter((k) => hasPositiveKeywordMention4(task, k)).length >= 2;
5222
5663
  const broadTask = !ctx.explicitScope && task.split(/\s+/).length > 16;
5223
5664
  const hasSensitiveFiles = sensitiveAreas.length > 0 || changedFiles.some((f) => HIGH_RISK_KEYWORDS.some((k) => normalize(f).includes(k)));
5224
5665
  const noLearning = learnedContext.length === 0 && ((_f = ctx.similarRunsCount) != null ? _f : 0) === 0;
@@ -5483,6 +5924,19 @@ async function getSensitivePathStates(cwd) {
5483
5924
  return /* @__PURE__ */ new Map();
5484
5925
  }
5485
5926
  }
5927
+ function extractBoundaryLabels(forbiddenScope) {
5928
+ const labels = /* @__PURE__ */ new Set();
5929
+ for (const line of forbiddenScope) {
5930
+ const lower = line.toLowerCase();
5931
+ if (lower.includes("billing") || lower.includes("subscription") || lower.includes("payment")) labels.add("billing");
5932
+ if (lower.includes("auth") || lower.includes("session") || lower.includes("jwt")) labels.add("auth");
5933
+ if (lower.includes("middleware") || lower.includes("proxy")) labels.add("middleware");
5934
+ if (lower.includes(".env") || lower.includes("secret")) labels.add("env");
5935
+ if (lower.includes("cli")) labels.add("cli");
5936
+ if (lower.includes("mcp")) labels.add("mcp");
5937
+ }
5938
+ return [...labels];
5939
+ }
5486
5940
  function nowId() {
5487
5941
  const d = /* @__PURE__ */ new Date();
5488
5942
  const pad = (n) => String(n).padStart(2, "0");
@@ -5544,8 +5998,21 @@ function buildApprovalLevel(risk, task, category) {
5544
5998
  const text = `${task}
5545
5999
  ${category}`.toLowerCase();
5546
6000
  const highSystems = ["auth", "billing", "payment", "dodo", "stripe", "webhook", "database", "migration", "rls", "middleware", "env", "secret"];
6001
+ const hasNegationNear5 = (source, index) => {
6002
+ const start = Math.max(0, index - 64);
6003
+ const window = source.slice(start, index + 8);
6004
+ 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);
6005
+ };
6006
+ const hasPositiveKeyword = (source, keyword) => {
6007
+ let idx = source.indexOf(keyword);
6008
+ while (idx !== -1) {
6009
+ if (!hasNegationNear5(source, idx)) return true;
6010
+ idx = source.indexOf(keyword, idx + keyword.length);
6011
+ }
6012
+ return false;
6013
+ };
5547
6014
  if (risk === "high" || risk === "critical") return "required";
5548
- if (highSystems.some((k) => text.includes(k))) return "required";
6015
+ if (highSystems.some((k) => hasPositiveKeyword(text, k))) return "required";
5549
6016
  if (risk === "medium") return "recommended";
5550
6017
  return "no";
5551
6018
  }
@@ -5586,6 +6053,7 @@ function writePreviewArtifacts(cwd, preview) {
5586
6053
  "",
5587
6054
  "Forbidden:",
5588
6055
  ...preview.forbiddenScope.length > 0 ? preview.forbiddenScope.slice(0, 8).map((f) => `- ${f}`) : ["- none"],
6056
+ ...preview.boundariesDetected.length > 0 ? ["", `Boundaries detected: ${preview.boundariesDetected.join(", ")} will be forbidden, not treated as active scope.`] : [],
5589
6057
  "",
5590
6058
  "Learned context:",
5591
6059
  ...preview.learnedContext.length > 0 ? preview.learnedContext.map((x) => `- ${x}`) : ["- learning not available yet"],
@@ -5625,6 +6093,9 @@ async function runAgentPreview(task) {
5625
6093
  console.log("");
5626
6094
  console.log(GO_ACCENT.bold("Forbidden"));
5627
6095
  for (const item of preview.forbiddenScope.slice(0, 6)) console.log(DIM(" - ") + chalk.white(item));
6096
+ if (preview.boundariesDetected.length > 0) {
6097
+ console.log(DIM(" Boundaries detected: ") + chalk.white(`${preview.boundariesDetected.join(", ")} will be forbidden, not treated as active scope.`));
6098
+ }
5628
6099
  console.log("");
5629
6100
  console.log(GO_ACCENT.bold("Patch strategy"));
5630
6101
  for (let i = 0; i < preview.patchStrategy.length; i += 1) {
@@ -5689,6 +6160,7 @@ async function buildAgentPreview(task) {
5689
6160
  filesToInspect,
5690
6161
  allowedScope: contract.contract.relevantScope,
5691
6162
  forbiddenScope: contract.contract.forbiddenScope,
6163
+ boundariesDetected: extractBoundaryLabels(contract.contract.forbiddenScope),
5692
6164
  sensitiveAreas: [...contract.contract.sensitiveScope, ...plan.sensitiveAreas].slice(0, 10),
5693
6165
  stopRules: contract.contract.stopRules,
5694
6166
  successCriteria: contract.contract.successCriteria,
@@ -7005,6 +7477,24 @@ async function buildRuntrimCreateContractMcp(cwd, args) {
7005
7477
  isError: true
7006
7478
  };
7007
7479
  }
7480
+ const repoCheck = await assertFreeRepoAllowed(cwd);
7481
+ if (!repoCheck.allowed) {
7482
+ 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.";
7483
+ const blockedPayload = {
7484
+ contract_created: false,
7485
+ task: taskRaw,
7486
+ error: repoCheck.status === "blocked_repair" ? "repo_registry_repair_required" : "repo_limit_blocked",
7487
+ guidance,
7488
+ next_action: guidance,
7489
+ finish_command: "runtrim finish",
7490
+ approval_command_example: 'runtrim approve "Allow <path> for this run only"'
7491
+ };
7492
+ return {
7493
+ content: [{ type: "text", text: JSON.stringify(blockedPayload, null, 2) }],
7494
+ structuredContent: blockedPayload,
7495
+ isError: true
7496
+ };
7497
+ }
7008
7498
  const latest = loadLatestRun(cwd);
7009
7499
  if ((latest == null ? void 0 : latest.status) === "guarded") {
7010
7500
  const blockedPayload = {
@@ -7666,6 +8156,21 @@ function isInteractiveTerminal() {
7666
8156
  async function ensureRepoAllowedForFree(cwd) {
7667
8157
  var _a2, _b;
7668
8158
  const check = await assertFreeRepoAllowed(cwd);
8159
+ if (check.status === "blocked_repair") {
8160
+ console.log(chalk.yellow(" RunTrim local state needs repair."));
8161
+ console.log(chalk.yellow(" Free includes 1 tracked repo."));
8162
+ console.log(chalk.yellow(" Your local repo registry changed unexpectedly."));
8163
+ console.log("");
8164
+ console.log(DIM(" Next:"));
8165
+ console.log(chalk.white(" - runtrim repo status"));
8166
+ console.log(chalk.white(" - runtrim repo repair"));
8167
+ console.log(chalk.white(" - runtrim repo repair --use-current"));
8168
+ console.log(chalk.white(" - runtrim repo unlink --force"));
8169
+ console.log(chalk.white(" - upgrade to Builder for unlimited repos"));
8170
+ console.log(chalk.white(" - sign in to restore cloud entitlements"));
8171
+ console.log("");
8172
+ return false;
8173
+ }
7669
8174
  if (check.allowed) {
7670
8175
  await registerCurrentRepo(cwd);
7671
8176
  return true;
@@ -7679,9 +8184,12 @@ async function ensureRepoAllowedForFree(cwd) {
7679
8184
  console.log(chalk.white(` ${check.currentRepo.path}`));
7680
8185
  console.log("");
7681
8186
  console.log(DIM(" Next:"));
7682
- console.log(chalk.white(" - continue in the tracked repo"));
7683
- console.log(chalk.white(" - unlink the tracked repo with runtrim repo unlink --force"));
7684
- console.log(chalk.white(" - join Builder early access for unlimited repos"));
8187
+ console.log(
8188
+ chalk.white(
8189
+ " 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."
8190
+ )
8191
+ );
8192
+ console.log(chalk.white(" Agent instructions were not installed because this repo is not tracked."));
7685
8193
  console.log("");
7686
8194
  console.log(
7687
8195
  DIM(
@@ -8048,7 +8556,8 @@ var PROTOCOL_POINTER_BLOCK = `
8048
8556
  ${PROTOCOL_BLOCK_START}
8049
8557
  This repo uses RunTrim as the guarded AI coding protocol.
8050
8558
  Before editing code, read RUNTRIM.md.
8051
- Start every task with: runtrim go "<task>"
8559
+ Start every task with: runtrim start
8560
+ Then run: runtrim agent "Your task" --copy
8052
8561
  Stay inside .runtrim/contracts/latest.md.
8053
8562
  After edits, ask the user to run: runtrim finish
8054
8563
  ${PROTOCOL_BLOCK_END}
@@ -8744,6 +9253,8 @@ program.command("execute <task>").description("Create a controlled execution pac
8744
9253
  var agentCommand = program.command("agent").description("Start a guarded AI coding run with contract, scope, memory, and handoff");
8745
9254
  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) => {
8746
9255
  if (task == null ? void 0 : task.trim()) {
9256
+ const allowed = await ensureRepoAllowedForFree(process.cwd());
9257
+ if (!allowed) return;
8747
9258
  const normalizedTask = (task != null ? task : "").trim();
8748
9259
  if (options == null ? void 0 : options.bridge) {
8749
9260
  const bridge = await ensureBridgeRunningForAgent(process.cwd());
@@ -9212,12 +9723,54 @@ repoCommand.command("status").description("Show local tracked repo status").acti
9212
9723
  console.log(DIM(" Current repo ") + chalk.white(identity.path));
9213
9724
  console.log(DIM(" Tracked repo ") + chalk.white((_b = tracked == null ? void 0 : tracked.path) != null ? _b : "(none)"));
9214
9725
  console.log(DIM(" Allowed ") + chalk.white(check.allowed ? "yes" : "no"));
9726
+ console.log(DIM(" State ") + chalk.white(check.status));
9215
9727
  console.log("");
9728
+ if (check.status === "blocked_repair") {
9729
+ console.log(chalk.yellow(" RunTrim local state needs repair."));
9730
+ console.log(chalk.yellow(" Free includes 1 tracked repo."));
9731
+ console.log(chalk.yellow(" The local repo registry changed unexpectedly."));
9732
+ console.log(DIM(" Run: runtrim repo repair"));
9733
+ console.log("");
9734
+ }
9216
9735
  if (tracked) {
9217
9736
  console.log(DIM(" A tracked repo is one codebase with its own .runtrim workspace."));
9218
9737
  console.log("");
9219
9738
  }
9220
9739
  });
9740
+ 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) => {
9741
+ const cwd = process.cwd();
9742
+ const before = await assertFreeRepoAllowed(cwd);
9743
+ console.log("");
9744
+ console.log(BOLD("RunTrim") + DIM(" repo repair"));
9745
+ console.log("");
9746
+ if (!before.repairRequired) {
9747
+ console.log(DIM(" Local state is healthy. No repair required."));
9748
+ console.log("");
9749
+ return;
9750
+ }
9751
+ if (!options.useCurrent) {
9752
+ console.log(chalk.yellow(" RunTrim local state needs repair."));
9753
+ console.log(chalk.yellow(" Free includes 1 tracked repo."));
9754
+ console.log(chalk.yellow(" Your local repo registry changed unexpectedly."));
9755
+ console.log("");
9756
+ console.log(DIM(" Safe next actions:"));
9757
+ console.log(chalk.white(" - runtrim repo repair --use-current"));
9758
+ console.log(chalk.white(" - runtrim repo unlink --force"));
9759
+ console.log(chalk.white(" - upgrade to Builder for unlimited repos"));
9760
+ console.log(chalk.white(" - sign in to restore cloud entitlements"));
9761
+ console.log("");
9762
+ return;
9763
+ }
9764
+ const result = await repairGlobalRegistry(cwd, { useCurrentRepo: true });
9765
+ if (result.repaired) {
9766
+ console.log(ACCENT.bold(" Local registry repaired."));
9767
+ console.log(DIM(" Current repo is now the tracked Free repo."));
9768
+ console.log("");
9769
+ return;
9770
+ }
9771
+ console.log(DIM(" No repair changes applied."));
9772
+ console.log("");
9773
+ });
9221
9774
  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) => {
9222
9775
  const cwd = process.cwd();
9223
9776
  const result = await unlinkCurrentRepo(cwd, Boolean(options.force));
@@ -10384,7 +10937,7 @@ program.command("continue").description("Create a safe continuation prompt from
10384
10937
  const audit = (_a2 = loadProjectAudit(cwd)) != null ? _a2 : performBaselineProjectAudit(cwd, null);
10385
10938
  const reason = normalizeContinuationReason(options.reason);
10386
10939
  const agent = normalizeContinuationAgent(options.agent, config.defaultAgent);
10387
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
10940
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
10388
10941
  const continuationPath = resolveContinuationPath(cwd);
10389
10942
  const latestPromptPath = resolvePromptPath(config, cwd);
10390
10943
  const latestPrompt = import_fs13.default.existsSync(latestPromptPath) ? import_fs13.default.readFileSync(latestPromptPath, "utf-8").trim() : "";
@@ -10574,14 +11127,14 @@ program.command("continue").description("Create a safe continuation prompt from
10574
11127
  import_fs13.default.writeFileSync(continuationPath, continuationPrompt, "utf-8");
10575
11128
  const copied = await copyToClipboardSafe(continuationPrompt);
10576
11129
  if (memory) {
10577
- const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso);
11130
+ const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso2);
10578
11131
  import_fs13.default.writeFileSync(import_path13.default.join(getConfigDir(cwd), "memory.md"), memoryWithContinuation, "utf-8");
10579
11132
  }
10580
11133
  if (hasConfig) {
10581
11134
  const nextConfig = __spreadProps(__spreadValues({}, config), {
10582
11135
  lastContinuationReason: reason,
10583
11136
  continuationPromptPath: continuationPath.replace(/\\/g, "/"),
10584
- continuationCreatedAt: nowIso
11137
+ continuationCreatedAt: nowIso2
10585
11138
  });
10586
11139
  saveConfig(nextConfig, cwd);
10587
11140
  }
@@ -11373,6 +11926,8 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
11373
11926
  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) => {
11374
11927
  var _a2, _b;
11375
11928
  const cwd = process.cwd();
11929
+ const allowed = await ensureRepoAllowedForFree(cwd);
11930
+ if (!allowed) return;
11376
11931
  console.log("");
11377
11932
  console.log(BOLD("RunTrim") + DIM(" cloud sync"));
11378
11933
  console.log("");