runtrim 0.1.17 → 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.
@@ -687,6 +687,28 @@ function buildCategoryScope(category, hasSrc, hasApp, hasPages) {
687
687
  "Check no regression in adjacent routes"
688
688
  ]
689
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
+ };
690
712
  default:
691
713
  return {
692
714
  allowedHints: [],
@@ -2598,7 +2620,7 @@ function buildSyncPayload(input) {
2598
2620
  var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t;
2599
2621
  const { cwd, projectName, config, projectAudit, memoryMarkdown, runs } = input;
2600
2622
  const latest = runs[0];
2601
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2623
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
2602
2624
  const localProjectId = buildLocalProjectId(cwd);
2603
2625
  const latestPromptText = readTextFileIfExists(import_path6.default.join(cwd, ".runtrim", "latest-prompt.md"));
2604
2626
  const continuationPromptText = readTextFileIfExists(
@@ -2660,7 +2682,7 @@ function buildSyncPayload(input) {
2660
2682
  name: resolveProjectName2(cwd, projectName, projectAudit == null ? void 0 : projectAudit.projectName),
2661
2683
  stack: (_a2 = projectAudit == null ? void 0 : projectAudit.detectedStack) != null ? _a2 : config.stack ? config.stack.split(",").map((s) => s.trim()).filter(Boolean) : ["auto"],
2662
2684
  packageManager: (_c = (_b = projectAudit == null ? void 0 : projectAudit.packageManager) != null ? _b : config.packageManager) != null ? _c : null,
2663
- lastUpdated: nowIso
2685
+ lastUpdated: nowIso2
2664
2686
  },
2665
2687
  memory: {
2666
2688
  markdown: memoryMarkdown,
@@ -2742,7 +2764,8 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2742
2764
  "## How to start an AI coding task",
2743
2765
  "",
2744
2766
  "```",
2745
- 'runtrim go "<task>"',
2767
+ "runtrim start",
2768
+ 'runtrim agent "Your task" --copy',
2746
2769
  "```",
2747
2770
  "",
2748
2771
  "RunTrim creates a scoped contract, loads project memory, and generates a guarded prompt.",
@@ -2763,7 +2786,7 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2763
2786
  "",
2764
2787
  "1. Read `.runtrim/contracts/latest.md`.",
2765
2788
  " - If `Status: active` \u2014 a live task exists. Follow the contract strictly.",
2766
- ' - 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`.',
2767
2790
  "2. Do not assume any prior task is still active.",
2768
2791
  "3. Stay inside the allowed scope defined in the contract.",
2769
2792
  "4. Stop and ask before touching any forbidden area.",
@@ -2788,7 +2811,8 @@ function writeRestingContract(cwd = process.cwd()) {
2788
2811
  "Start one with:",
2789
2812
  "",
2790
2813
  "```",
2791
- 'runtrim go "<your task>"',
2814
+ "runtrim start",
2815
+ 'runtrim agent "Your task" --copy',
2792
2816
  "```",
2793
2817
  "",
2794
2818
  "---",
@@ -2811,7 +2835,8 @@ function writeRestingMemory(cwd = process.cwd()) {
2811
2835
  "Start a new session with:",
2812
2836
  "",
2813
2837
  "```",
2814
- 'runtrim go "<your task>"',
2838
+ "runtrim start",
2839
+ 'runtrim agent "Your task" --copy',
2815
2840
  "```",
2816
2841
  "",
2817
2842
  "---",
@@ -2932,7 +2957,7 @@ function writeBridgeInstructions(cwd = process.cwd()) {
2932
2957
  "1. Read `RUNTRIM.md`.",
2933
2958
  "2. Read `.runtrim/contracts/latest.md`.",
2934
2959
  " - If `Status: active` \u2014 follow the contract strictly.",
2935
- ' - 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`.',
2936
2961
  "3. If the contract is active, read `.runtrim/memory/current.md` for session context.",
2937
2962
  " If no active session, read `.runtrim/memory/baseline.md` for project baseline.",
2938
2963
  "",
@@ -3006,6 +3031,28 @@ function buildBridgePrompt(contractText, ctx) {
3006
3031
  // src/lib/run-watch.ts
3007
3032
  function normalizeScopeKeywords2(scope) {
3008
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
+ ]);
3009
3056
  const words = /* @__PURE__ */ new Set();
3010
3057
  for (const line of scope) {
3011
3058
  const lower = line.toLowerCase();
@@ -3015,7 +3062,7 @@ function normalizeScopeKeywords2(scope) {
3015
3062
  }
3016
3063
  const cleaned = lower.replace(/[^a-z0-9_./\s-]/g, " ").split(/\s+/).filter(Boolean);
3017
3064
  for (const token of cleaned) {
3018
- if (token.length >= 4) words.add(token);
3065
+ if (token.length >= 4 && !genericStopwords.has(token)) words.add(token);
3019
3066
  }
3020
3067
  }
3021
3068
  return [...words];
@@ -3112,15 +3159,7 @@ var import_fs8 = __toESM(require("fs"), 1);
3112
3159
  var import_os = __toESM(require("os"), 1);
3113
3160
  var import_path8 = __toESM(require("path"), 1);
3114
3161
  var import_execa2 = require("execa");
3115
- var DEFAULT_REGISTRY = {
3116
- version: 1,
3117
- plan: "free",
3118
- trackedRepos: [],
3119
- telemetry: {
3120
- enabled: false,
3121
- anonymousId: ""
3122
- }
3123
- };
3162
+ var EMPTY_TELEMETRY = { enabled: false, anonymousId: "" };
3124
3163
  function normalizeRepoPath(input) {
3125
3164
  const resolved = import_path8.default.resolve(input);
3126
3165
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
@@ -3128,41 +3167,208 @@ function normalizeRepoPath(input) {
3128
3167
  function hashValue(value) {
3129
3168
  return import_crypto2.default.createHash("sha256").update(value).digest("hex").slice(0, 16);
3130
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
+ }
3131
3176
  function getGlobalRunTrimDir() {
3132
3177
  return import_path8.default.join(import_os.default.homedir(), ".runtrim");
3133
3178
  }
3134
3179
  function getGlobalRegistryPath() {
3135
3180
  return import_path8.default.join(getGlobalRunTrimDir(), "global.json");
3136
3181
  }
3137
- 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();
3138
3302
  const registryPath = getGlobalRegistryPath();
3139
- 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
+ }
3140
3314
  try {
3141
3315
  const raw = JSON.parse(import_fs8.default.readFileSync(registryPath, "utf-8"));
3142
- return {
3143
- 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,
3144
3320
  plan: raw.plan === "free" ? "free" : "free",
3145
- trackedRepos: Array.isArray(raw.trackedRepos) ? raw.trackedRepos.filter((item) => Boolean(item && typeof item === "object")).map((item) => ({
3146
- id: String(item.id || ""),
3147
- name: String(item.name || ""),
3148
- path: normalizeRepoPath(String(item.path || "")),
3149
- gitRemote: String(item.gitRemote || ""),
3150
- createdAt: String(item.createdAt || ""),
3151
- lastSeenAt: String(item.lastSeenAt || "")
3152
- })).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,
3153
3332
  telemetry: {
3154
3333
  enabled: typeof raw.telemetry === "object" && raw.telemetry !== null && Boolean(raw.telemetry.enabled),
3155
3334
  anonymousId: typeof raw.telemetry === "object" && raw.telemetry !== null && typeof raw.telemetry.anonymousId === "string" ? String(raw.telemetry.anonymousId).slice(0, 120) : ""
3156
3335
  }
3157
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 };
3158
3359
  } catch (e) {
3159
- return __spreadValues({}, DEFAULT_REGISTRY);
3360
+ return {
3361
+ registry: defaultRegistry,
3362
+ needsRepair: true,
3363
+ repairReason: "registry_corrupt"
3364
+ };
3160
3365
  }
3161
3366
  }
3367
+ function loadGlobalRegistry() {
3368
+ return inspectGlobalRegistry().registry;
3369
+ }
3162
3370
  function saveGlobalRegistry(registry) {
3163
- const dir = getGlobalRunTrimDir();
3164
- if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
3165
- import_fs8.default.writeFileSync(getGlobalRegistryPath(), JSON.stringify(registry, null, 2), "utf-8");
3371
+ saveRegistryWithSeal(registry);
3166
3372
  }
3167
3373
  async function getCurrentRepoIdentity(cwd = process.cwd()) {
3168
3374
  const normalizedPath = normalizeRepoPath(cwd);
@@ -3192,36 +3398,71 @@ function findTrackedRepo(trackedRepos, currentRepo) {
3192
3398
  return byPath != null ? byPath : null;
3193
3399
  }
3194
3400
  async function assertFreeRepoAllowed(cwd = process.cwd()) {
3195
- const registry = loadGlobalRegistry();
3401
+ const inspected = inspectGlobalRegistry();
3402
+ const registry = inspected.registry;
3196
3403
  const currentRepo = await getCurrentRepoIdentity(cwd);
3197
3404
  const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
3198
- if (registry.plan !== "free") {
3199
- 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
+ });
3200
3418
  }
3201
- if (trackedRepo) {
3202
- 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
+ });
3203
3426
  }
3204
- if (registry.trackedRepos.length === 0) {
3205
- 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
+ });
3206
3434
  }
3207
- return {
3435
+ return __spreadProps(__spreadValues({}, base), {
3208
3436
  allowed: false,
3209
- plan: registry.plan,
3210
- currentRepo,
3437
+ status: "blocked_limit",
3438
+ repairRequired: false,
3439
+ repairReason: null,
3211
3440
  trackedRepo: registry.trackedRepos[0]
3212
- };
3441
+ });
3213
3442
  }
3214
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
+ }
3215
3448
  const registry = loadGlobalRegistry();
3216
3449
  const currentRepo = await getCurrentRepoIdentity(cwd);
3217
- const now = (/* @__PURE__ */ new Date()).toISOString();
3450
+ const now = nowIso();
3218
3451
  const existing = findTrackedRepo(registry.trackedRepos, currentRepo);
3219
3452
  if (existing) {
3220
3453
  existing.lastSeenAt = now;
3221
3454
  existing.name = currentRepo.name;
3222
3455
  existing.path = currentRepo.path;
3223
3456
  existing.gitRemote = currentRepo.gitRemote;
3224
- 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);
3225
3466
  return existing;
3226
3467
  }
