runtrim 0.1.17 → 0.1.19

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.
@@ -97,8 +97,45 @@ function getConfigPath(cwd = process.cwd()) {
97
97
  return path.join(getConfigDir(cwd), "config.json");
98
98
  }
99
99
  function getRunsDir(cwd = process.cwd()) {
100
+ return path.join(getInternalDir(cwd), "runs");
101
+ }
102
+ function getLegacyRunsDir(cwd = process.cwd()) {
100
103
  return path.join(getConfigDir(cwd), "runs");
101
104
  }
105
+ function getInternalDir(cwd = process.cwd()) {
106
+ return path.join(getConfigDir(cwd), "internal");
107
+ }
108
+ function getPreviewsDir(cwd = process.cwd()) {
109
+ return path.join(getInternalDir(cwd), "previews");
110
+ }
111
+ function getLegacyPreviewsDir(cwd = process.cwd()) {
112
+ return path.join(getConfigDir(cwd), "previews");
113
+ }
114
+ function getRestoresDir(cwd = process.cwd()) {
115
+ return path.join(getInternalDir(cwd), "restores");
116
+ }
117
+ function getLegacyRestoresDir(cwd = process.cwd()) {
118
+ return path.join(getConfigDir(cwd), "restores");
119
+ }
120
+ function getContractsArchiveDir(cwd = process.cwd()) {
121
+ return path.join(getInternalDir(cwd), "contracts-archive");
122
+ }
123
+ function getAgentArchiveDir(cwd = process.cwd()) {
124
+ return path.join(getInternalDir(cwd), "agent-archive");
125
+ }
126
+ function ensureInternalArtifactDirs(cwd = process.cwd()) {
127
+ const dirs = [
128
+ getInternalDir(cwd),
129
+ getRunsDir(cwd),
130
+ getPreviewsDir(cwd),
131
+ getRestoresDir(cwd),
132
+ getContractsArchiveDir(cwd),
133
+ getAgentArchiveDir(cwd)
134
+ ];
135
+ for (const dir of dirs) {
136
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
137
+ }
138
+ }
102
139
  function configExists(cwd = process.cwd()) {
103
140
  return fs.existsSync(getConfigPath(cwd));
104
141
  }
@@ -666,6 +703,28 @@ function buildCategoryScope(category, hasSrc, hasApp, hasPages) {
666
703
  "Check no regression in adjacent routes"
667
704
  ]
668
705
  };
706
+ case "docs":
707
+ return {
708
+ allowedHints: [
709
+ "README.md - project documentation",
710
+ "docs/ - documentation files",
711
+ "CHANGELOG.md or CONTRIBUTING.md if task-specific"
712
+ ],
713
+ forbiddenAdditions: [
714
+ "Do not touch auth internals, session logic, or JWT handling",
715
+ "Do not touch billing, subscription, payment, or webhook logic",
716
+ "Do not touch database schema or migrations",
717
+ "Do not touch .env files or secrets"
718
+ ],
719
+ stopRules: [
720
+ "Stop if the requested change requires code-path behavior changes outside docs",
721
+ "Stop if sensitive files or secrets are referenced"
722
+ ],
723
+ verificationSteps: [
724
+ "Confirm documentation text matches the requested task",
725
+ "Check markdown formatting renders correctly"
726
+ ]
727
+ };
669
728
  default:
670
729
  return {
671
730
  allowedHints: [],
@@ -1530,35 +1589,38 @@ function saveRun(task, audit, contract, cwd = process.cwd()) {
1530
1589
  return record;
1531
1590
  }
1532
1591
  function loadLatestRun(cwd = process.cwd()) {
1533
- const runsDir = getRunsDir(cwd);
1534
- if (!fs3.existsSync(runsDir)) return null;
1535
- const files = fs3.readdirSync(runsDir).filter((f) => f.endsWith(".json")).map((f) => ({
1536
- name: f,
1537
- time: fs3.statSync(path3.join(runsDir, f)).mtime.getTime()
1538
- })).sort((a, b) => b.time - a.time);
1592
+ const candidateDirs = [getRunsDir(cwd), getLegacyRunsDir(cwd)].filter((dir, idx, arr) => arr.indexOf(dir) === idx);
1593
+ const files = candidateDirs.filter((dir) => fs3.existsSync(dir)).flatMap(
1594
+ (dir) => fs3.readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => ({
1595
+ dir,
1596
+ name: f,
1597
+ time: fs3.statSync(path3.join(dir, f)).mtime.getTime()
1598
+ }))
1599
+ ).sort((a, b) => b.time - a.time);
1539
1600
  if (files.length === 0) return null;
1540
1601
  try {
1541
1602
  return JSON.parse(
1542
- fs3.readFileSync(path3.join(runsDir, files[0].name), "utf-8")
1603
+ fs3.readFileSync(path3.join(files[0].dir, files[0].name), "utf-8")
1543
1604
  );
1544
1605
  } catch (e) {
1545
1606
  return null;
1546
1607
  }
1547
1608
  }
1548
1609
  function updateRun(runId, updates, cwd = process.cwd()) {
1549
- const filePath = path3.join(getRunsDir(cwd), `${runId}.json`);
1610
+ const preferredPath = path3.join(getRunsDir(cwd), `${runId}.json`);
1611
+ const legacyPath = path3.join(getLegacyRunsDir(cwd), `${runId}.json`);
1612
+ const filePath = fs3.existsSync(preferredPath) ? preferredPath : legacyPath;
1550
1613
  if (!fs3.existsSync(filePath)) return;
1551
1614
  const existing = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
1552
1615
  fs3.writeFileSync(filePath, JSON.stringify(__spreadValues(__spreadValues({}, existing), updates), null, 2));
1553
1616
  }
