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.
@@ -666,6 +666,28 @@ function buildCategoryScope(category, hasSrc, hasApp, hasPages) {
666
666
  "Check no regression in adjacent routes"
667
667
  ]
668
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
+ };
669
691
  default:
670
692
  return {
671
693
  allowedHints: [],
@@ -2577,7 +2599,7 @@ function buildSyncPayload(input) {
2577
2599
  var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t;
2578
2600
  const { cwd, projectName, config, projectAudit, memoryMarkdown, runs } = input;
2579
2601
  const latest = runs[0];
2580
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2602
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
2581
2603
  const localProjectId = buildLocalProjectId(cwd);
2582
2604
  const latestPromptText = readTextFileIfExists(path6.join(cwd, ".runtrim", "latest-prompt.md"));
2583
2605
  const continuationPromptText = readTextFileIfExists(
@@ -2639,7 +2661,7 @@ function buildSyncPayload(input) {
2639
2661
  name: resolveProjectName2(cwd, projectName, projectAudit == null ? void 0 : projectAudit.projectName),
2640
2662
  stack: (_a2 = projectAudit == null ? void 0 : projectAudit.detectedStack) != null ? _a2 : config.stack ? config.stack.split(",").map((s) => s.trim()).filter(Boolean) : ["auto"],
2641
2663
  packageManager: (_c = (_b = projectAudit == null ? void 0 : projectAudit.packageManager) != null ? _b : config.packageManager) != null ? _c : null,
2642
- lastUpdated: nowIso
2664
+ lastUpdated: nowIso2
2643
2665
  },
2644
2666
  memory: {
2645
2667
  markdown: memoryMarkdown,
@@ -2721,7 +2743,8 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2721
2743
  "## How to start an AI coding task",
2722
2744
  "",
2723
2745
  "```",
2724
- 'runtrim go "<task>"',
2746
+ "runtrim start",
2747
+ 'runtrim agent "Your task" --copy',
2725
2748
  "```",
2726
2749
  "",
2727
2750
  "RunTrim creates a scoped contract, loads project memory, and generates a guarded prompt.",
@@ -2742,7 +2765,7 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2742
2765
  "",
2743
2766
  "1. Read `.runtrim/contracts/latest.md`.",
2744
2767
  " - If `Status: active` \u2014 a live task exists. Follow the contract strictly.",
2745
- ' - 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`.',
2746
2769
  "2. Do not assume any prior task is still active.",
2747
2770
  "3. Stay inside the allowed scope defined in the contract.",
2748
2771
  "4. Stop and ask before touching any forbidden area.",
@@ -2767,7 +2790,8 @@ function writeRestingContract(cwd = process.cwd()) {
2767
2790
  "Start one with:",
2768
2791
  "",
2769
2792
  "```",
2770
- 'runtrim go "<your task>"',
2793
+ "runtrim start",
2794
+ 'runtrim agent "Your task" --copy',
2771
2795
  "```",
2772
2796
  "",
2773
2797
  "---",
@@ -2790,7 +2814,8 @@ function writeRestingMemory(cwd = process.cwd()) {
2790
2814
  "Start a new session with:",
2791
2815
  "",
2792
2816
  "```",
2793
- 'runtrim go "<your task>"',
2817
+ "runtrim start",
2818
+ 'runtrim agent "Your task" --copy',
2794
2819
  "```",
2795
2820
  "",
2796
2821
  "---",
@@ -2911,7 +2936,7 @@ function writeBridgeInstructions(cwd = process.cwd()) {
2911
2936
  "1. Read `RUNTRIM.md`.",
2912
2937
  "2. Read `.runtrim/contracts/latest.md`.",
2913
2938
  " - If `Status: active` \u2014 follow the contract strictly.",
2914
- ' - 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`.',
2915
2940
  "3. If the contract is active, read `.runtrim/memory/current.md` for session context.",
2916
2941
  " If no active session, read `.runtrim/memory/baseline.md` for project baseline.",
2917
2942
  "",
@@ -2985,6 +3010,28 @@ function buildBridgePrompt(contractText, ctx) {
2985
3010
  // src/lib/run-watch.ts
2986
3011
  function normalizeScopeKeywords2(scope) {
2987
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
+ ]);
2988
3035
  const words = /* @__PURE__ */ new Set();
2989
3036
  for (const line of scope) {
2990
3037
  const lower = line.toLowerCase();
@@ -2994,7 +3041,7 @@ function normalizeScopeKeywords2(scope) {
2994
3041
  }
2995
3042
  const cleaned = lower.replace(/[^a-z0-9_./\s-]/g, " ").split(/\s+/).filter(Boolean);
2996
3043
  for (const token of cleaned) {
2997
- if (token.length >= 4) words.add(token);
3044
+ if (token.length >= 4 && !genericStopwords.has(token)) words.add(token);
2998
3045
  }
2999
3046
  }
3000
3047
  return [...words];
@@ -3091,15 +3138,7 @@ import fs8 from "fs";
3091
3138
  import os from "os";
3092
3139
  import path8 from "path";
3093
3140
  import { execa as execa2 } from "execa";
3094
- var DEFAULT_REGISTRY = {
3095
- version: 1,
3096
- plan: "free",
3097
- trackedRepos: [],
3098
- telemetry: {
3099
- enabled: false,
3100
- anonymousId: ""
3101
- }
3102
- };
3141
+ var EMPTY_TELEMETRY = { enabled: false, anonymousId: "" };
3103
3142
  function normalizeRepoPath(input) {
3104
3143
  const resolved = path8.resolve(input);
3105
3144
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
@@ -3107,41 +3146,208 @@ function normalizeRepoPath(input) {
3107
3146
  function hashValue(value) {
3108
3147
  return crypto.createHash("sha256").update(value).digest("hex").slice(0, 16);
3109
3148
  }
3149
+ function nowIso() {
3150
+ return (/* @__PURE__ */ new Date()).toISOString();
3151
+ }
3152
+ function randomId(prefix) {
3153
+ return `${prefix}_${crypto.randomBytes(12).toString("hex")}`;
3154
+ }
3110
3155
  function getGlobalRunTrimDir() {
3111
3156
  return path8.join(os.homedir(), ".runtrim");
3112
3157
  }
3113
3158
  function getGlobalRegistryPath() {
3114
3159
  return path8.join(getGlobalRunTrimDir(), "global.json");
3115
3160
  }
3116
- 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();
3117
3281
  const registryPath = getGlobalRegistryPath();
3118
- 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
+ }
3119
3293
  try {
3120
3294
  const raw = JSON.parse(fs8.readFileSync(registryPath, "utf-8"));
3121
- return {
3122
- 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,
3123
3299
  plan: raw.plan === "free" ? "free" : "free",
3124
- trackedRepos: Array.isArray(raw.trackedRepos) ? raw.trackedRepos.filter((item) => Boolean(item && typeof item === "object")).map((item) => ({
3125
- id: String(item.id || ""),
3126
- name: String(item.name || ""),
3127
- path: normalizeRepoPath(String(item.path || "")),
3128
- gitRemote: String(item.gitRemote || ""),
3129
- createdAt: String(item.createdAt || ""),
3130
- lastSeenAt: String(item.lastSeenAt || "")
3131
- })).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,
3132
3311
  telemetry: {
3133
3312
  enabled: typeof raw.telemetry === "object" && raw.telemetry !== null && Boolean(raw.telemetry.enabled),
3134
3313
  anonymousId: typeof raw.telemetry === "object" && raw.telemetry !== null && typeof raw.telemetry.anonymousId === "string" ? String(raw.telemetry.anonymousId).slice(0, 120) : ""
3135
3314
  }
3136
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 };
3137
3338
  } catch (e) {
3138
- return __spreadValues({}, DEFAULT_REGISTRY);
3339
+ return {
3340
+ registry: defaultRegistry,
3341
+ needsRepair: true,
3342
+ repairReason: "registry_corrupt"
3343
+ };
3139
3344
  }
3140
3345
  }
3346
+ function loadGlobalRegistry() {
3347
+ return inspectGlobalRegistry().registry;
3348
+ }
3141
3349
  function saveGlobalRegistry(registry) {
3142
- const dir = getGlobalRunTrimDir();
3143
- if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
3144
- fs8.writeFileSync(getGlobalRegistryPath(), JSON.stringify(registry, null, 2), "utf-8");
3350
+ saveRegistryWithSeal(registry);
3145
3351
  }
3146
3352
  async function getCurrentRepoIdentity(cwd = process.cwd()) {
3147
3353
  const normalizedPath = normalizeRepoPath(cwd);
@@ -3171,36 +3377,71 @@ function findTrackedRepo(trackedRepos, currentRepo) {
3171
3377
  return byPath != null ? byPath : null;
3172
3378
  }
3173
3379
  async function assertFreeRepoAllowed(cwd = process.cwd()) {
3174
- const registry = loadGlobalRegistry();
3380
+ const inspected = inspectGlobalRegistry();
3381
+ const registry = inspected.registry;
3175
3382
  const currentRepo = await getCurrentRepoIdentity(cwd);
3176
3383
  const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
3177
- if (registry.plan !== "free") {
3178
- 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
+ });
3179
3397
  }
3180
- if (trackedRepo) {
3181
- 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
+ });
3182
3405
  }
3183
- if (registry.trackedRepos.length === 0) {
3184
- 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
+ });
3185
3413
  }
3186
- return {
3414
+ return __spreadProps(__spreadValues({}, base), {
3187
3415
  allowed: false,
3188
- plan: registry.plan,
3189
- currentRepo,
3416
+ status: "blocked_limit",
3417
+ repairRequired: false,
3418
+ repairReason: null,
3190
3419
  trackedRepo: registry.trackedRepos[0]
3191
- };
3420
+ });
3192
3421
  }
3193
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
+ }
3194
3427
  const registry = loadGlobalRegistry();
3195
3428
  const currentRepo = await getCurrentRepoIdentity(cwd);
3196
- const now = (/* @__PURE__ */ new Date()).toISOString();
3429
+ const now = nowIso();
3197
3430
  const existing = findTrackedRepo(registry.trackedRepos, currentRepo);
3198
3431
  if (existing) {
3199
3432
  existing.lastSeenAt = now;
3200
3433
  existing.name = currentRepo.name;
3201
3434
  existing.path = currentRepo.path;
3202
3435
  existing.gitRemote = currentRepo.gitRemote;
3203
- 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);
3204
3445
  return existing;
3205
3446
  }