3227
3468
  const entry = {
@@ -3232,24 +3473,110 @@ async function registerCurrentRepo(cwd = process.cwd()) {
3232
3473
  createdAt: now,
3233
3474
  lastSeenAt: now
3234
3475
  };
3235
- registry.trackedRepos.push(entry);
3236
- 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);
3237
3486
  return entry;
3238
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
+ }
3239
3522
  async function unlinkCurrentRepo(cwd = process.cwd(), force = false) {
3240
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
+ }
3241
3552
  const registry = loadGlobalRegistry();
3242
3553
  const currentRepo = await getCurrentRepoIdentity(cwd);
3243
3554
  const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
3244
3555
  if (trackedRepo) {
3245
3556
  registry.trackedRepos = registry.trackedRepos.filter((repo) => repo.id !== trackedRepo.id);
3246
- 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);
3247
3566
  return { removed: true, forced: false, currentRepo, trackedRepo };
3248
3567
  }
3249
3568
  if (force && registry.trackedRepos.length > 0) {
3250
3569
  const first = registry.trackedRepos[0];
3251
3570
  registry.trackedRepos = [];
3252
- 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);
3253
3580
  return { removed: true, forced: true, currentRepo, trackedRepo: first };
3254
3581
  }
3255
3582
  return {
@@ -7150,6 +7477,24 @@ async function buildRuntrimCreateContractMcp(cwd, args) {
7150
7477
  isError: true
7151
7478
  };
7152
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
+ }
7153
7498
  const latest = loadLatestRun(cwd);