1554
1617
  function loadAllRuns(cwd = process.cwd()) {
1555
- const runsDir = getRunsDir(cwd);
1556
- if (!fs3.existsSync(runsDir)) return [];
1557
- return fs3.readdirSync(runsDir).filter((f) => f.endsWith(".json")).map((f) => {
1618
+ const candidateDirs = [getRunsDir(cwd), getLegacyRunsDir(cwd)].filter((dir, idx, arr) => arr.indexOf(dir) === idx);
1619
+ const files = candidateDirs.filter((dir) => fs3.existsSync(dir)).flatMap((dir) => fs3.readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => path3.join(dir, f)));
1620
+ const deduped = [...new Set(files)];
1621
+ return deduped.map((filePath) => {
1558
1622
  try {
1559
- return JSON.parse(
1560
- fs3.readFileSync(path3.join(runsDir, f), "utf-8")
1561
- );
1623
+ return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
1562
1624
  } catch (e) {
1563
1625
  return null;
1564
1626
  }
@@ -2577,7 +2639,7 @@ function buildSyncPayload(input) {
2577
2639
  var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t;
2578
2640
  const { cwd, projectName, config, projectAudit, memoryMarkdown, runs } = input;
2579
2641
  const latest = runs[0];
2580
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2642
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
2581
2643
  const localProjectId = buildLocalProjectId(cwd);
2582
2644
  const latestPromptText = readTextFileIfExists(path6.join(cwd, ".runtrim", "latest-prompt.md"));
2583
2645
  const continuationPromptText = readTextFileIfExists(
@@ -2639,7 +2701,7 @@ function buildSyncPayload(input) {
2639
2701
  name: resolveProjectName2(cwd, projectName, projectAudit == null ? void 0 : projectAudit.projectName),
2640
2702
  stack: (_a2 = projectAudit == null ? void 0 : projectAudit.detectedStack) != null ? _a2 : config.stack ? config.stack.split(",").map((s) => s.trim()).filter(Boolean) : ["auto"],
2641
2703
  packageManager: (_c = (_b = projectAudit == null ? void 0 : projectAudit.packageManager) != null ? _b : config.packageManager) != null ? _c : null,
2642
- lastUpdated: nowIso
2704
+ lastUpdated: nowIso2
2643
2705
  },
2644
2706
  memory: {
2645
2707
  markdown: memoryMarkdown,
@@ -2721,7 +2783,8 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2721
2783
  "## How to start an AI coding task",
2722
2784
  "",
2723
2785
  "```",
2724
- 'runtrim go "<task>"',
2786
+ "runtrim start",
2787
+ 'runtrim agent "Your task" --copy',
2725
2788
  "```",
2726
2789
  "",
2727
2790
  "RunTrim creates a scoped contract, loads project memory, and generates a guarded prompt.",
@@ -2742,7 +2805,7 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2742
2805
  "",
2743
2806
  "1. Read `.runtrim/contracts/latest.md`.",
2744
2807
  " - 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.',
2808
+ ' - If `Status: none` \u2014 no active task. Ask the user to run `runtrim start` then `runtrim agent "Your task" --copy`.',
2746
2809
  "2. Do not assume any prior task is still active.",
2747
2810
  "3. Stay inside the allowed scope defined in the contract.",
2748
2811
  "4. Stop and ask before touching any forbidden area.",
@@ -2750,7 +2813,7 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2750
2813
  "6. After editing, tell the user to run: `runtrim finish`",
2751
2814
  "",
2752
2815
  "---",
2753
- `Protocol: runtrim init. Updated: ${(/* @__PURE__ */ new Date()).toISOString()}`
2816
+ `Protocol: runtrim start. Updated: ${(/* @__PURE__ */ new Date()).toISOString()}`
2754
2817
  ];
2755
2818
  fs7.writeFileSync(path7.join(cwd, "RUNTRIM.md"), lines.join("\n"), "utf-8");
2756
2819
  }
@@ -2767,7 +2830,8 @@ function writeRestingContract(cwd = process.cwd()) {
2767
2830
  "Start one with:",
2768
2831
  "",
2769
2832
  "```",
2770
- 'runtrim go "<your task>"',
2833
+ "runtrim start",
2834
+ 'runtrim agent "Your task" --copy',
2771
2835
  "```",
2772
2836
  "",
2773
2837
  "---",
@@ -2790,7 +2854,8 @@ function writeRestingMemory(cwd = process.cwd()) {
2790
2854
  "Start a new session with:",
2791
2855
  "",
2792
2856
  "```",
2793
- 'runtrim go "<your task>"',
2857
+ "runtrim start",
2858
+ 'runtrim agent "Your task" --copy',
2794
2859
  "```",
2795
2860
  "",
2796
2861
  "---",
@@ -2804,7 +2869,7 @@ function archiveContract(cwd, runId) {
2804
2869
  if (!fs7.existsSync(latestPath)) return;
2805
2870
  const content = fs7.readFileSync(latestPath, "utf-8");
2806
2871
  if (content.includes("Status: none")) return;
2807
- const archiveDir = path7.join(contractsDir, "archive");
2872
+ const archiveDir = getContractsArchiveDir(cwd);
2808
2873
  if (!fs7.existsSync(archiveDir)) fs7.mkdirSync(archiveDir, { recursive: true });
2809
2874
  fs7.writeFileSync(path7.join(archiveDir, `${runId}.md`), content, "utf-8");
2810
2875
  }
@@ -2911,7 +2976,7 @@ function writeBridgeInstructions(cwd = process.cwd()) {
2911
2976
  "1. Read `RUNTRIM.md`.",
2912
2977
  "2. Read `.runtrim/contracts/latest.md`.",
2913
2978
  " - If `Status: active` \u2014 follow the contract strictly.",
2914
- ' - If `Status: none` \u2014 stop. Ask the user to run `runtrim go "<task>"` first.',
2979
+ ' - If `Status: none` \u2014 stop. Ask the user to run `runtrim start` then `runtrim agent "Your task" --copy`.',
2915
2980
  "3. If the contract is active, read `.runtrim/memory/current.md` for session context.",
2916
2981
  " If no active session, read `.runtrim/memory/baseline.md` for project baseline.",
2917
2982
  "",
@@ -2985,6 +3050,28 @@ function buildBridgePrompt(contractText, ctx) {
2985
3050
  // src/lib/run-watch.ts
2986
3051
  function normalizeScopeKeywords2(scope) {
2987
3052
  var _a2;
3053
+ const genericStopwords = /* @__PURE__ */ new Set([
3054
+ "read",
3055
+ "write",
3056
+ "reference",
3057
+ "touch",
3058
+ "modify",
3059
+ "change",
3060
+ "update",
3061
+ "allow",
3062
+ "scope",
3063
+ "paths",
3064
+ "path",
3065
+ "files",
3066
+ "file",
3067
+ "only",
3068
+ "with",
3069
+ "without",
3070
+ "before",
3071
+ "after",
3072
+ "inside",
3073
+ "outside"
3074
+ ]);
2988
3075
  const words = /* @__PURE__ */ new Set();
2989
3076
  for (const line of scope) {
2990
3077
  const lower = line.toLowerCase();
@@ -2994,7 +3081,7 @@ function normalizeScopeKeywords2(scope) {
2994
3081
  }
2995
3082
  const cleaned = lower.replace(/[^a-z0-9_./\s-]/g, " ").split(/\s+/).filter(Boolean);
2996
3083
  for (const token of cleaned) {
2997
- if (token.length >= 4) words.add(token);
3084
+ if (token.length >= 4 && !genericStopwords.has(token)) words.add(token);
2998
3085
  }
2999
3086
  }
3000
3087
  return [...words];
@@ -3091,15 +3178,7 @@ import fs8 from "fs";
3091
3178
  import os from "os";
3092
3179
  import path8 from "path";
3093
3180
  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
- };
3181
+ var EMPTY_TELEMETRY = { enabled: false, anonymousId: "" };
3103
3182
  function normalizeRepoPath(input) {
3104
3183
  const resolved = path8.resolve(input);
3105
3184
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
@@ -3107,41 +3186,208 @@ function normalizeRepoPath(input) {
3107
3186
  function hashValue(value) {
3108
3187
  return crypto.createHash("sha256").update(value).digest("hex").slice(0, 16);
3109
3188
  }
3189
+ function nowIso() {
3190
+ return (/* @__PURE__ */ new Date()).toISOString();
3191
+ }
3192
+ function randomId(prefix) {
3193
+ return `${prefix}_${crypto.randomBytes(12).toString("hex")}`;
3194
+ }
3110
3195
  function getGlobalRunTrimDir() {
3111
3196
  return path8.join(os.homedir(), ".runtrim");
3112
3197
  }
3113
3198
  function getGlobalRegistryPath() {
3114
3199
  return path8.join(getGlobalRunTrimDir(), "global.json");
3115
3200
  }
3116
- function loadGlobalRegistry() {
3201
+ function getInstallStatePath() {
3202
+ return path8.join(getGlobalRunTrimDir(), "install-state.json");
3203
+ }
3204
+ function buildSealInput(registry) {
3205
+ const tracked = [...registry.trackedRepos].map((r) => ({
3206
+ id: r.id,
3207
+ name: r.name,
3208
+ path: normalizeRepoPath(r.path),
3209
+ gitRemote: r.gitRemote,
3210
+ createdAt: r.createdAt,
3211
+ lastSeenAt: r.lastSeenAt
3212
+ })).sort((a, b) => `${a.id}:${a.path}`.localeCompare(`${b.id}:${b.path}`));
3213
+ const payload = {
3214
+ version: registry.version,
3215
+ stateVersion: registry.stateVersion,
3216
+ plan: registry.plan,
3217
+ machineInstallId: registry.machineInstallId,
3218
+ createdAt: registry.createdAt,
3219
+ updatedAt: registry.updatedAt,
3220
+ trackedRepos: tracked,
3221
+ lastKnownRepo: registry.lastKnownRepo ? __spreadProps(__spreadValues({}, registry.lastKnownRepo), {
3222
+ path: normalizeRepoPath(registry.lastKnownRepo.path)
3223
+ }) : null
3224
+ };
3225
+ return JSON.stringify(payload);
3226
+ }
3227
+ function computeSeal(registry) {
3228
+ return crypto.createHash("sha256").update(buildSealInput(registry)).digest("hex");
3229
+ }
3230
+ function sanitizeTrackedRepoEntry(input) {
3231
+ var _a2, _b, _c, _d, _e, _f;
3232
+ const id = String((_a2 = input.id) != null ? _a2 : "").trim();
3233
+ const rawPath = String((_b = input.path) != null ? _b : "").trim();
3234
+ if (!id || !rawPath) return null;
3235
+ return {
3236
+ id,
3237
+ name: String((_c = input.name) != null ? _c : "").trim(),
3238
+ path: normalizeRepoPath(rawPath),
3239
+ gitRemote: String((_d = input.gitRemote) != null ? _d : "").trim(),
3240
+ createdAt: String((_e = input.createdAt) != null ? _e : "").trim(),
3241
+ lastSeenAt: String((_f = input.lastSeenAt) != null ? _f : "").trim()
3242
+ };
3243
+ }
3244
+ function readInstallStateRaw() {
3245
+ var _a2, _b, _c;
3246
+ const p = getInstallStatePath();
3247
+ if (!fs8.existsSync(p)) return { exists: false, state: null };
3248
+ try {
3249
+ const parsed = JSON.parse(fs8.readFileSync(p, "utf-8"));
3250
+ const machineInstallId = String((_a2 = parsed.machineInstallId) != null ? _a2 : "").trim();
3251
+ if (!machineInstallId) return { exists: true, state: null };
3252
+ return {
3253
+ exists: true,
3254
+ state: {
3255
+ machineInstallId,
3256
+ createdAt: String((_b = parsed.createdAt) != null ? _b : "").trim() || nowIso(),
3257
+ updatedAt: String((_c = parsed.updatedAt) != null ? _c : "").trim() || nowIso()
3258
+ }
3259
+ };
3260
+ } catch (e) {
3261
+ return { exists: true, state: null };
3262
+ }
3263
+ }
3264
+ function writeInstallState(state) {
3265
+ const dir = getGlobalRunTrimDir();
3266
+ if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
3267
+ fs8.writeFileSync(getInstallStatePath(), JSON.stringify(state, null, 2), "utf-8");
3268
+ }
3269
+ function ensureInstallState() {
3270
+ const raw = readInstallStateRaw();
3271
+ if (raw.exists && raw.state) return raw.state;
3272
+ const created = {
3273
+ machineInstallId: randomId("rt_install"),
3274
+ createdAt: nowIso(),
3275
+ updatedAt: nowIso()
3276
+ };
3277
+ writeInstallState(created);
3278
+ return created;
3279
+ }
3280
+ function buildDefaultRegistry(install) {
3281
+ const base = {
3282
+ version: 2,
3283
+ stateVersion: 2,
3284
+ plan: "free",
3285
+ machineInstallId: install.machineInstallId,
3286
+ createdAt: nowIso(),
3287
+ updatedAt: nowIso(),
3288
+ trackedRepos: [],
3289
+ lastKnownRepo: null,
3290
+ telemetry: __spreadValues({}, EMPTY_TELEMETRY)
3291
+ };
3292
+ return __spreadProps(__spreadValues({}, base), {
3293
+ integrity: {
3294
+ algorithm: "sha256-local-seal-v1",
3295
+ seal: computeSeal(base)
3296
+ }
3297
+ });
3298
+ }
3299
+ function saveRegistryWithSeal(registry) {
3300
+ var _a2;
3301
+ const dir = getGlobalRunTrimDir();
3302
+ if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
3303
+ const normalizedBase = __spreadProps(__spreadValues({}, registry), {
3304
+ version: 2,
3305
+ stateVersion: 2,
3306
+ trackedRepos: registry.trackedRepos.map((r) => __spreadProps(__spreadValues({}, r), { path: normalizeRepoPath(r.path) })),
3307
+ telemetry: (_a2 = registry.telemetry) != null ? _a2 : __spreadValues({}, EMPTY_TELEMETRY)
3308
+ });
3309
+ const sealed = __spreadProps(__spreadValues({}, normalizedBase), {
3310
+ integrity: {
3311
+ algorithm: "sha256-local-seal-v1",
3312
+ seal: computeSeal(normalizedBase)
3313
+ }
3314
+ });
3315
+ fs8.writeFileSync(getGlobalRegistryPath(), JSON.stringify(sealed, null, 2), "utf-8");
3316
+ }
3317
+ function inspectGlobalRegistry() {
3318
+ var _a2, _b, _c, _d, _e, _f, _g, _h, _i;
3319
+ const installRaw = readInstallStateRaw();
3320
+ const install = (_a2 = installRaw.state) != null ? _a2 : ensureInstallState();
3117
3321
  const registryPath = getGlobalRegistryPath();
3118
- if (!fs8.existsSync(registryPath)) return __spreadValues({}, DEFAULT_REGISTRY);
3322
+ const defaultRegistry = buildDefaultRegistry(install);
3323
+ if (!fs8.existsSync(registryPath)) {
3324
+ if (installRaw.exists) {
3325
+ return {
3326
+ registry: defaultRegistry,
3327
+ needsRepair: true,
3328
+ repairReason: "missing_registry_after_initialization"
3329
+ };
3330
+ }
3331
+ return { registry: defaultRegistry, needsRepair: false, repairReason: null };
3332
+ }
3119
3333
  try {
3120
3334
  const raw = JSON.parse(fs8.readFileSync(registryPath, "utf-8"));
3121
- return {
3122
- version: 1,
3335
+ const trackedRepos = Array.isArray(raw.trackedRepos) ? raw.trackedRepos.map((item) => sanitizeTrackedRepoEntry(item)).filter((item) => Boolean(item)) : [];
3336
+ const base = {
3337
+ version: 2,
3338
+ stateVersion: 2,
3123
3339
  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)) : [],
3340
+ machineInstallId: String((_b = raw.machineInstallId) != null ? _b : "").trim() || install.machineInstallId,
3341
+ createdAt: String((_c = raw.createdAt) != null ? _c : "").trim() || nowIso(),
3342
+ updatedAt: String((_d = raw.updatedAt) != null ? _d : "").trim() || nowIso(),
3343
+ trackedRepos,
3344
+ lastKnownRepo: raw.lastKnownRepo && typeof raw.lastKnownRepo === "object" ? {
3345
+ id: String((_e = raw.lastKnownRepo.id) != null ? _e : "").trim(),
3346
+ name: String((_f = raw.lastKnownRepo.name) != null ? _f : "").trim(),
3347
+ path: normalizeRepoPath(String((_g = raw.lastKnownRepo.path) != null ? _g : "")),
3348
+ gitRemote: String((_h = raw.lastKnownRepo.gitRemote) != null ? _h : "").trim(),
3349
+ lastSeenAt: String((_i = raw.lastKnownRepo.lastSeenAt) != null ? _i : "").trim() || nowIso()
3350
+ } : null,
3132
3351
  telemetry: {
3133
3352
  enabled: typeof raw.telemetry === "object" && raw.telemetry !== null && Boolean(raw.telemetry.enabled),
3134
3353
  anonymousId: typeof raw.telemetry === "object" && raw.telemetry !== null && typeof raw.telemetry.anonymousId === "string" ? String(raw.telemetry.anonymousId).slice(0, 120) : ""
3135
3354
  }
3136
3355
  };
3356
+ const normalized = __spreadProps(__spreadValues({}, base), {
3357
+ integrity: {
3358
+ algorithm: "sha256-local-seal-v1",
3359
+ seal: raw.integrity && typeof raw.integrity === "object" && typeof raw.integrity.seal === "string" ? String(raw.integrity.seal) : ""
3360
+ }
3361
+ });
3362
+ if (normalized.machineInstallId !== install.machineInstallId) {
3363
+ return {
3364
+ registry: normalized,
3365
+ needsRepair: true,
3366
+ repairReason: "machine_install_id_mismatch"
3367
+ };
3368
+ }
3369
+ const expectedSeal = computeSeal(base);
3370
+ if (!normalized.integrity.seal || normalized.integrity.seal !== expectedSeal) {
3371
+ return {
3372
+ registry: normalized,
3373
+ needsRepair: true,
3374
+ repairReason: "integrity_seal_mismatch"
3375
+ };
3376
+ }
3377
+ return { registry: normalized, needsRepair: false, repairReason: null };
3137
3378
  } catch (e) {
3138
- return __spreadValues({}, DEFAULT_REGISTRY);
3379
+ return {
3380
+ registry: defaultRegistry,
3381
+ needsRepair: true,
3382
+ repairReason: "registry_corrupt"
3383
+ };
3139
3384
  }
3140
3385
  }
3386
+ function loadGlobalRegistry() {
3387
+ return inspectGlobalRegistry().registry;
3388
+ }
3141
3389
  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");
3390
+ saveRegistryWithSeal(registry);
3145
3391
  }
3146
3392
  async function getCurrentRepoIdentity(cwd = process.cwd()) {
3147
3393
  const normalizedPath = normalizeRepoPath(cwd);
@@ -3171,36 +3417,71 @@ function findTrackedRepo(trackedRepos, currentRepo) {
3171
3417
  return byPath != null ? byPath : null;
3172
3418
  }
3173
3419
  async function assertFreeRepoAllowed(cwd = process.cwd()) {
3174
- const registry = loadGlobalRegistry();
3420
+ const inspected = inspectGlobalRegistry();
3421
+ const registry = inspected.registry;
3175
3422
  const currentRepo = await getCurrentRepoIdentity(cwd);
3176
3423
  const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
3177
- if (registry.plan !== "free") {
3178
- return { allowed: true, plan: registry.plan, currentRepo, trackedRepo };
3424
+ const base = {
3425
+ plan: registry.plan,
3426
+ currentRepo,
3427
+ trackedRepo,
3428
+ registryPath: getGlobalRegistryPath()
3429
+ };
3430
+ if (inspected.needsRepair) {
3431
+ return __spreadProps(__spreadValues({}, base), {
3432
+ allowed: false,
3433
+ status: "blocked_repair",
3434
+ repairRequired: true,
3435
+ repairReason: inspected.repairReason
3436
+ });
3179
3437
  }
3180
- if (trackedRepo) {
3181
- return { allowed: true, plan: registry.plan, currentRepo, trackedRepo };
3438
+ if (registry.plan !== "free") {
3439
+ return __spreadProps(__spreadValues({}, base), {
3440
+ allowed: true,
3441
+ status: "allowed",
3442
+ repairRequired: false,
3443
+ repairReason: null
3444
+ });
3182
3445
  }
3183
- if (registry.trackedRepos.length === 0) {
3184
- return { allowed: true, plan: registry.plan, currentRepo, trackedRepo: null };
3446
+ if (trackedRepo || registry.trackedRepos.length === 0) {
3447
+ return __spreadProps(__spreadValues({}, base), {
3448
+ allowed: true,
3449
+ status: "allowed",
3450
+ repairRequired: false,
3451
+ repairReason: null
3452
+ });
3185
3453
  }
3186
- return {
3454
+ return __spreadProps(__spreadValues({}, base), {
3187
3455
  allowed: false,
3188
- plan: registry.plan,
3189
- currentRepo,
3456
+ status: "blocked_limit",
3457
+ repairRequired: false,
3458
+ repairReason: null,
3190
3459
  trackedRepo: registry.trackedRepos[0]
3191
- };
3460
+ });
3192
3461
  }
3193
3462
  async function registerCurrentRepo(cwd = process.cwd()) {
3463
+ const check = await assertFreeRepoAllowed(cwd);
3464
+ if (!check.allowed && check.status === "blocked_repair") {
3465
+ throw new Error("runtrim_local_state_repair_required");
3466
+ }
3194
3467
  const registry = loadGlobalRegistry();
3195
3468
  const currentRepo = await getCurrentRepoIdentity(cwd);
3196
- const now = (/* @__PURE__ */ new Date()).toISOString();
3469
+ const now = nowIso();
3197
3470
  const existing = findTrackedRepo(registry.trackedRepos, currentRepo);
3198
3471
  if (existing) {
3199
3472
  existing.lastSeenAt = now;
3200
3473
  existing.name = currentRepo.name;
3201
3474
  existing.path = currentRepo.path;
3202
3475
  existing.gitRemote = currentRepo.gitRemote;
3203
- saveGlobalRegistry(registry);
3476
+ registry.updatedAt = now;
3477
+ registry.lastKnownRepo = {
3478
+ id: currentRepo.id,
3479
+ name: currentRepo.name,
3480
+ path: currentRepo.path,
3481
+ gitRemote: currentRepo.gitRemote,
3482
+ lastSeenAt: now
3483
+ };
3484
+ saveRegistryWithSeal(registry);
3204
3485
  return existing;
3205
3486
  }
3206
3487
  const entry = {
@@ -3211,24 +3492,110 @@ async function registerCurrentRepo(cwd = process.cwd()) {
3211
3492
  createdAt: now,
3212
3493
  lastSeenAt: now
3213
3494
  };
3214
- registry.trackedRepos.push(entry);
3215
- saveGlobalRegistry(registry);
3495
+ registry.trackedRepos = [entry];
3496
+ registry.updatedAt = now;
3497
+ registry.lastKnownRepo = {
3498
+ id: entry.id,
3499
+ name: entry.name,
3500
+ path: entry.path,
3501
+ gitRemote: entry.gitRemote,
3502
+ lastSeenAt: now
3503
+ };
3504
+ saveRegistryWithSeal(registry);
3216
3505
  return entry;
3217
3506
  }
3507
+ async function repairGlobalRegistry(cwd = process.cwd(), options = {}) {
3508
+ const before = await assertFreeRepoAllowed(cwd);
3509
+ if (!before.repairRequired) {
3510
+ return { repaired: false, check: before };
3511
+ }
3512
+ const install = ensureInstallState();
3513
+ const now = nowIso();
3514
+ const repaired = buildDefaultRegistry(install);
3515
+ repaired.createdAt = now;
3516
+ repaired.updatedAt = now;
3517
+ if (options.useCurrentRepo) {
3518
+ const currentRepo = await getCurrentRepoIdentity(cwd);
3519
+ repaired.trackedRepos = [
3520
+ {
3521
+ id: currentRepo.id,
3522
+ name: currentRepo.name,
3523
+ path: currentRepo.path,
3524
+ gitRemote: currentRepo.gitRemote,
3525
+ createdAt: now,
3526
+ lastSeenAt: now
3527
+ }
3528
+ ];
3529
+ repaired.lastKnownRepo = {
3530
+ id: currentRepo.id,
3531
+ name: currentRepo.name,
3532
+ path: currentRepo.path,
3533
+ gitRemote: currentRepo.gitRemote,
3534
+ lastSeenAt: now
3535
+ };
3536
+ }
3537
+ saveRegistryWithSeal(repaired);
3538
+ const check = await assertFreeRepoAllowed(cwd);
3539
+ return { repaired: true, check };
3540
+ }
3218
3541
  async function unlinkCurrentRepo(cwd = process.cwd(), force = false) {
3219
3542
  var _a2;
3543
+ const check = await assertFreeRepoAllowed(cwd);
3544
+ if (check.status === "blocked_repair") {
3545
+ if (!force) {
3546
+ return {
3547
+ removed: false,
3548
+ forced: false,
3549
+ currentRepo: check.currentRepo,
3550
+ trackedRepo: null
3551
+ };
3552
+ }
3553
+ const install = ensureInstallState();
3554
+ const repaired = buildDefaultRegistry(install);
3555
+ repaired.updatedAt = nowIso();
3556
+ repaired.lastKnownRepo = {
3557
+ id: check.currentRepo.id,
3558
+ name: check.currentRepo.name,
3559
+ path: check.currentRepo.path,
3560
+ gitRemote: check.currentRepo.gitRemote,
3561
+ lastSeenAt: repaired.updatedAt
3562
+ };
3563
+ saveRegistryWithSeal(repaired);
3564
+ return {
3565
+ removed: true,
3566
+ forced: true,
3567
+ currentRepo: check.currentRepo,
3568
+ trackedRepo: null
3569
+ };
3570
+ }
3220
3571
  const registry = loadGlobalRegistry();
3221
3572
  const currentRepo = await getCurrentRepoIdentity(cwd);
3222
3573
  const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
3223
3574
  if (trackedRepo) {
3224
3575
  registry.trackedRepos = registry.trackedRepos.filter((repo) => repo.id !== trackedRepo.id);
3225
- saveGlobalRegistry(registry);
3576
+ registry.updatedAt = nowIso();
3577
+ registry.lastKnownRepo = {
3578
+ id: trackedRepo.id,
3579
+ name: trackedRepo.name,
3580
+ path: trackedRepo.path,
3581
+ gitRemote: trackedRepo.gitRemote,
3582
+ lastSeenAt: registry.updatedAt
3583
+ };
3584
+ saveRegistryWithSeal(registry);
3226
3585
  return { removed: true, forced: false, currentRepo, trackedRepo };
3227
3586
  }
3228
3587
  if (force && registry.trackedRepos.length > 0) {
3229
3588
  const first = registry.trackedRepos[0];
3230
3589
  registry.trackedRepos = [];
3231
- saveGlobalRegistry(registry);
3590
+ registry.updatedAt = nowIso();
3591
+ registry.lastKnownRepo = {
3592
+ id: first.id,
3593
+ name: first.name,
3594
+ path: first.path,
3595
+ gitRemote: first.gitRemote,
3596
+ lastSeenAt: registry.updatedAt
3597
+ };
3598
+ saveRegistryWithSeal(registry);
3232
3599
  return { removed: true, forced: true, currentRepo, trackedRepo: first };
3233
3600
  }
3234
3601
  return {
@@ -3832,9 +4199,15 @@ async function getPanelState(cwd, monitorMode) {
3832
4199
  let runs = [];
3833
4200
  let latest = null;
3834
4201
  let registry = {
3835
- version: 1,
4202
+ version: 2,
4203
+ stateVersion: 2,
3836
4204
  plan: "free",
4205
+ machineInstallId: "",
4206
+ createdAt: "",
4207
+ updatedAt: "",
3837
4208
  trackedRepos: [],
4209
+ lastKnownRepo: null,
4210
+ integrity: { algorithm: "sha256-local-seal-v1", seal: "" },
3838
4211
  telemetry: {
3839
4212
  enabled: false,
3840
4213
  anonymousId: ""
@@ -3876,9 +4249,15 @@ async function getPanelState(cwd, monitorMode) {
3876
4249
  } catch (e) {
3877
4250
  warnings.push("global_registry_failed");
3878
4251
  registry = {
3879
- version: 1,
4252
+ version: 2,
4253
+ stateVersion: 2,
3880
4254
  plan: "free",
4255
+ machineInstallId: "",
4256
+ createdAt: "",
4257
+ updatedAt: "",
3881
4258
  trackedRepos: [],
4259
+ lastKnownRepo: null,
4260
+ integrity: { algorithm: "sha256-local-seal-v1", seal: "" },
3882
4261
  telemetry: {
3883
4262
  enabled: false,
3884
4263
  anonymousId: ""
@@ -4346,7 +4725,7 @@ Before editing code:
4346
4725
  If no active RunTrim contract exists:
4347
4726
  - Do not edit code without one.
4348
4727
  - Ask the user to start a guarded run:
4349
- runtrim go "<task>"
4728
+ runtrim agent "<task>" --copy
4350
4729
 
4351
4730
  If the task requires leaving the current scope:
4352
4731
  - Stop.
@@ -4442,7 +4821,7 @@ ${BASE_PROTOCOL}
4442
4821
  Before using any tool or executing any command:
4443
4822
  1. Confirm a RunTrim contract is active at .runtrim/contracts/latest.md.
4444
4823
  2. If no active contract exists, do not proceed. Ask for:
4445
- runtrim go "<task>"
4824
+ runtrim agent "<task>" --copy
4446
4825
  3. Do not call shell commands, write files, or read env vars outside the contract.
4447
4826
  `.trim(),
4448
4827
  custom: `
@@ -4495,7 +4874,7 @@ function getCursorMdcContent() {
4495
4874
  "## If no active contract",
4496
4875
  "",
4497
4876
  "Ask the user to start a guarded run:",
4498
- '`runtrim go "<task>"`',
4877
+ '`runtrim agent "<task>" --copy`',
4499
4878
  "",
4500
4879
  "Any agent. One run boundary."
4501
4880
  ].join("\n");
@@ -4757,7 +5136,7 @@ Before editing code:
4757
5136
  - If the task touches auth, billing, payments, webhooks, database, middleware,
4758
5137
  env vars, secrets, or subscriptions, stop and require an active RunTrim contract.
4759
5138
  Ask the user to run:
4760
- runtrim go "<task>"
5139
+ runtrim agent "<task>" --copy
4761
5140
  - For low-risk work (UI polish, copy, docs, isolated component styling):
4762
5141
  Fast Path is allowed if no unfinished changes exist.
4763
5142
  Keep the change minimal.
@@ -4780,7 +5159,7 @@ No active RunTrim contract means no code edits.
4780
5159
  If no active contract exists at .runtrim/contracts/latest.md:
4781
5160
  - Do not edit any file.
4782
5161
  - Ask the user to start a guarded run:
4783
- runtrim go "<task>"
5162
+ runtrim agent "<task>" --copy
4784
5163
 
4785
5164
  After every editing session:
4786
5165
  - Ask the user to run:
@@ -4794,7 +5173,7 @@ Fast Path is allowed for low and medium risk work.
4794
5173
 
4795
5174
  Critical systems (auth, billing, payments, webhooks, database, middleware,
4796
5175
  env vars, secrets, subscriptions) still require a RunTrim contract:
4797
- runtrim go "<task>"
5176
+ runtrim agent "<task>" --copy
4798
5177
 
4799
5178
  After any edits:
4800
5179
  - runtrim finish is required before continuing to another task.
@@ -4805,7 +5184,7 @@ RunTrim Auto-guard: Off
4805
5184
 
4806
5185
  Auto-guard is disabled for this project.
4807
5186
  RunTrim can still be used manually:
4808
- runtrim go "<task>"
5187
+ runtrim agent "<task>" --copy
4809
5188
  runtrim finish
4810
5189
  `.trim();
4811
5190
  }
@@ -4831,7 +5210,7 @@ function saveFastRunRecord(cwd, changedFiles, risk) {
4831
5210
  reportParts.push(`${sensitive.length} sensitive path${sensitive.length === 1 ? "" : "s"} touched.`);
4832
5211
  }
4833
5212
  reportParts.push("No pre-run contract was captured for this run.");
4834
- const nextSafeAction = changedFiles.length > 0 ? 'Create a contract before the next change: runtrim go "<task>"' : 'Start a guarded run: runtrim go "<task>"';
5213
+ const nextSafeAction = changedFiles.length > 0 ? 'Create a contract before the next change: runtrim agent "<task>" --copy' : 'Start a guarded run: runtrim agent "<task>" --copy';
4835
5214
  const summary = {
4836
5215
  id,
4837
5216
  task,
@@ -5351,7 +5730,7 @@ function recommendProviderRouting(ctx) {
5351
5730
  } else if (route === "split-required") {
5352
5731
  routingReason = "This spans multiple critical systems, so RunTrim should split into audit, implementation, and verification.";
5353
5732
  }
5354
- let nextCommand = `runtrim go "${ctx.task}"`;
5733
+ let nextCommand = `runtrim agent "${ctx.task}" --copy`;
5355
5734
  if (route === "split-required") {
5356
5735
  nextCommand = "split into:\n1. audit only\n2. implementation only\n3. verification only";
5357
5736
  } else if (route === "preview-only") {
@@ -5373,6 +5752,7 @@ var _a;
5373
5752
  var oraFactory = typeof ora === "function" ? ora : (_a = ora.default) != null ? _a : ora;
5374
5753
  var ACCENT = chalk.hex("#C8901A");
5375
5754
  var GO_ACCENT = chalk.hex("#8B7CFF");
5755
+ var RUNTRIM_AGENT_INSTRUCTIONS_VERSION = "2";
5376
5756
  var DIM = chalk.gray;
5377
5757
  var BOLD = chalk.white.bold;
5378
5758
  var program = new Command();
@@ -5508,6 +5888,98 @@ async function copyToClipboardSafe(value) {
5508
5888
  function dedupeFiles(files) {
5509
5889
  return [...new Set(files.filter(Boolean).map((f) => f.replace(/\\/g, "/")))];
5510
5890
  }
5891
+ var RUNTRIM_GITIGNORE_BLOCK_START = "# BEGIN RUNTRIM_ARTIFACTS";
5892
+ var RUNTRIM_GITIGNORE_BLOCK_END = "# END RUNTRIM_ARTIFACTS";
5893
+ function ensureRuntrimReadme(cwd) {
5894
+ const readmePath = path13.join(getConfigDir(cwd), "README.md");
5895
+ const content = [
5896
+ "# RunTrim Local Files",
5897
+ "",
5898
+ "RunTrim stores local metadata in this folder.",
5899
+ "",
5900
+ "Human-facing files:",
5901
+ "- `agent/instructions.md` and `agent/latest.md`",
5902
+ "- `contracts/latest.md`",
5903
+ "- `memory/current.md` and `memory/baseline.md`",
5904
+ "- `mcp/*.json`",
5905
+ "- `config.json`",
5906
+ "",
5907
+ "Internal artifacts:",
5908
+ "- `.runtrim/internal/runs/`",
5909
+ "- `.runtrim/internal/previews/`",
5910
+ "- `.runtrim/internal/restores/`",
5911
+ "- `.runtrim/internal/contracts-archive/`",
5912
+ "- `.runtrim/internal/agent-archive/`",
5913
+ "",
5914
+ "Notes:",
5915
+ "- Artifacts are local-first.",
5916
+ "- Source code is not uploaded by local storage.",
5917
+ "- Restore metadata is path-only and does not store secret contents."
5918
+ ].join("\n");
5919
+ fs13.writeFileSync(readmePath, content + "\n", "utf-8");
5920
+ }
5921
+ function ensureRuntrimGitignoreGuidance(cwd) {
5922
+ const gitignorePath = path13.join(cwd, ".gitignore");
5923
+ if (!fs13.existsSync(gitignorePath)) return;
5924
+ const desired = [
5925
+ RUNTRIM_GITIGNORE_BLOCK_START,
5926
+ "# RunTrim local artifacts",
5927
+ ".runtrim/internal/",
5928
+ ".runtrim/runs/",
5929
+ ".runtrim/previews/",
5930
+ ".runtrim/restores/",
5931
+ ".runtrim/contracts/archive/",
5932
+ ".runtrim/agent/*.json",
5933
+ RUNTRIM_GITIGNORE_BLOCK_END
5934
+ ].join("\n");
5935
+ const current = fs13.readFileSync(gitignorePath, "utf-8");
5936
+ const start = current.indexOf(RUNTRIM_GITIGNORE_BLOCK_START);
5937
+ const end = current.indexOf(RUNTRIM_GITIGNORE_BLOCK_END);
5938
+ if (start !== -1 && end !== -1 && end > start) {
5939
+ const next = current.slice(0, start).trimEnd() + "\n\n" + desired + "\n" + current.slice(end + RUNTRIM_GITIGNORE_BLOCK_END.length).replace(/^\n+/, "\n");
5940
+ if (next !== current) fs13.writeFileSync(gitignorePath, next, "utf-8");
5941
+ return;
5942
+ }
5943
+ if (!current.includes(".runtrim/internal/")) {
5944
+ fs13.writeFileSync(gitignorePath, current.trimEnd() + "\n\n" + desired + "\n", "utf-8");
5945
+ }
5946
+ }
5947
+ function listFilesIfExists(dir) {
5948
+ if (!fs13.existsSync(dir)) return [];
5949
+ return fs13.readdirSync(dir).map((name) => path13.join(dir, name)).filter((p) => fs13.existsSync(p) && fs13.statSync(p).isFile());
5950
+ }
5951
+ function listArtifactFiles(cwd) {
5952
+ const dirs = [
5953
+ getRunsDir(cwd),
5954
+ getPreviewsDir(cwd),
5955
+ getRestoresDir(cwd),
5956
+ getContractsArchiveDir(cwd),
5957
+ path13.join(getConfigDir(cwd), "internal", "agent-archive"),
5958
+ getLegacyRunsDir(cwd),
5959
+ getLegacyPreviewsDir(cwd),
5960
+ getLegacyRestoresDir(cwd),
5961
+ path13.join(getConfigDir(cwd), "contracts", "archive"),
5962
+ path13.join(getConfigDir(cwd), "agent")
5963
+ ];
5964
+ const files = dirs.flatMap((dir) => listFilesIfExists(dir));
5965
+ return files.filter((filePath) => {
5966
+ const base = path13.basename(filePath).toLowerCase();
5967
+ const rel = path13.relative(cwd, filePath).replace(/\\/g, "/");
5968
+ if (rel === ".runtrim/previews/latest.md") return false;
5969
+ if (base === "latest.md" || base === "instructions.md" || base === "current.md" || base === "baseline.md") return false;
5970
+ return true;
5971
+ });
5972
+ }
5973
+ function parseRunIdFromArtifact(filePath) {
5974
+ const base = path13.basename(filePath);
5975
+ const direct = base.match(/^([a-zA-Z0-9_-]{6,})\.json$/);
5976
+ if (direct) return direct[1];
5977
+ const report = base.match(/^([a-zA-Z0-9_-]{6,})\.report\.\d+\.json$/);
5978
+ if (report) return report[1];
5979
+ const out = base.match(/^([a-zA-Z0-9_-]{6,})\.output\.txt$/);
5980
+ if (out) return out[1];
5981
+ return null;
5982
+ }
5511
5983
  function normalizeContractPathPattern(pattern) {
5512
5984
  let p = pattern.trim().replace(/\\/g, "/");
5513
5985
  if (!p || p === "-" || p.toLowerCase() === "none") return "";
@@ -5677,10 +6149,11 @@ function buildRecommendedNextCommand(task, approval, filesToInspect) {
5677
6149
  return `runtrim go "${task}"`;
5678
6150
  }
5679
6151
  function writePreviewArtifacts(cwd, preview) {
5680
- const previewsDir = path13.join(cwd, ".runtrim", "previews");
6152
+ const previewsDir = getPreviewsDir(cwd);
5681
6153
  if (!fs13.existsSync(previewsDir)) fs13.mkdirSync(previewsDir, { recursive: true });
5682
6154
  const jsonPath = path13.join(previewsDir, `${preview.id}.json`);
5683
- const markdownPath = path13.join(previewsDir, "latest.md");
6155
+ const markdownPath = path13.join(getLegacyPreviewsDir(cwd), "latest.md");
6156
+ if (!fs13.existsSync(path13.dirname(markdownPath))) fs13.mkdirSync(path13.dirname(markdownPath), { recursive: true });
5684
6157
  fs13.writeFileSync(jsonPath, JSON.stringify(preview, null, 2), "utf-8");
5685
6158
  const lines = [
5686
6159
  "RunTrim Agent Preview",
@@ -5719,7 +6192,7 @@ function writePreviewArtifacts(cwd, preview) {
5719
6192
  "Next:",
5720
6193
  preview.recommendedNextCommand,
5721
6194
  "",
5722
- `Preview JSON: .runtrim/previews/${preview.id}.json`
6195
+ `Preview JSON: .runtrim/internal/previews/${preview.id}.json`
5723
6196
  ];
5724
6197
  fs13.writeFileSync(markdownPath, lines.join("\n"), "utf-8");
5725
6198
  return { jsonPath, markdownPath };
@@ -6101,6 +6574,7 @@ async function runAgentApply(task, mode) {
6101
6574
  const copied = mode.copy ? await copyToClipboardSafe(fs13.readFileSync(handoff.markdownPath, "utf-8")) : false;
6102
6575
  const run = saveRun(task, previewResult.audit, previewResult.contract, cwd);
6103
6576
  updateRun(run.id, { status: "guarded" }, cwd);
6577
+ await captureRestorePoint(cwd, run.id, task);
6104
6578
  const riskColor = (_a2 = { low: chalk.green, medium: chalk.yellow, high: chalk.hex("#FF8C00"), critical: chalk.red }[risk]) != null ? _a2 : chalk.white;
6105
6579
  if (mode.primary) {
6106
6580
  console.log("");
@@ -6287,6 +6761,7 @@ async function runControlledExecution(task, mode) {
6287
6761
  controlledExecutionId: id,
6288
6762
  controlledExecutionStatus: status
6289
6763
  }, cwd);
6764
+ await captureRestorePoint(cwd, run.id, task);
6290
6765
  }
6291
6766
  if (status === "blocked") {
6292
6767
  console.log("");
@@ -6577,14 +7052,18 @@ ${block}
6577
7052
  function buildRuntrimAgentBlockContent() {
6578
7053
  return [
6579
7054
  "RunTrim project instructions",
7055
+ `RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`,
6580
7056
  "- Before editing, read RunTrim project memory and profile context.",
6581
- "- Verify active contract first. If MCP is available and no contract is active, call runtrim_create_contract with the user's task.",
7057
+ "- Before editing, create or verify an active RunTrim contract for the current user task.",
7058
+ "- If MCP is available and no active contract exists, call runtrim_create_contract with the user's task.",
6582
7059
  '- If MCP is unavailable, ask the user to run: runtrim agent "task" --copy',
6583
- "- Stay inside active contract scope.",
6584
- "- Before high-risk edits (auth, billing, middleware/proxy, migrations, sensitive files, broad app-wide changes), check scope first. If MCP is available, call runtrim_check_path.",
7060
+ "- Before touching risky paths (auth, billing, middleware/proxy, migrations, env/sensitive files, broad app-wide refactors), call runtrim_check_path when available.",
7061
+ "- Use RunTrim project memory and stay inside active contract scope.",
7062
+ "- Do not silently expand scope. Preserve existing project style and logic.",
6585
7063
  "- Never read or print env file contents.",
6586
- '- Do not continue outside scope silently. Request: runtrim approve "Allow <path/scope> for this run only"',
6587
- "- At the end, run or ask for runtrim finish. Do not claim completion before finish verification.",
7064
+ '- If scope must expand, stop and request approval: runtrim approve "Allow <path/scope> for this run only"',
7065
+ "- At the end, run runtrim finish if terminal execution is available, otherwise ask the user to run runtrim finish.",
7066
+ "- Do not claim completion before finish verification when RunTrim is active.",
6588
7067
  "",
6589
7068
  "RunTrim MCP:",
6590
7069
  "- runtrim mcp instructions",
@@ -6744,12 +7223,16 @@ function updateStartAgentInstructions(cwd) {
6744
7223
  const existing = fs13.existsSync(instructionsPath) ? fs13.readFileSync(instructionsPath, "utf-8") : "";
6745
7224
  const body = [
6746
7225
  "RunTrim agent instructions:",
7226
+ `RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`,
6747
7227
  "- Use RunTrim project memory and profile before editing.",
6748
- "- Verify active contract before edits. If MCP is available and contract is missing, call runtrim_create_contract with the user's task.",
7228
+ "- Create or verify an active RunTrim contract before edits.",
7229
+ "- If MCP is available and contract is missing, call runtrim_create_contract with the user's task.",
6749
7230
  '- If MCP is unavailable, ask user to run: runtrim agent "task" --copy',
6750
- "- For high-risk paths (auth, billing, middleware/proxy, migrations, sensitive files, broad app-wide changes), check scope first. If MCP is available, call runtrim_check_path.",
7231
+ "- For high-risk paths (auth, billing, middleware/proxy, migrations, env/sensitive files, broad app-wide changes), check scope first. If MCP is available, call runtrim_check_path.",
7232
+ "- Stay inside active contract scope and preserve existing project style and logic.",
6751
7233
  '- If scope must expand, request: runtrim approve "Allow <path/scope> for this run only"',
6752
- "- Run finish verification at the end. Do not claim completion before runtrim finish.",
7234
+ "- Run runtrim finish when terminal execution is available, otherwise ask the user to run runtrim finish.",
7235
+ "- Do not claim completion before runtrim finish verification when RunTrim is active.",
6753
7236
  "- Never read or print env file contents.",
6754
7237
  "",
6755
7238
  "RunTrim MCP:",
@@ -6822,6 +7305,202 @@ function detectKnownMcpConfigPresence() {
6822
7305
  cursorConfigFound: Boolean(cursorMatch)
6823
7306
  };
6824
7307
  }
7308
+ function getRestorePointPath(cwd, runId) {
7309
+ return path13.join(getRestoresDir(cwd), `${runId}.json`);
7310
+ }
7311
+ function getLegacyRestorePointPath(cwd, runId) {
7312
+ return path13.join(getLegacyRestoresDir(cwd), `${runId}.json`);
7313
+ }
7314
+ async function isGitRepo(cwd) {
7315
+ try {
7316
+ await execa3("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
7317
+ return true;
7318
+ } catch (e) {
7319
+ return false;
7320
+ }
7321
+ }
7322
+ async function captureRestorePoint(cwd, runId, task) {
7323
+ const dir = getRestoresDir(cwd);
7324
+ if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
7325
+ const existingPath = getRestorePointPath(cwd, runId);
7326
+ if (fs13.existsSync(existingPath) || fs13.existsSync(getLegacyRestorePointPath(cwd, runId))) return;
7327
+ let commit = null;
7328
+ let changedBeforeRun = [];
7329
+ if (await isGitRepo(cwd)) {
7330
+ try {
7331
+ const { stdout } = await execa3("git", ["rev-parse", "HEAD"], { cwd });
7332
+ commit = stdout.trim() || null;
7333
+ } catch (e) {
7334
+ commit = null;
7335
+ }
7336
+ try {
7337
+ const changed = await getGitChangedFiles(cwd);
7338
+ changedBeforeRun = dedupeFiles(changed.map((c) => c.path));
7339
+ } catch (e) {
7340
+ changedBeforeRun = [];
7341
+ }
7342
+ }
7343
+ const record = {
7344
+ runId,
7345
+ task,
7346
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7347
+ preRun: {
7348
+ commit,
7349
+ dirty: changedBeforeRun.length > 0,
7350
+ changedBeforeRun
7351
+ }
7352
+ };
7353
+ fs13.writeFileSync(existingPath, JSON.stringify(record, null, 2), "utf-8");
7354
+ }
7355
+ function loadRestorePoint(cwd, runId) {
7356
+ const preferred = getRestorePointPath(cwd, runId);
7357
+ const legacy = getLegacyRestorePointPath(cwd, runId);
7358
+ const p = fs13.existsSync(preferred) ? preferred : legacy;
7359
+ if (!fs13.existsSync(p)) return null;
7360
+ try {
7361
+ return JSON.parse(fs13.readFileSync(p, "utf-8"));
7362
+ } catch (e) {
7363
+ return null;
7364
+ }
7365
+ }
7366
+ function saveRestorePoint(cwd, record) {
7367
+ const dir = getRestoresDir(cwd);
7368
+ if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
7369
+ fs13.writeFileSync(getRestorePointPath(cwd, record.runId), JSON.stringify(record, null, 2), "utf-8");
7370
+ }
7371
+ function isSecretLikePath(file) {
7372
+ const n = file.replace(/\\/g, "/").toLowerCase();
7373
+ return n.includes(".env") || n.endsWith(".env") || n.endsWith(".pem") || n.endsWith(".key") || n.includes("id_rsa") || n.includes("id_ed25519") || n.includes("private-key");
7374
+ }
7375
+ function isDocsLikePath(file) {
7376
+ const n = file.replace(/\\/g, "/").toLowerCase();
7377
+ return n === "readme.md" || n.startsWith("docs/") || n.endsWith(".md") || n.endsWith(".mdx") || n.includes("changelog");
7378
+ }
7379
+ function isUiCheckoutPath(file) {
7380
+ const n = file.replace(/\\/g, "/").toLowerCase();
7381
+ return n.includes("/checkout/") && !n.includes("/api/") && !n.includes("webhook") && !n.includes("stripe") && !n.includes("dodo") && !n.includes("provider") && !n.includes("session");
7382
+ }
7383
+ function isHighRiskLogicPath(file) {
7384
+ const n = file.replace(/\\/g, "/").toLowerCase();
7385
+ if (isSecretLikePath(n)) return true;
7386
+ if (n.endsWith("middleware.ts") || n.endsWith("middleware.js") || n.endsWith("proxy.ts") || n.endsWith("proxy.js")) return true;
7387
+ if (n.includes("supabase/migrations") || n.includes("prisma/migrations") || n.includes("/migrations/")) return true;
7388
+ if (n.includes("/auth/") || n.includes("session") || n.includes("jwt")) return true;
7389
+ if (n.includes("webhook")) return true;
7390
+ if ((n.includes("billing") || n.includes("payment") || n.includes("stripe") || n.includes("dodo")) && (n.includes("/api/") || n.includes("/lib/") || n.includes("route.ts") || n.includes("route.js") || n.includes("server"))) return true;
7391
+ if (n.includes("/checkout/") && !isUiCheckoutPath(n) && (n.includes("/api/") || n.includes("provider") || n.includes("session") || n.includes("route.ts") || n.includes("route.js"))) return true;
7392
+ return false;
7393
+ }
7394
+ function matchesAnyContractRule(file, rules) {
7395
+ return rules.some((rule) => matchesContractPattern(file, rule));
7396
+ }
7397
+ async function detectCiChangedFiles(cwd, base, head) {
7398
+ var _a2;
7399
+ const warnings = [];
7400
+ let baseUsed = (base == null ? void 0 : base.trim()) || "";
7401
+ let headUsed = (head == null ? void 0 : head.trim()) || "";
7402
+ if (!headUsed) {
7403
+ try {
7404
+ const { stdout } = await execa3("git", ["rev-parse", "HEAD"], { cwd });
7405
+ headUsed = stdout.trim();
7406
+ } catch (e) {
7407
+ headUsed = "HEAD";
7408
+ }
7409
+ }
7410
+ if (!baseUsed) {
7411
+ const ghBase = (_a2 = process.env.GITHUB_BASE_REF) == null ? void 0 : _a2.trim();
7412
+ if (ghBase) {
7413
+ baseUsed = `origin/${ghBase}`;
7414
+ }
7415
+ }
7416
+ if (!baseUsed) {
7417
+ for (const candidate of ["origin/main", "origin/master", "main", "master", "HEAD~1"]) {
7418
+ try {
7419
+ await execa3("git", ["rev-parse", "--verify", candidate], { cwd });
7420
+ baseUsed = candidate;
7421
+ break;
7422
+ } catch (e) {
7423
+ continue;
7424
+ }
7425
+ }
7426
+ }
7427
+ if (!baseUsed) {
7428
+ warnings.push("Could not infer a base ref. Using working tree changes only.");
7429
+ }
7430
+ let diffFiles = [];
7431
+ if (baseUsed) {
7432
+ try {
7433
+ const { stdout } = await execa3("git", ["diff", "--name-only", `${baseUsed}...${headUsed}`], { cwd });
7434
+ diffFiles = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
7435
+ } catch (e) {
7436
+ warnings.push(`Could not diff ${baseUsed}...${headUsed}. Falling back to local changes.`);
7437
+ }
7438
+ }
7439
+ if (diffFiles.length === 0) {
7440
+ try {
7441
+ const { stdout } = await execa3("git", ["diff", "--name-only"], { cwd });
7442
+ diffFiles = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
7443
+ } catch (e) {
7444
+ }
7445
+ }
7446
+ try {
7447
+ const { stdout } = await execa3("git", ["status", "--porcelain"], { cwd });
7448
+ const untracked = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("?? ")).map((line) => line.slice(3).trim());
7449
+ diffFiles = dedupeFiles([...diffFiles, ...untracked]);
7450
+ } catch (e) {
7451
+ }
7452
+ return {
7453
+ files: dedupeFiles(diffFiles.map((f) => f.replace(/\\/g, "/"))),
7454
+ baseUsed: baseUsed || "(local)",
7455
+ headUsed,
7456
+ warnings
7457
+ };
7458
+ }
7459
+ function detectMcpConfigState(configPath, found) {
7460
+ if (!found || !configPath) return "not found";
7461
+ try {
7462
+ const raw = JSON.parse(fs13.readFileSync(configPath, "utf-8"));
7463
+ const servers = raw.mcpServers && typeof raw.mcpServers === "object" ? raw.mcpServers : {};
7464
+ return servers.runtrim ? "configured" : "missing runtrim";
7465
+ } catch (e) {
7466
+ return "missing runtrim";
7467
+ }
7468
+ }
7469
+ function hasCurrentRuntrimBlock(filePath) {
7470
+ if (!fs13.existsSync(filePath)) return { exists: false, current: false };
7471
+ const content = fs13.readFileSync(filePath, "utf-8");
7472
+ const hasBlock = content.includes("<!-- RUNTRIM:START -->") && content.includes("<!-- RUNTRIM:END -->");
7473
+ const hasVersion = content.includes(`RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`);
7474
+ return { exists: hasBlock, current: hasBlock && hasVersion };
7475
+ }
7476
+ function readMcpLastUsed(cwd) {
7477
+ const p = path13.join(getProjectMcpDir(cwd), "last-used.json");
7478
+ if (!fs13.existsSync(p)) return { tracked: false, tool: null, usedAt: null };
7479
+ try {
7480
+ const parsed = JSON.parse(fs13.readFileSync(p, "utf-8"));
7481
+ return {
7482
+ tracked: true,
7483
+ tool: typeof parsed.tool === "string" ? parsed.tool : null,
7484
+ usedAt: typeof parsed.usedAt === "string" ? parsed.usedAt : null
7485
+ };
7486
+ } catch (e) {
7487
+ return { tracked: false, tool: null, usedAt: null };
7488
+ }
7489
+ }
7490
+ function writeMcpLastUsed(cwd, tool) {
7491
+ try {
7492
+ const dir = getProjectMcpDir(cwd);
7493
+ if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
7494
+ const payload = {
7495
+ tool,
7496
+ usedAt: (/* @__PURE__ */ new Date()).toISOString(),
7497
+ projectPath: cwd
7498
+ };
7499
+ fs13.writeFileSync(path13.join(dir, "last-used.json"), `${JSON.stringify(payload, null, 2)}
7500
+ `, "utf-8");
7501
+ } catch (e) {
7502
+ }
7503
+ }
6825
7504
  function appendContractAmendment(cwd, approvalText) {
6826
7505
  const p = path13.join(cwd, ".runtrim", "contracts", "latest.md");
6827
7506
  if (!fs13.existsSync(p)) return { ok: false, reason: "missing_contract" };
@@ -7129,6 +7808,24 @@ async function buildRuntrimCreateContractMcp(cwd, args) {
7129
7808
  isError: true
7130
7809
  };
7131
7810
  }
7811
+ const repoCheck = await assertFreeRepoAllowed(cwd);
7812
+ if (!repoCheck.allowed) {
7813
+ 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.";
7814
+ const blockedPayload = {
7815
+ contract_created: false,
7816
+ task: taskRaw,
7817
+ error: repoCheck.status === "blocked_repair" ? "repo_registry_repair_required" : "repo_limit_blocked",
7818
+ guidance,
7819
+ next_action: guidance,
7820
+ finish_command: "runtrim finish",
7821
+ approval_command_example: 'runtrim approve "Allow <path> for this run only"'
7822
+ };
7823
+ return {
7824
+ content: [{ type: "text", text: JSON.stringify(blockedPayload, null, 2) }],
7825
+ structuredContent: blockedPayload,
7826
+ isError: true
7827
+ };
7828
+ }
7132
7829
  const latest = loadLatestRun(cwd);
7133
7830
  if ((latest == null ? void 0 : latest.status) === "guarded") {
7134
7831
  const blockedPayload = {
@@ -7176,6 +7873,7 @@ async function buildRuntrimCreateContractMcp(cwd, args) {
7176
7873
  const handoff = writeAgentHandoffArtifacts(cwd, apply, path13.relative(cwd, previewPath));
7177
7874
  const run = saveRun(mergedTask, previewResult.audit, previewResult.contract, cwd);
7178
7875
  updateRun(run.id, { status: "guarded" }, cwd);
7876
+ await captureRestorePoint(cwd, run.id, mergedTask);
7179
7877
  const payload = {
7180
7878
  contract_created: true,
7181
7879
  task: taskRaw,
@@ -7370,6 +8068,7 @@ async function startMcpServerStdio(cwd) {
7370
8068
  });
7371
8069
  return;
7372
8070
  }
8071
+ writeMcpLastUsed(cwd, name);
7373
8072
  send({
7374
8073
  jsonrpc: "2.0",
7375
8074
  id,
@@ -7790,6 +8489,21 @@ function isInteractiveTerminal() {
7790
8489
  async function ensureRepoAllowedForFree(cwd) {
7791
8490
  var _a2, _b;
7792
8491
  const check = await assertFreeRepoAllowed(cwd);
8492
+ if (check.status === "blocked_repair") {
8493
+ console.log(chalk.yellow(" RunTrim local state needs repair."));
8494
+ console.log(chalk.yellow(" Free includes 1 tracked repo."));
8495
+ console.log(chalk.yellow(" Your local repo registry changed unexpectedly."));
8496
+ console.log("");
8497
+ console.log(DIM(" Next:"));
8498
+ console.log(chalk.white(" - runtrim repo status"));
8499
+ console.log(chalk.white(" - runtrim repo repair"));
8500
+ console.log(chalk.white(" - runtrim repo repair --use-current"));
8501
+ console.log(chalk.white(" - runtrim repo unlink --force"));
8502
+ console.log(chalk.white(" - upgrade to Builder for unlimited repos"));
8503
+ console.log(chalk.white(" - sign in to restore cloud entitlements"));
8504
+ console.log("");
8505
+ return false;
8506
+ }
7793
8507
  if (check.allowed) {
7794
8508
  await registerCurrentRepo(cwd);
7795
8509
  return true;
@@ -7803,9 +8517,12 @@ async function ensureRepoAllowedForFree(cwd) {
7803
8517
  console.log(chalk.white(` ${check.currentRepo.path}`));
7804
8518
  console.log("");
7805
8519
  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"));
8520
+ console.log(
8521
+ chalk.white(
8522
+ " 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."
8523
+ )
8524
+ );
8525
+ console.log(chalk.white(" Agent instructions were not installed because this repo is not tracked."));
7809
8526
  console.log("");
7810
8527
  console.log(
7811
8528
  DIM(
@@ -7839,6 +8556,7 @@ async function initializeRunTrim(cwd, options = {}) {
7839
8556
  const runsDir = getRunsDir(cwd);
7840
8557
  if (!fs13.existsSync(configDir)) fs13.mkdirSync(configDir, { recursive: true });
7841
8558
  if (!fs13.existsSync(runsDir)) fs13.mkdirSync(runsDir, { recursive: true });
8559
+ ensureInternalArtifactDirs(cwd);
7842
8560
  const existingConfig = hadConfig ? loadConfig(cwd) : null;
7843
8561
  const baseConfig = __spreadValues(__spreadValues({}, DEFAULT_CONFIG), detectProjectInfo(cwd));
7844
8562
  const nextConfig = options.refresh && existingConfig ? __spreadValues({}, existingConfig) : baseConfig;
@@ -7858,13 +8576,8 @@ async function initializeRunTrim(cwd, options = {}) {
7858
8576
  fs13.writeFileSync(memoryPath, buildBaselineMemoryMarkdown(baseline), "utf-8");
7859
8577
  }
7860
8578
  ensureStarterPromptIfMissing(cwd);
7861
- const gitignorePath = path13.join(cwd, ".gitignore");
7862
- if (fs13.existsSync(gitignorePath)) {
7863
- const content = fs13.readFileSync(gitignorePath, "utf-8");
7864
- if (!content.includes(".runtrim/runs")) {
7865
- fs13.appendFileSync(gitignorePath, "\n# RunTrim run history\n.runtrim/runs/\n");
7866
- }
7867
- }
8579
+ ensureRuntrimReadme(cwd);
8580
+ ensureRuntrimGitignoreGuidance(cwd);
7868
8581
  return { ok: true };
7869
8582
  }
7870
8583
  async function runPrepareTask(task, options) {
@@ -7899,7 +8612,7 @@ async function runPrepareTask(task, options) {
7899
8612
  console.log("");
7900
8613
  console.log(DIM(" Task ") + chalk.white(truncate(task, 70)));
7901
8614
  console.log(DIM(" Prompt ") + chalk.white(promptPath2));
7902
- console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
8615
+ console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
7903
8616
  console.log("");
7904
8617
  printPrepareAgentInstructions(selectedAgent, config.lastPromptPath);
7905
8618
  console.log("");
@@ -7919,6 +8632,7 @@ async function runPrepareTask(task, options) {
7919
8632
  const promptPath = writeLatestPromptFile(contract.contractText, config, cwd);
7920
8633
  if (options.copy !== false) await copyToClipboardSafe(contract.contractText);
7921
8634
  updateRun(run.id, { status: "guarded" }, cwd);
8635
+ await captureRestorePoint(cwd, run.id, task);
7922
8636
  const riskColors = {
7923
8637
  low: chalk.green,
7924
8638
  medium: chalk.yellow,
@@ -7941,7 +8655,7 @@ async function runPrepareTask(task, options) {
7941
8655
  );
7942
8656
  console.log(DIM(" Reduction ") + chalk.white(contract.riskReductionPercent + "%"));
7943
8657
  console.log(DIM(" Prompt ") + chalk.white(promptPath));
7944
- console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
8658
+ console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
7945
8659
  console.log("");
7946
8660
  printPrepareAgentInstructions(selectedAgent, config.lastPromptPath);
7947
8661
  console.log("");
@@ -8166,13 +8880,116 @@ program.command("start").description("Guided RunTrim onboarding and daily loop")
8166
8880
  console.log("");
8167
8881
  }
8168
8882
  });
8883
+ program.command("doctor").description("Check project readiness for RunTrim agent auto-control").action(async () => {
8884
+ const cwd = process.cwd();
8885
+ const repoCheck = await assertFreeRepoAllowed(cwd);
8886
+ const profilePath = path13.join(getConfigDir(cwd), "project-profile.json");
8887
+ const memoryPath = path13.join(getConfigDir(cwd), "memory", "current.md");
8888
+ const instructionsPath = path13.join(getConfigDir(cwd), "agent", "instructions.md");
8889
+ const snippetsDir = getProjectMcpDir(cwd);
8890
+ const snippetFiles = [
8891
+ path13.join(snippetsDir, "claude-desktop.json"),
8892
+ path13.join(snippetsDir, "cursor.json"),
8893
+ path13.join(snippetsDir, "generic.json")
8894
+ ];
8895
+ const profileReady = fs13.existsSync(profilePath);
8896
+ const memoryReady = fs13.existsSync(memoryPath) && fs13.readFileSync(memoryPath, "utf-8").trim().length > 0;
8897
+ const instructionsReady = fs13.existsSync(instructionsPath) && fs13.readFileSync(instructionsPath, "utf-8").trim().length > 0;
8898
+ const claudeBlock = hasCurrentRuntrimBlock(path13.join(cwd, "CLAUDE.md"));
8899
+ const agentsBlock = hasCurrentRuntrimBlock(path13.join(cwd, "AGENTS.md"));
8900
+ const cursorRulePath = path13.join(cwd, ".cursor", "rules", "runtrim.mdc");
8901
+ const cursorRuleExists = fs13.existsSync(cursorRulePath);
8902
+ const cursorRuleCurrent = cursorRuleExists ? fs13.readFileSync(cursorRulePath, "utf-8").includes(`RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`) : false;
8903
+ const anySurfaceInstalled = claudeBlock.exists || agentsBlock.exists || cursorRuleExists;
8904
+ const runtrimBlockCurrent = [claudeBlock.current, agentsBlock.current, cursorRuleCurrent].some(Boolean);
8905
+ const snippetsGenerated = snippetFiles.every((p) => fs13.existsSync(p));
8906
+ const knownMcp = detectKnownMcpConfigPresence();
8907
+ const claudeMcpState = detectMcpConfigState(knownMcp.claudeConfigPath, knownMcp.claudeConfigFound);
8908
+ const cursorMcpState = detectMcpConfigState(knownMcp.cursorConfigPath, knownMcp.cursorConfigFound);
8909
+ const genericReady = snippetsGenerated ? "ready" : "missing";
8910
+ const tools = buildMcpTools();
8911
+ const hasContractTool = tools.some((t) => t.name === "runtrim_create_contract");
8912
+ const hasPathTool = tools.some((t) => t.name === "runtrim_check_path");
8913
+ const hasApprovalTool = tools.some((t) => t.name === "runtrim_suggest_approval");
8914
+ const hasFinishTool = tools.some((t) => t.name === "runtrim_finish_guidance");
8915
+ const contract = parseContractSummary(cwd);
8916
+ const lastMcp = readMcpLastUsed(cwd);
8917
+ const setupCorrupt = configExists(cwd) && (!profileReady || !memoryReady || !instructionsReady);
8918
+ const artifactFiles = listArtifactFiles(cwd);
8919
+ const artifactCount = artifactFiles.length;
8920
+ let readiness = "partial";
8921
+ if (!repoCheck.allowed || setupCorrupt) {
8922
+ readiness = "blocked";
8923
+ } else if (profileReady && memoryReady && instructionsReady && anySurfaceInstalled && snippetsGenerated && hasContractTool && hasPathTool && hasApprovalTool && hasFinishTool) {
8924
+ readiness = "ready";
8925
+ }
8926
+ console.log("");
8927
+ console.log(BOLD("RunTrim") + DIM(" doctor"));
8928
+ console.log("");
8929
+ console.log(BOLD("Project"));
8930
+ console.log(chalk.white(`- Project profile: ${profileReady ? "ready" : "missing"}`));
8931
+ console.log(chalk.white(`- Project memory: ${memoryReady ? "ready" : "missing"}`));
8932
+ console.log(chalk.white(`- Agent instructions: ${instructionsReady ? "ready" : "missing"}`));
8933
+ console.log(chalk.white(`- Active contract: ${contract.active ? "active" : "none"}`));
8934
+ console.log("");
8935
+ console.log(BOLD("Agent rules"));
8936
+ console.log(chalk.white(`- CLAUDE.md: ${claudeBlock.exists ? "installed" : "not found"}`));
8937
+ console.log(chalk.white(`- AGENTS.md: ${agentsBlock.exists ? "installed" : "not found"}`));
8938
+ console.log(chalk.white(`- Cursor rules: ${cursorRuleExists ? "installed" : "not found"}`));
8939
+ console.log(chalk.white(`- RunTrim instruction block: ${runtrimBlockCurrent ? "current" : anySurfaceInstalled ? "stale" : "missing"}`));
8940
+ console.log("");
8941
+ console.log(BOLD("MCP"));
8942
+ console.log(chalk.white("- MCP server: available"));
8943
+ console.log(chalk.white(`- Project snippets: ${snippetsGenerated ? "generated" : "missing"}`));
8944
+ console.log(chalk.white(`- Claude Desktop config: ${claudeMcpState}`));
8945
+ console.log(chalk.white(`- Cursor MCP config: ${cursorMcpState}`));
8946
+ console.log(chalk.white(`- Generic config: ${genericReady}`));
8947
+ if (lastMcp.tracked && lastMcp.tool && lastMcp.usedAt) {
8948
+ console.log(chalk.white(`- Last MCP tool call: ${lastMcp.tool}, ${lastMcp.usedAt}`));
8949
+ } else {
8950
+ console.log(chalk.white("- Last MCP tool call: not tracked yet"));
8951
+ }
8952
+ console.log("");
8953
+ console.log(BOLD("Automation readiness"));
8954
+ console.log(chalk.white(`- Contract creation tool: ${hasContractTool ? "available" : "missing"}`));
8955
+ console.log(chalk.white(`- Path check tool: ${hasPathTool ? "available" : "missing"}`));
8956
+ console.log(chalk.white(`- Approval tool: ${hasApprovalTool ? "available" : "missing"}`));
8957
+ console.log(chalk.white(`- Finish guidance tool: ${hasFinishTool ? "available" : "missing"}`));
8958
+ console.log(chalk.white(`- Local artifacts: ${artifactCount}`));
8959
+ console.log("");
8960
+ console.log(BOLD("Readiness"));
8961
+ console.log(chalk.white(`- State: ${readiness}`));
8962
+ console.log("");
8963
+ console.log(BOLD("Next"));
8964
+ if (repoCheck.status === "blocked_repair") {
8965
+ console.log(chalk.yellow("- RunTrim local state needs repair. Run runtrim repo repair."));
8966
+ } else if (repoCheck.status === "blocked_limit") {
8967
+ console.log(chalk.yellow("- Free includes 1 tracked repo. Continue in tracked repo, unlink with runtrim repo unlink --force, or upgrade to Builder."));
8968
+ } else if (readiness === "ready") {
8969
+ if (claudeMcpState !== "configured" && cursorMcpState !== "configured") {
8970
+ console.log(chalk.white("- Ready locally, MCP client not connected."));
8971
+ console.log(chalk.white("- Run runtrim mcp instructions or copy .runtrim/mcp/cursor.json into your MCP client."));
8972
+ } else {
8973
+ console.log(chalk.white("- Your project is RunTrim-aware. Open Cursor/Claude/Codex and use normal language."));
8974
+ }
8975
+ } else {
8976
+ console.log(chalk.white("- Run runtrim start."));
8977
+ console.log(chalk.white("- Run runtrim mcp instructions."));
8978
+ console.log(chalk.white("- Run runtrim mcp config --print."));
8979
+ }
8980
+ if (artifactCount > 25) {
8981
+ console.log(chalk.white(`- Local artifacts: ${artifactCount}. Run runtrim clean --dry-run to review cleanup.`));
8982
+ }
8983
+ console.log("");
8984
+ });
8169
8985
  var PROTOCOL_BLOCK_START = "<!-- RUNTRIM_PROTOCOL_START -->";
8170
8986
  var PROTOCOL_BLOCK_END = "<!-- RUNTRIM_PROTOCOL_END -->";
8171
8987
  var PROTOCOL_POINTER_BLOCK = `
8172
8988
  ${PROTOCOL_BLOCK_START}
8173
8989
  This repo uses RunTrim as the guarded AI coding protocol.
8174
8990
  Before editing code, read RUNTRIM.md.
8175
- Start every task with: runtrim go "<task>"
8991
+ Start every task with: runtrim start
8992
+ Then run: runtrim agent "Your task" --copy
8176
8993
  Stay inside .runtrim/contracts/latest.md.
8177
8994
  After edits, ask the user to run: runtrim finish
8178
8995
  ${PROTOCOL_BLOCK_END}
@@ -8868,6 +9685,8 @@ program.command("execute <task>").description("Create a controlled execution pac
8868
9685
  var agentCommand = program.command("agent").description("Start a guarded AI coding run with contract, scope, memory, and handoff");
8869
9686
  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
9687
  if (task == null ? void 0 : task.trim()) {
9688
+ const allowed = await ensureRepoAllowedForFree(process.cwd());
9689
+ if (!allowed) return;
8871
9690
  const normalizedTask = (task != null ? task : "").trim();
8872
9691
  if (options == null ? void 0 : options.bridge) {
8873
9692
  const bridge = await ensureBridgeRunningForAgent(process.cwd());
@@ -9267,6 +10086,146 @@ agentCommand.command("prompt-mode <mode>").description("Set how guarded contract
9267
10086
  console.log(ACCENT.bold(" Agent prompt mode updated to: " + m));
9268
10087
  console.log("");
9269
10088
  });
10089
+ var ciCommand = program.command("ci").description("RunTrim CI checks for pull requests and merges");
10090
+ ciCommand.command("check").description("Evaluate changed files and RunTrim context for CI-safe PASS/WARN/BLOCKED verdict").option("--base <ref>", "Base ref to diff from").option("--head <ref>", "Head ref to diff to").option("--strict", "Treat WARN as failing and require RunTrim context").option("--allow-warn", "Allow WARN even when --strict is set").option("--json", "Print machine-readable JSON output").option("--report", "Include extra diagnostic context in output").action(async (options) => {
10091
+ const cwd = process.cwd();
10092
+ const strict = options.strict === true;
10093
+ const allowWarn = options.allowWarn === true;
10094
+ const outputJson = options.json === true;
10095
+ const report = options.report === true;
10096
+ const repoCheck = await assertFreeRepoAllowed(cwd);
10097
+ const detection = await detectCiChangedFiles(cwd, options.base, options.head);
10098
+ const changedFiles = detection.files;
10099
+ const contract = parseContractSummary(cwd);
10100
+ const latestRun = loadLatestRun(cwd);
10101
+ const hasRuntrimContext = contract.exists || Boolean(latestRun);
10102
+ const issues = [];
10103
+ const warnings = [...detection.warnings];
10104
+ const nextSteps = [];
10105
+ let verdict = "PASS";
10106
+ if (repoCheck.status === "blocked_repair") {
10107
+ issues.push("RunTrim local state needs repair before CI can trust local guard state.");
10108
+ nextSteps.push("Run: runtrim repo repair");
10109
+ verdict = "BLOCKED";
10110
+ } else if (repoCheck.status === "blocked_limit") {
10111
+ issues.push("Free includes 1 tracked repo and this repo is not currently tracked.");
10112
+ nextSteps.push("Continue in tracked repo, unlink with runtrim repo unlink --force, or upgrade to Builder.");
10113
+ verdict = "BLOCKED";
10114
+ }
10115
+ if (changedFiles.length === 0) {
10116
+ warnings.push("No changed files detected for this diff.");
10117
+ }
10118
+ const secretFiles = changedFiles.filter(isSecretLikePath);
10119
+ if (secretFiles.length > 0) {
10120
+ issues.push(`Sensitive file path changed: ${secretFiles[0]}`);
10121
+ nextSteps.push("Remove secret/env/key/cert files from this PR before merge.");
10122
+ verdict = "BLOCKED";
10123
+ }
10124
+ const highRiskFiles = changedFiles.filter((f) => isHighRiskLogicPath(f) && !isSecretLikePath(f));
10125
+ if (highRiskFiles.length > 0) {
10126
+ if (!hasRuntrimContext) {
10127
+ issues.push(`High-risk path changed without RunTrim context: ${highRiskFiles[0]}`);
10128
+ nextSteps.push("Create a dedicated guarded run and finish it before merging.");
10129
+ verdict = "BLOCKED";
10130
+ } else if (contract.allowedPaths.length > 0 && !matchesAnyContractRule(highRiskFiles[0], contract.allowedPaths)) {
10131
+ issues.push(`High-risk path is outside contract allowed scope: ${highRiskFiles[0]}`);
10132
+ nextSteps.push(`Run: runtrim approve "Allow ${highRiskFiles[0]} for this run only"`);
10133
+ verdict = "BLOCKED";
10134
+ }
10135
+ }
10136
+ if (contract.forbiddenPaths.length > 0) {
10137
+ const forbiddenTouched = changedFiles.filter((f) => matchesAnyContractRule(f, contract.forbiddenPaths));
10138
+ if (forbiddenTouched.length > 0) {
10139
+ issues.push(`Contract-forbidden path touched: ${forbiddenTouched[0]}`);
10140
+ nextSteps.push("Split the task or get explicit scoped approval before merge.");
10141
+ verdict = "BLOCKED";
10142
+ }
10143
+ }
10144
+ if (contract.allowedPaths.length > 0) {
10145
+ const outOfScope = changedFiles.filter((f) => !matchesAnyContractRule(f, contract.allowedPaths));
10146
+ if (outOfScope.length > 0) {
10147
+ issues.push(`Changed file is outside latest contract scope: ${outOfScope[0]}`);
10148
+ nextSteps.push(`Run: runtrim approve "Allow ${outOfScope[0]} for this run only"`);
10149
+ verdict = "BLOCKED";
10150
+ }
10151
+ }
10152
+ const docsOnly = changedFiles.length > 0 && changedFiles.every((f) => isDocsLikePath(f));
10153
+ if (!hasRuntrimContext && verdict !== "BLOCKED") {
10154
+ if (docsOnly) {
10155
+ warnings.push("No RunTrim contract/report found. Docs-only change treated as low risk.");
10156
+ verdict = "WARN";
10157
+ } else {
10158
+ warnings.push("No RunTrim contract/report found. CI could not fully verify scope context.");
10159
+ verdict = "WARN";
10160
+ }
10161
+ }
10162
+ if (strict && !hasRuntrimContext && verdict !== "BLOCKED") {
10163
+ issues.push("Strict mode requires RunTrim contract/report context.");
10164
+ verdict = "BLOCKED";
10165
+ }
10166
+ if (verdict === "PASS" && warnings.length > 0) {
10167
+ verdict = "WARN";
10168
+ }
10169
+ if (nextSteps.length === 0) {
10170
+ if (verdict === "PASS") {
10171
+ nextSteps.push("Safe to merge under current RunTrim CI policy.");
10172
+ } else if (verdict === "WARN") {
10173
+ nextSteps.push("Run runtrim finish locally to strengthen verification context.");
10174
+ } else {
10175
+ nextSteps.push("Resolve blocked issues, then rerun runtrim ci check.");
10176
+ }
10177
+ }
10178
+ let exitCode = 0;
10179
+ if (verdict === "BLOCKED") exitCode = 1;
10180
+ if (verdict === "WARN" && strict && !allowWarn) exitCode = 1;
10181
+ const jsonPayload = {
10182
+ verdict: verdict.toLowerCase(),
10183
+ exitCode,
10184
+ changedFiles,
10185
+ issues,
10186
+ warnings,
10187
+ nextSteps
10188
+ };
10189
+ if (outputJson) {
10190
+ process.stdout.write(`${JSON.stringify(jsonPayload, null, 2)}
10191
+ `);
10192
+ process.exit(exitCode);
10193
+ return;
10194
+ }
10195
+ console.log("");
10196
+ console.log(BOLD("RunTrim") + DIM(" CI Check"));
10197
+ console.log("");
10198
+ const verdictColor = verdict === "PASS" ? chalk.green : verdict === "WARN" ? chalk.yellow : chalk.red;
10199
+ console.log(DIM(" Verdict: ") + verdictColor(verdict));
10200
+ console.log("");
10201
+ console.log(DIM(" Changed files:"));
10202
+ if (changedFiles.length === 0) console.log(chalk.white(" - (none detected)"));
10203
+ for (const f of changedFiles.slice(0, 20)) console.log(chalk.white(" - " + f));
10204
+ if (changedFiles.length > 20) console.log(DIM(` ... and ${changedFiles.length - 20} more`));
10205
+ console.log("");
10206
+ if (issues.length > 0) {
10207
+ console.log(DIM(" Issues:"));
10208
+ for (const i of issues) console.log(chalk.red(" - " + i));
10209
+ console.log("");
10210
+ }
10211
+ if (warnings.length > 0) {
10212
+ console.log(DIM(" Warnings:"));
10213
+ for (const w of warnings) console.log(chalk.yellow(" - " + w));
10214
+ console.log("");
10215
+ }
10216
+ console.log(DIM(" Next:"));
10217
+ for (const n of nextSteps) console.log(chalk.white(" - " + n));
10218
+ if (report) {
10219
+ console.log("");
10220
+ console.log(DIM(" Report:"));
10221
+ console.log(chalk.white(` - Base: ${detection.baseUsed}`));
10222
+ console.log(chalk.white(` - Head: ${detection.headUsed}`));
10223
+ console.log(chalk.white(` - Contract: ${contract.exists ? contract.active ? "active" : "present" : "none"}`));
10224
+ console.log(chalk.white(` - Latest run: ${latestRun ? latestRun.status : "none"}`));
10225
+ }
10226
+ console.log("");
10227
+ process.exit(exitCode);
10228
+ });
9270
10229
  var authCommand = program.command("auth").description("Configure sync token for dashboard sync");
9271
10230
  authCommand.command("set <token>").description("Set sync token and enable sync").action((token) => {
9272
10231
  const cwd = process.cwd();
@@ -9336,12 +10295,54 @@ repoCommand.command("status").description("Show local tracked repo status").acti
9336
10295
  console.log(DIM(" Current repo ") + chalk.white(identity.path));
9337
10296
  console.log(DIM(" Tracked repo ") + chalk.white((_b = tracked == null ? void 0 : tracked.path) != null ? _b : "(none)"));
9338
10297
  console.log(DIM(" Allowed ") + chalk.white(check.allowed ? "yes" : "no"));
10298
+ console.log(DIM(" State ") + chalk.white(check.status));
9339
10299
  console.log("");
10300
+ if (check.status === "blocked_repair") {
10301
+ console.log(chalk.yellow(" RunTrim local state needs repair."));
10302
+ console.log(chalk.yellow(" Free includes 1 tracked repo."));
10303
+ console.log(chalk.yellow(" The local repo registry changed unexpectedly."));
10304
+ console.log(DIM(" Run: runtrim repo repair"));
10305
+ console.log("");
10306
+ }
9340
10307
  if (tracked) {
9341
10308
  console.log(DIM(" A tracked repo is one codebase with its own .runtrim workspace."));
9342
10309
  console.log("");
9343
10310
  }
9344
10311
  });
10312
+ 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) => {
10313
+ const cwd = process.cwd();
10314
+ const before = await assertFreeRepoAllowed(cwd);
10315
+ console.log("");
10316
+ console.log(BOLD("RunTrim") + DIM(" repo repair"));
10317
+ console.log("");
10318
+ if (!before.repairRequired) {
10319
+ console.log(DIM(" Local state is healthy. No repair required."));
10320
+ console.log("");
10321
+ return;
10322
+ }
10323
+ if (!options.useCurrent) {
10324
+ console.log(chalk.yellow(" RunTrim local state needs repair."));
10325
+ console.log(chalk.yellow(" Free includes 1 tracked repo."));
10326
+ console.log(chalk.yellow(" Your local repo registry changed unexpectedly."));
10327
+ console.log("");
10328
+ console.log(DIM(" Safe next actions:"));
10329
+ console.log(chalk.white(" - runtrim repo repair --use-current"));
10330
+ console.log(chalk.white(" - runtrim repo unlink --force"));
10331
+ console.log(chalk.white(" - upgrade to Builder for unlimited repos"));
10332
+ console.log(chalk.white(" - sign in to restore cloud entitlements"));
10333
+ console.log("");
10334
+ return;
10335
+ }
10336
+ const result = await repairGlobalRegistry(cwd, { useCurrentRepo: true });
10337
+ if (result.repaired) {
10338
+ console.log(ACCENT.bold(" Local registry repaired."));
10339
+ console.log(DIM(" Current repo is now the tracked Free repo."));
10340
+ console.log("");
10341
+ return;
10342
+ }
10343
+ console.log(DIM(" No repair changes applied."));
10344
+ console.log("");
10345
+ });
9345
10346
  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
10347
  const cwd = process.cwd();
9347
10348
  const result = await unlinkCurrentRepo(cwd, Boolean(options.force));
@@ -9366,6 +10367,214 @@ repoCommand.command("unlink").description("Unlink tracked repo from local free-p
9366
10367
  console.log(DIM(" No tracked repo found."));
9367
10368
  console.log("");
9368
10369
  });
10370
+ program.command("clean").description("Clean old local RunTrim artifacts while preserving active project state").option("--dry-run", "Preview files that would be removed").option("--keep <n>", "Number of latest run-linked artifacts to keep", "10").action(async (options) => {
10371
+ var _a2;
10372
+ const cwd = process.cwd();
10373
+ const dryRun = (options == null ? void 0 : options.dryRun) === true;
10374
+ const keep = Math.max(1, Number.parseInt((_a2 = options == null ? void 0 : options.keep) != null ? _a2 : "10", 10) || 10);
10375
+ ensureInternalArtifactDirs(cwd);
10376
+ const runs = loadAllRuns(cwd);
10377
+ const keepRunIds = new Set(runs.slice(0, keep).map((r) => r.id));
10378
+ const files = listArtifactFiles(cwd);
10379
+ const removable = files.filter((filePath) => {
10380
+ const runId = parseRunIdFromArtifact(filePath);
10381
+ if (!runId) return true;
10382
+ return !keepRunIds.has(runId);
10383
+ });
10384
+ const byCategory = {
10385
+ runs: 0,
10386
+ previews: 0,
10387
+ restores: 0,
10388
+ archives: 0,
10389
+ other: 0
10390
+ };
10391
+ for (const filePath of removable) {
10392
+ const rel = path13.relative(cwd, filePath).replace(/\\/g, "/").toLowerCase();
10393
+ if (rel.includes(".runtrim/internal/runs/") || rel.includes(".runtrim/runs/")) byCategory.runs += 1;
10394
+ else if (rel.includes(".runtrim/internal/previews/") || rel.includes(".runtrim/previews/")) byCategory.previews += 1;
10395
+ else if (rel.includes(".runtrim/internal/restores/") || rel.includes(".runtrim/restores/")) byCategory.restores += 1;
10396
+ else if (rel.includes("archive")) byCategory.archives += 1;
10397
+ else byCategory.other += 1;
10398
+ }
10399
+ if (!dryRun) {
10400
+ for (const filePath of removable) {
10401
+ try {
10402
+ fs13.rmSync(filePath, { force: true });
10403
+ } catch (e) {
10404
+ }
10405
+ }
10406
+ }
10407
+ console.log("");
10408
+ console.log(BOLD("RunTrim") + DIM(" clean"));
10409
+ console.log("");
10410
+ console.log(DIM(" Mode ") + chalk.white(dryRun ? "dry-run" : "apply"));
10411
+ console.log(DIM(" Keep latest runs ") + chalk.white(String(keep)));
10412
+ console.log(DIM(" Candidate files ") + chalk.white(String(removable.length)));
10413
+ console.log("");
10414
+ console.log(DIM(" Cleanup summary"));
10415
+ console.log(chalk.white(` - runs: ${byCategory.runs}`));
10416
+ console.log(chalk.white(` - previews: ${byCategory.previews}`));
10417
+ console.log(chalk.white(` - restores: ${byCategory.restores}`));
10418
+ console.log(chalk.white(` - archives: ${byCategory.archives}`));
10419
+ if (byCategory.other > 0) console.log(chalk.white(` - other: ${byCategory.other}`));
10420
+ console.log("");
10421
+ if (removable.length > 0) {
10422
+ const sample = removable.slice(0, 10).map((p) => path13.relative(cwd, p).replace(/\\/g, "/"));
10423
+ console.log(DIM(" Sample files"));
10424
+ for (const rel of sample) console.log(chalk.white(` - ${rel}`));
10425
+ if (removable.length > sample.length) {
10426
+ console.log(DIM(` ... and ${removable.length - sample.length} more`));
10427
+ }
10428
+ console.log("");
10429
+ }
10430
+ if (dryRun) {
10431
+ console.log(chalk.white(" No files were removed."));
10432
+ console.log(chalk.white(" Re-run without --dry-run to apply cleanup."));
10433
+ } else {
10434
+ console.log(chalk.white(" Cleanup complete."));
10435
+ console.log(chalk.white(" Active contract, latest files, memory current, and MCP snippets were preserved."));
10436
+ }
10437
+ console.log("");
10438
+ });
10439
+ var restoreCommand = program.command("restore").description("Preview or apply local restore for guarded runs");
10440
+ restoreCommand.argument("[runId]", "Run ID to restore, or use 'last'").option("--preview", "Preview restore plan").option("--apply", "Apply restore plan").option("--force", "Apply even if unrelated new changes are detected").action(async (runIdArg, options) => {
10441
+ var _a2, _b, _c, _d;
10442
+ const cwd = process.cwd();
10443
+ const doPreview = (options == null ? void 0 : options.preview) === true;
10444
+ const doApply = (options == null ? void 0 : options.apply) === true;
10445
+ const force = (options == null ? void 0 : options.force) === true;
10446
+ if (!doPreview && !doApply) {
10447
+ console.log("");
10448
+ console.log(chalk.yellow("Choose --preview or --apply."));
10449
+ console.log("");
10450
+ return;
10451
+ }
10452
+ if (doPreview && doApply) {
10453
+ console.log("");
10454
+ console.log(chalk.yellow("Use either --preview or --apply, not both."));
10455
+ console.log("");
10456
+ return;
10457
+ }
10458
+ const runs = loadAllRuns(cwd);
10459
+ const runIdInput = (runIdArg != null ? runIdArg : "last").trim();
10460
+ let targetRunId = runIdInput;
10461
+ if (runIdInput === "last") {
10462
+ const latest = runs.find((r) => r.status === "completed" || r.status === "guarded" || r.status === "executed" || r.status === "checked");
10463
+ if (!latest) {
10464
+ console.log("");
10465
+ console.log(chalk.yellow("No runs available for restore."));
10466
+ console.log("");
10467
+ return;
10468
+ }
10469
+ targetRunId = latest.id;
10470
+ }
10471
+ const run = runs.find((r) => r.id === targetRunId);
10472
+ if (!run) {
10473
+ console.log("");
10474
+ console.log(chalk.yellow(`Run not found: ${targetRunId}`));
10475
+ console.log("");
10476
+ return;
10477
+ }
10478
+ const restore = loadRestorePoint(cwd, targetRunId);
10479
+ if (!restore) {
10480
+ console.log("");
10481
+ console.log(chalk.yellow("No restore point found for this run."));
10482
+ console.log("");
10483
+ return;
10484
+ }
10485
+ const postFiles = (_b = (_a2 = restore.postRun) == null ? void 0 : _a2.changedFiles) != null ? _b : [];
10486
+ const sensitive = postFiles.filter((f) => isSensitivePath(f) || isSecretLikePath(f));
10487
+ const safeFiles = postFiles.filter((f) => !sensitive.includes(f));
10488
+ const gitAvailable = await isGitRepo(cwd);
10489
+ const method = gitAvailable && restore.preRun.commit ? "git checkout from pre-run commit" : "metadata-only (limited)";
10490
+ const nowChanged = gitAvailable ? dedupeFiles((await getGitChangedFiles(cwd)).map((f) => f.path)) : [];
10491
+ const unrelated = nowChanged.filter((f) => !postFiles.includes(f));
10492
+ if (doPreview) {
10493
+ console.log("");
10494
+ console.log(BOLD("RunTrim") + DIM(" restore preview"));
10495
+ console.log("");
10496
+ console.log(DIM(" Run ID ") + chalk.white(targetRunId));
10497
+ console.log(DIM(" Task ") + chalk.white(truncate(run.task, 80)));
10498
+ console.log(DIM(" Method ") + chalk.white(method));
10499
+ console.log(DIM(" Verdict ") + chalk.white((_d = (_c = restore.postRun) == null ? void 0 : _c.finishVerdict) != null ? _d : "unknown"));
10500
+ console.log("");
10501
+ console.log(DIM(" Files to restore"));
10502
+ if (safeFiles.length === 0) console.log(chalk.white(" - (none)"));
10503
+ for (const f of safeFiles.slice(0, 20)) console.log(chalk.white(" - " + f));
10504
+ if (safeFiles.length > 20) console.log(DIM(` ... and ${safeFiles.length - 20} more`));
10505
+ if (sensitive.length > 0) {
10506
+ console.log("");
10507
+ console.log(DIM(" Sensitive files (listed by path only)"));
10508
+ for (const f of sensitive.slice(0, 20)) console.log(chalk.yellow(" - " + f));
10509
+ console.log(chalk.yellow(" Sensitive files are skipped by default and not auto-restored."));
10510
+ }
10511
+ if (unrelated.length > 0) {
10512
+ console.log("");
10513
+ console.log(chalk.yellow(" Warning: repo has new changes after this run."));
10514
+ console.log(chalk.yellow(" Use --apply --force or review manually before restore."));
10515
+ }
10516
+ console.log("");
10517
+ return;
10518
+ }
10519
+ if (!doApply) return;
10520
+ if (!gitAvailable || !restore.preRun.commit) {
10521
+ console.log("");
10522
+ console.log(chalk.red("Restore apply requires git and a pre-run commit restore point."));
10523
+ console.log("");
10524
+ process.exit(1);
10525
+ return;
10526
+ }
10527
+ if (unrelated.length > 0 && !force) {
10528
+ console.log("");
10529
+ console.log(chalk.red("Restore blocked: new unrelated changes detected after this run."));
10530
+ console.log(chalk.red("Re-run with --apply --force after manual review."));
10531
+ console.log("");
10532
+ process.exit(1);
10533
+ return;
10534
+ }
10535
+ const restored = [];
10536
+ const skippedSensitive = [...sensitive];
10537
+ const failed = [];
10538
+ for (const file of safeFiles) {
10539
+ try {
10540
+ let existedBefore = false;
10541
+ try {
10542
+ await execa3("git", ["cat-file", "-e", `${restore.preRun.commit}:${file}`], { cwd });
10543
+ existedBefore = true;
10544
+ } catch (e) {
10545
+ existedBefore = false;
10546
+ }
10547
+ if (existedBefore) {
10548
+ await execa3("git", ["checkout", restore.preRun.commit, "--", file], { cwd });
10549
+ } else if (fs13.existsSync(path13.join(cwd, file))) {
10550
+ fs13.rmSync(path13.join(cwd, file), { force: true });
10551
+ }
10552
+ restored.push(file);
10553
+ } catch (e) {
10554
+ failed.push(file);
10555
+ }
10556
+ }
10557
+ const report = {
10558
+ runId: targetRunId,
10559
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
10560
+ restored,
10561
+ skippedSensitive,
10562
+ failed,
10563
+ forced: force
10564
+ };
10565
+ const reportPath = path13.join(getRestoresDir(cwd), `${targetRunId}.report.${Date.now()}.json`);
10566
+ fs13.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
10567
+ console.log("");
10568
+ console.log(BOLD("RunTrim") + DIM(" restore apply"));
10569
+ console.log("");
10570
+ console.log(DIM(" Run ID ") + chalk.white(targetRunId));
10571
+ console.log(DIM(" Restored files ") + chalk.white(String(restored.length)));
10572
+ console.log(DIM(" Skipped sensitive ") + chalk.white(String(skippedSensitive.length)));
10573
+ console.log(DIM(" Failed ") + chalk.white(String(failed.length)));
10574
+ console.log(DIM(" Report ") + chalk.white(path13.relative(cwd, reportPath)));
10575
+ console.log("");
10576
+ if (failed.length > 0) process.exit(1);
10577
+ });
9369
10578
  program.command("guard <task>").description("Audit a task and generate a guarded run contract").action(async (task) => {
9370
10579
  var _a2, _b, _c;
9371
10580
  const cwd = process.cwd();
@@ -9453,7 +10662,7 @@ program.command("guard <task>").description("Audit a task and generate a guarded
9453
10662
  const run2 = saveRun(task, audit, contract, cwd);
9454
10663
  updateRun(run2.id, { status: "blocked" }, cwd);
9455
10664
  console.log("");
9456
- console.log(DIM(" Run saved: .runtrim/runs/" + run2.id + ".json (status: blocked)"));
10665
+ console.log(DIM(" Run saved: .runtrim/internal/runs/" + run2.id + ".json (status: blocked)"));
9457
10666
  console.log("");
9458
10667
  console.log(DIM(" Next: ") + chalk.white('runtrim guard "<one system at a time>"'));
9459
10668
  console.log("");
@@ -9533,7 +10742,7 @@ program.command("guard <task>").description("Audit a task and generate a guarded
9533
10742
  }
9534
10743
  const run = saveRun(task, audit, contract, cwd);
9535
10744
  console.log("");
9536
- console.log(DIM(" Run saved: .runtrim/runs/" + run.id + ".json"));
10745
+ console.log(DIM(" Run saved: .runtrim/internal/runs/" + run.id + ".json"));
9537
10746
  console.log("");
9538
10747
  console.log(DIM(" After your agent run: ") + chalk.white("runtrim check"));
9539
10748
  console.log("");
@@ -9583,6 +10792,7 @@ program.command("run <task>").description("Guard then run configured local agent
9583
10792
  },
9584
10793
  cwd
9585
10794
  );
10795
+ await captureRestorePoint(cwd, run.id, task);
9586
10796
  const copySavings = estimateSavingsFromTokens2(
9587
10797
  parseEstimatedNumber3(String(contract.estimatedTokensTrimmed))
9588
10798
  );
@@ -9663,6 +10873,7 @@ program.command("run <task>").description("Guard then run configured local agent
9663
10873
  },
9664
10874
  cwd
9665
10875
  );
10876
+ await captureRestorePoint(cwd, run.id, task);
9666
10877
  console.log(DIM(" Execution cancelled. Guarded contract copied to clipboard."));
9667
10878
  console.log("");
9668
10879
  return;
@@ -9719,6 +10930,7 @@ program.command("run <task>").description("Guard then run configured local agent
9719
10930
  },
9720
10931
  cwd
9721
10932
  );
10933
+ await captureRestorePoint(cwd, run.id, task);
9722
10934
  console.log("");
9723
10935
  console.log(chalk.yellow(" Agent command not found. Falling back to copy mode guidance."));
9724
10936
  console.log(DIM(" Configure with: npm run runtrim -- agent set claude|codex|custom"));
@@ -9752,6 +10964,7 @@ program.command("run <task>").description("Guard then run configured local agent
9752
10964
  },
9753
10965
  cwd
9754
10966
  );
10967
+ await captureRestorePoint(cwd, run.id, task);
9755
10968
  console.log("");
9756
10969
  console.log(chalk.yellow(" Agent command not found. Falling back to copy mode guidance."));
9757
10970
  console.log(DIM(" Configure with: npm run runtrim -- agent set claude|codex|custom"));
@@ -9795,7 +11008,7 @@ ${stderr}`, "utf-8");
9795
11008
  exitCode,
9796
11009
  stdoutPreview: stdout.slice(0, OUTPUT_PREVIEW_MAX),
9797
11010
  stderrPreview: stderr.slice(0, OUTPUT_PREVIEW_MAX),
9798
- outputPath: `.runtrim/runs/${run.id}.output.txt`
11011
+ outputPath: `.runtrim/internal/runs/${run.id}.output.txt`
9799
11012
  },
9800
11013
  evaluation
9801
11014
  },
@@ -9945,6 +11158,7 @@ program.command("go <task>").description("Bridge Mode: generate a scoped contrac
9945
11158
  memoryUsed,
9946
11159
  providerRouting
9947
11160
  }, cwd);
11161
+ await captureRestorePoint(cwd, run.id, task);
9948
11162
  const bridgeCtx = deriveBridgeContext(task, contract, runs, projectName);
9949
11163
  let bridgeWritten = [];
9950
11164
  let bridgeManagedPaths = [];
@@ -10003,7 +11217,7 @@ program.command("go <task>").description("Bridge Mode: generate a scoped contrac
10003
11217
  if (contract.contract.stopRules.length > 0) {
10004
11218
  console.log(DIM(" Stop rule ") + chalk.white(truncate((_g = contract.contract.stopRules[contract.contract.stopRules.length - 1]) != null ? _g : "", 60)));
10005
11219
  }
10006
- console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
11220
+ console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
10007
11221
  console.log(DIM(" Contract ") + chalk.white("created"));
10008
11222
  console.log("");
10009
11223
  if (bridgeWritten.length > 0) {
@@ -10508,7 +11722,7 @@ program.command("continue").description("Create a safe continuation prompt from
10508
11722
  const audit = (_a2 = loadProjectAudit(cwd)) != null ? _a2 : performBaselineProjectAudit(cwd, null);
10509
11723
  const reason = normalizeContinuationReason(options.reason);
10510
11724
  const agent = normalizeContinuationAgent(options.agent, config.defaultAgent);
10511
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
11725
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
10512
11726
  const continuationPath = resolveContinuationPath(cwd);
10513
11727
  const latestPromptPath = resolvePromptPath(config, cwd);
10514
11728
  const latestPrompt = fs13.existsSync(latestPromptPath) ? fs13.readFileSync(latestPromptPath, "utf-8").trim() : "";
@@ -10698,14 +11912,14 @@ program.command("continue").description("Create a safe continuation prompt from
10698
11912
  fs13.writeFileSync(continuationPath, continuationPrompt, "utf-8");
10699
11913
  const copied = await copyToClipboardSafe(continuationPrompt);
10700
11914
  if (memory) {
10701
- const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso);
11915
+ const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso2);
10702
11916
  fs13.writeFileSync(path13.join(getConfigDir(cwd), "memory.md"), memoryWithContinuation, "utf-8");
10703
11917
  }
10704
11918
  if (hasConfig) {
10705
11919
  const nextConfig = __spreadProps(__spreadValues({}, config), {
10706
11920
  lastContinuationReason: reason,
10707
11921
  continuationPromptPath: continuationPath.replace(/\\/g, "/"),
10708
- continuationCreatedAt: nowIso
11922
+ continuationCreatedAt: nowIso2
10709
11923
  });
10710
11924
  saveConfig(nextConfig, cwd);
10711
11925
  }
@@ -11358,6 +12572,18 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
11358
12572
  const blockedByExistingHard = scope.forbiddenFiles.length > 0 || scope.status === "limit_exceeded";
11359
12573
  const warnBySensitiveIgnored = sensitiveIgnored.length > 0;
11360
12574
  const finishVerdict = blockedBySensitive || blockedByContract || blockedByExistingHard ? "BLOCKED" : warnBySensitiveIgnored || scopeDriftStatus !== "passed" || evaluation.status === "needs_verification" || evaluation.status === "partial" ? "WARN" : "PASS";
12575
+ const restoreRecord = loadRestorePoint(cwd, activeRun.id);
12576
+ if (restoreRecord) {
12577
+ restoreRecord.postRun = {
12578
+ changedFiles,
12579
+ forbiddenFiles: [...scope.forbiddenFiles, ...forbiddenPathFiles],
12580
+ sensitiveFiles: scope.sensitiveFiles,
12581
+ outOfScopeFiles: [...outOfContractFiles, ...scope.outOfScopeFiles],
12582
+ finishVerdict,
12583
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
12584
+ };
12585
+ saveRestorePoint(cwd, restoreRecord);
12586
+ }
11361
12587
  const verdictColor = finishVerdict === "PASS" ? chalk.green : finishVerdict === "WARN" ? chalk.yellow : chalk.red;
11362
12588
  const scopeColor = scopeDriftStatus === "passed" ? chalk.green : scopeDriftStatus === "forbidden_touched" ? chalk.red : chalk.yellow;
11363
12589
  const riskAfter = (_m = activeRun.contract.wasteRiskAfter) != null ? _m : "medium";
@@ -11497,6 +12723,8 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
11497
12723
  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
12724
  var _a2, _b;
11499
12725
  const cwd = process.cwd();
12726
+ const allowed = await ensureRepoAllowedForFree(cwd);
12727
+ if (!allowed) return;
11500
12728
  console.log("");
11501
12729
  console.log(BOLD("RunTrim") + DIM(" cloud sync"));
11502
12730
  console.log("");