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.
@@ -118,8 +118,45 @@ function getConfigPath(cwd = process.cwd()) {
118
118
  return import_path.default.join(getConfigDir(cwd), "config.json");
119
119
  }
120
120
  function getRunsDir(cwd = process.cwd()) {
121
+ return import_path.default.join(getInternalDir(cwd), "runs");
122
+ }
123
+ function getLegacyRunsDir(cwd = process.cwd()) {
121
124
  return import_path.default.join(getConfigDir(cwd), "runs");
122
125
  }
126
+ function getInternalDir(cwd = process.cwd()) {
127
+ return import_path.default.join(getConfigDir(cwd), "internal");
128
+ }
129
+ function getPreviewsDir(cwd = process.cwd()) {
130
+ return import_path.default.join(getInternalDir(cwd), "previews");
131
+ }
132
+ function getLegacyPreviewsDir(cwd = process.cwd()) {
133
+ return import_path.default.join(getConfigDir(cwd), "previews");
134
+ }
135
+ function getRestoresDir(cwd = process.cwd()) {
136
+ return import_path.default.join(getInternalDir(cwd), "restores");
137
+ }
138
+ function getLegacyRestoresDir(cwd = process.cwd()) {
139
+ return import_path.default.join(getConfigDir(cwd), "restores");
140
+ }
141
+ function getContractsArchiveDir(cwd = process.cwd()) {
142
+ return import_path.default.join(getInternalDir(cwd), "contracts-archive");
143
+ }
144
+ function getAgentArchiveDir(cwd = process.cwd()) {
145
+ return import_path.default.join(getInternalDir(cwd), "agent-archive");
146
+ }
147
+ function ensureInternalArtifactDirs(cwd = process.cwd()) {
148
+ const dirs = [
149
+ getInternalDir(cwd),
150
+ getRunsDir(cwd),
151
+ getPreviewsDir(cwd),
152
+ getRestoresDir(cwd),
153
+ getContractsArchiveDir(cwd),
154
+ getAgentArchiveDir(cwd)
155
+ ];
156
+ for (const dir of dirs) {
157
+ if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
158
+ }
159
+ }
123
160
  function configExists(cwd = process.cwd()) {
124
161
  return import_fs.default.existsSync(getConfigPath(cwd));
125
162
  }
@@ -687,6 +724,28 @@ function buildCategoryScope(category, hasSrc, hasApp, hasPages) {
687
724
  "Check no regression in adjacent routes"
688
725
  ]
689
726
  };
727
+ case "docs":
728
+ return {
729
+ allowedHints: [
730
+ "README.md - project documentation",
731
+ "docs/ - documentation files",
732
+ "CHANGELOG.md or CONTRIBUTING.md if task-specific"
733
+ ],
734
+ forbiddenAdditions: [
735
+ "Do not touch auth internals, session logic, or JWT handling",
736
+ "Do not touch billing, subscription, payment, or webhook logic",
737
+ "Do not touch database schema or migrations",
738
+ "Do not touch .env files or secrets"
739
+ ],
740
+ stopRules: [
741
+ "Stop if the requested change requires code-path behavior changes outside docs",
742
+ "Stop if sensitive files or secrets are referenced"
743
+ ],
744
+ verificationSteps: [
745
+ "Confirm documentation text matches the requested task",
746
+ "Check markdown formatting renders correctly"
747
+ ]
748
+ };
690
749
  default:
691
750
  return {
692
751
  allowedHints: [],
@@ -1551,35 +1610,38 @@ function saveRun(task, audit, contract, cwd = process.cwd()) {
1551
1610
  return record;
1552
1611
  }
1553
1612
  function loadLatestRun(cwd = process.cwd()) {
1554
- const runsDir = getRunsDir(cwd);
1555
- if (!import_fs3.default.existsSync(runsDir)) return null;
1556
- const files = import_fs3.default.readdirSync(runsDir).filter((f) => f.endsWith(".json")).map((f) => ({
1557
- name: f,
1558
- time: import_fs3.default.statSync(import_path3.default.join(runsDir, f)).mtime.getTime()
1559
- })).sort((a, b) => b.time - a.time);
1613
+ const candidateDirs = [getRunsDir(cwd), getLegacyRunsDir(cwd)].filter((dir, idx, arr) => arr.indexOf(dir) === idx);
1614
+ const files = candidateDirs.filter((dir) => import_fs3.default.existsSync(dir)).flatMap(
1615
+ (dir) => import_fs3.default.readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => ({
1616
+ dir,
1617
+ name: f,
1618
+ time: import_fs3.default.statSync(import_path3.default.join(dir, f)).mtime.getTime()
1619
+ }))
1620
+ ).sort((a, b) => b.time - a.time);
1560
1621
  if (files.length === 0) return null;
1561
1622
  try {
1562
1623
  return JSON.parse(
1563
- import_fs3.default.readFileSync(import_path3.default.join(runsDir, files[0].name), "utf-8")
1624
+ import_fs3.default.readFileSync(import_path3.default.join(files[0].dir, files[0].name), "utf-8")
1564
1625
  );
1565
1626
  } catch (e) {
1566
1627
  return null;
1567
1628
  }
1568
1629
  }
1569
1630
  function updateRun(runId, updates, cwd = process.cwd()) {
1570
- const filePath = import_path3.default.join(getRunsDir(cwd), `${runId}.json`);
1631
+ const preferredPath = import_path3.default.join(getRunsDir(cwd), `${runId}.json`);
1632
+ const legacyPath = import_path3.default.join(getLegacyRunsDir(cwd), `${runId}.json`);
1633
+ const filePath = import_fs3.default.existsSync(preferredPath) ? preferredPath : legacyPath;
1571
1634
  if (!import_fs3.default.existsSync(filePath)) return;
1572
1635
  const existing = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1573
1636
  import_fs3.default.writeFileSync(filePath, JSON.stringify(__spreadValues(__spreadValues({}, existing), updates), null, 2));
1574
1637
  }