3206
3447
  const entry = {
@@ -3211,24 +3452,110 @@ async function registerCurrentRepo(cwd = process.cwd()) {
3211
3452
  createdAt: now,
3212
3453
  lastSeenAt: now
3213
3454
  };
3214
- registry.trackedRepos.push(entry);
3215
- 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);
3216
3465
  return entry;
3217
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
+ }
3218
3501
  async function unlinkCurrentRepo(cwd = process.cwd(), force = false) {
3219
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
+ }
3220
3531
  const registry = loadGlobalRegistry();
3221
3532
  const currentRepo = await getCurrentRepoIdentity(cwd);
3222
3533
  const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
3223
3534
  if (trackedRepo) {
3224
3535
  registry.trackedRepos = registry.trackedRepos.filter((repo) => repo.id !== trackedRepo.id);
3225
- 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);
3226
3545
  return { removed: true, forced: false, currentRepo, trackedRepo };
3227
3546
  }
3228
3547
  if (force && registry.trackedRepos.length > 0) {
3229
3548
  const first = registry.trackedRepos[0];
3230
3549
  registry.trackedRepos = [];
3231
- 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);
3232
3559
  return { removed: true, forced: true, currentRepo, trackedRepo: first };
3233
3560
  }
3234
3561
  return {
@@ -7129,6 +7456,24 @@ async function buildRuntrimCreateContractMcp(cwd, args) {
7129
7456
  isError: true
7130
7457
  };
7131
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
+ }
7132
7477
  const latest = loadLatestRun(cwd);