7154
7499
  if ((latest == null ? void 0 : latest.status) === "guarded") {
7155
7500
  const blockedPayload = {
@@ -7811,6 +8156,21 @@ function isInteractiveTerminal() {
7811
8156
  async function ensureRepoAllowedForFree(cwd) {
7812
8157
  var _a2, _b;
7813
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
+ }
7814
8174
  if (check.allowed) {
7815
8175
  await registerCurrentRepo(cwd);
7816
8176
  return true;
@@ -7824,9 +8184,12 @@ async function ensureRepoAllowedForFree(cwd) {
7824
8184
  console.log(chalk.white(` ${check.currentRepo.path}`));
7825
8185
  console.log("");
7826
8186
  console.log(DIM(" Next:"));
7827
- console.log(chalk.white(" - continue in the tracked repo"));
7828
- console.log(chalk.white(" - unlink the tracked repo with runtrim repo unlink --force"));
7829
- 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."));
7830
8193
  console.log("");
7831
8194
  console.log(
7832
8195
  DIM(
@@ -8193,7 +8556,8 @@ var PROTOCOL_POINTER_BLOCK = `
8193
8556
  ${PROTOCOL_BLOCK_START}
8194
8557
  This repo uses RunTrim as the guarded AI coding protocol.
8195
8558
  Before editing code, read RUNTRIM.md.
8196
- Start every task with: runtrim go "<task>"
8559
+ Start every task with: runtrim start
8560
+ Then run: runtrim agent "Your task" --copy
8197
8561
  Stay inside .runtrim/contracts/latest.md.
8198
8562
  After edits, ask the user to run: runtrim finish
8199
8563
  ${PROTOCOL_BLOCK_END}
@@ -8889,6 +9253,8 @@ program.command("execute <task>").description("Create a controlled execution pac
8889
9253
  var agentCommand = program.command("agent").description("Start a guarded AI coding run with contract, scope, memory, and handoff");
8890
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) => {
8891
9255
  if (task == null ? void 0 : task.trim()) {
9256
+ const allowed = await ensureRepoAllowedForFree(process.cwd());
9257
+ if (!allowed) return;
8892
9258
  const normalizedTask = (task != null ? task : "").trim();
8893
9259
  if (options == null ? void 0 : options.bridge) {
8894
9260
  const bridge = await ensureBridgeRunningForAgent(process.cwd());
@@ -9357,12 +9723,54 @@ repoCommand.command("status").description("Show local tracked repo status").acti
9357
9723
  console.log(DIM(" Current repo ") + chalk.white(identity.path));
9358
9724
  console.log(DIM(" Tracked repo ") + chalk.white((_b = tracked == null ? void 0 : tracked.path) != null ? _b : "(none)"));
9359
9725
  console.log(DIM(" Allowed ") + chalk.white(check.allowed ? "yes" : "no"));
9726
+ console.log(DIM(" State ") + chalk.white(check.status));
9360
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
+ }
9361
9735
  if (tracked) {
9362
9736
  console.log(DIM(" A tracked repo is one codebase with its own .runtrim workspace."));
9363
9737
  console.log("");
9364
9738
  }
9365
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
+ });
9366
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) => {
9367
9775
  const cwd = process.cwd();
9368
9776
  const result = await unlinkCurrentRepo(cwd, Boolean(options.force));
@@ -10529,7 +10937,7 @@ program.command("continue").description("Create a safe continuation prompt from
10529
10937
  const audit = (_a2 = loadProjectAudit(cwd)) != null ? _a2 : performBaselineProjectAudit(cwd, null);
10530
10938
  const reason = normalizeContinuationReason(options.reason);
10531
10939
  const agent = normalizeContinuationAgent(options.agent, config.defaultAgent);
10532
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
10940
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
10533
10941
  const continuationPath = resolveContinuationPath(cwd);
10534
10942
  const latestPromptPath = resolvePromptPath(config, cwd);
10535
10943
  const latestPrompt = import_fs13.default.existsSync(latestPromptPath) ? import_fs13.default.readFileSync(latestPromptPath, "utf-8").trim() : "";
@@ -10719,14 +11127,14 @@ program.command("continue").description("Create a safe continuation prompt from
10719
11127
  import_fs13.default.writeFileSync(continuationPath, continuationPrompt, "utf-8");
10720
11128
  const copied = await copyToClipboardSafe(continuationPrompt);
10721
11129
  if (memory) {
10722
- const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso);
11130
+ const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso2);
10723
11131
  import_fs13.default.writeFileSync(import_path13.default.join(getConfigDir(cwd), "memory.md"), memoryWithContinuation, "utf-8");
10724
11132
  }
10725
11133
  if (hasConfig) {
10726
11134
  const nextConfig = __spreadProps(__spreadValues({}, config), {
10727
11135
  lastContinuationReason: reason,
10728
11136
  continuationPromptPath: continuationPath.replace(/\\/g, "/"),
10729
- continuationCreatedAt: nowIso
11137
+ continuationCreatedAt: nowIso2
10730
11138
  });
10731
11139
  saveConfig(nextConfig, cwd);
10732
11140
  }
@@ -11518,6 +11926,8 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
11518
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) => {
11519
11927
  var _a2, _b;
11520
11928
  const cwd = process.cwd();
11929
+ const allowed = await ensureRepoAllowedForFree(cwd);
11930
+ if (!allowed) return;
11521
11931
  console.log("");
11522
11932
  console.log(BOLD("RunTrim") + DIM(" cloud sync"));
11523
11933
  console.log("");