1575
1638
  function loadAllRuns(cwd = process.cwd()) {
1576
- const runsDir = getRunsDir(cwd);
1577
- if (!import_fs3.default.existsSync(runsDir)) return [];
1578
- return import_fs3.default.readdirSync(runsDir).filter((f) => f.endsWith(".json")).map((f) => {
1639
+ const candidateDirs = [getRunsDir(cwd), getLegacyRunsDir(cwd)].filter((dir, idx, arr) => arr.indexOf(dir) === idx);
1640
+ const files = candidateDirs.filter((dir) => import_fs3.default.existsSync(dir)).flatMap((dir) => import_fs3.default.readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => import_path3.default.join(dir, f)));
1641
+ const deduped = [...new Set(files)];
1642
+ return deduped.map((filePath) => {
1579
1643
  try {
1580
- return JSON.parse(
1581
- import_fs3.default.readFileSync(import_path3.default.join(runsDir, f), "utf-8")
1582
- );
1644
+ return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1583
1645
  } catch (e) {
1584
1646
  return null;
1585
1647
  }
@@ -2598,7 +2660,7 @@ function buildSyncPayload(input) {
2598
2660
  var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t;
2599
2661
  const { cwd, projectName, config, projectAudit, memoryMarkdown, runs } = input;
2600
2662
  const latest = runs[0];
2601
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2663
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
2602
2664
  const localProjectId = buildLocalProjectId(cwd);
2603
2665
  const latestPromptText = readTextFileIfExists(import_path6.default.join(cwd, ".runtrim", "latest-prompt.md"));
2604
2666
  const continuationPromptText = readTextFileIfExists(
@@ -2660,7 +2722,7 @@ function buildSyncPayload(input) {
2660
2722
  name: resolveProjectName2(cwd, projectName, projectAudit == null ? void 0 : projectAudit.projectName),
2661
2723
  stack: (_a2 = projectAudit == null ? void 0 : projectAudit.detectedStack) != null ? _a2 : config.stack ? config.stack.split(",").map((s) => s.trim()).filter(Boolean) : ["auto"],
2662
2724
  packageManager: (_c = (_b = projectAudit == null ? void 0 : projectAudit.packageManager) != null ? _b : config.packageManager) != null ? _c : null,
2663
- lastUpdated: nowIso
2725
+ lastUpdated: nowIso2
2664
2726
  },
2665
2727
  memory: {
2666
2728
  markdown: memoryMarkdown,
@@ -2742,7 +2804,8 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2742
2804
  "## How to start an AI coding task",
2743
2805
  "",
2744
2806
  "```",
2745
- 'runtrim go "<task>"',
2807
+ "runtrim start",
2808
+ 'runtrim agent "Your task" --copy',
2746
2809
  "```",
2747
2810
  "",
2748
2811
  "RunTrim creates a scoped contract, loads project memory, and generates a guarded prompt.",
@@ -2763,7 +2826,7 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2763
2826
  "",
2764
2827
  "1. Read `.runtrim/contracts/latest.md`.",
2765
2828
  " - If `Status: active` \u2014 a live task exists. Follow the contract strictly.",
2766
- ' - If `Status: none` \u2014 no active task. Ask the user to run `runtrim go "<task>"` first.',
2829
+ ' - If `Status: none` \u2014 no active task. Ask the user to run `runtrim start` then `runtrim agent "Your task" --copy`.',
2767
2830
  "2. Do not assume any prior task is still active.",
2768
2831
  "3. Stay inside the allowed scope defined in the contract.",
2769
2832
  "4. Stop and ask before touching any forbidden area.",
@@ -2771,7 +2834,7 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2771
2834
  "6. After editing, tell the user to run: `runtrim finish`",
2772
2835
  "",
2773
2836
  "---",
2774
- `Protocol: runtrim init. Updated: ${(/* @__PURE__ */ new Date()).toISOString()}`
2837
+ `Protocol: runtrim start. Updated: ${(/* @__PURE__ */ new Date()).toISOString()}`
2775
2838
  ];
2776
2839
  import_fs7.default.writeFileSync(import_path7.default.join(cwd, "RUNTRIM.md"), lines.join("\n"), "utf-8");
2777
2840
  }
@@ -2788,7 +2851,8 @@ function writeRestingContract(cwd = process.cwd()) {
2788
2851
  "Start one with:",
2789
2852
  "",
2790
2853
  "```",
2791
- 'runtrim go "<your task>"',
2854
+ "runtrim start",
2855
+ 'runtrim agent "Your task" --copy',
2792
2856
  "```",
2793
2857
  "",
2794
2858
  "---",
@@ -2811,7 +2875,8 @@ function writeRestingMemory(cwd = process.cwd()) {
2811
2875
  "Start a new session with:",
2812
2876
  "",
2813
2877
  "```",
2814
- 'runtrim go "<your task>"',
2878
+ "runtrim start",
2879
+ 'runtrim agent "Your task" --copy',
2815
2880
  "```",
2816
2881
  "",
2817
2882
  "---",
@@ -2825,7 +2890,7 @@ function archiveContract(cwd, runId) {
2825
2890
  if (!import_fs7.default.existsSync(latestPath)) return;
2826
2891
  const content = import_fs7.default.readFileSync(latestPath, "utf-8");
2827
2892
  if (content.includes("Status: none")) return;
2828
- const archiveDir = import_path7.default.join(contractsDir, "archive");
2893
+ const archiveDir = getContractsArchiveDir(cwd);
2829
2894
  if (!import_fs7.default.existsSync(archiveDir)) import_fs7.default.mkdirSync(archiveDir, { recursive: true });
2830
2895
  import_fs7.default.writeFileSync(import_path7.default.join(archiveDir, `${runId}.md`), content, "utf-8");
2831
2896
  }
@@ -2932,7 +2997,7 @@ function writeBridgeInstructions(cwd = process.cwd()) {
2932
2997
  "1. Read `RUNTRIM.md`.",
2933
2998
  "2. Read `.runtrim/contracts/latest.md`.",
2934
2999
  " - If `Status: active` \u2014 follow the contract strictly.",
2935
- ' - If `Status: none` \u2014 stop. Ask the user to run `runtrim go "<task>"` first.',
3000
+ ' - If `Status: none` \u2014 stop. Ask the user to run `runtrim start` then `runtrim agent "Your task" --copy`.',
2936
3001
  "3. If the contract is active, read `.runtrim/memory/current.md` for session context.",
2937
3002
  " If no active session, read `.runtrim/memory/baseline.md` for project baseline.",
2938
3003
  "",
@@ -3006,6 +3071,28 @@ function buildBridgePrompt(contractText, ctx) {
3006
3071
  // src/lib/run-watch.ts
3007
3072
  function normalizeScopeKeywords2(scope) {
3008
3073
  var _a2;
3074
+ const genericStopwords = /* @__PURE__ */ new Set([
3075
+ "read",
3076
+ "write",
3077
+ "reference",
3078
+ "touch",
3079
+ "modify",
3080
+ "change",
3081
+ "update",
3082
+ "allow",
3083
+ "scope",
3084
+ "paths",
3085
+ "path",
3086
+ "files",
3087
+ "file",
3088
+ "only",
3089
+ "with",
3090
+ "without",
3091
+ "before",
3092
+ "after",
3093
+ "inside",
3094
+ "outside"
3095
+ ]);
3009
3096
  const words = /* @__PURE__ */ new Set();
3010
3097
  for (const line of scope) {
3011
3098
  const lower = line.toLowerCase();
@@ -3015,7 +3102,7 @@ function normalizeScopeKeywords2(scope) {
3015
3102
  }
3016
3103
  const cleaned = lower.replace(/[^a-z0-9_./\s-]/g, " ").split(/\s+/).filter(Boolean);
3017
3104
  for (const token of cleaned) {
3018
- if (token.length >= 4) words.add(token);
3105
+ if (token.length >= 4 && !genericStopwords.has(token)) words.add(token);
3019
3106
  }
3020
3107
  }
3021
3108
  return [...words];
@@ -3112,15 +3199,7 @@ var import_fs8 = __toESM(require("fs"), 1);
3112
3199
  var import_os = __toESM(require("os"), 1);
3113
3200
  var import_path8 = __toESM(require("path"), 1);
3114
3201
  var import_execa2 = require("execa");
3115
- var DEFAULT_REGISTRY = {
3116
- version: 1,
3117
- plan: "free",
3118
- trackedRepos: [],
3119
- telemetry: {
3120
- enabled: false,
3121
- anonymousId: ""
3122
- }
3123
- };
3202
+ var EMPTY_TELEMETRY = { enabled: false, anonymousId: "" };
3124
3203
  function normalizeRepoPath(input) {
3125
3204
  const resolved = import_path8.default.resolve(input);
3126
3205
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
@@ -3128,41 +3207,208 @@ function normalizeRepoPath(input) {
3128
3207
  function hashValue(value) {
3129
3208
  return import_crypto2.default.createHash("sha256").update(value).digest("hex").slice(0, 16);
3130
3209
  }
3210
+ function nowIso() {
3211
+ return (/* @__PURE__ */ new Date()).toISOString();
3212
+ }
3213
+ function randomId(prefix) {
3214
+ return `${prefix}_${import_crypto2.default.randomBytes(12).toString("hex")}`;
3215
+ }
3131
3216
  function getGlobalRunTrimDir() {
3132
3217
  return import_path8.default.join(import_os.default.homedir(), ".runtrim");
3133
3218
  }
3134
3219
  function getGlobalRegistryPath() {
3135
3220
  return import_path8.default.join(getGlobalRunTrimDir(), "global.json");
3136
3221
  }
3137
- function loadGlobalRegistry() {
3222
+ function getInstallStatePath() {
3223
+ return import_path8.default.join(getGlobalRunTrimDir(), "install-state.json");
3224
+ }
3225
+ function buildSealInput(registry) {
3226
+ const tracked = [...registry.trackedRepos].map((r) => ({
3227
+ id: r.id,
3228
+ name: r.name,
3229
+ path: normalizeRepoPath(r.path),
3230
+ gitRemote: r.gitRemote,
3231
+ createdAt: r.createdAt,
3232
+ lastSeenAt: r.lastSeenAt
3233
+ })).sort((a, b) => `${a.id}:${a.path}`.localeCompare(`${b.id}:${b.path}`));
3234
+ const payload = {
3235
+ version: registry.version,
3236
+ stateVersion: registry.stateVersion,
3237
+ plan: registry.plan,
3238
+ machineInstallId: registry.machineInstallId,
3239
+ createdAt: registry.createdAt,
3240
+ updatedAt: registry.updatedAt,
3241
+ trackedRepos: tracked,
3242
+ lastKnownRepo: registry.lastKnownRepo ? __spreadProps(__spreadValues({}, registry.lastKnownRepo), {
3243
+ path: normalizeRepoPath(registry.lastKnownRepo.path)
3244
+ }) : null
3245
+ };
3246
+ return JSON.stringify(payload);
3247
+ }
3248
+ function computeSeal(registry) {
3249
+ return import_crypto2.default.createHash("sha256").update(buildSealInput(registry)).digest("hex");
3250
+ }
3251
+ function sanitizeTrackedRepoEntry(input) {
3252
+ var _a2, _b, _c, _d, _e, _f;
3253
+ const id = String((_a2 = input.id) != null ? _a2 : "").trim();
3254
+ const rawPath = String((_b = input.path) != null ? _b : "").trim();
3255
+ if (!id || !rawPath) return null;
3256
+ return {
3257
+ id,
3258
+ name: String((_c = input.name) != null ? _c : "").trim(),
3259
+ path: normalizeRepoPath(rawPath),
3260
+ gitRemote: String((_d = input.gitRemote) != null ? _d : "").trim(),
3261
+ createdAt: String((_e = input.createdAt) != null ? _e : "").trim(),
3262
+ lastSeenAt: String((_f = input.lastSeenAt) != null ? _f : "").trim()
3263
+ };
3264
+ }
3265
+ function readInstallStateRaw() {
3266
+ var _a2, _b, _c;
3267
+ const p = getInstallStatePath();
3268
+ if (!import_fs8.default.existsSync(p)) return { exists: false, state: null };
3269
+ try {
3270
+ const parsed = JSON.parse(import_fs8.default.readFileSync(p, "utf-8"));
3271
+ const machineInstallId = String((_a2 = parsed.machineInstallId) != null ? _a2 : "").trim();
3272
+ if (!machineInstallId) return { exists: true, state: null };
3273
+ return {
3274
+ exists: true,
3275
+ state: {
3276
+ machineInstallId,
3277
+ createdAt: String((_b = parsed.createdAt) != null ? _b : "").trim() || nowIso(),
3278
+ updatedAt: String((_c = parsed.updatedAt) != null ? _c : "").trim() || nowIso()
3279
+ }
3280
+ };
3281
+ } catch (e) {
3282
+ return { exists: true, state: null };
3283
+ }
3284
+ }
3285
+ function writeInstallState(state) {
3286
+ const dir = getGlobalRunTrimDir();
3287
+ if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
3288
+ import_fs8.default.writeFileSync(getInstallStatePath(), JSON.stringify(state, null, 2), "utf-8");
3289
+ }
3290
+ function ensureInstallState() {
3291
+ const raw = readInstallStateRaw();
3292
+ if (raw.exists && raw.state) return raw.state;
3293
+ const created = {
3294
+ machineInstallId: randomId("rt_install"),
3295
+ createdAt: nowIso(),
3296
+ updatedAt: nowIso()
3297
+ };
3298
+ writeInstallState(created);
3299
+ return created;
3300
+ }
3301
+ function buildDefaultRegistry(install) {
3302
+ const base = {
3303
+ version: 2,
3304
+ stateVersion: 2,
3305
+ plan: "free",
3306
+ machineInstallId: install.machineInstallId,
3307
+ createdAt: nowIso(),
3308
+ updatedAt: nowIso(),
3309
+ trackedRepos: [],
3310
+ lastKnownRepo: null,
3311
+ telemetry: __spreadValues({}, EMPTY_TELEMETRY)
3312
+ };
3313
+ return __spreadProps(__spreadValues({}, base), {
3314
+ integrity: {
3315
+ algorithm: "sha256-local-seal-v1",
3316
+ seal: computeSeal(base)
3317
+ }
3318
+ });
3319
+ }
3320
+ function saveRegistryWithSeal(registry) {
3321
+ var _a2;
3322
+ const dir = getGlobalRunTrimDir();
3323
+ if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
3324
+ const normalizedBase = __spreadProps(__spreadValues({}, registry), {
3325
+ version: 2,
3326
+ stateVersion: 2,
3327
+ trackedRepos: registry.trackedRepos.map((r) => __spreadProps(__spreadValues({}, r), { path: normalizeRepoPath(r.path) })),
3328
+ telemetry: (_a2 = registry.telemetry) != null ? _a2 : __spreadValues({}, EMPTY_TELEMETRY)
3329
+ });
3330
+ const sealed = __spreadProps(__spreadValues({}, normalizedBase), {
3331
+ integrity: {
3332
+ algorithm: "sha256-local-seal-v1",
3333
+ seal: computeSeal(normalizedBase)
3334
+ }
3335
+ });
3336
+ import_fs8.default.writeFileSync(getGlobalRegistryPath(), JSON.stringify(sealed, null, 2), "utf-8");
3337
+ }
3338
+ function inspectGlobalRegistry() {
3339
+ var _a2, _b, _c, _d, _e, _f, _g, _h, _i;
3340
+ const installRaw = readInstallStateRaw();
3341
+ const install = (_a2 = installRaw.state) != null ? _a2 : ensureInstallState();
3138
3342
  const registryPath = getGlobalRegistryPath();
3139
- if (!import_fs8.default.existsSync(registryPath)) return __spreadValues({}, DEFAULT_REGISTRY);
3343
+ const defaultRegistry = buildDefaultRegistry(install);
3344
+ if (!import_fs8.default.existsSync(registryPath)) {
3345
+ if (installRaw.exists) {
3346
+ return {
3347
+ registry: defaultRegistry,
3348
+ needsRepair: true,
3349
+ repairReason: "missing_registry_after_initialization"
3350
+ };
3351
+ }
3352
+ return { registry: defaultRegistry, needsRepair: false, repairReason: null };
3353
+ }
3140
3354
  try {
3141
3355
  const raw = JSON.parse(import_fs8.default.readFileSync(registryPath, "utf-8"));
3142
- return {
3143
- version: 1,
3356
+ const trackedRepos = Array.isArray(raw.trackedRepos) ? raw.trackedRepos.map((item) => sanitizeTrackedRepoEntry(item)).filter((item) => Boolean(item)) : [];
3357
+ const base = {
3358
+ version: 2,
3359
+ stateVersion: 2,
3144
3360
  plan: raw.plan === "free" ? "free" : "free",
3145
- trackedRepos: Array.isArray(raw.trackedRepos) ? raw.trackedRepos.filter((item) => Boolean(item && typeof item === "object")).map((item) => ({
3146
- id: String(item.id || ""),
3147
- name: String(item.name || ""),
3148
- path: normalizeRepoPath(String(item.path || "")),
3149
- gitRemote: String(item.gitRemote || ""),
3150
- createdAt: String(item.createdAt || ""),
3151
- lastSeenAt: String(item.lastSeenAt || "")
3152
- })).filter((item) => Boolean(item.id && item.path)) : [],
3361
+ machineInstallId: String((_b = raw.machineInstallId) != null ? _b : "").trim() || install.machineInstallId,
3362
+ createdAt: String((_c = raw.createdAt) != null ? _c : "").trim() || nowIso(),
3363
+ updatedAt: String((_d = raw.updatedAt) != null ? _d : "").trim() || nowIso(),
3364
+ trackedRepos,
3365
+ lastKnownRepo: raw.lastKnownRepo && typeof raw.lastKnownRepo === "object" ? {
3366
+ id: String((_e = raw.lastKnownRepo.id) != null ? _e : "").trim(),
3367
+ name: String((_f = raw.lastKnownRepo.name) != null ? _f : "").trim(),
3368
+ path: normalizeRepoPath(String((_g = raw.lastKnownRepo.path) != null ? _g : "")),
3369
+ gitRemote: String((_h = raw.lastKnownRepo.gitRemote) != null ? _h : "").trim(),
3370
+ lastSeenAt: String((_i = raw.lastKnownRepo.lastSeenAt) != null ? _i : "").trim() || nowIso()
3371
+ } : null,
3153
3372
  telemetry: {
3154
3373
  enabled: typeof raw.telemetry === "object" && raw.telemetry !== null && Boolean(raw.telemetry.enabled),
3155
3374
  anonymousId: typeof raw.telemetry === "object" && raw.telemetry !== null && typeof raw.telemetry.anonymousId === "string" ? String(raw.telemetry.anonymousId).slice(0, 120) : ""
3156
3375
  }
3157
3376
  };
3377
+ const normalized = __spreadProps(__spreadValues({}, base), {
3378
+ integrity: {
3379
+ algorithm: "sha256-local-seal-v1",
3380
+ seal: raw.integrity && typeof raw.integrity === "object" && typeof raw.integrity.seal === "string" ? String(raw.integrity.seal) : ""
3381
+ }
3382
+ });
3383
+ if (normalized.machineInstallId !== install.machineInstallId) {
3384
+ return {
3385
+ registry: normalized,
3386
+ needsRepair: true,
3387
+ repairReason: "machine_install_id_mismatch"
3388
+ };
3389
+ }
3390
+ const expectedSeal = computeSeal(base);
3391
+ if (!normalized.integrity.seal || normalized.integrity.seal !== expectedSeal) {
3392
+ return {
3393
+ registry: normalized,
3394
+ needsRepair: true,
3395
+ repairReason: "integrity_seal_mismatch"
3396
+ };
3397
+ }
3398
+ return { registry: normalized, needsRepair: false, repairReason: null };
3158
3399
  } catch (e) {
3159
- return __spreadValues({}, DEFAULT_REGISTRY);
3400
+ return {
3401
+ registry: defaultRegistry,
3402
+ needsRepair: true,
3403
+ repairReason: "registry_corrupt"
3404
+ };
3160
3405
  }
3161
3406
  }
3407
+ function loadGlobalRegistry() {
3408
+ return inspectGlobalRegistry().registry;
3409
+ }
3162
3410
  function saveGlobalRegistry(registry) {
3163
- const dir = getGlobalRunTrimDir();
3164
- if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
3165
- import_fs8.default.writeFileSync(getGlobalRegistryPath(), JSON.stringify(registry, null, 2), "utf-8");
3411
+ saveRegistryWithSeal(registry);
3166
3412
  }
3167
3413
  async function getCurrentRepoIdentity(cwd = process.cwd()) {
3168
3414
  const normalizedPath = normalizeRepoPath(cwd);
@@ -3192,36 +3438,71 @@ function findTrackedRepo(trackedRepos, currentRepo) {
3192
3438
  return byPath != null ? byPath : null;
3193
3439
  }
3194
3440
  async function assertFreeRepoAllowed(cwd = process.cwd()) {
3195
- const registry = loadGlobalRegistry();
3441
+ const inspected = inspectGlobalRegistry();
3442
+ const registry = inspected.registry;
3196
3443
  const currentRepo = await getCurrentRepoIdentity(cwd);
3197
3444
  const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
3198
- if (registry.plan !== "free") {
3199
- return { allowed: true, plan: registry.plan, currentRepo, trackedRepo };
3445
+ const base = {
3446
+ plan: registry.plan,
3447
+ currentRepo,
3448
+ trackedRepo,
3449
+ registryPath: getGlobalRegistryPath()
3450
+ };
3451
+ if (inspected.needsRepair) {
3452
+ return __spreadProps(__spreadValues({}, base), {
3453
+ allowed: false,
3454
+ status: "blocked_repair",
3455
+ repairRequired: true,
3456
+ repairReason: inspected.repairReason
3457
+ });
3200
3458
  }
3201
- if (trackedRepo) {
3202
- return { allowed: true, plan: registry.plan, currentRepo, trackedRepo };
3459
+ if (registry.plan !== "free") {
3460
+ return __spreadProps(__spreadValues({}, base), {
3461
+ allowed: true,
3462
+ status: "allowed",
3463
+ repairRequired: false,
3464
+ repairReason: null
3465
+ });
3203
3466
  }
3204
- if (registry.trackedRepos.length === 0) {
3205
- return { allowed: true, plan: registry.plan, currentRepo, trackedRepo: null };
3467
+ if (trackedRepo || registry.trackedRepos.length === 0) {
3468
+ return __spreadProps(__spreadValues({}, base), {
3469
+ allowed: true,
3470
+ status: "allowed",
3471
+ repairRequired: false,
3472
+ repairReason: null
3473
+ });
3206
3474
  }
3207
- return {
3475
+ return __spreadProps(__spreadValues({}, base), {
3208
3476
  allowed: false,
3209
- plan: registry.plan,
3210
- currentRepo,
3477
+ status: "blocked_limit",
3478
+ repairRequired: false,
3479
+ repairReason: null,
3211
3480
  trackedRepo: registry.trackedRepos[0]
3212
- };
3481
+ });
3213
3482
  }
3214
3483
  async function registerCurrentRepo(cwd = process.cwd()) {
3484
+ const check = await assertFreeRepoAllowed(cwd);
3485
+ if (!check.allowed && check.status === "blocked_repair") {
3486
+ throw new Error("runtrim_local_state_repair_required");
3487
+ }
3215
3488
  const registry = loadGlobalRegistry();
3216
3489
  const currentRepo = await getCurrentRepoIdentity(cwd);
3217
- const now = (/* @__PURE__ */ new Date()).toISOString();
3490
+ const now = nowIso();
3218
3491
  const existing = findTrackedRepo(registry.trackedRepos, currentRepo);
3219
3492
  if (existing) {
3220
3493
  existing.lastSeenAt = now;
3221
3494
  existing.name = currentRepo.name;
3222
3495
  existing.path = currentRepo.path;
3223
3496
  existing.gitRemote = currentRepo.gitRemote;
3224
- saveGlobalRegistry(registry);
3497
+ registry.updatedAt = now;
3498
+ registry.lastKnownRepo = {
3499
+ id: currentRepo.id,
3500
+ name: currentRepo.name,
3501
+ path: currentRepo.path,
3502
+ gitRemote: currentRepo.gitRemote,
3503
+ lastSeenAt: now
3504
+ };
3505
+ saveRegistryWithSeal(registry);
3225
3506
  return existing;
3226
3507
  }
3227
3508
  const entry = {
@@ -3232,24 +3513,110 @@ async function registerCurrentRepo(cwd = process.cwd()) {
3232
3513
  createdAt: now,
3233
3514
  lastSeenAt: now
3234
3515
  };
3235
- registry.trackedRepos.push(entry);
3236
- saveGlobalRegistry(registry);
3516
+ registry.trackedRepos = [entry];
3517
+ registry.updatedAt = now;
3518
+ registry.lastKnownRepo = {
3519
+ id: entry.id,
3520
+ name: entry.name,
3521
+ path: entry.path,
3522
+ gitRemote: entry.gitRemote,
3523
+ lastSeenAt: now
3524
+ };
3525
+ saveRegistryWithSeal(registry);
3237
3526
  return entry;
3238
3527
  }
3528
+ async function repairGlobalRegistry(cwd = process.cwd(), options = {}) {
3529
+ const before = await assertFreeRepoAllowed(cwd);
3530
+ if (!before.repairRequired) {
3531
+ return { repaired: false, check: before };
3532
+ }
3533
+ const install = ensureInstallState();
3534
+ const now = nowIso();
3535
+ const repaired = buildDefaultRegistry(install);
3536
+ repaired.createdAt = now;
3537
+ repaired.updatedAt = now;
3538
+ if (options.useCurrentRepo) {
3539
+ const currentRepo = await getCurrentRepoIdentity(cwd);
3540
+ repaired.trackedRepos = [
3541
+ {
3542
+ id: currentRepo.id,
3543
+ name: currentRepo.name,
3544
+ path: currentRepo.path,
3545
+ gitRemote: currentRepo.gitRemote,
3546
+ createdAt: now,
3547
+ lastSeenAt: now
3548
+ }
3549
+ ];
3550
+ repaired.lastKnownRepo = {
3551
+ id: currentRepo.id,
3552
+ name: currentRepo.name,
3553
+ path: currentRepo.path,
3554
+ gitRemote: currentRepo.gitRemote,
3555
+ lastSeenAt: now
3556
+ };
3557
+ }
3558
+ saveRegistryWithSeal(repaired);
3559
+ const check = await assertFreeRepoAllowed(cwd);
3560
+ return { repaired: true, check };
3561
+ }
3239
3562
  async function unlinkCurrentRepo(cwd = process.cwd(), force = false) {
3240
3563
  var _a2;
3564
+ const check = await assertFreeRepoAllowed(cwd);
3565
+ if (check.status === "blocked_repair") {
3566
+ if (!force) {
3567
+ return {
3568
+ removed: false,
3569
+ forced: false,
3570
+ currentRepo: check.currentRepo,
3571
+ trackedRepo: null
3572
+ };
3573
+ }
3574
+ const install = ensureInstallState();
3575
+ const repaired = buildDefaultRegistry(install);
3576
+ repaired.updatedAt = nowIso();
3577
+ repaired.lastKnownRepo = {
3578
+ id: check.currentRepo.id,
3579
+ name: check.currentRepo.name,
3580
+ path: check.currentRepo.path,
3581
+ gitRemote: check.currentRepo.gitRemote,
3582
+ lastSeenAt: repaired.updatedAt
3583
+ };
3584
+ saveRegistryWithSeal(repaired);
3585
+ return {
3586
+ removed: true,
3587
+ forced: true,
3588
+ currentRepo: check.currentRepo,
3589
+ trackedRepo: null
3590
+ };
3591
+ }
3241
3592
  const registry = loadGlobalRegistry();
3242
3593
  const currentRepo = await getCurrentRepoIdentity(cwd);
3243
3594
  const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
3244
3595
  if (trackedRepo) {
3245
3596
  registry.trackedRepos = registry.trackedRepos.filter((repo) => repo.id !== trackedRepo.id);
3246
- saveGlobalRegistry(registry);
3597
+ registry.updatedAt = nowIso();
3598
+ registry.lastKnownRepo = {
3599
+ id: trackedRepo.id,
3600
+ name: trackedRepo.name,
3601
+ path: trackedRepo.path,
3602
+ gitRemote: trackedRepo.gitRemote,
3603
+ lastSeenAt: registry.updatedAt
3604
+ };
3605
+ saveRegistryWithSeal(registry);
3247
3606
  return { removed: true, forced: false, currentRepo, trackedRepo };
3248
3607
  }
3249
3608
  if (force && registry.trackedRepos.length > 0) {
3250
3609
  const first = registry.trackedRepos[0];
3251
3610
  registry.trackedRepos = [];
3252
- saveGlobalRegistry(registry);
3611
+ registry.updatedAt = nowIso();
3612
+ registry.lastKnownRepo = {
3613
+ id: first.id,
3614
+ name: first.name,
3615
+ path: first.path,
3616
+ gitRemote: first.gitRemote,
3617
+ lastSeenAt: registry.updatedAt
3618
+ };
3619
+ saveRegistryWithSeal(registry);
3253
3620
  return { removed: true, forced: true, currentRepo, trackedRepo: first };
3254
3621
  }
3255
3622
  return {
@@ -3853,9 +4220,15 @@ async function getPanelState(cwd, monitorMode) {
3853
4220
  let runs = [];
3854
4221
  let latest = null;
3855
4222
  let registry = {
3856
- version: 1,
4223
+ version: 2,
4224
+ stateVersion: 2,
3857
4225
  plan: "free",
4226
+ machineInstallId: "",
4227
+ createdAt: "",
4228
+ updatedAt: "",
3858
4229
  trackedRepos: [],
4230
+ lastKnownRepo: null,
4231
+ integrity: { algorithm: "sha256-local-seal-v1", seal: "" },
3859
4232
  telemetry: {
3860
4233
  enabled: false,
3861
4234
  anonymousId: ""
@@ -3897,9 +4270,15 @@ async function getPanelState(cwd, monitorMode) {
3897
4270
  } catch (e) {
3898
4271
  warnings.push("global_registry_failed");
3899
4272
  registry = {
3900
- version: 1,
4273
+ version: 2,
4274
+ stateVersion: 2,
3901
4275
  plan: "free",
4276
+ machineInstallId: "",
4277
+ createdAt: "",
4278
+ updatedAt: "",
3902
4279
  trackedRepos: [],
4280
+ lastKnownRepo: null,
4281
+ integrity: { algorithm: "sha256-local-seal-v1", seal: "" },
3903
4282
  telemetry: {
3904
4283
  enabled: false,
3905
4284
  anonymousId: ""
@@ -4367,7 +4746,7 @@ Before editing code:
4367
4746
  If no active RunTrim contract exists:
4368
4747
  - Do not edit code without one.
4369
4748
  - Ask the user to start a guarded run:
4370
- runtrim go "<task>"
4749
+ runtrim agent "<task>" --copy
4371
4750
 
4372
4751
  If the task requires leaving the current scope:
4373
4752
  - Stop.
@@ -4463,7 +4842,7 @@ ${BASE_PROTOCOL}
4463
4842
  Before using any tool or executing any command:
4464
4843
  1. Confirm a RunTrim contract is active at .runtrim/contracts/latest.md.
4465
4844
  2. If no active contract exists, do not proceed. Ask for:
4466
- runtrim go "<task>"
4845
+ runtrim agent "<task>" --copy
4467
4846
  3. Do not call shell commands, write files, or read env vars outside the contract.
4468
4847
  `.trim(),
4469
4848
  custom: `
@@ -4516,7 +4895,7 @@ function getCursorMdcContent() {
4516
4895
  "## If no active contract",
4517
4896
  "",
4518
4897
  "Ask the user to start a guarded run:",
4519
- '`runtrim go "<task>"`',
4898
+ '`runtrim agent "<task>" --copy`',
4520
4899
  "",
4521
4900
  "Any agent. One run boundary."
4522
4901
  ].join("\n");
@@ -4778,7 +5157,7 @@ Before editing code:
4778
5157
  - If the task touches auth, billing, payments, webhooks, database, middleware,
4779
5158
  env vars, secrets, or subscriptions, stop and require an active RunTrim contract.
4780
5159
  Ask the user to run:
4781
- runtrim go "<task>"
5160
+ runtrim agent "<task>" --copy
4782
5161
  - For low-risk work (UI polish, copy, docs, isolated component styling):
4783
5162
  Fast Path is allowed if no unfinished changes exist.
4784
5163
  Keep the change minimal.
@@ -4801,7 +5180,7 @@ No active RunTrim contract means no code edits.
4801
5180
  If no active contract exists at .runtrim/contracts/latest.md:
4802
5181
  - Do not edit any file.
4803
5182
  - Ask the user to start a guarded run:
4804
- runtrim go "<task>"
5183
+ runtrim agent "<task>" --copy
4805
5184
 
4806
5185
  After every editing session:
4807
5186
  - Ask the user to run:
@@ -4815,7 +5194,7 @@ Fast Path is allowed for low and medium risk work.
4815
5194
 
4816
5195
  Critical systems (auth, billing, payments, webhooks, database, middleware,
4817
5196
  env vars, secrets, subscriptions) still require a RunTrim contract:
4818
- runtrim go "<task>"
5197
+ runtrim agent "<task>" --copy
4819
5198
 
4820
5199
  After any edits:
4821
5200
  - runtrim finish is required before continuing to another task.
@@ -4826,7 +5205,7 @@ RunTrim Auto-guard: Off
4826
5205
 
4827
5206
  Auto-guard is disabled for this project.
4828
5207
  RunTrim can still be used manually:
4829
- runtrim go "<task>"
5208
+ runtrim agent "<task>" --copy
4830
5209
  runtrim finish
4831
5210
  `.trim();
4832
5211
  }
@@ -4852,7 +5231,7 @@ function saveFastRunRecord(cwd, changedFiles, risk) {
4852
5231
  reportParts.push(`${sensitive.length} sensitive path${sensitive.length === 1 ? "" : "s"} touched.`);
4853
5232
  }
4854
5233
  reportParts.push("No pre-run contract was captured for this run.");
4855
- const nextSafeAction = changedFiles.length > 0 ? 'Create a contract before the next change: runtrim go "<task>"' : 'Start a guarded run: runtrim go "<task>"';
5234
+ const nextSafeAction = changedFiles.length > 0 ? 'Create a contract before the next change: runtrim agent "<task>" --copy' : 'Start a guarded run: runtrim agent "<task>" --copy';
4856
5235
  const summary = {
4857
5236
  id,
4858
5237
  task,
@@ -5372,7 +5751,7 @@ function recommendProviderRouting(ctx) {
5372
5751
  } else if (route === "split-required") {
5373
5752
  routingReason = "This spans multiple critical systems, so RunTrim should split into audit, implementation, and verification.";
5374
5753
  }
5375
- let nextCommand = `runtrim go "${ctx.task}"`;
5754
+ let nextCommand = `runtrim agent "${ctx.task}" --copy`;
5376
5755
  if (route === "split-required") {
5377
5756
  nextCommand = "split into:\n1. audit only\n2. implementation only\n3. verification only";
5378
5757
  } else if (route === "preview-only") {
@@ -5394,6 +5773,7 @@ var _a;
5394
5773
  var oraFactory = typeof import_ora.default === "function" ? import_ora.default : (_a = import_ora.default.default) != null ? _a : import_ora.default;
5395
5774
  var ACCENT = chalk.hex("#C8901A");
5396
5775
  var GO_ACCENT = chalk.hex("#8B7CFF");
5776
+ var RUNTRIM_AGENT_INSTRUCTIONS_VERSION = "2";
5397
5777
  var DIM = chalk.gray;
5398
5778
  var BOLD = chalk.white.bold;
5399
5779
  var program = new import_commander.Command();
@@ -5529,6 +5909,98 @@ async function copyToClipboardSafe(value) {
5529
5909
  function dedupeFiles(files) {
5530
5910
  return [...new Set(files.filter(Boolean).map((f) => f.replace(/\\/g, "/")))];
5531
5911
  }
5912
+ var RUNTRIM_GITIGNORE_BLOCK_START = "# BEGIN RUNTRIM_ARTIFACTS";
5913
+ var RUNTRIM_GITIGNORE_BLOCK_END = "# END RUNTRIM_ARTIFACTS";
5914
+ function ensureRuntrimReadme(cwd) {
5915
+ const readmePath = import_path13.default.join(getConfigDir(cwd), "README.md");
5916
+ const content = [
5917
+ "# RunTrim Local Files",
5918
+ "",
5919
+ "RunTrim stores local metadata in this folder.",
5920
+ "",
5921
+ "Human-facing files:",
5922
+ "- `agent/instructions.md` and `agent/latest.md`",
5923
+ "- `contracts/latest.md`",
5924
+ "- `memory/current.md` and `memory/baseline.md`",
5925
+ "- `mcp/*.json`",
5926
+ "- `config.json`",
5927
+ "",
5928
+ "Internal artifacts:",
5929
+ "- `.runtrim/internal/runs/`",
5930
+ "- `.runtrim/internal/previews/`",
5931
+ "- `.runtrim/internal/restores/`",
5932
+ "- `.runtrim/internal/contracts-archive/`",
5933
+ "- `.runtrim/internal/agent-archive/`",
5934
+ "",
5935
+ "Notes:",
5936
+ "- Artifacts are local-first.",
5937
+ "- Source code is not uploaded by local storage.",
5938
+ "- Restore metadata is path-only and does not store secret contents."
5939
+ ].join("\n");
5940
+ import_fs13.default.writeFileSync(readmePath, content + "\n", "utf-8");
5941
+ }
5942
+ function ensureRuntrimGitignoreGuidance(cwd) {
5943
+ const gitignorePath = import_path13.default.join(cwd, ".gitignore");
5944
+ if (!import_fs13.default.existsSync(gitignorePath)) return;
5945
+ const desired = [
5946
+ RUNTRIM_GITIGNORE_BLOCK_START,
5947
+ "# RunTrim local artifacts",
5948
+ ".runtrim/internal/",
5949
+ ".runtrim/runs/",
5950
+ ".runtrim/previews/",
5951
+ ".runtrim/restores/",
5952
+ ".runtrim/contracts/archive/",
5953
+ ".runtrim/agent/*.json",
5954
+ RUNTRIM_GITIGNORE_BLOCK_END
5955
+ ].join("\n");
5956
+ const current = import_fs13.default.readFileSync(gitignorePath, "utf-8");
5957
+ const start = current.indexOf(RUNTRIM_GITIGNORE_BLOCK_START);
5958
+ const end = current.indexOf(RUNTRIM_GITIGNORE_BLOCK_END);
5959
+ if (start !== -1 && end !== -1 && end > start) {
5960
+ const next = current.slice(0, start).trimEnd() + "\n\n" + desired + "\n" + current.slice(end + RUNTRIM_GITIGNORE_BLOCK_END.length).replace(/^\n+/, "\n");
5961
+ if (next !== current) import_fs13.default.writeFileSync(gitignorePath, next, "utf-8");
5962
+ return;
5963
+ }
5964
+ if (!current.includes(".runtrim/internal/")) {
5965
+ import_fs13.default.writeFileSync(gitignorePath, current.trimEnd() + "\n\n" + desired + "\n", "utf-8");
5966
+ }
5967
+ }
5968
+ function listFilesIfExists(dir) {
5969
+ if (!import_fs13.default.existsSync(dir)) return [];
5970
+ return import_fs13.default.readdirSync(dir).map((name) => import_path13.default.join(dir, name)).filter((p) => import_fs13.default.existsSync(p) && import_fs13.default.statSync(p).isFile());
5971
+ }
5972
+ function listArtifactFiles(cwd) {
5973
+ const dirs = [
5974
+ getRunsDir(cwd),
5975
+ getPreviewsDir(cwd),
5976
+ getRestoresDir(cwd),
5977
+ getContractsArchiveDir(cwd),
5978
+ import_path13.default.join(getConfigDir(cwd), "internal", "agent-archive"),
5979
+ getLegacyRunsDir(cwd),
5980
+ getLegacyPreviewsDir(cwd),
5981
+ getLegacyRestoresDir(cwd),
5982
+ import_path13.default.join(getConfigDir(cwd), "contracts", "archive"),
5983
+ import_path13.default.join(getConfigDir(cwd), "agent")
5984
+ ];
5985
+ const files = dirs.flatMap((dir) => listFilesIfExists(dir));
5986
+ return files.filter((filePath) => {
5987
+ const base = import_path13.default.basename(filePath).toLowerCase();
5988
+ const rel = import_path13.default.relative(cwd, filePath).replace(/\\/g, "/");
5989
+ if (rel === ".runtrim/previews/latest.md") return false;
5990
+ if (base === "latest.md" || base === "instructions.md" || base === "current.md" || base === "baseline.md") return false;
5991
+ return true;
5992
+ });
5993
+ }
5994
+ function parseRunIdFromArtifact(filePath) {
5995
+ const base = import_path13.default.basename(filePath);
5996
+ const direct = base.match(/^([a-zA-Z0-9_-]{6,})\.json$/);
5997
+ if (direct) return direct[1];
5998
+ const report = base.match(/^([a-zA-Z0-9_-]{6,})\.report\.\d+\.json$/);
5999
+ if (report) return report[1];
6000
+ const out = base.match(/^([a-zA-Z0-9_-]{6,})\.output\.txt$/);
6001
+ if (out) return out[1];
6002
+ return null;
6003
+ }
5532
6004
  function normalizeContractPathPattern(pattern) {
5533
6005
  let p = pattern.trim().replace(/\\/g, "/");
5534
6006
  if (!p || p === "-" || p.toLowerCase() === "none") return "";
@@ -5698,10 +6170,11 @@ function buildRecommendedNextCommand(task, approval, filesToInspect) {
5698
6170
  return `runtrim go "${task}"`;
5699
6171
  }
5700
6172
  function writePreviewArtifacts(cwd, preview) {
5701
- const previewsDir = import_path13.default.join(cwd, ".runtrim", "previews");
6173
+ const previewsDir = getPreviewsDir(cwd);
5702
6174
  if (!import_fs13.default.existsSync(previewsDir)) import_fs13.default.mkdirSync(previewsDir, { recursive: true });
5703
6175
  const jsonPath = import_path13.default.join(previewsDir, `${preview.id}.json`);
5704
- const markdownPath = import_path13.default.join(previewsDir, "latest.md");
6176
+ const markdownPath = import_path13.default.join(getLegacyPreviewsDir(cwd), "latest.md");
6177
+ if (!import_fs13.default.existsSync(import_path13.default.dirname(markdownPath))) import_fs13.default.mkdirSync(import_path13.default.dirname(markdownPath), { recursive: true });
5705
6178
  import_fs13.default.writeFileSync(jsonPath, JSON.stringify(preview, null, 2), "utf-8");
5706
6179
  const lines = [
5707
6180
  "RunTrim Agent Preview",
@@ -5740,7 +6213,7 @@ function writePreviewArtifacts(cwd, preview) {
5740
6213
  "Next:",
5741
6214
  preview.recommendedNextCommand,
5742
6215
  "",
5743
- `Preview JSON: .runtrim/previews/${preview.id}.json`
6216
+ `Preview JSON: .runtrim/internal/previews/${preview.id}.json`
5744
6217
  ];
5745
6218
  import_fs13.default.writeFileSync(markdownPath, lines.join("\n"), "utf-8");
5746
6219
  return { jsonPath, markdownPath };
@@ -6122,6 +6595,7 @@ async function runAgentApply(task, mode) {
6122
6595
  const copied = mode.copy ? await copyToClipboardSafe(import_fs13.default.readFileSync(handoff.markdownPath, "utf-8")) : false;
6123
6596
  const run = saveRun(task, previewResult.audit, previewResult.contract, cwd);
6124
6597
  updateRun(run.id, { status: "guarded" }, cwd);
6598
+ await captureRestorePoint(cwd, run.id, task);
6125
6599
  const riskColor = (_a2 = { low: chalk.green, medium: chalk.yellow, high: chalk.hex("#FF8C00"), critical: chalk.red }[risk]) != null ? _a2 : chalk.white;
6126
6600
  if (mode.primary) {
6127
6601
  console.log("");
@@ -6308,6 +6782,7 @@ async function runControlledExecution(task, mode) {
6308
6782
  controlledExecutionId: id,
6309
6783
  controlledExecutionStatus: status
6310
6784
  }, cwd);
6785
+ await captureRestorePoint(cwd, run.id, task);
6311
6786
  }
6312
6787
  if (status === "blocked") {
6313
6788
  console.log("");
@@ -6598,14 +7073,18 @@ ${block}
6598
7073
  function buildRuntrimAgentBlockContent() {
6599
7074
  return [
6600
7075
  "RunTrim project instructions",
7076
+ `RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`,
6601
7077
  "- Before editing, read RunTrim project memory and profile context.",
6602
- "- Verify active contract first. If MCP is available and no contract is active, call runtrim_create_contract with the user's task.",
7078
+ "- Before editing, create or verify an active RunTrim contract for the current user task.",
7079
+ "- If MCP is available and no active contract exists, call runtrim_create_contract with the user's task.",
6603
7080
  '- If MCP is unavailable, ask the user to run: runtrim agent "task" --copy',
6604
- "- Stay inside active contract scope.",
6605
- "- 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.",
7081
+ "- Before touching risky paths (auth, billing, middleware/proxy, migrations, env/sensitive files, broad app-wide refactors), call runtrim_check_path when available.",
7082
+ "- Use RunTrim project memory and stay inside active contract scope.",
7083
+ "- Do not silently expand scope. Preserve existing project style and logic.",
6606
7084
  "- Never read or print env file contents.",
6607
- '- Do not continue outside scope silently. Request: runtrim approve "Allow <path/scope> for this run only"',
6608
- "- At the end, run or ask for runtrim finish. Do not claim completion before finish verification.",
7085
+ '- If scope must expand, stop and request approval: runtrim approve "Allow <path/scope> for this run only"',
7086
+ "- At the end, run runtrim finish if terminal execution is available, otherwise ask the user to run runtrim finish.",
7087
+ "- Do not claim completion before finish verification when RunTrim is active.",
6609
7088
  "",
6610
7089
  "RunTrim MCP:",
6611
7090
  "- runtrim mcp instructions",
@@ -6765,12 +7244,16 @@ function updateStartAgentInstructions(cwd) {
6765
7244
  const existing = import_fs13.default.existsSync(instructionsPath) ? import_fs13.default.readFileSync(instructionsPath, "utf-8") : "";
6766
7245
  const body = [
6767
7246
  "RunTrim agent instructions:",
7247
+ `RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`,
6768
7248
  "- Use RunTrim project memory and profile before editing.",
6769
- "- Verify active contract before edits. If MCP is available and contract is missing, call runtrim_create_contract with the user's task.",
7249
+ "- Create or verify an active RunTrim contract before edits.",
7250
+ "- If MCP is available and contract is missing, call runtrim_create_contract with the user's task.",
6770
7251
  '- If MCP is unavailable, ask user to run: runtrim agent "task" --copy',
6771
- "- 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.",
7252
+ "- 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.",
7253
+ "- Stay inside active contract scope and preserve existing project style and logic.",
6772
7254
  '- If scope must expand, request: runtrim approve "Allow <path/scope> for this run only"',
6773
- "- Run finish verification at the end. Do not claim completion before runtrim finish.",
7255
+ "- Run runtrim finish when terminal execution is available, otherwise ask the user to run runtrim finish.",
7256
+ "- Do not claim completion before runtrim finish verification when RunTrim is active.",
6774
7257
  "- Never read or print env file contents.",
6775
7258
  "",
6776
7259
  "RunTrim MCP:",
@@ -6843,6 +7326,202 @@ function detectKnownMcpConfigPresence() {
6843
7326
  cursorConfigFound: Boolean(cursorMatch)
6844
7327
  };
6845
7328
  }
7329
+ function getRestorePointPath(cwd, runId) {
7330
+ return import_path13.default.join(getRestoresDir(cwd), `${runId}.json`);
7331
+ }
7332
+ function getLegacyRestorePointPath(cwd, runId) {
7333
+ return import_path13.default.join(getLegacyRestoresDir(cwd), `${runId}.json`);
7334
+ }
7335
+ async function isGitRepo(cwd) {
7336
+ try {
7337
+ await (0, import_execa3.execa)("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
7338
+ return true;
7339
+ } catch (e) {
7340
+ return false;
7341
+ }
7342
+ }
7343
+ async function captureRestorePoint(cwd, runId, task) {
7344
+ const dir = getRestoresDir(cwd);
7345
+ if (!import_fs13.default.existsSync(dir)) import_fs13.default.mkdirSync(dir, { recursive: true });
7346
+ const existingPath = getRestorePointPath(cwd, runId);
7347
+ if (import_fs13.default.existsSync(existingPath) || import_fs13.default.existsSync(getLegacyRestorePointPath(cwd, runId))) return;
7348
+ let commit = null;
7349
+ let changedBeforeRun = [];
7350
+ if (await isGitRepo(cwd)) {
7351
+ try {
7352
+ const { stdout } = await (0, import_execa3.execa)("git", ["rev-parse", "HEAD"], { cwd });
7353
+ commit = stdout.trim() || null;
7354
+ } catch (e) {
7355
+ commit = null;
7356
+ }
7357
+ try {
7358
+ const changed = await getGitChangedFiles(cwd);
7359
+ changedBeforeRun = dedupeFiles(changed.map((c) => c.path));
7360
+ } catch (e) {
7361
+ changedBeforeRun = [];
7362
+ }
7363
+ }
7364
+ const record = {
7365
+ runId,
7366
+ task,
7367
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7368
+ preRun: {
7369
+ commit,
7370
+ dirty: changedBeforeRun.length > 0,
7371
+ changedBeforeRun
7372
+ }
7373
+ };
7374
+ import_fs13.default.writeFileSync(existingPath, JSON.stringify(record, null, 2), "utf-8");
7375
+ }
7376
+ function loadRestorePoint(cwd, runId) {
7377
+ const preferred = getRestorePointPath(cwd, runId);
7378
+ const legacy = getLegacyRestorePointPath(cwd, runId);
7379
+ const p = import_fs13.default.existsSync(preferred) ? preferred : legacy;
7380
+ if (!import_fs13.default.existsSync(p)) return null;
7381
+ try {
7382
+ return JSON.parse(import_fs13.default.readFileSync(p, "utf-8"));
7383
+ } catch (e) {
7384
+ return null;
7385
+ }
7386
+ }
7387
+ function saveRestorePoint(cwd, record) {
7388
+ const dir = getRestoresDir(cwd);
7389
+ if (!import_fs13.default.existsSync(dir)) import_fs13.default.mkdirSync(dir, { recursive: true });
7390
+ import_fs13.default.writeFileSync(getRestorePointPath(cwd, record.runId), JSON.stringify(record, null, 2), "utf-8");
7391
+ }
7392
+ function isSecretLikePath(file) {
7393
+ const n = file.replace(/\\/g, "/").toLowerCase();
7394
+ 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");
7395
+ }
7396
+ function isDocsLikePath(file) {
7397
+ const n = file.replace(/\\/g, "/").toLowerCase();
7398
+ return n === "readme.md" || n.startsWith("docs/") || n.endsWith(".md") || n.endsWith(".mdx") || n.includes("changelog");
7399
+ }
7400
+ function isUiCheckoutPath(file) {
7401
+ const n = file.replace(/\\/g, "/").toLowerCase();
7402
+ return n.includes("/checkout/") && !n.includes("/api/") && !n.includes("webhook") && !n.includes("stripe") && !n.includes("dodo") && !n.includes("provider") && !n.includes("session");
7403
+ }
7404
+ function isHighRiskLogicPath(file) {
7405
+ const n = file.replace(/\\/g, "/").toLowerCase();
7406
+ if (isSecretLikePath(n)) return true;
7407
+ if (n.endsWith("middleware.ts") || n.endsWith("middleware.js") || n.endsWith("proxy.ts") || n.endsWith("proxy.js")) return true;
7408
+ if (n.includes("supabase/migrations") || n.includes("prisma/migrations") || n.includes("/migrations/")) return true;
7409
+ if (n.includes("/auth/") || n.includes("session") || n.includes("jwt")) return true;
7410
+ if (n.includes("webhook")) return true;
7411
+ 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;
7412
+ 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;
7413
+ return false;
7414
+ }
7415
+ function matchesAnyContractRule(file, rules) {
7416
+ return rules.some((rule) => matchesContractPattern(file, rule));
7417
+ }
7418
+ async function detectCiChangedFiles(cwd, base, head) {
7419
+ var _a2;
7420
+ const warnings = [];
7421
+ let baseUsed = (base == null ? void 0 : base.trim()) || "";
7422
+ let headUsed = (head == null ? void 0 : head.trim()) || "";
7423
+ if (!headUsed) {
7424
+ try {
7425
+ const { stdout } = await (0, import_execa3.execa)("git", ["rev-parse", "HEAD"], { cwd });
7426
+ headUsed = stdout.trim();
7427
+ } catch (e) {
7428
+ headUsed = "HEAD";
7429
+ }
7430
+ }
7431
+ if (!baseUsed) {
7432
+ const ghBase = (_a2 = process.env.GITHUB_BASE_REF) == null ? void 0 : _a2.trim();
7433
+ if (ghBase) {
7434
+ baseUsed = `origin/${ghBase}`;
7435
+ }
7436
+ }
7437
+ if (!baseUsed) {
7438
+ for (const candidate of ["origin/main", "origin/master", "main", "master", "HEAD~1"]) {
7439
+ try {
7440
+ await (0, import_execa3.execa)("git", ["rev-parse", "--verify", candidate], { cwd });
7441
+ baseUsed = candidate;
7442
+ break;
7443
+ } catch (e) {
7444
+ continue;
7445
+ }
7446
+ }
7447
+ }
7448
+ if (!baseUsed) {
7449
+ warnings.push("Could not infer a base ref. Using working tree changes only.");
7450
+ }
7451
+ let diffFiles = [];
7452
+ if (baseUsed) {
7453
+ try {
7454
+ const { stdout } = await (0, import_execa3.execa)("git", ["diff", "--name-only", `${baseUsed}...${headUsed}`], { cwd });
7455
+ diffFiles = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
7456
+ } catch (e) {
7457
+ warnings.push(`Could not diff ${baseUsed}...${headUsed}. Falling back to local changes.`);
7458
+ }
7459
+ }
7460
+ if (diffFiles.length === 0) {
7461
+ try {
7462
+ const { stdout } = await (0, import_execa3.execa)("git", ["diff", "--name-only"], { cwd });
7463
+ diffFiles = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
7464
+ } catch (e) {
7465
+ }
7466
+ }
7467
+ try {
7468
+ const { stdout } = await (0, import_execa3.execa)("git", ["status", "--porcelain"], { cwd });
7469
+ const untracked = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("?? ")).map((line) => line.slice(3).trim());
7470
+ diffFiles = dedupeFiles([...diffFiles, ...untracked]);
7471
+ } catch (e) {
7472
+ }
7473
+ return {
7474
+ files: dedupeFiles(diffFiles.map((f) => f.replace(/\\/g, "/"))),
7475
+ baseUsed: baseUsed || "(local)",
7476
+ headUsed,
7477
+ warnings
7478
+ };
7479
+ }
7480
+ function detectMcpConfigState(configPath, found) {
7481
+ if (!found || !configPath) return "not found";
7482
+ try {
7483
+ const raw = JSON.parse(import_fs13.default.readFileSync(configPath, "utf-8"));
7484
+ const servers = raw.mcpServers && typeof raw.mcpServers === "object" ? raw.mcpServers : {};
7485
+ return servers.runtrim ? "configured" : "missing runtrim";
7486
+ } catch (e) {
7487
+ return "missing runtrim";
7488
+ }
7489
+ }
7490
+ function hasCurrentRuntrimBlock(filePath) {
7491
+ if (!import_fs13.default.existsSync(filePath)) return { exists: false, current: false };
7492
+ const content = import_fs13.default.readFileSync(filePath, "utf-8");
7493
+ const hasBlock = content.includes("<!-- RUNTRIM:START -->") && content.includes("<!-- RUNTRIM:END -->");
7494
+ const hasVersion = content.includes(`RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`);
7495
+ return { exists: hasBlock, current: hasBlock && hasVersion };
7496
+ }
7497
+ function readMcpLastUsed(cwd) {
7498
+ const p = import_path13.default.join(getProjectMcpDir(cwd), "last-used.json");
7499
+ if (!import_fs13.default.existsSync(p)) return { tracked: false, tool: null, usedAt: null };
7500
+ try {
7501
+ const parsed = JSON.parse(import_fs13.default.readFileSync(p, "utf-8"));
7502
+ return {
7503
+ tracked: true,
7504
+ tool: typeof parsed.tool === "string" ? parsed.tool : null,
7505
+ usedAt: typeof parsed.usedAt === "string" ? parsed.usedAt : null
7506
+ };
7507
+ } catch (e) {
7508
+ return { tracked: false, tool: null, usedAt: null };
7509
+ }
7510
+ }
7511
+ function writeMcpLastUsed(cwd, tool) {
7512
+ try {
7513
+ const dir = getProjectMcpDir(cwd);
7514
+ if (!import_fs13.default.existsSync(dir)) import_fs13.default.mkdirSync(dir, { recursive: true });
7515
+ const payload = {
7516
+ tool,
7517
+ usedAt: (/* @__PURE__ */ new Date()).toISOString(),
7518
+ projectPath: cwd
7519
+ };
7520
+ import_fs13.default.writeFileSync(import_path13.default.join(dir, "last-used.json"), `${JSON.stringify(payload, null, 2)}
7521
+ `, "utf-8");
7522
+ } catch (e) {
7523
+ }
7524
+ }
6846
7525
  function appendContractAmendment(cwd, approvalText) {
6847
7526
  const p = import_path13.default.join(cwd, ".runtrim", "contracts", "latest.md");
6848
7527
  if (!import_fs13.default.existsSync(p)) return { ok: false, reason: "missing_contract" };
@@ -7150,6 +7829,24 @@ async function buildRuntrimCreateContractMcp(cwd, args) {
7150
7829
  isError: true
7151
7830
  };
7152
7831
  }
7832
+ const repoCheck = await assertFreeRepoAllowed(cwd);
7833
+ if (!repoCheck.allowed) {
7834
+ 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.";
7835
+ const blockedPayload = {
7836
+ contract_created: false,
7837
+ task: taskRaw,
7838
+ error: repoCheck.status === "blocked_repair" ? "repo_registry_repair_required" : "repo_limit_blocked",
7839
+ guidance,
7840
+ next_action: guidance,
7841
+ finish_command: "runtrim finish",
7842
+ approval_command_example: 'runtrim approve "Allow <path> for this run only"'
7843
+ };
7844
+ return {
7845
+ content: [{ type: "text", text: JSON.stringify(blockedPayload, null, 2) }],
7846
+ structuredContent: blockedPayload,
7847
+ isError: true
7848
+ };
7849
+ }
7153
7850
  const latest = loadLatestRun(cwd);
7154
7851
  if ((latest == null ? void 0 : latest.status) === "guarded") {
7155
7852
  const blockedPayload = {
@@ -7197,6 +7894,7 @@ async function buildRuntrimCreateContractMcp(cwd, args) {
7197
7894
  const handoff = writeAgentHandoffArtifacts(cwd, apply, import_path13.default.relative(cwd, previewPath));
7198
7895
  const run = saveRun(mergedTask, previewResult.audit, previewResult.contract, cwd);
7199
7896
  updateRun(run.id, { status: "guarded" }, cwd);
7897
+ await captureRestorePoint(cwd, run.id, mergedTask);
7200
7898
  const payload = {
7201
7899
  contract_created: true,
7202
7900
  task: taskRaw,
@@ -7391,6 +8089,7 @@ async function startMcpServerStdio(cwd) {
7391
8089
  });
7392
8090
  return;
7393
8091
  }
8092
+ writeMcpLastUsed(cwd, name);
7394
8093
  send({
7395
8094
  jsonrpc: "2.0",
7396
8095
  id,
@@ -7811,6 +8510,21 @@ function isInteractiveTerminal() {
7811
8510
  async function ensureRepoAllowedForFree(cwd) {
7812
8511
  var _a2, _b;
7813
8512
  const check = await assertFreeRepoAllowed(cwd);
8513
+ if (check.status === "blocked_repair") {
8514
+ console.log(chalk.yellow(" RunTrim local state needs repair."));
8515
+ console.log(chalk.yellow(" Free includes 1 tracked repo."));
8516
+ console.log(chalk.yellow(" Your local repo registry changed unexpectedly."));
8517
+ console.log("");
8518
+ console.log(DIM(" Next:"));
8519
+ console.log(chalk.white(" - runtrim repo status"));
8520
+ console.log(chalk.white(" - runtrim repo repair"));
8521
+ console.log(chalk.white(" - runtrim repo repair --use-current"));
8522
+ console.log(chalk.white(" - runtrim repo unlink --force"));
8523
+ console.log(chalk.white(" - upgrade to Builder for unlimited repos"));
8524
+ console.log(chalk.white(" - sign in to restore cloud entitlements"));
8525
+ console.log("");
8526
+ return false;
8527
+ }
7814
8528
  if (check.allowed) {
7815
8529
  await registerCurrentRepo(cwd);
7816
8530
  return true;
@@ -7824,9 +8538,12 @@ async function ensureRepoAllowedForFree(cwd) {
7824
8538
  console.log(chalk.white(` ${check.currentRepo.path}`));
7825
8539
  console.log("");
7826
8540
  console.log(DIM(" Next:"));
7827
- console.log(chalk.white(" - continue in the tracked repo"));
7828
- console.log(chalk.white(" - unlink the tracked repo with runtrim repo unlink --force"));
7829
- console.log(chalk.white(" - join Builder early access for unlimited repos"));
8541
+ console.log(
8542
+ chalk.white(
8543
+ " 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."
8544
+ )
8545
+ );
8546
+ console.log(chalk.white(" Agent instructions were not installed because this repo is not tracked."));
7830
8547
  console.log("");
7831
8548
  console.log(
7832
8549
  DIM(
@@ -7860,6 +8577,7 @@ async function initializeRunTrim(cwd, options = {}) {
7860
8577
  const runsDir = getRunsDir(cwd);
7861
8578
  if (!import_fs13.default.existsSync(configDir)) import_fs13.default.mkdirSync(configDir, { recursive: true });
7862
8579
  if (!import_fs13.default.existsSync(runsDir)) import_fs13.default.mkdirSync(runsDir, { recursive: true });
8580
+ ensureInternalArtifactDirs(cwd);
7863
8581
  const existingConfig = hadConfig ? loadConfig(cwd) : null;
7864
8582
  const baseConfig = __spreadValues(__spreadValues({}, DEFAULT_CONFIG), detectProjectInfo(cwd));
7865
8583
  const nextConfig = options.refresh && existingConfig ? __spreadValues({}, existingConfig) : baseConfig;
@@ -7879,13 +8597,8 @@ async function initializeRunTrim(cwd, options = {}) {
7879
8597
  import_fs13.default.writeFileSync(memoryPath, buildBaselineMemoryMarkdown(baseline), "utf-8");
7880
8598
  }
7881
8599
  ensureStarterPromptIfMissing(cwd);
7882
- const gitignorePath = import_path13.default.join(cwd, ".gitignore");
7883
- if (import_fs13.default.existsSync(gitignorePath)) {
7884
- const content = import_fs13.default.readFileSync(gitignorePath, "utf-8");
7885
- if (!content.includes(".runtrim/runs")) {
7886
- import_fs13.default.appendFileSync(gitignorePath, "\n# RunTrim run history\n.runtrim/runs/\n");
7887
- }
7888
- }
8600
+ ensureRuntrimReadme(cwd);
8601
+ ensureRuntrimGitignoreGuidance(cwd);
7889
8602
  return { ok: true };
7890
8603
  }
7891
8604
  async function runPrepareTask(task, options) {
@@ -7920,7 +8633,7 @@ async function runPrepareTask(task, options) {
7920
8633
  console.log("");
7921
8634
  console.log(DIM(" Task ") + chalk.white(truncate(task, 70)));
7922
8635
  console.log(DIM(" Prompt ") + chalk.white(promptPath2));
7923
- console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
8636
+ console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
7924
8637
  console.log("");
7925
8638
  printPrepareAgentInstructions(selectedAgent, config.lastPromptPath);
7926
8639
  console.log("");
@@ -7940,6 +8653,7 @@ async function runPrepareTask(task, options) {
7940
8653
  const promptPath = writeLatestPromptFile(contract.contractText, config, cwd);
7941
8654
  if (options.copy !== false) await copyToClipboardSafe(contract.contractText);
7942
8655
  updateRun(run.id, { status: "guarded" }, cwd);
8656
+ await captureRestorePoint(cwd, run.id, task);
7943
8657
  const riskColors = {
7944
8658
  low: chalk.green,
7945
8659
  medium: chalk.yellow,
@@ -7962,7 +8676,7 @@ async function runPrepareTask(task, options) {
7962
8676
  );
7963
8677
  console.log(DIM(" Reduction ") + chalk.white(contract.riskReductionPercent + "%"));
7964
8678
  console.log(DIM(" Prompt ") + chalk.white(promptPath));
7965
- console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
8679
+ console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
7966
8680
  console.log("");
7967
8681
  printPrepareAgentInstructions(selectedAgent, config.lastPromptPath);
7968
8682
  console.log("");
@@ -8187,13 +8901,116 @@ program.command("start").description("Guided RunTrim onboarding and daily loop")
8187
8901
  console.log("");
8188
8902
  }
8189
8903
  });
8904
+ program.command("doctor").description("Check project readiness for RunTrim agent auto-control").action(async () => {
8905
+ const cwd = process.cwd();
8906
+ const repoCheck = await assertFreeRepoAllowed(cwd);
8907
+ const profilePath = import_path13.default.join(getConfigDir(cwd), "project-profile.json");
8908
+ const memoryPath = import_path13.default.join(getConfigDir(cwd), "memory", "current.md");
8909
+ const instructionsPath = import_path13.default.join(getConfigDir(cwd), "agent", "instructions.md");
8910
+ const snippetsDir = getProjectMcpDir(cwd);
8911
+ const snippetFiles = [
8912
+ import_path13.default.join(snippetsDir, "claude-desktop.json"),
8913
+ import_path13.default.join(snippetsDir, "cursor.json"),
8914
+ import_path13.default.join(snippetsDir, "generic.json")
8915
+ ];
8916
+ const profileReady = import_fs13.default.existsSync(profilePath);
8917
+ const memoryReady = import_fs13.default.existsSync(memoryPath) && import_fs13.default.readFileSync(memoryPath, "utf-8").trim().length > 0;
8918
+ const instructionsReady = import_fs13.default.existsSync(instructionsPath) && import_fs13.default.readFileSync(instructionsPath, "utf-8").trim().length > 0;
8919
+ const claudeBlock = hasCurrentRuntrimBlock(import_path13.default.join(cwd, "CLAUDE.md"));
8920
+ const agentsBlock = hasCurrentRuntrimBlock(import_path13.default.join(cwd, "AGENTS.md"));
8921
+ const cursorRulePath = import_path13.default.join(cwd, ".cursor", "rules", "runtrim.mdc");
8922
+ const cursorRuleExists = import_fs13.default.existsSync(cursorRulePath);
8923
+ const cursorRuleCurrent = cursorRuleExists ? import_fs13.default.readFileSync(cursorRulePath, "utf-8").includes(`RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`) : false;
8924
+ const anySurfaceInstalled = claudeBlock.exists || agentsBlock.exists || cursorRuleExists;
8925
+ const runtrimBlockCurrent = [claudeBlock.current, agentsBlock.current, cursorRuleCurrent].some(Boolean);
8926
+ const snippetsGenerated = snippetFiles.every((p) => import_fs13.default.existsSync(p));
8927
+ const knownMcp = detectKnownMcpConfigPresence();
8928
+ const claudeMcpState = detectMcpConfigState(knownMcp.claudeConfigPath, knownMcp.claudeConfigFound);
8929
+ const cursorMcpState = detectMcpConfigState(knownMcp.cursorConfigPath, knownMcp.cursorConfigFound);
8930
+ const genericReady = snippetsGenerated ? "ready" : "missing";
8931
+ const tools = buildMcpTools();
8932
+ const hasContractTool = tools.some((t) => t.name === "runtrim_create_contract");
8933
+ const hasPathTool = tools.some((t) => t.name === "runtrim_check_path");
8934
+ const hasApprovalTool = tools.some((t) => t.name === "runtrim_suggest_approval");
8935
+ const hasFinishTool = tools.some((t) => t.name === "runtrim_finish_guidance");
8936
+ const contract = parseContractSummary(cwd);
8937
+ const lastMcp = readMcpLastUsed(cwd);
8938
+ const setupCorrupt = configExists(cwd) && (!profileReady || !memoryReady || !instructionsReady);
8939
+ const artifactFiles = listArtifactFiles(cwd);
8940
+ const artifactCount = artifactFiles.length;
8941
+ let readiness = "partial";
8942
+ if (!repoCheck.allowed || setupCorrupt) {
8943
+ readiness = "blocked";
8944
+ } else if (profileReady && memoryReady && instructionsReady && anySurfaceInstalled && snippetsGenerated && hasContractTool && hasPathTool && hasApprovalTool && hasFinishTool) {
8945
+ readiness = "ready";
8946
+ }
8947
+ console.log("");
8948
+ console.log(BOLD("RunTrim") + DIM(" doctor"));
8949
+ console.log("");
8950
+ console.log(BOLD("Project"));
8951
+ console.log(chalk.white(`- Project profile: ${profileReady ? "ready" : "missing"}`));
8952
+ console.log(chalk.white(`- Project memory: ${memoryReady ? "ready" : "missing"}`));
8953
+ console.log(chalk.white(`- Agent instructions: ${instructionsReady ? "ready" : "missing"}`));
8954
+ console.log(chalk.white(`- Active contract: ${contract.active ? "active" : "none"}`));
8955
+ console.log("");
8956
+ console.log(BOLD("Agent rules"));
8957
+ console.log(chalk.white(`- CLAUDE.md: ${claudeBlock.exists ? "installed" : "not found"}`));
8958
+ console.log(chalk.white(`- AGENTS.md: ${agentsBlock.exists ? "installed" : "not found"}`));
8959
+ console.log(chalk.white(`- Cursor rules: ${cursorRuleExists ? "installed" : "not found"}`));
8960
+ console.log(chalk.white(`- RunTrim instruction block: ${runtrimBlockCurrent ? "current" : anySurfaceInstalled ? "stale" : "missing"}`));
8961
+ console.log("");
8962
+ console.log(BOLD("MCP"));
8963
+ console.log(chalk.white("- MCP server: available"));
8964
+ console.log(chalk.white(`- Project snippets: ${snippetsGenerated ? "generated" : "missing"}`));
8965
+ console.log(chalk.white(`- Claude Desktop config: ${claudeMcpState}`));
8966
+ console.log(chalk.white(`- Cursor MCP config: ${cursorMcpState}`));
8967
+ console.log(chalk.white(`- Generic config: ${genericReady}`));
8968
+ if (lastMcp.tracked && lastMcp.tool && lastMcp.usedAt) {
8969
+ console.log(chalk.white(`- Last MCP tool call: ${lastMcp.tool}, ${lastMcp.usedAt}`));
8970
+ } else {
8971
+ console.log(chalk.white("- Last MCP tool call: not tracked yet"));
8972
+ }
8973
+ console.log("");
8974
+ console.log(BOLD("Automation readiness"));
8975
+ console.log(chalk.white(`- Contract creation tool: ${hasContractTool ? "available" : "missing"}`));
8976
+ console.log(chalk.white(`- Path check tool: ${hasPathTool ? "available" : "missing"}`));
8977
+ console.log(chalk.white(`- Approval tool: ${hasApprovalTool ? "available" : "missing"}`));
8978
+ console.log(chalk.white(`- Finish guidance tool: ${hasFinishTool ? "available" : "missing"}`));
8979
+ console.log(chalk.white(`- Local artifacts: ${artifactCount}`));
8980
+ console.log("");
8981
+ console.log(BOLD("Readiness"));
8982
+ console.log(chalk.white(`- State: ${readiness}`));
8983
+ console.log("");
8984
+ console.log(BOLD("Next"));
8985
+ if (repoCheck.status === "blocked_repair") {
8986
+ console.log(chalk.yellow("- RunTrim local state needs repair. Run runtrim repo repair."));
8987
+ } else if (repoCheck.status === "blocked_limit") {
8988
+ console.log(chalk.yellow("- Free includes 1 tracked repo. Continue in tracked repo, unlink with runtrim repo unlink --force, or upgrade to Builder."));
8989
+ } else if (readiness === "ready") {
8990
+ if (claudeMcpState !== "configured" && cursorMcpState !== "configured") {
8991
+ console.log(chalk.white("- Ready locally, MCP client not connected."));
8992
+ console.log(chalk.white("- Run runtrim mcp instructions or copy .runtrim/mcp/cursor.json into your MCP client."));
8993
+ } else {
8994
+ console.log(chalk.white("- Your project is RunTrim-aware. Open Cursor/Claude/Codex and use normal language."));
8995
+ }
8996
+ } else {
8997
+ console.log(chalk.white("- Run runtrim start."));
8998
+ console.log(chalk.white("- Run runtrim mcp instructions."));
8999
+ console.log(chalk.white("- Run runtrim mcp config --print."));
9000
+ }
9001
+ if (artifactCount > 25) {
9002
+ console.log(chalk.white(`- Local artifacts: ${artifactCount}. Run runtrim clean --dry-run to review cleanup.`));
9003
+ }
9004
+ console.log("");
9005
+ });
8190
9006
  var PROTOCOL_BLOCK_START = "<!-- RUNTRIM_PROTOCOL_START -->";
8191
9007
  var PROTOCOL_BLOCK_END = "<!-- RUNTRIM_PROTOCOL_END -->";
8192
9008
  var PROTOCOL_POINTER_BLOCK = `
8193
9009
  ${PROTOCOL_BLOCK_START}
8194
9010
  This repo uses RunTrim as the guarded AI coding protocol.
8195
9011
  Before editing code, read RUNTRIM.md.
8196
- Start every task with: runtrim go "<task>"
9012
+ Start every task with: runtrim start
9013
+ Then run: runtrim agent "Your task" --copy
8197
9014
  Stay inside .runtrim/contracts/latest.md.
8198
9015
  After edits, ask the user to run: runtrim finish
8199
9016
  ${PROTOCOL_BLOCK_END}
@@ -8889,6 +9706,8 @@ program.command("execute <task>").description("Create a controlled execution pac
8889
9706
  var agentCommand = program.command("agent").description("Start a guarded AI coding run with contract, scope, memory, and handoff");
8890
9707
  agentCommand.argument("[task]").option("--copy", "Copy the handoff to clipboard").option("--bridge", "Ensure local bridge is running for this agent run").option("--preview", "Generate an execution preview instead of running any agent").option("--apply", "Generate Agent Apply handoff artifacts").option("--execute", "Create a controlled execution packet and handoff").option("--run", "Alias for --execute").option("--dry-run", "Create execution packet in pending mode without ready status").option("--confirm", "Confirm high-risk apply handoff creation").action(async (task, options) => {
8891
9708
  if (task == null ? void 0 : task.trim()) {
9709
+ const allowed = await ensureRepoAllowedForFree(process.cwd());
9710
+ if (!allowed) return;
8892
9711
  const normalizedTask = (task != null ? task : "").trim();
8893
9712
  if (options == null ? void 0 : options.bridge) {
8894
9713
  const bridge = await ensureBridgeRunningForAgent(process.cwd());
@@ -9288,6 +10107,146 @@ agentCommand.command("prompt-mode <mode>").description("Set how guarded contract
9288
10107
  console.log(ACCENT.bold(" Agent prompt mode updated to: " + m));
9289
10108
  console.log("");
9290
10109
  });
10110
+ var ciCommand = program.command("ci").description("RunTrim CI checks for pull requests and merges");
10111
+ 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) => {
10112
+ const cwd = process.cwd();
10113
+ const strict = options.strict === true;
10114
+ const allowWarn = options.allowWarn === true;
10115
+ const outputJson = options.json === true;
10116
+ const report = options.report === true;
10117
+ const repoCheck = await assertFreeRepoAllowed(cwd);
10118
+ const detection = await detectCiChangedFiles(cwd, options.base, options.head);
10119
+ const changedFiles = detection.files;
10120
+ const contract = parseContractSummary(cwd);
10121
+ const latestRun = loadLatestRun(cwd);
10122
+ const hasRuntrimContext = contract.exists || Boolean(latestRun);
10123
+ const issues = [];
10124
+ const warnings = [...detection.warnings];
10125
+ const nextSteps = [];
10126
+ let verdict = "PASS";
10127
+ if (repoCheck.status === "blocked_repair") {
10128
+ issues.push("RunTrim local state needs repair before CI can trust local guard state.");
10129
+ nextSteps.push("Run: runtrim repo repair");
10130
+ verdict = "BLOCKED";
10131
+ } else if (repoCheck.status === "blocked_limit") {
10132
+ issues.push("Free includes 1 tracked repo and this repo is not currently tracked.");
10133
+ nextSteps.push("Continue in tracked repo, unlink with runtrim repo unlink --force, or upgrade to Builder.");
10134
+ verdict = "BLOCKED";
10135
+ }
10136
+ if (changedFiles.length === 0) {
10137
+ warnings.push("No changed files detected for this diff.");
10138
+ }
10139
+ const secretFiles = changedFiles.filter(isSecretLikePath);
10140
+ if (secretFiles.length > 0) {
10141
+ issues.push(`Sensitive file path changed: ${secretFiles[0]}`);
10142
+ nextSteps.push("Remove secret/env/key/cert files from this PR before merge.");
10143
+ verdict = "BLOCKED";
10144
+ }
10145
+ const highRiskFiles = changedFiles.filter((f) => isHighRiskLogicPath(f) && !isSecretLikePath(f));
10146
+ if (highRiskFiles.length > 0) {
10147
+ if (!hasRuntrimContext) {
10148
+ issues.push(`High-risk path changed without RunTrim context: ${highRiskFiles[0]}`);
10149
+ nextSteps.push("Create a dedicated guarded run and finish it before merging.");
10150
+ verdict = "BLOCKED";
10151
+ } else if (contract.allowedPaths.length > 0 && !matchesAnyContractRule(highRiskFiles[0], contract.allowedPaths)) {
10152
+ issues.push(`High-risk path is outside contract allowed scope: ${highRiskFiles[0]}`);
10153
+ nextSteps.push(`Run: runtrim approve "Allow ${highRiskFiles[0]} for this run only"`);
10154
+ verdict = "BLOCKED";
10155
+ }
10156
+ }
10157
+ if (contract.forbiddenPaths.length > 0) {
10158
+ const forbiddenTouched = changedFiles.filter((f) => matchesAnyContractRule(f, contract.forbiddenPaths));
10159
+ if (forbiddenTouched.length > 0) {
10160
+ issues.push(`Contract-forbidden path touched: ${forbiddenTouched[0]}`);
10161
+ nextSteps.push("Split the task or get explicit scoped approval before merge.");
10162
+ verdict = "BLOCKED";
10163
+ }
10164
+ }
10165
+ if (contract.allowedPaths.length > 0) {
10166
+ const outOfScope = changedFiles.filter((f) => !matchesAnyContractRule(f, contract.allowedPaths));
10167
+ if (outOfScope.length > 0) {
10168
+ issues.push(`Changed file is outside latest contract scope: ${outOfScope[0]}`);
10169
+ nextSteps.push(`Run: runtrim approve "Allow ${outOfScope[0]} for this run only"`);
10170
+ verdict = "BLOCKED";
10171
+ }
10172
+ }
10173
+ const docsOnly = changedFiles.length > 0 && changedFiles.every((f) => isDocsLikePath(f));
10174
+ if (!hasRuntrimContext && verdict !== "BLOCKED") {
10175
+ if (docsOnly) {
10176
+ warnings.push("No RunTrim contract/report found. Docs-only change treated as low risk.");
10177
+ verdict = "WARN";
10178
+ } else {
10179
+ warnings.push("No RunTrim contract/report found. CI could not fully verify scope context.");
10180
+ verdict = "WARN";
10181
+ }
10182
+ }
10183
+ if (strict && !hasRuntrimContext && verdict !== "BLOCKED") {
10184
+ issues.push("Strict mode requires RunTrim contract/report context.");
10185
+ verdict = "BLOCKED";
10186
+ }
10187
+ if (verdict === "PASS" && warnings.length > 0) {
10188
+ verdict = "WARN";
10189
+ }
10190
+ if (nextSteps.length === 0) {
10191
+ if (verdict === "PASS") {
10192
+ nextSteps.push("Safe to merge under current RunTrim CI policy.");
10193
+ } else if (verdict === "WARN") {
10194
+ nextSteps.push("Run runtrim finish locally to strengthen verification context.");
10195
+ } else {
10196
+ nextSteps.push("Resolve blocked issues, then rerun runtrim ci check.");
10197
+ }
10198
+ }
10199
+ let exitCode = 0;
10200
+ if (verdict === "BLOCKED") exitCode = 1;
10201
+ if (verdict === "WARN" && strict && !allowWarn) exitCode = 1;
10202
+ const jsonPayload = {
10203
+ verdict: verdict.toLowerCase(),
10204
+ exitCode,
10205
+ changedFiles,
10206
+ issues,
10207
+ warnings,
10208
+ nextSteps
10209
+ };
10210
+ if (outputJson) {
10211
+ process.stdout.write(`${JSON.stringify(jsonPayload, null, 2)}
10212
+ `);
10213
+ process.exit(exitCode);
10214
+ return;
10215
+ }
10216
+ console.log("");
10217
+ console.log(BOLD("RunTrim") + DIM(" CI Check"));
10218
+ console.log("");
10219
+ const verdictColor = verdict === "PASS" ? chalk.green : verdict === "WARN" ? chalk.yellow : chalk.red;
10220
+ console.log(DIM(" Verdict: ") + verdictColor(verdict));
10221
+ console.log("");
10222
+ console.log(DIM(" Changed files:"));
10223
+ if (changedFiles.length === 0) console.log(chalk.white(" - (none detected)"));
10224
+ for (const f of changedFiles.slice(0, 20)) console.log(chalk.white(" - " + f));
10225
+ if (changedFiles.length > 20) console.log(DIM(` ... and ${changedFiles.length - 20} more`));
10226
+ console.log("");
10227
+ if (issues.length > 0) {
10228
+ console.log(DIM(" Issues:"));
10229
+ for (const i of issues) console.log(chalk.red(" - " + i));
10230
+ console.log("");
10231
+ }
10232
+ if (warnings.length > 0) {
10233
+ console.log(DIM(" Warnings:"));
10234
+ for (const w of warnings) console.log(chalk.yellow(" - " + w));
10235
+ console.log("");
10236
+ }
10237
+ console.log(DIM(" Next:"));
10238
+ for (const n of nextSteps) console.log(chalk.white(" - " + n));
10239
+ if (report) {
10240
+ console.log("");
10241
+ console.log(DIM(" Report:"));
10242
+ console.log(chalk.white(` - Base: ${detection.baseUsed}`));
10243
+ console.log(chalk.white(` - Head: ${detection.headUsed}`));
10244
+ console.log(chalk.white(` - Contract: ${contract.exists ? contract.active ? "active" : "present" : "none"}`));
10245
+ console.log(chalk.white(` - Latest run: ${latestRun ? latestRun.status : "none"}`));
10246
+ }
10247
+ console.log("");
10248
+ process.exit(exitCode);
10249
+ });
9291
10250
  var authCommand = program.command("auth").description("Configure sync token for dashboard sync");
9292
10251
  authCommand.command("set <token>").description("Set sync token and enable sync").action((token) => {
9293
10252
  const cwd = process.cwd();
@@ -9357,12 +10316,54 @@ repoCommand.command("status").description("Show local tracked repo status").acti
9357
10316
  console.log(DIM(" Current repo ") + chalk.white(identity.path));
9358
10317
  console.log(DIM(" Tracked repo ") + chalk.white((_b = tracked == null ? void 0 : tracked.path) != null ? _b : "(none)"));
9359
10318
  console.log(DIM(" Allowed ") + chalk.white(check.allowed ? "yes" : "no"));
10319
+ console.log(DIM(" State ") + chalk.white(check.status));
9360
10320
  console.log("");
10321
+ if (check.status === "blocked_repair") {
10322
+ console.log(chalk.yellow(" RunTrim local state needs repair."));
10323
+ console.log(chalk.yellow(" Free includes 1 tracked repo."));
10324
+ console.log(chalk.yellow(" The local repo registry changed unexpectedly."));
10325
+ console.log(DIM(" Run: runtrim repo repair"));
10326
+ console.log("");
10327
+ }
9361
10328
  if (tracked) {
9362
10329
  console.log(DIM(" A tracked repo is one codebase with its own .runtrim workspace."));
9363
10330
  console.log("");
9364
10331
  }
9365
10332
  });
10333
+ 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) => {
10334
+ const cwd = process.cwd();
10335
+ const before = await assertFreeRepoAllowed(cwd);
10336
+ console.log("");
10337
+ console.log(BOLD("RunTrim") + DIM(" repo repair"));
10338
+ console.log("");
10339
+ if (!before.repairRequired) {
10340
+ console.log(DIM(" Local state is healthy. No repair required."));
10341
+ console.log("");
10342
+ return;
10343
+ }
10344
+ if (!options.useCurrent) {
10345
+ console.log(chalk.yellow(" RunTrim local state needs repair."));
10346
+ console.log(chalk.yellow(" Free includes 1 tracked repo."));
10347
+ console.log(chalk.yellow(" Your local repo registry changed unexpectedly."));
10348
+ console.log("");
10349
+ console.log(DIM(" Safe next actions:"));
10350
+ console.log(chalk.white(" - runtrim repo repair --use-current"));
10351
+ console.log(chalk.white(" - runtrim repo unlink --force"));
10352
+ console.log(chalk.white(" - upgrade to Builder for unlimited repos"));
10353
+ console.log(chalk.white(" - sign in to restore cloud entitlements"));
10354
+ console.log("");
10355
+ return;
10356
+ }
10357
+ const result = await repairGlobalRegistry(cwd, { useCurrentRepo: true });
10358
+ if (result.repaired) {
10359
+ console.log(ACCENT.bold(" Local registry repaired."));
10360
+ console.log(DIM(" Current repo is now the tracked Free repo."));
10361
+ console.log("");
10362
+ return;
10363
+ }
10364
+ console.log(DIM(" No repair changes applied."));
10365
+ console.log("");
10366
+ });
9366
10367
  repoCommand.command("unlink").description("Unlink tracked repo from local free-plan registry").option("--force", "Force unlink tracked repo even when running from another path").action(async (options) => {
9367
10368
  const cwd = process.cwd();
9368
10369
  const result = await unlinkCurrentRepo(cwd, Boolean(options.force));
@@ -9387,6 +10388,214 @@ repoCommand.command("unlink").description("Unlink tracked repo from local free-p
9387
10388
  console.log(DIM(" No tracked repo found."));
9388
10389
  console.log("");
9389
10390
  });
10391
+ 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) => {
10392
+ var _a2;
10393
+ const cwd = process.cwd();
10394
+ const dryRun = (options == null ? void 0 : options.dryRun) === true;
10395
+ const keep = Math.max(1, Number.parseInt((_a2 = options == null ? void 0 : options.keep) != null ? _a2 : "10", 10) || 10);
10396
+ ensureInternalArtifactDirs(cwd);
10397
+ const runs = loadAllRuns(cwd);
10398
+ const keepRunIds = new Set(runs.slice(0, keep).map((r) => r.id));
10399
+ const files = listArtifactFiles(cwd);
10400
+ const removable = files.filter((filePath) => {
10401
+ const runId = parseRunIdFromArtifact(filePath);
10402
+ if (!runId) return true;
10403
+ return !keepRunIds.has(runId);
10404
+ });
10405
+ const byCategory = {
10406
+ runs: 0,
10407
+ previews: 0,
10408
+ restores: 0,
10409
+ archives: 0,
10410
+ other: 0
10411
+ };
10412
+ for (const filePath of removable) {
10413
+ const rel = import_path13.default.relative(cwd, filePath).replace(/\\/g, "/").toLowerCase();
10414
+ if (rel.includes(".runtrim/internal/runs/") || rel.includes(".runtrim/runs/")) byCategory.runs += 1;
10415
+ else if (rel.includes(".runtrim/internal/previews/") || rel.includes(".runtrim/previews/")) byCategory.previews += 1;
10416
+ else if (rel.includes(".runtrim/internal/restores/") || rel.includes(".runtrim/restores/")) byCategory.restores += 1;
10417
+ else if (rel.includes("archive")) byCategory.archives += 1;
10418
+ else byCategory.other += 1;
10419
+ }
10420
+ if (!dryRun) {
10421
+ for (const filePath of removable) {
10422
+ try {
10423
+ import_fs13.default.rmSync(filePath, { force: true });
10424
+ } catch (e) {
10425
+ }
10426
+ }
10427
+ }
10428
+ console.log("");
10429
+ console.log(BOLD("RunTrim") + DIM(" clean"));
10430
+ console.log("");
10431
+ console.log(DIM(" Mode ") + chalk.white(dryRun ? "dry-run" : "apply"));
10432
+ console.log(DIM(" Keep latest runs ") + chalk.white(String(keep)));
10433
+ console.log(DIM(" Candidate files ") + chalk.white(String(removable.length)));
10434
+ console.log("");
10435
+ console.log(DIM(" Cleanup summary"));
10436
+ console.log(chalk.white(` - runs: ${byCategory.runs}`));
10437
+ console.log(chalk.white(` - previews: ${byCategory.previews}`));
10438
+ console.log(chalk.white(` - restores: ${byCategory.restores}`));
10439
+ console.log(chalk.white(` - archives: ${byCategory.archives}`));
10440
+ if (byCategory.other > 0) console.log(chalk.white(` - other: ${byCategory.other}`));
10441
+ console.log("");
10442
+ if (removable.length > 0) {
10443
+ const sample = removable.slice(0, 10).map((p) => import_path13.default.relative(cwd, p).replace(/\\/g, "/"));
10444
+ console.log(DIM(" Sample files"));
10445
+ for (const rel of sample) console.log(chalk.white(` - ${rel}`));
10446
+ if (removable.length > sample.length) {
10447
+ console.log(DIM(` ... and ${removable.length - sample.length} more`));
10448
+ }
10449
+ console.log("");
10450
+ }
10451
+ if (dryRun) {
10452
+ console.log(chalk.white(" No files were removed."));
10453
+ console.log(chalk.white(" Re-run without --dry-run to apply cleanup."));
10454
+ } else {
10455
+ console.log(chalk.white(" Cleanup complete."));
10456
+ console.log(chalk.white(" Active contract, latest files, memory current, and MCP snippets were preserved."));
10457
+ }
10458
+ console.log("");
10459
+ });
10460
+ var restoreCommand = program.command("restore").description("Preview or apply local restore for guarded runs");
10461
+ 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) => {
10462
+ var _a2, _b, _c, _d;
10463
+ const cwd = process.cwd();
10464
+ const doPreview = (options == null ? void 0 : options.preview) === true;
10465
+ const doApply = (options == null ? void 0 : options.apply) === true;
10466
+ const force = (options == null ? void 0 : options.force) === true;
10467
+ if (!doPreview && !doApply) {
10468
+ console.log("");
10469
+ console.log(chalk.yellow("Choose --preview or --apply."));
10470
+ console.log("");
10471
+ return;
10472
+ }
10473
+ if (doPreview && doApply) {
10474
+ console.log("");
10475
+ console.log(chalk.yellow("Use either --preview or --apply, not both."));
10476
+ console.log("");
10477
+ return;
10478
+ }
10479
+ const runs = loadAllRuns(cwd);
10480
+ const runIdInput = (runIdArg != null ? runIdArg : "last").trim();
10481
+ let targetRunId = runIdInput;
10482
+ if (runIdInput === "last") {
10483
+ const latest = runs.find((r) => r.status === "completed" || r.status === "guarded" || r.status === "executed" || r.status === "checked");
10484
+ if (!latest) {
10485
+ console.log("");
10486
+ console.log(chalk.yellow("No runs available for restore."));
10487
+ console.log("");
10488
+ return;
10489
+ }
10490
+ targetRunId = latest.id;
10491
+ }
10492
+ const run = runs.find((r) => r.id === targetRunId);
10493
+ if (!run) {
10494
+ console.log("");
10495
+ console.log(chalk.yellow(`Run not found: ${targetRunId}`));
10496
+ console.log("");
10497
+ return;
10498
+ }
10499
+ const restore = loadRestorePoint(cwd, targetRunId);
10500
+ if (!restore) {
10501
+ console.log("");
10502
+ console.log(chalk.yellow("No restore point found for this run."));
10503
+ console.log("");
10504
+ return;
10505
+ }
10506
+ const postFiles = (_b = (_a2 = restore.postRun) == null ? void 0 : _a2.changedFiles) != null ? _b : [];
10507
+ const sensitive = postFiles.filter((f) => isSensitivePath(f) || isSecretLikePath(f));
10508
+ const safeFiles = postFiles.filter((f) => !sensitive.includes(f));
10509
+ const gitAvailable = await isGitRepo(cwd);
10510
+ const method = gitAvailable && restore.preRun.commit ? "git checkout from pre-run commit" : "metadata-only (limited)";
10511
+ const nowChanged = gitAvailable ? dedupeFiles((await getGitChangedFiles(cwd)).map((f) => f.path)) : [];
10512
+ const unrelated = nowChanged.filter((f) => !postFiles.includes(f));
10513
+ if (doPreview) {
10514
+ console.log("");
10515
+ console.log(BOLD("RunTrim") + DIM(" restore preview"));
10516
+ console.log("");
10517
+ console.log(DIM(" Run ID ") + chalk.white(targetRunId));
10518
+ console.log(DIM(" Task ") + chalk.white(truncate(run.task, 80)));
10519
+ console.log(DIM(" Method ") + chalk.white(method));
10520
+ console.log(DIM(" Verdict ") + chalk.white((_d = (_c = restore.postRun) == null ? void 0 : _c.finishVerdict) != null ? _d : "unknown"));
10521
+ console.log("");
10522
+ console.log(DIM(" Files to restore"));
10523
+ if (safeFiles.length === 0) console.log(chalk.white(" - (none)"));
10524
+ for (const f of safeFiles.slice(0, 20)) console.log(chalk.white(" - " + f));
10525
+ if (safeFiles.length > 20) console.log(DIM(` ... and ${safeFiles.length - 20} more`));
10526
+ if (sensitive.length > 0) {
10527
+ console.log("");
10528
+ console.log(DIM(" Sensitive files (listed by path only)"));
10529
+ for (const f of sensitive.slice(0, 20)) console.log(chalk.yellow(" - " + f));
10530
+ console.log(chalk.yellow(" Sensitive files are skipped by default and not auto-restored."));
10531
+ }
10532
+ if (unrelated.length > 0) {
10533
+ console.log("");
10534
+ console.log(chalk.yellow(" Warning: repo has new changes after this run."));
10535
+ console.log(chalk.yellow(" Use --apply --force or review manually before restore."));
10536
+ }
10537
+ console.log("");
10538
+ return;
10539
+ }
10540
+ if (!doApply) return;
10541
+ if (!gitAvailable || !restore.preRun.commit) {
10542
+ console.log("");
10543
+ console.log(chalk.red("Restore apply requires git and a pre-run commit restore point."));
10544
+ console.log("");
10545
+ process.exit(1);
10546
+ return;
10547
+ }
10548
+ if (unrelated.length > 0 && !force) {
10549
+ console.log("");
10550
+ console.log(chalk.red("Restore blocked: new unrelated changes detected after this run."));
10551
+ console.log(chalk.red("Re-run with --apply --force after manual review."));
10552
+ console.log("");
10553
+ process.exit(1);
10554
+ return;
10555
+ }
10556
+ const restored = [];
10557
+ const skippedSensitive = [...sensitive];
10558
+ const failed = [];
10559
+ for (const file of safeFiles) {
10560
+ try {
10561
+ let existedBefore = false;
10562
+ try {
10563
+ await (0, import_execa3.execa)("git", ["cat-file", "-e", `${restore.preRun.commit}:${file}`], { cwd });
10564
+ existedBefore = true;
10565
+ } catch (e) {
10566
+ existedBefore = false;
10567
+ }
10568
+ if (existedBefore) {
10569
+ await (0, import_execa3.execa)("git", ["checkout", restore.preRun.commit, "--", file], { cwd });
10570
+ } else if (import_fs13.default.existsSync(import_path13.default.join(cwd, file))) {
10571
+ import_fs13.default.rmSync(import_path13.default.join(cwd, file), { force: true });
10572
+ }
10573
+ restored.push(file);
10574
+ } catch (e) {
10575
+ failed.push(file);
10576
+ }
10577
+ }
10578
+ const report = {
10579
+ runId: targetRunId,
10580
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
10581
+ restored,
10582
+ skippedSensitive,
10583
+ failed,
10584
+ forced: force
10585
+ };
10586
+ const reportPath = import_path13.default.join(getRestoresDir(cwd), `${targetRunId}.report.${Date.now()}.json`);
10587
+ import_fs13.default.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
10588
+ console.log("");
10589
+ console.log(BOLD("RunTrim") + DIM(" restore apply"));
10590
+ console.log("");
10591
+ console.log(DIM(" Run ID ") + chalk.white(targetRunId));
10592
+ console.log(DIM(" Restored files ") + chalk.white(String(restored.length)));
10593
+ console.log(DIM(" Skipped sensitive ") + chalk.white(String(skippedSensitive.length)));
10594
+ console.log(DIM(" Failed ") + chalk.white(String(failed.length)));
10595
+ console.log(DIM(" Report ") + chalk.white(import_path13.default.relative(cwd, reportPath)));
10596
+ console.log("");
10597
+ if (failed.length > 0) process.exit(1);
10598
+ });
9390
10599
  program.command("guard <task>").description("Audit a task and generate a guarded run contract").action(async (task) => {
9391
10600
  var _a2, _b, _c;
9392
10601
  const cwd = process.cwd();
@@ -9474,7 +10683,7 @@ program.command("guard <task>").description("Audit a task and generate a guarded
9474
10683
  const run2 = saveRun(task, audit, contract, cwd);
9475
10684
  updateRun(run2.id, { status: "blocked" }, cwd);
9476
10685
  console.log("");
9477
- console.log(DIM(" Run saved: .runtrim/runs/" + run2.id + ".json (status: blocked)"));
10686
+ console.log(DIM(" Run saved: .runtrim/internal/runs/" + run2.id + ".json (status: blocked)"));
9478
10687
  console.log("");
9479
10688
  console.log(DIM(" Next: ") + chalk.white('runtrim guard "<one system at a time>"'));
9480
10689
  console.log("");
@@ -9554,7 +10763,7 @@ program.command("guard <task>").description("Audit a task and generate a guarded
9554
10763
  }
9555
10764
  const run = saveRun(task, audit, contract, cwd);
9556
10765
  console.log("");
9557
- console.log(DIM(" Run saved: .runtrim/runs/" + run.id + ".json"));
10766
+ console.log(DIM(" Run saved: .runtrim/internal/runs/" + run.id + ".json"));
9558
10767
  console.log("");
9559
10768
  console.log(DIM(" After your agent run: ") + chalk.white("runtrim check"));
9560
10769
  console.log("");
@@ -9604,6 +10813,7 @@ program.command("run <task>").description("Guard then run configured local agent
9604
10813
  },
9605
10814
  cwd
9606
10815
  );
10816
+ await captureRestorePoint(cwd, run.id, task);
9607
10817
  const copySavings = estimateSavingsFromTokens2(
9608
10818
  parseEstimatedNumber3(String(contract.estimatedTokensTrimmed))
9609
10819
  );
@@ -9684,6 +10894,7 @@ program.command("run <task>").description("Guard then run configured local agent
9684
10894
  },
9685
10895
  cwd
9686
10896
  );
10897
+ await captureRestorePoint(cwd, run.id, task);
9687
10898
  console.log(DIM(" Execution cancelled. Guarded contract copied to clipboard."));
9688
10899
  console.log("");
9689
10900
  return;
@@ -9740,6 +10951,7 @@ program.command("run <task>").description("Guard then run configured local agent
9740
10951
  },
9741
10952
  cwd
9742
10953
  );
10954
+ await captureRestorePoint(cwd, run.id, task);
9743
10955
  console.log("");
9744
10956
  console.log(chalk.yellow(" Agent command not found. Falling back to copy mode guidance."));
9745
10957
  console.log(DIM(" Configure with: npm run runtrim -- agent set claude|codex|custom"));
@@ -9773,6 +10985,7 @@ program.command("run <task>").description("Guard then run configured local agent
9773
10985
  },
9774
10986
  cwd
9775
10987
  );
10988
+ await captureRestorePoint(cwd, run.id, task);
9776
10989
  console.log("");
9777
10990
  console.log(chalk.yellow(" Agent command not found. Falling back to copy mode guidance."));
9778
10991
  console.log(DIM(" Configure with: npm run runtrim -- agent set claude|codex|custom"));
@@ -9816,7 +11029,7 @@ ${stderr}`, "utf-8");
9816
11029
  exitCode,
9817
11030
  stdoutPreview: stdout.slice(0, OUTPUT_PREVIEW_MAX),
9818
11031
  stderrPreview: stderr.slice(0, OUTPUT_PREVIEW_MAX),
9819
- outputPath: `.runtrim/runs/${run.id}.output.txt`
11032
+ outputPath: `.runtrim/internal/runs/${run.id}.output.txt`
9820
11033
  },
9821
11034
  evaluation
9822
11035
  },
@@ -9966,6 +11179,7 @@ program.command("go <task>").description("Bridge Mode: generate a scoped contrac
9966
11179
  memoryUsed,
9967
11180
  providerRouting
9968
11181
  }, cwd);
11182
+ await captureRestorePoint(cwd, run.id, task);
9969
11183
  const bridgeCtx = deriveBridgeContext(task, contract, runs, projectName);
9970
11184
  let bridgeWritten = [];
9971
11185
  let bridgeManagedPaths = [];
@@ -10024,7 +11238,7 @@ program.command("go <task>").description("Bridge Mode: generate a scoped contrac
10024
11238
  if (contract.contract.stopRules.length > 0) {
10025
11239
  console.log(DIM(" Stop rule ") + chalk.white(truncate((_g = contract.contract.stopRules[contract.contract.stopRules.length - 1]) != null ? _g : "", 60)));
10026
11240
  }
10027
- console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
11241
+ console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
10028
11242
  console.log(DIM(" Contract ") + chalk.white("created"));
10029
11243
  console.log("");
10030
11244
  if (bridgeWritten.length > 0) {
@@ -10529,7 +11743,7 @@ program.command("continue").description("Create a safe continuation prompt from
10529
11743
  const audit = (_a2 = loadProjectAudit(cwd)) != null ? _a2 : performBaselineProjectAudit(cwd, null);
10530
11744
  const reason = normalizeContinuationReason(options.reason);
10531
11745
  const agent = normalizeContinuationAgent(options.agent, config.defaultAgent);
10532
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
11746
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
10533
11747
  const continuationPath = resolveContinuationPath(cwd);
10534
11748
  const latestPromptPath = resolvePromptPath(config, cwd);
10535
11749
  const latestPrompt = import_fs13.default.existsSync(latestPromptPath) ? import_fs13.default.readFileSync(latestPromptPath, "utf-8").trim() : "";
@@ -10719,14 +11933,14 @@ program.command("continue").description("Create a safe continuation prompt from
10719
11933
  import_fs13.default.writeFileSync(continuationPath, continuationPrompt, "utf-8");
10720
11934
  const copied = await copyToClipboardSafe(continuationPrompt);
10721
11935
  if (memory) {
10722
- const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso);
11936
+ const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso2);
10723
11937
  import_fs13.default.writeFileSync(import_path13.default.join(getConfigDir(cwd), "memory.md"), memoryWithContinuation, "utf-8");
10724
11938
  }
10725
11939
  if (hasConfig) {
10726
11940
  const nextConfig = __spreadProps(__spreadValues({}, config), {
10727
11941
  lastContinuationReason: reason,
10728
11942
  continuationPromptPath: continuationPath.replace(/\\/g, "/"),
10729
- continuationCreatedAt: nowIso
11943
+ continuationCreatedAt: nowIso2
10730
11944
  });
10731
11945
  saveConfig(nextConfig, cwd);
10732
11946
  }
@@ -11379,6 +12593,18 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
11379
12593
  const blockedByExistingHard = scope.forbiddenFiles.length > 0 || scope.status === "limit_exceeded";
11380
12594
  const warnBySensitiveIgnored = sensitiveIgnored.length > 0;
11381
12595
  const finishVerdict = blockedBySensitive || blockedByContract || blockedByExistingHard ? "BLOCKED" : warnBySensitiveIgnored || scopeDriftStatus !== "passed" || evaluation.status === "needs_verification" || evaluation.status === "partial" ? "WARN" : "PASS";
12596
+ const restoreRecord = loadRestorePoint(cwd, activeRun.id);
12597
+ if (restoreRecord) {
12598
+ restoreRecord.postRun = {
12599
+ changedFiles,
12600
+ forbiddenFiles: [...scope.forbiddenFiles, ...forbiddenPathFiles],
12601
+ sensitiveFiles: scope.sensitiveFiles,
12602
+ outOfScopeFiles: [...outOfContractFiles, ...scope.outOfScopeFiles],
12603
+ finishVerdict,
12604
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
12605
+ };
12606
+ saveRestorePoint(cwd, restoreRecord);
12607
+ }
11382
12608
  const verdictColor = finishVerdict === "PASS" ? chalk.green : finishVerdict === "WARN" ? chalk.yellow : chalk.red;
11383
12609
  const scopeColor = scopeDriftStatus === "passed" ? chalk.green : scopeDriftStatus === "forbidden_touched" ? chalk.red : chalk.yellow;
11384
12610
  const riskAfter = (_m = activeRun.contract.wasteRiskAfter) != null ? _m : "medium";
@@ -11518,6 +12744,8 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
11518
12744
  program.command("sync").description("Sync local run history and project memory to your RunTrim dashboard").option("--dry-run", "Show what would be synced without uploading").action(async (opts) => {
11519
12745
  var _a2, _b;
11520
12746
  const cwd = process.cwd();
12747
+ const allowed = await ensureRepoAllowedForFree(cwd);
12748
+ if (!allowed) return;
11521
12749
  console.log("");
11522
12750
  console.log(BOLD("RunTrim") + DIM(" cloud sync"));
11523
12751
  console.log("");