7133
7478
  if ((latest == null ? void 0 : latest.status) === "guarded") {
7134
7479
  const blockedPayload = {
@@ -7790,6 +8135,21 @@ function isInteractiveTerminal() {
7790
8135
  async function ensureRepoAllowedForFree(cwd) {
7791
8136
  var _a2, _b;
7792
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
+ }
7793
8153
  if (check.allowed) {
7794
8154
  await registerCurrentRepo(cwd);
7795
8155
  return true;
@@ -7803,9 +8163,12 @@ async function ensureRepoAllowedForFree(cwd) {
7803
8163
  console.log(chalk.white(` ${check.currentRepo.path}`));
7804
8164
  console.log("");
7805
8165
  console.log(DIM(" Next:"));
7806
- console.log(chalk.white(" - continue in the tracked repo"));
7807
- console.log(chalk.white(" - unlink the tracked repo with runtrim repo unlink --force"));
7808
- 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."));
7809
8172
  console.log("");
7810
8173
  console.log(
7811
8174
  DIM(
@@ -8172,7 +8535,8 @@ var PROTOCOL_POINTER_BLOCK = `
8172
8535
  ${PROTOCOL_BLOCK_START}
8173
8536
  This repo uses RunTrim as the guarded AI coding protocol.
8174
8537
  Before editing code, read RUNTRIM.md.
8175
- Start every task with: runtrim go "<task>"
8538
+ Start every task with: runtrim start
8539
+ Then run: runtrim agent "Your task" --copy
8176
8540
  Stay inside .runtrim/contracts/latest.md.
8177
8541
  After edits, ask the user to run: runtrim finish
8178
8542
  ${PROTOCOL_BLOCK_END}
@@ -8868,6 +9232,8 @@ program.command("execute <task>").description("Create a controlled execution pac
8868
9232
  var agentCommand = program.command("agent").description("Start a guarded AI coding run with contract, scope, memory, and handoff");
8869
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) => {
8870
9234
  if (task == null ? void 0 : task.trim()) {
9235
+ const allowed = await ensureRepoAllowedForFree(process.cwd());
9236
+ if (!allowed) return;
8871
9237
  const normalizedTask = (task != null ? task : "").trim();
8872
9238
  if (options == null ? void 0 : options.bridge) {
8873
9239
  const bridge = await ensureBridgeRunningForAgent(process.cwd());
@@ -9336,12 +9702,54 @@ repoCommand.command("status").description("Show local tracked repo status").acti
9336
9702
  console.log(DIM(" Current repo ") + chalk.white(identity.path));
9337
9703
  console.log(DIM(" Tracked repo ") + chalk.white((_b = tracked == null ? void 0 : tracked.path) != null ? _b : "(none)"));
9338
9704
  console.log(DIM(" Allowed ") + chalk.white(check.allowed ? "yes" : "no"));
9705
+ console.log(DIM(" State ") + chalk.white(check.status));
9339
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
+ }
9340
9714
  if (tracked) {
9341
9715
  console.log(DIM(" A tracked repo is one codebase with its own .runtrim workspace."));
9342
9716
  console.log("");
9343
9717
  }
9344
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
+ });
9345
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) => {
9346
9754
  const cwd = process.cwd();
9347
9755
  const result = await unlinkCurrentRepo(cwd, Boolean(options.force));
@@ -10508,7 +10916,7 @@ program.command("continue").description("Create a safe continuation prompt from
10508
10916
  const audit = (_a2 = loadProjectAudit(cwd)) != null ? _a2 : performBaselineProjectAudit(cwd, null);
10509
10917
  const reason = normalizeContinuationReason(options.reason);
10510
10918
  const agent = normalizeContinuationAgent(options.agent, config.defaultAgent);
10511
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
10919
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
10512
10920
  const continuationPath = resolveContinuationPath(cwd);
10513
10921
  const latestPromptPath = resolvePromptPath(config, cwd);
10514
10922
  const latestPrompt = fs13.existsSync(latestPromptPath) ? fs13.readFileSync(latestPromptPath, "utf-8").trim() : "";
@@ -10698,14 +11106,14 @@ program.command("continue").description("Create a safe continuation prompt from
10698
11106
  fs13.writeFileSync(continuationPath, continuationPrompt, "utf-8");
10699
11107
  const copied = await copyToClipboardSafe(continuationPrompt);
10700
11108
  if (memory) {
10701
- const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso);
11109
+ const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso2);
10702
11110
  fs13.writeFileSync(path13.join(getConfigDir(cwd), "memory.md"), memoryWithContinuation, "utf-8");
10703
11111
  }
10704
11112
  if (hasConfig) {
10705
11113
  const nextConfig = __spreadProps(__spreadValues({}, config), {
10706
11114
  lastContinuationReason: reason,
10707
11115
  continuationPromptPath: continuationPath.replace(/\\/g, "/"),
10708
- continuationCreatedAt: nowIso
11116
+ continuationCreatedAt: nowIso2
10709
11117
  });
10710
11118
  saveConfig(nextConfig, cwd);
10711
11119
  }
@@ -11497,6 +11905,8 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
11497
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) => {
11498
11906
  var _a2, _b;
11499
11907
  const cwd = process.cwd();
11908
+ const allowed = await ensureRepoAllowedForFree(cwd);
11909
+ if (!allowed) return;
11500
11910
  console.log("");
11501
11911
  console.log(BOLD("RunTrim") + DIM(" cloud sync"));
11502
11912
  console.log("");