runtrim 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -97,8 +97,45 @@ function getConfigPath(cwd = process.cwd()) {
97
97
  return path.join(getConfigDir(cwd), "config.json");
98
98
  }
99
99
  function getRunsDir(cwd = process.cwd()) {
100
+ return path.join(getInternalDir(cwd), "runs");
101
+ }
102
+ function getLegacyRunsDir(cwd = process.cwd()) {
100
103
  return path.join(getConfigDir(cwd), "runs");
101
104
  }
105
+ function getInternalDir(cwd = process.cwd()) {
106
+ return path.join(getConfigDir(cwd), "internal");
107
+ }
108
+ function getPreviewsDir(cwd = process.cwd()) {
109
+ return path.join(getInternalDir(cwd), "previews");
110
+ }
111
+ function getLegacyPreviewsDir(cwd = process.cwd()) {
112
+ return path.join(getConfigDir(cwd), "previews");
113
+ }
114
+ function getRestoresDir(cwd = process.cwd()) {
115
+ return path.join(getInternalDir(cwd), "restores");
116
+ }
117
+ function getLegacyRestoresDir(cwd = process.cwd()) {
118
+ return path.join(getConfigDir(cwd), "restores");
119
+ }
120
+ function getContractsArchiveDir(cwd = process.cwd()) {
121
+ return path.join(getInternalDir(cwd), "contracts-archive");
122
+ }
123
+ function getAgentArchiveDir(cwd = process.cwd()) {
124
+ return path.join(getInternalDir(cwd), "agent-archive");
125
+ }
126
+ function ensureInternalArtifactDirs(cwd = process.cwd()) {
127
+ const dirs = [
128
+ getInternalDir(cwd),
129
+ getRunsDir(cwd),
130
+ getPreviewsDir(cwd),
131
+ getRestoresDir(cwd),
132
+ getContractsArchiveDir(cwd),
133
+ getAgentArchiveDir(cwd)
134
+ ];
135
+ for (const dir of dirs) {
136
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
137
+ }
138
+ }
102
139
  function configExists(cwd = process.cwd()) {
103
140
  return fs.existsSync(getConfigPath(cwd));
104
141
  }
@@ -1552,35 +1589,38 @@ function saveRun(task, audit, contract, cwd = process.cwd()) {
1552
1589
  return record;
1553
1590
  }
1554
1591
  function loadLatestRun(cwd = process.cwd()) {
1555
- const runsDir = getRunsDir(cwd);
1556
- if (!fs3.existsSync(runsDir)) return null;
1557
- const files = fs3.readdirSync(runsDir).filter((f) => f.endsWith(".json")).map((f) => ({
1558
- name: f,
1559
- time: fs3.statSync(path3.join(runsDir, f)).mtime.getTime()
1560
- })).sort((a, b) => b.time - a.time);
1592
+ const candidateDirs = [getRunsDir(cwd), getLegacyRunsDir(cwd)].filter((dir, idx, arr) => arr.indexOf(dir) === idx);
1593
+ const files = candidateDirs.filter((dir) => fs3.existsSync(dir)).flatMap(
1594
+ (dir) => fs3.readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => ({
1595
+ dir,
1596
+ name: f,
1597
+ time: fs3.statSync(path3.join(dir, f)).mtime.getTime()
1598
+ }))
1599
+ ).sort((a, b) => b.time - a.time);
1561
1600
  if (files.length === 0) return null;
1562
1601
  try {
1563
1602
  return JSON.parse(
1564
- fs3.readFileSync(path3.join(runsDir, files[0].name), "utf-8")
1603
+ fs3.readFileSync(path3.join(files[0].dir, files[0].name), "utf-8")
1565
1604
  );
1566
1605
  } catch (e) {
1567
1606
  return null;
1568
1607
  }
1569
1608
  }
1570
1609
  function updateRun(runId, updates, cwd = process.cwd()) {
1571
- const filePath = path3.join(getRunsDir(cwd), `${runId}.json`);
1610
+ const preferredPath = path3.join(getRunsDir(cwd), `${runId}.json`);
1611
+ const legacyPath = path3.join(getLegacyRunsDir(cwd), `${runId}.json`);
1612
+ const filePath = fs3.existsSync(preferredPath) ? preferredPath : legacyPath;
1572
1613
  if (!fs3.existsSync(filePath)) return;
1573
1614
  const existing = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
1574
1615
  fs3.writeFileSync(filePath, JSON.stringify(__spreadValues(__spreadValues({}, existing), updates), null, 2));
1575
1616
  }
1576
1617
  function loadAllRuns(cwd = process.cwd()) {
1577
- const runsDir = getRunsDir(cwd);
1578
- if (!fs3.existsSync(runsDir)) return [];
1579
- return fs3.readdirSync(runsDir).filter((f) => f.endsWith(".json")).map((f) => {
1618
+ const candidateDirs = [getRunsDir(cwd), getLegacyRunsDir(cwd)].filter((dir, idx, arr) => arr.indexOf(dir) === idx);
1619
+ const files = candidateDirs.filter((dir) => fs3.existsSync(dir)).flatMap((dir) => fs3.readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => path3.join(dir, f)));
1620
+ const deduped = [...new Set(files)];
1621
+ return deduped.map((filePath) => {
1580
1622
  try {
1581
- return JSON.parse(
1582
- fs3.readFileSync(path3.join(runsDir, f), "utf-8")
1583
- );
1623
+ return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
1584
1624
  } catch (e) {
1585
1625
  return null;
1586
1626
  }
@@ -2773,7 +2813,7 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2773
2813
  "6. After editing, tell the user to run: `runtrim finish`",
2774
2814
  "",
2775
2815
  "---",
2776
- `Protocol: runtrim init. Updated: ${(/* @__PURE__ */ new Date()).toISOString()}`
2816
+ `Protocol: runtrim start. Updated: ${(/* @__PURE__ */ new Date()).toISOString()}`
2777
2817
  ];
2778
2818
  fs7.writeFileSync(path7.join(cwd, "RUNTRIM.md"), lines.join("\n"), "utf-8");
2779
2819
  }
@@ -2829,7 +2869,7 @@ function archiveContract(cwd, runId) {
2829
2869
  if (!fs7.existsSync(latestPath)) return;
2830
2870
  const content = fs7.readFileSync(latestPath, "utf-8");
2831
2871
  if (content.includes("Status: none")) return;
2832
- const archiveDir = path7.join(contractsDir, "archive");
2872
+ const archiveDir = getContractsArchiveDir(cwd);
2833
2873
  if (!fs7.existsSync(archiveDir)) fs7.mkdirSync(archiveDir, { recursive: true });
2834
2874
  fs7.writeFileSync(path7.join(archiveDir, `${runId}.md`), content, "utf-8");
2835
2875
  }
@@ -4159,9 +4199,15 @@ async function getPanelState(cwd, monitorMode) {
4159
4199
  let runs = [];
4160
4200
  let latest = null;
4161
4201
  let registry = {
4162
- version: 1,
4202
+ version: 2,
4203
+ stateVersion: 2,
4163
4204
  plan: "free",
4205
+ machineInstallId: "",
4206
+ createdAt: "",
4207
+ updatedAt: "",
4164
4208
  trackedRepos: [],
4209
+ lastKnownRepo: null,
4210
+ integrity: { algorithm: "sha256-local-seal-v1", seal: "" },
4165
4211
  telemetry: {
4166
4212
  enabled: false,
4167
4213
  anonymousId: ""
@@ -4203,9 +4249,15 @@ async function getPanelState(cwd, monitorMode) {
4203
4249
  } catch (e) {
4204
4250
  warnings.push("global_registry_failed");
4205
4251
  registry = {
4206
- version: 1,
4252
+ version: 2,
4253
+ stateVersion: 2,
4207
4254
  plan: "free",
4255
+ machineInstallId: "",
4256
+ createdAt: "",
4257
+ updatedAt: "",
4208
4258
  trackedRepos: [],
4259
+ lastKnownRepo: null,
4260
+ integrity: { algorithm: "sha256-local-seal-v1", seal: "" },
4209
4261
  telemetry: {
4210
4262
  enabled: false,
4211
4263
  anonymousId: ""
@@ -4673,7 +4725,7 @@ Before editing code:
4673
4725
  If no active RunTrim contract exists:
4674
4726
  - Do not edit code without one.
4675
4727
  - Ask the user to start a guarded run:
4676
- runtrim go "<task>"
4728
+ runtrim agent "<task>" --copy
4677
4729
 
4678
4730
  If the task requires leaving the current scope:
4679
4731
  - Stop.
@@ -4769,7 +4821,7 @@ ${BASE_PROTOCOL}
4769
4821
  Before using any tool or executing any command:
4770
4822
  1. Confirm a RunTrim contract is active at .runtrim/contracts/latest.md.
4771
4823
  2. If no active contract exists, do not proceed. Ask for:
4772
- runtrim go "<task>"
4824
+ runtrim agent "<task>" --copy
4773
4825
  3. Do not call shell commands, write files, or read env vars outside the contract.
4774
4826
  `.trim(),
4775
4827
  custom: `
@@ -4822,7 +4874,7 @@ function getCursorMdcContent() {
4822
4874
  "## If no active contract",
4823
4875
  "",
4824
4876
  "Ask the user to start a guarded run:",
4825
- '`runtrim go "<task>"`',
4877
+ '`runtrim agent "<task>" --copy`',
4826
4878
  "",
4827
4879
  "Any agent. One run boundary."
4828
4880
  ].join("\n");
@@ -5084,7 +5136,7 @@ Before editing code:
5084
5136
  - If the task touches auth, billing, payments, webhooks, database, middleware,
5085
5137
  env vars, secrets, or subscriptions, stop and require an active RunTrim contract.
5086
5138
  Ask the user to run:
5087
- runtrim go "<task>"
5139
+ runtrim agent "<task>" --copy
5088
5140
  - For low-risk work (UI polish, copy, docs, isolated component styling):
5089
5141
  Fast Path is allowed if no unfinished changes exist.
5090
5142
  Keep the change minimal.
@@ -5107,7 +5159,7 @@ No active RunTrim contract means no code edits.
5107
5159
  If no active contract exists at .runtrim/contracts/latest.md:
5108
5160
  - Do not edit any file.
5109
5161
  - Ask the user to start a guarded run:
5110
- runtrim go "<task>"
5162
+ runtrim agent "<task>" --copy
5111
5163
 
5112
5164
  After every editing session:
5113
5165
  - Ask the user to run:
@@ -5121,7 +5173,7 @@ Fast Path is allowed for low and medium risk work.
5121
5173
 
5122
5174
  Critical systems (auth, billing, payments, webhooks, database, middleware,
5123
5175
  env vars, secrets, subscriptions) still require a RunTrim contract:
5124
- runtrim go "<task>"
5176
+ runtrim agent "<task>" --copy
5125
5177
 
5126
5178
  After any edits:
5127
5179
  - runtrim finish is required before continuing to another task.
@@ -5132,7 +5184,7 @@ RunTrim Auto-guard: Off
5132
5184
 
5133
5185
  Auto-guard is disabled for this project.
5134
5186
  RunTrim can still be used manually:
5135
- runtrim go "<task>"
5187
+ runtrim agent "<task>" --copy
5136
5188
  runtrim finish
5137
5189
  `.trim();
5138
5190
  }
@@ -5158,7 +5210,7 @@ function saveFastRunRecord(cwd, changedFiles, risk) {
5158
5210
  reportParts.push(`${sensitive.length} sensitive path${sensitive.length === 1 ? "" : "s"} touched.`);
5159
5211
  }
5160
5212
  reportParts.push("No pre-run contract was captured for this run.");
5161
- const nextSafeAction = changedFiles.length > 0 ? 'Create a contract before the next change: runtrim go "<task>"' : 'Start a guarded run: runtrim go "<task>"';
5213
+ const nextSafeAction = changedFiles.length > 0 ? 'Create a contract before the next change: runtrim agent "<task>" --copy' : 'Start a guarded run: runtrim agent "<task>" --copy';
5162
5214
  const summary = {
5163
5215
  id,
5164
5216
  task,
@@ -5678,7 +5730,7 @@ function recommendProviderRouting(ctx) {
5678
5730
  } else if (route === "split-required") {
5679
5731
  routingReason = "This spans multiple critical systems, so RunTrim should split into audit, implementation, and verification.";
5680
5732
  }
5681
- let nextCommand = `runtrim go "${ctx.task}"`;
5733
+ let nextCommand = `runtrim agent "${ctx.task}" --copy`;
5682
5734
  if (route === "split-required") {
5683
5735
  nextCommand = "split into:\n1. audit only\n2. implementation only\n3. verification only";
5684
5736
  } else if (route === "preview-only") {
@@ -5700,6 +5752,7 @@ var _a;
5700
5752
  var oraFactory = typeof ora === "function" ? ora : (_a = ora.default) != null ? _a : ora;
5701
5753
  var ACCENT = chalk.hex("#C8901A");
5702
5754
  var GO_ACCENT = chalk.hex("#8B7CFF");
5755
+ var RUNTRIM_AGENT_INSTRUCTIONS_VERSION = "2";
5703
5756
  var DIM = chalk.gray;
5704
5757
  var BOLD = chalk.white.bold;
5705
5758
  var program = new Command();
@@ -5835,6 +5888,98 @@ async function copyToClipboardSafe(value) {
5835
5888
  function dedupeFiles(files) {
5836
5889
  return [...new Set(files.filter(Boolean).map((f) => f.replace(/\\/g, "/")))];
5837
5890
  }
5891
+ var RUNTRIM_GITIGNORE_BLOCK_START = "# BEGIN RUNTRIM_ARTIFACTS";
5892
+ var RUNTRIM_GITIGNORE_BLOCK_END = "# END RUNTRIM_ARTIFACTS";
5893
+ function ensureRuntrimReadme(cwd) {
5894
+ const readmePath = path13.join(getConfigDir(cwd), "README.md");
5895
+ const content = [
5896
+ "# RunTrim Local Files",
5897
+ "",
5898
+ "RunTrim stores local metadata in this folder.",
5899
+ "",
5900
+ "Human-facing files:",
5901
+ "- `agent/instructions.md` and `agent/latest.md`",
5902
+ "- `contracts/latest.md`",
5903
+ "- `memory/current.md` and `memory/baseline.md`",
5904
+ "- `mcp/*.json`",
5905
+ "- `config.json`",
5906
+ "",
5907
+ "Internal artifacts:",
5908
+ "- `.runtrim/internal/runs/`",
5909
+ "- `.runtrim/internal/previews/`",
5910
+ "- `.runtrim/internal/restores/`",
5911
+ "- `.runtrim/internal/contracts-archive/`",
5912
+ "- `.runtrim/internal/agent-archive/`",
5913
+ "",
5914
+ "Notes:",
5915
+ "- Artifacts are local-first.",
5916
+ "- Source code is not uploaded by local storage.",
5917
+ "- Restore metadata is path-only and does not store secret contents."
5918
+ ].join("\n");
5919
+ fs13.writeFileSync(readmePath, content + "\n", "utf-8");
5920
+ }
5921
+ function ensureRuntrimGitignoreGuidance(cwd) {
5922
+ const gitignorePath = path13.join(cwd, ".gitignore");
5923
+ if (!fs13.existsSync(gitignorePath)) return;
5924
+ const desired = [
5925
+ RUNTRIM_GITIGNORE_BLOCK_START,
5926
+ "# RunTrim local artifacts",
5927
+ ".runtrim/internal/",
5928
+ ".runtrim/runs/",
5929
+ ".runtrim/previews/",
5930
+ ".runtrim/restores/",
5931
+ ".runtrim/contracts/archive/",
5932
+ ".runtrim/agent/*.json",
5933
+ RUNTRIM_GITIGNORE_BLOCK_END
5934
+ ].join("\n");
5935
+ const current = fs13.readFileSync(gitignorePath, "utf-8");
5936
+ const start = current.indexOf(RUNTRIM_GITIGNORE_BLOCK_START);
5937
+ const end = current.indexOf(RUNTRIM_GITIGNORE_BLOCK_END);
5938
+ if (start !== -1 && end !== -1 && end > start) {
5939
+ const next = current.slice(0, start).trimEnd() + "\n\n" + desired + "\n" + current.slice(end + RUNTRIM_GITIGNORE_BLOCK_END.length).replace(/^\n+/, "\n");
5940
+ if (next !== current) fs13.writeFileSync(gitignorePath, next, "utf-8");
5941
+ return;
5942
+ }
5943
+ if (!current.includes(".runtrim/internal/")) {
5944
+ fs13.writeFileSync(gitignorePath, current.trimEnd() + "\n\n" + desired + "\n", "utf-8");
5945
+ }
5946
+ }
5947
+ function listFilesIfExists(dir) {
5948
+ if (!fs13.existsSync(dir)) return [];
5949
+ return fs13.readdirSync(dir).map((name) => path13.join(dir, name)).filter((p) => fs13.existsSync(p) && fs13.statSync(p).isFile());
5950
+ }
5951
+ function listArtifactFiles(cwd) {
5952
+ const dirs = [
5953
+ getRunsDir(cwd),
5954
+ getPreviewsDir(cwd),
5955
+ getRestoresDir(cwd),
5956
+ getContractsArchiveDir(cwd),
5957
+ path13.join(getConfigDir(cwd), "internal", "agent-archive"),
5958
+ getLegacyRunsDir(cwd),
5959
+ getLegacyPreviewsDir(cwd),
5960
+ getLegacyRestoresDir(cwd),
5961
+ path13.join(getConfigDir(cwd), "contracts", "archive"),
5962
+ path13.join(getConfigDir(cwd), "agent")
5963
+ ];
5964
+ const files = dirs.flatMap((dir) => listFilesIfExists(dir));
5965
+ return files.filter((filePath) => {
5966
+ const base = path13.basename(filePath).toLowerCase();
5967
+ const rel = path13.relative(cwd, filePath).replace(/\\/g, "/");
5968
+ if (rel === ".runtrim/previews/latest.md") return false;
5969
+ if (base === "latest.md" || base === "instructions.md" || base === "current.md" || base === "baseline.md") return false;
5970
+ return true;
5971
+ });
5972
+ }
5973
+ function parseRunIdFromArtifact(filePath) {
5974
+ const base = path13.basename(filePath);
5975
+ const direct = base.match(/^([a-zA-Z0-9_-]{6,})\.json$/);
5976
+ if (direct) return direct[1];
5977
+ const report = base.match(/^([a-zA-Z0-9_-]{6,})\.report\.\d+\.json$/);
5978
+ if (report) return report[1];
5979
+ const out = base.match(/^([a-zA-Z0-9_-]{6,})\.output\.txt$/);
5980
+ if (out) return out[1];
5981
+ return null;
5982
+ }
5838
5983
  function normalizeContractPathPattern(pattern) {
5839
5984
  let p = pattern.trim().replace(/\\/g, "/");
5840
5985
  if (!p || p === "-" || p.toLowerCase() === "none") return "";
@@ -6004,10 +6149,11 @@ function buildRecommendedNextCommand(task, approval, filesToInspect) {
6004
6149
  return `runtrim go "${task}"`;
6005
6150
  }
6006
6151
  function writePreviewArtifacts(cwd, preview) {
6007
- const previewsDir = path13.join(cwd, ".runtrim", "previews");
6152
+ const previewsDir = getPreviewsDir(cwd);
6008
6153
  if (!fs13.existsSync(previewsDir)) fs13.mkdirSync(previewsDir, { recursive: true });
6009
6154
  const jsonPath = path13.join(previewsDir, `${preview.id}.json`);
6010
- const markdownPath = path13.join(previewsDir, "latest.md");
6155
+ const markdownPath = path13.join(getLegacyPreviewsDir(cwd), "latest.md");
6156
+ if (!fs13.existsSync(path13.dirname(markdownPath))) fs13.mkdirSync(path13.dirname(markdownPath), { recursive: true });
6011
6157
  fs13.writeFileSync(jsonPath, JSON.stringify(preview, null, 2), "utf-8");
6012
6158
  const lines = [
6013
6159
  "RunTrim Agent Preview",
@@ -6046,7 +6192,7 @@ function writePreviewArtifacts(cwd, preview) {
6046
6192
  "Next:",
6047
6193
  preview.recommendedNextCommand,
6048
6194
  "",
6049
- `Preview JSON: .runtrim/previews/${preview.id}.json`
6195
+ `Preview JSON: .runtrim/internal/previews/${preview.id}.json`
6050
6196
  ];
6051
6197
  fs13.writeFileSync(markdownPath, lines.join("\n"), "utf-8");
6052
6198
  return { jsonPath, markdownPath };
@@ -6428,6 +6574,7 @@ async function runAgentApply(task, mode) {
6428
6574
  const copied = mode.copy ? await copyToClipboardSafe(fs13.readFileSync(handoff.markdownPath, "utf-8")) : false;
6429
6575
  const run = saveRun(task, previewResult.audit, previewResult.contract, cwd);
6430
6576
  updateRun(run.id, { status: "guarded" }, cwd);
6577
+ await captureRestorePoint(cwd, run.id, task);
6431
6578
  const riskColor = (_a2 = { low: chalk.green, medium: chalk.yellow, high: chalk.hex("#FF8C00"), critical: chalk.red }[risk]) != null ? _a2 : chalk.white;
6432
6579
  if (mode.primary) {
6433
6580
  console.log("");
@@ -6614,6 +6761,7 @@ async function runControlledExecution(task, mode) {
6614
6761
  controlledExecutionId: id,
6615
6762
  controlledExecutionStatus: status
6616
6763
  }, cwd);
6764
+ await captureRestorePoint(cwd, run.id, task);
6617
6765
  }
6618
6766
  if (status === "blocked") {
6619
6767
  console.log("");
@@ -6904,14 +7052,18 @@ ${block}
6904
7052
  function buildRuntrimAgentBlockContent() {
6905
7053
  return [
6906
7054
  "RunTrim project instructions",
7055
+ `RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`,
6907
7056
  "- Before editing, read RunTrim project memory and profile context.",
6908
- "- Verify active contract first. If MCP is available and no contract is active, call runtrim_create_contract with the user's task.",
7057
+ "- Before editing, create or verify an active RunTrim contract for the current user task.",
7058
+ "- If MCP is available and no active contract exists, call runtrim_create_contract with the user's task.",
6909
7059
  '- If MCP is unavailable, ask the user to run: runtrim agent "task" --copy',
6910
- "- Stay inside active contract scope.",
6911
- "- Before high-risk edits (auth, billing, middleware/proxy, migrations, sensitive files, broad app-wide changes), check scope first. If MCP is available, call runtrim_check_path.",
7060
+ "- Before touching risky paths (auth, billing, middleware/proxy, migrations, env/sensitive files, broad app-wide refactors), call runtrim_check_path when available.",
7061
+ "- Use RunTrim project memory and stay inside active contract scope.",
7062
+ "- Do not silently expand scope. Preserve existing project style and logic.",
6912
7063
  "- Never read or print env file contents.",
6913
- '- Do not continue outside scope silently. Request: runtrim approve "Allow <path/scope> for this run only"',
6914
- "- At the end, run or ask for runtrim finish. Do not claim completion before finish verification.",
7064
+ '- If scope must expand, stop and request approval: runtrim approve "Allow <path/scope> for this run only"',
7065
+ "- At the end, run runtrim finish if terminal execution is available, otherwise ask the user to run runtrim finish.",
7066
+ "- Do not claim completion before finish verification when RunTrim is active.",
6915
7067
  "",
6916
7068
  "RunTrim MCP:",
6917
7069
  "- runtrim mcp instructions",
@@ -7071,12 +7223,16 @@ function updateStartAgentInstructions(cwd) {
7071
7223
  const existing = fs13.existsSync(instructionsPath) ? fs13.readFileSync(instructionsPath, "utf-8") : "";
7072
7224
  const body = [
7073
7225
  "RunTrim agent instructions:",
7226
+ `RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`,
7074
7227
  "- Use RunTrim project memory and profile before editing.",
7075
- "- Verify active contract before edits. If MCP is available and contract is missing, call runtrim_create_contract with the user's task.",
7228
+ "- Create or verify an active RunTrim contract before edits.",
7229
+ "- If MCP is available and contract is missing, call runtrim_create_contract with the user's task.",
7076
7230
  '- If MCP is unavailable, ask user to run: runtrim agent "task" --copy',
7077
- "- For high-risk paths (auth, billing, middleware/proxy, migrations, sensitive files, broad app-wide changes), check scope first. If MCP is available, call runtrim_check_path.",
7231
+ "- For high-risk paths (auth, billing, middleware/proxy, migrations, env/sensitive files, broad app-wide changes), check scope first. If MCP is available, call runtrim_check_path.",
7232
+ "- Stay inside active contract scope and preserve existing project style and logic.",
7078
7233
  '- If scope must expand, request: runtrim approve "Allow <path/scope> for this run only"',
7079
- "- Run finish verification at the end. Do not claim completion before runtrim finish.",
7234
+ "- Run runtrim finish when terminal execution is available, otherwise ask the user to run runtrim finish.",
7235
+ "- Do not claim completion before runtrim finish verification when RunTrim is active.",
7080
7236
  "- Never read or print env file contents.",
7081
7237
  "",
7082
7238
  "RunTrim MCP:",
@@ -7149,6 +7305,202 @@ function detectKnownMcpConfigPresence() {
7149
7305
  cursorConfigFound: Boolean(cursorMatch)
7150
7306
  };
7151
7307
  }
7308
+ function getRestorePointPath(cwd, runId) {
7309
+ return path13.join(getRestoresDir(cwd), `${runId}.json`);
7310
+ }
7311
+ function getLegacyRestorePointPath(cwd, runId) {
7312
+ return path13.join(getLegacyRestoresDir(cwd), `${runId}.json`);
7313
+ }
7314
+ async function isGitRepo(cwd) {
7315
+ try {
7316
+ await execa3("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
7317
+ return true;
7318
+ } catch (e) {
7319
+ return false;
7320
+ }
7321
+ }
7322
+ async function captureRestorePoint(cwd, runId, task) {
7323
+ const dir = getRestoresDir(cwd);
7324
+ if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
7325
+ const existingPath = getRestorePointPath(cwd, runId);
7326
+ if (fs13.existsSync(existingPath) || fs13.existsSync(getLegacyRestorePointPath(cwd, runId))) return;
7327
+ let commit = null;
7328
+ let changedBeforeRun = [];
7329
+ if (await isGitRepo(cwd)) {
7330
+ try {
7331
+ const { stdout } = await execa3("git", ["rev-parse", "HEAD"], { cwd });
7332
+ commit = stdout.trim() || null;
7333
+ } catch (e) {
7334
+ commit = null;
7335
+ }
7336
+ try {
7337
+ const changed = await getGitChangedFiles(cwd);
7338
+ changedBeforeRun = dedupeFiles(changed.map((c) => c.path));
7339
+ } catch (e) {
7340
+ changedBeforeRun = [];
7341
+ }
7342
+ }
7343
+ const record = {
7344
+ runId,
7345
+ task,
7346
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7347
+ preRun: {
7348
+ commit,
7349
+ dirty: changedBeforeRun.length > 0,
7350
+ changedBeforeRun
7351
+ }
7352
+ };
7353
+ fs13.writeFileSync(existingPath, JSON.stringify(record, null, 2), "utf-8");
7354
+ }
7355
+ function loadRestorePoint(cwd, runId) {
7356
+ const preferred = getRestorePointPath(cwd, runId);
7357
+ const legacy = getLegacyRestorePointPath(cwd, runId);
7358
+ const p = fs13.existsSync(preferred) ? preferred : legacy;
7359
+ if (!fs13.existsSync(p)) return null;
7360
+ try {
7361
+ return JSON.parse(fs13.readFileSync(p, "utf-8"));
7362
+ } catch (e) {
7363
+ return null;
7364
+ }
7365
+ }
7366
+ function saveRestorePoint(cwd, record) {
7367
+ const dir = getRestoresDir(cwd);
7368
+ if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
7369
+ fs13.writeFileSync(getRestorePointPath(cwd, record.runId), JSON.stringify(record, null, 2), "utf-8");
7370
+ }
7371
+ function isSecretLikePath(file) {
7372
+ const n = file.replace(/\\/g, "/").toLowerCase();
7373
+ return n.includes(".env") || n.endsWith(".env") || n.endsWith(".pem") || n.endsWith(".key") || n.includes("id_rsa") || n.includes("id_ed25519") || n.includes("private-key");
7374
+ }
7375
+ function isDocsLikePath(file) {
7376
+ const n = file.replace(/\\/g, "/").toLowerCase();
7377
+ return n === "readme.md" || n.startsWith("docs/") || n.endsWith(".md") || n.endsWith(".mdx") || n.includes("changelog");
7378
+ }
7379
+ function isUiCheckoutPath(file) {
7380
+ const n = file.replace(/\\/g, "/").toLowerCase();
7381
+ return n.includes("/checkout/") && !n.includes("/api/") && !n.includes("webhook") && !n.includes("stripe") && !n.includes("dodo") && !n.includes("provider") && !n.includes("session");
7382
+ }
7383
+ function isHighRiskLogicPath(file) {
7384
+ const n = file.replace(/\\/g, "/").toLowerCase();
7385
+ if (isSecretLikePath(n)) return true;
7386
+ if (n.endsWith("middleware.ts") || n.endsWith("middleware.js") || n.endsWith("proxy.ts") || n.endsWith("proxy.js")) return true;
7387
+ if (n.includes("supabase/migrations") || n.includes("prisma/migrations") || n.includes("/migrations/")) return true;
7388
+ if (n.includes("/auth/") || n.includes("session") || n.includes("jwt")) return true;
7389
+ if (n.includes("webhook")) return true;
7390
+ if ((n.includes("billing") || n.includes("payment") || n.includes("stripe") || n.includes("dodo")) && (n.includes("/api/") || n.includes("/lib/") || n.includes("route.ts") || n.includes("route.js") || n.includes("server"))) return true;
7391
+ if (n.includes("/checkout/") && !isUiCheckoutPath(n) && (n.includes("/api/") || n.includes("provider") || n.includes("session") || n.includes("route.ts") || n.includes("route.js"))) return true;
7392
+ return false;
7393
+ }
7394
+ function matchesAnyContractRule(file, rules) {
7395
+ return rules.some((rule) => matchesContractPattern(file, rule));
7396
+ }
7397
+ async function detectCiChangedFiles(cwd, base, head) {
7398
+ var _a2;
7399
+ const warnings = [];
7400
+ let baseUsed = (base == null ? void 0 : base.trim()) || "";
7401
+ let headUsed = (head == null ? void 0 : head.trim()) || "";
7402
+ if (!headUsed) {
7403
+ try {
7404
+ const { stdout } = await execa3("git", ["rev-parse", "HEAD"], { cwd });
7405
+ headUsed = stdout.trim();
7406
+ } catch (e) {
7407
+ headUsed = "HEAD";
7408
+ }
7409
+ }
7410
+ if (!baseUsed) {
7411
+ const ghBase = (_a2 = process.env.GITHUB_BASE_REF) == null ? void 0 : _a2.trim();
7412
+ if (ghBase) {
7413
+ baseUsed = `origin/${ghBase}`;
7414
+ }
7415
+ }
7416
+ if (!baseUsed) {
7417
+ for (const candidate of ["origin/main", "origin/master", "main", "master", "HEAD~1"]) {
7418
+ try {
7419
+ await execa3("git", ["rev-parse", "--verify", candidate], { cwd });
7420
+ baseUsed = candidate;
7421
+ break;
7422
+ } catch (e) {
7423
+ continue;
7424
+ }
7425
+ }
7426
+ }
7427
+ if (!baseUsed) {
7428
+ warnings.push("Could not infer a base ref. Using working tree changes only.");
7429
+ }
7430
+ let diffFiles = [];
7431
+ if (baseUsed) {
7432
+ try {
7433
+ const { stdout } = await execa3("git", ["diff", "--name-only", `${baseUsed}...${headUsed}`], { cwd });
7434
+ diffFiles = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
7435
+ } catch (e) {
7436
+ warnings.push(`Could not diff ${baseUsed}...${headUsed}. Falling back to local changes.`);
7437
+ }
7438
+ }
7439
+ if (diffFiles.length === 0) {
7440
+ try {
7441
+ const { stdout } = await execa3("git", ["diff", "--name-only"], { cwd });
7442
+ diffFiles = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
7443
+ } catch (e) {
7444
+ }
7445
+ }
7446
+ try {
7447
+ const { stdout } = await execa3("git", ["status", "--porcelain"], { cwd });
7448
+ const untracked = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("?? ")).map((line) => line.slice(3).trim());
7449
+ diffFiles = dedupeFiles([...diffFiles, ...untracked]);
7450
+ } catch (e) {
7451
+ }
7452
+ return {
7453
+ files: dedupeFiles(diffFiles.map((f) => f.replace(/\\/g, "/"))),
7454
+ baseUsed: baseUsed || "(local)",
7455
+ headUsed,
7456
+ warnings
7457
+ };
7458
+ }
7459
+ function detectMcpConfigState(configPath, found) {
7460
+ if (!found || !configPath) return "not found";
7461
+ try {
7462
+ const raw = JSON.parse(fs13.readFileSync(configPath, "utf-8"));
7463
+ const servers = raw.mcpServers && typeof raw.mcpServers === "object" ? raw.mcpServers : {};
7464
+ return servers.runtrim ? "configured" : "missing runtrim";
7465
+ } catch (e) {
7466
+ return "missing runtrim";
7467
+ }
7468
+ }
7469
+ function hasCurrentRuntrimBlock(filePath) {
7470
+ if (!fs13.existsSync(filePath)) return { exists: false, current: false };
7471
+ const content = fs13.readFileSync(filePath, "utf-8");
7472
+ const hasBlock = content.includes("<!-- RUNTRIM:START -->") && content.includes("<!-- RUNTRIM:END -->");
7473
+ const hasVersion = content.includes(`RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`);
7474
+ return { exists: hasBlock, current: hasBlock && hasVersion };
7475
+ }
7476
+ function readMcpLastUsed(cwd) {
7477
+ const p = path13.join(getProjectMcpDir(cwd), "last-used.json");
7478
+ if (!fs13.existsSync(p)) return { tracked: false, tool: null, usedAt: null };
7479
+ try {
7480
+ const parsed = JSON.parse(fs13.readFileSync(p, "utf-8"));
7481
+ return {
7482
+ tracked: true,
7483
+ tool: typeof parsed.tool === "string" ? parsed.tool : null,
7484
+ usedAt: typeof parsed.usedAt === "string" ? parsed.usedAt : null
7485
+ };
7486
+ } catch (e) {
7487
+ return { tracked: false, tool: null, usedAt: null };
7488
+ }
7489
+ }
7490
+ function writeMcpLastUsed(cwd, tool) {
7491
+ try {
7492
+ const dir = getProjectMcpDir(cwd);
7493
+ if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
7494
+ const payload = {
7495
+ tool,
7496
+ usedAt: (/* @__PURE__ */ new Date()).toISOString(),
7497
+ projectPath: cwd
7498
+ };
7499
+ fs13.writeFileSync(path13.join(dir, "last-used.json"), `${JSON.stringify(payload, null, 2)}
7500
+ `, "utf-8");
7501
+ } catch (e) {
7502
+ }
7503
+ }
7152
7504
  function appendContractAmendment(cwd, approvalText) {
7153
7505
  const p = path13.join(cwd, ".runtrim", "contracts", "latest.md");
7154
7506
  if (!fs13.existsSync(p)) return { ok: false, reason: "missing_contract" };
@@ -7521,6 +7873,7 @@ async function buildRuntrimCreateContractMcp(cwd, args) {
7521
7873
  const handoff = writeAgentHandoffArtifacts(cwd, apply, path13.relative(cwd, previewPath));
7522
7874
  const run = saveRun(mergedTask, previewResult.audit, previewResult.contract, cwd);
7523
7875
  updateRun(run.id, { status: "guarded" }, cwd);
7876
+ await captureRestorePoint(cwd, run.id, mergedTask);
7524
7877
  const payload = {
7525
7878
  contract_created: true,
7526
7879
  task: taskRaw,
@@ -7715,6 +8068,7 @@ async function startMcpServerStdio(cwd) {
7715
8068
  });
7716
8069
  return;
7717
8070
  }
8071
+ writeMcpLastUsed(cwd, name);
7718
8072
  send({
7719
8073
  jsonrpc: "2.0",
7720
8074
  id,
@@ -8202,6 +8556,7 @@ async function initializeRunTrim(cwd, options = {}) {
8202
8556
  const runsDir = getRunsDir(cwd);
8203
8557
  if (!fs13.existsSync(configDir)) fs13.mkdirSync(configDir, { recursive: true });
8204
8558
  if (!fs13.existsSync(runsDir)) fs13.mkdirSync(runsDir, { recursive: true });
8559
+ ensureInternalArtifactDirs(cwd);
8205
8560
  const existingConfig = hadConfig ? loadConfig(cwd) : null;
8206
8561
  const baseConfig = __spreadValues(__spreadValues({}, DEFAULT_CONFIG), detectProjectInfo(cwd));
8207
8562
  const nextConfig = options.refresh && existingConfig ? __spreadValues({}, existingConfig) : baseConfig;
@@ -8221,13 +8576,8 @@ async function initializeRunTrim(cwd, options = {}) {
8221
8576
  fs13.writeFileSync(memoryPath, buildBaselineMemoryMarkdown(baseline), "utf-8");
8222
8577
  }
8223
8578
  ensureStarterPromptIfMissing(cwd);
8224
- const gitignorePath = path13.join(cwd, ".gitignore");
8225
- if (fs13.existsSync(gitignorePath)) {
8226
- const content = fs13.readFileSync(gitignorePath, "utf-8");
8227
- if (!content.includes(".runtrim/runs")) {
8228
- fs13.appendFileSync(gitignorePath, "\n# RunTrim run history\n.runtrim/runs/\n");
8229
- }
8230
- }
8579
+ ensureRuntrimReadme(cwd);
8580
+ ensureRuntrimGitignoreGuidance(cwd);
8231
8581
  return { ok: true };
8232
8582
  }
8233
8583
  async function runPrepareTask(task, options) {
@@ -8262,7 +8612,7 @@ async function runPrepareTask(task, options) {
8262
8612
  console.log("");
8263
8613
  console.log(DIM(" Task ") + chalk.white(truncate(task, 70)));
8264
8614
  console.log(DIM(" Prompt ") + chalk.white(promptPath2));
8265
- console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
8615
+ console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
8266
8616
  console.log("");
8267
8617
  printPrepareAgentInstructions(selectedAgent, config.lastPromptPath);
8268
8618
  console.log("");
@@ -8282,6 +8632,7 @@ async function runPrepareTask(task, options) {
8282
8632
  const promptPath = writeLatestPromptFile(contract.contractText, config, cwd);
8283
8633
  if (options.copy !== false) await copyToClipboardSafe(contract.contractText);
8284
8634
  updateRun(run.id, { status: "guarded" }, cwd);
8635
+ await captureRestorePoint(cwd, run.id, task);
8285
8636
  const riskColors = {
8286
8637
  low: chalk.green,
8287
8638
  medium: chalk.yellow,
@@ -8304,7 +8655,7 @@ async function runPrepareTask(task, options) {
8304
8655
  );
8305
8656
  console.log(DIM(" Reduction ") + chalk.white(contract.riskReductionPercent + "%"));
8306
8657
  console.log(DIM(" Prompt ") + chalk.white(promptPath));
8307
- console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
8658
+ console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
8308
8659
  console.log("");
8309
8660
  printPrepareAgentInstructions(selectedAgent, config.lastPromptPath);
8310
8661
  console.log("");
@@ -8529,6 +8880,108 @@ program.command("start").description("Guided RunTrim onboarding and daily loop")
8529
8880
  console.log("");
8530
8881
  }
8531
8882
  });
8883
+ program.command("doctor").description("Check project readiness for RunTrim agent auto-control").action(async () => {
8884
+ const cwd = process.cwd();
8885
+ const repoCheck = await assertFreeRepoAllowed(cwd);
8886
+ const profilePath = path13.join(getConfigDir(cwd), "project-profile.json");
8887
+ const memoryPath = path13.join(getConfigDir(cwd), "memory", "current.md");
8888
+ const instructionsPath = path13.join(getConfigDir(cwd), "agent", "instructions.md");
8889
+ const snippetsDir = getProjectMcpDir(cwd);
8890
+ const snippetFiles = [
8891
+ path13.join(snippetsDir, "claude-desktop.json"),
8892
+ path13.join(snippetsDir, "cursor.json"),
8893
+ path13.join(snippetsDir, "generic.json")
8894
+ ];
8895
+ const profileReady = fs13.existsSync(profilePath);
8896
+ const memoryReady = fs13.existsSync(memoryPath) && fs13.readFileSync(memoryPath, "utf-8").trim().length > 0;
8897
+ const instructionsReady = fs13.existsSync(instructionsPath) && fs13.readFileSync(instructionsPath, "utf-8").trim().length > 0;
8898
+ const claudeBlock = hasCurrentRuntrimBlock(path13.join(cwd, "CLAUDE.md"));
8899
+ const agentsBlock = hasCurrentRuntrimBlock(path13.join(cwd, "AGENTS.md"));
8900
+ const cursorRulePath = path13.join(cwd, ".cursor", "rules", "runtrim.mdc");
8901
+ const cursorRuleExists = fs13.existsSync(cursorRulePath);
8902
+ const cursorRuleCurrent = cursorRuleExists ? fs13.readFileSync(cursorRulePath, "utf-8").includes(`RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`) : false;
8903
+ const anySurfaceInstalled = claudeBlock.exists || agentsBlock.exists || cursorRuleExists;
8904
+ const runtrimBlockCurrent = [claudeBlock.current, agentsBlock.current, cursorRuleCurrent].some(Boolean);
8905
+ const snippetsGenerated = snippetFiles.every((p) => fs13.existsSync(p));
8906
+ const knownMcp = detectKnownMcpConfigPresence();
8907
+ const claudeMcpState = detectMcpConfigState(knownMcp.claudeConfigPath, knownMcp.claudeConfigFound);
8908
+ const cursorMcpState = detectMcpConfigState(knownMcp.cursorConfigPath, knownMcp.cursorConfigFound);
8909
+ const genericReady = snippetsGenerated ? "ready" : "missing";
8910
+ const tools = buildMcpTools();
8911
+ const hasContractTool = tools.some((t) => t.name === "runtrim_create_contract");
8912
+ const hasPathTool = tools.some((t) => t.name === "runtrim_check_path");
8913
+ const hasApprovalTool = tools.some((t) => t.name === "runtrim_suggest_approval");
8914
+ const hasFinishTool = tools.some((t) => t.name === "runtrim_finish_guidance");
8915
+ const contract = parseContractSummary(cwd);
8916
+ const lastMcp = readMcpLastUsed(cwd);
8917
+ const setupCorrupt = configExists(cwd) && (!profileReady || !memoryReady || !instructionsReady);
8918
+ const artifactFiles = listArtifactFiles(cwd);
8919
+ const artifactCount = artifactFiles.length;
8920
+ let readiness = "partial";
8921
+ if (!repoCheck.allowed || setupCorrupt) {
8922
+ readiness = "blocked";
8923
+ } else if (profileReady && memoryReady && instructionsReady && anySurfaceInstalled && snippetsGenerated && hasContractTool && hasPathTool && hasApprovalTool && hasFinishTool) {
8924
+ readiness = "ready";
8925
+ }
8926
+ console.log("");
8927
+ console.log(BOLD("RunTrim") + DIM(" doctor"));
8928
+ console.log("");
8929
+ console.log(BOLD("Project"));
8930
+ console.log(chalk.white(`- Project profile: ${profileReady ? "ready" : "missing"}`));
8931
+ console.log(chalk.white(`- Project memory: ${memoryReady ? "ready" : "missing"}`));
8932
+ console.log(chalk.white(`- Agent instructions: ${instructionsReady ? "ready" : "missing"}`));
8933
+ console.log(chalk.white(`- Active contract: ${contract.active ? "active" : "none"}`));
8934
+ console.log("");
8935
+ console.log(BOLD("Agent rules"));
8936
+ console.log(chalk.white(`- CLAUDE.md: ${claudeBlock.exists ? "installed" : "not found"}`));
8937
+ console.log(chalk.white(`- AGENTS.md: ${agentsBlock.exists ? "installed" : "not found"}`));
8938
+ console.log(chalk.white(`- Cursor rules: ${cursorRuleExists ? "installed" : "not found"}`));
8939
+ console.log(chalk.white(`- RunTrim instruction block: ${runtrimBlockCurrent ? "current" : anySurfaceInstalled ? "stale" : "missing"}`));
8940
+ console.log("");
8941
+ console.log(BOLD("MCP"));
8942
+ console.log(chalk.white("- MCP server: available"));
8943
+ console.log(chalk.white(`- Project snippets: ${snippetsGenerated ? "generated" : "missing"}`));
8944
+ console.log(chalk.white(`- Claude Desktop config: ${claudeMcpState}`));
8945
+ console.log(chalk.white(`- Cursor MCP config: ${cursorMcpState}`));
8946
+ console.log(chalk.white(`- Generic config: ${genericReady}`));
8947
+ if (lastMcp.tracked && lastMcp.tool && lastMcp.usedAt) {
8948
+ console.log(chalk.white(`- Last MCP tool call: ${lastMcp.tool}, ${lastMcp.usedAt}`));
8949
+ } else {
8950
+ console.log(chalk.white("- Last MCP tool call: not tracked yet"));
8951
+ }
8952
+ console.log("");
8953
+ console.log(BOLD("Automation readiness"));
8954
+ console.log(chalk.white(`- Contract creation tool: ${hasContractTool ? "available" : "missing"}`));
8955
+ console.log(chalk.white(`- Path check tool: ${hasPathTool ? "available" : "missing"}`));
8956
+ console.log(chalk.white(`- Approval tool: ${hasApprovalTool ? "available" : "missing"}`));
8957
+ console.log(chalk.white(`- Finish guidance tool: ${hasFinishTool ? "available" : "missing"}`));
8958
+ console.log(chalk.white(`- Local artifacts: ${artifactCount}`));
8959
+ console.log("");
8960
+ console.log(BOLD("Readiness"));
8961
+ console.log(chalk.white(`- State: ${readiness}`));
8962
+ console.log("");
8963
+ console.log(BOLD("Next"));
8964
+ if (repoCheck.status === "blocked_repair") {
8965
+ console.log(chalk.yellow("- RunTrim local state needs repair. Run runtrim repo repair."));
8966
+ } else if (repoCheck.status === "blocked_limit") {
8967
+ console.log(chalk.yellow("- Free includes 1 tracked repo. Continue in tracked repo, unlink with runtrim repo unlink --force, or upgrade to Builder."));
8968
+ } else if (readiness === "ready") {
8969
+ if (claudeMcpState !== "configured" && cursorMcpState !== "configured") {
8970
+ console.log(chalk.white("- Ready locally, MCP client not connected."));
8971
+ console.log(chalk.white("- Run runtrim mcp instructions or copy .runtrim/mcp/cursor.json into your MCP client."));
8972
+ } else {
8973
+ console.log(chalk.white("- Your project is RunTrim-aware. Open Cursor/Claude/Codex and use normal language."));
8974
+ }
8975
+ } else {
8976
+ console.log(chalk.white("- Run runtrim start."));
8977
+ console.log(chalk.white("- Run runtrim mcp instructions."));
8978
+ console.log(chalk.white("- Run runtrim mcp config --print."));
8979
+ }
8980
+ if (artifactCount > 25) {
8981
+ console.log(chalk.white(`- Local artifacts: ${artifactCount}. Run runtrim clean --dry-run to review cleanup.`));
8982
+ }
8983
+ console.log("");
8984
+ });
8532
8985
  var PROTOCOL_BLOCK_START = "<!-- RUNTRIM_PROTOCOL_START -->";
8533
8986
  var PROTOCOL_BLOCK_END = "<!-- RUNTRIM_PROTOCOL_END -->";
8534
8987
  var PROTOCOL_POINTER_BLOCK = `
@@ -9633,6 +10086,146 @@ agentCommand.command("prompt-mode <mode>").description("Set how guarded contract
9633
10086
  console.log(ACCENT.bold(" Agent prompt mode updated to: " + m));
9634
10087
  console.log("");
9635
10088
  });
10089
+ var ciCommand = program.command("ci").description("RunTrim CI checks for pull requests and merges");
10090
+ ciCommand.command("check").description("Evaluate changed files and RunTrim context for CI-safe PASS/WARN/BLOCKED verdict").option("--base <ref>", "Base ref to diff from").option("--head <ref>", "Head ref to diff to").option("--strict", "Treat WARN as failing and require RunTrim context").option("--allow-warn", "Allow WARN even when --strict is set").option("--json", "Print machine-readable JSON output").option("--report", "Include extra diagnostic context in output").action(async (options) => {
10091
+ const cwd = process.cwd();
10092
+ const strict = options.strict === true;
10093
+ const allowWarn = options.allowWarn === true;
10094
+ const outputJson = options.json === true;
10095
+ const report = options.report === true;
10096
+ const repoCheck = await assertFreeRepoAllowed(cwd);
10097
+ const detection = await detectCiChangedFiles(cwd, options.base, options.head);
10098
+ const changedFiles = detection.files;
10099
+ const contract = parseContractSummary(cwd);
10100
+ const latestRun = loadLatestRun(cwd);
10101
+ const hasRuntrimContext = contract.exists || Boolean(latestRun);
10102
+ const issues = [];
10103
+ const warnings = [...detection.warnings];
10104
+ const nextSteps = [];
10105
+ let verdict = "PASS";
10106
+ if (repoCheck.status === "blocked_repair") {
10107
+ issues.push("RunTrim local state needs repair before CI can trust local guard state.");
10108
+ nextSteps.push("Run: runtrim repo repair");
10109
+ verdict = "BLOCKED";
10110
+ } else if (repoCheck.status === "blocked_limit") {
10111
+ issues.push("Free includes 1 tracked repo and this repo is not currently tracked.");
10112
+ nextSteps.push("Continue in tracked repo, unlink with runtrim repo unlink --force, or upgrade to Builder.");
10113
+ verdict = "BLOCKED";
10114
+ }
10115
+ if (changedFiles.length === 0) {
10116
+ warnings.push("No changed files detected for this diff.");
10117
+ }
10118
+ const secretFiles = changedFiles.filter(isSecretLikePath);
10119
+ if (secretFiles.length > 0) {
10120
+ issues.push(`Sensitive file path changed: ${secretFiles[0]}`);
10121
+ nextSteps.push("Remove secret/env/key/cert files from this PR before merge.");
10122
+ verdict = "BLOCKED";
10123
+ }
10124
+ const highRiskFiles = changedFiles.filter((f) => isHighRiskLogicPath(f) && !isSecretLikePath(f));
10125
+ if (highRiskFiles.length > 0) {
10126
+ if (!hasRuntrimContext) {
10127
+ issues.push(`High-risk path changed without RunTrim context: ${highRiskFiles[0]}`);
10128
+ nextSteps.push("Create a dedicated guarded run and finish it before merging.");
10129
+ verdict = "BLOCKED";
10130
+ } else if (contract.allowedPaths.length > 0 && !matchesAnyContractRule(highRiskFiles[0], contract.allowedPaths)) {
10131
+ issues.push(`High-risk path is outside contract allowed scope: ${highRiskFiles[0]}`);
10132
+ nextSteps.push(`Run: runtrim approve "Allow ${highRiskFiles[0]} for this run only"`);
10133
+ verdict = "BLOCKED";
10134
+ }
10135
+ }
10136
+ if (contract.forbiddenPaths.length > 0) {
10137
+ const forbiddenTouched = changedFiles.filter((f) => matchesAnyContractRule(f, contract.forbiddenPaths));
10138
+ if (forbiddenTouched.length > 0) {
10139
+ issues.push(`Contract-forbidden path touched: ${forbiddenTouched[0]}`);
10140
+ nextSteps.push("Split the task or get explicit scoped approval before merge.");
10141
+ verdict = "BLOCKED";
10142
+ }
10143
+ }
10144
+ if (contract.allowedPaths.length > 0) {
10145
+ const outOfScope = changedFiles.filter((f) => !matchesAnyContractRule(f, contract.allowedPaths));
10146
+ if (outOfScope.length > 0) {
10147
+ issues.push(`Changed file is outside latest contract scope: ${outOfScope[0]}`);
10148
+ nextSteps.push(`Run: runtrim approve "Allow ${outOfScope[0]} for this run only"`);
10149
+ verdict = "BLOCKED";
10150
+ }
10151
+ }
10152
+ const docsOnly = changedFiles.length > 0 && changedFiles.every((f) => isDocsLikePath(f));
10153
+ if (!hasRuntrimContext && verdict !== "BLOCKED") {
10154
+ if (docsOnly) {
10155
+ warnings.push("No RunTrim contract/report found. Docs-only change treated as low risk.");
10156
+ verdict = "WARN";
10157
+ } else {
10158
+ warnings.push("No RunTrim contract/report found. CI could not fully verify scope context.");
10159
+ verdict = "WARN";
10160
+ }
10161
+ }
10162
+ if (strict && !hasRuntrimContext && verdict !== "BLOCKED") {
10163
+ issues.push("Strict mode requires RunTrim contract/report context.");
10164
+ verdict = "BLOCKED";
10165
+ }
10166
+ if (verdict === "PASS" && warnings.length > 0) {
10167
+ verdict = "WARN";
10168
+ }
10169
+ if (nextSteps.length === 0) {
10170
+ if (verdict === "PASS") {
10171
+ nextSteps.push("Safe to merge under current RunTrim CI policy.");
10172
+ } else if (verdict === "WARN") {
10173
+ nextSteps.push("Run runtrim finish locally to strengthen verification context.");
10174
+ } else {
10175
+ nextSteps.push("Resolve blocked issues, then rerun runtrim ci check.");
10176
+ }
10177
+ }
10178
+ let exitCode = 0;
10179
+ if (verdict === "BLOCKED") exitCode = 1;
10180
+ if (verdict === "WARN" && strict && !allowWarn) exitCode = 1;
10181
+ const jsonPayload = {
10182
+ verdict: verdict.toLowerCase(),
10183
+ exitCode,
10184
+ changedFiles,
10185
+ issues,
10186
+ warnings,
10187
+ nextSteps
10188
+ };
10189
+ if (outputJson) {
10190
+ process.stdout.write(`${JSON.stringify(jsonPayload, null, 2)}
10191
+ `);
10192
+ process.exit(exitCode);
10193
+ return;
10194
+ }
10195
+ console.log("");
10196
+ console.log(BOLD("RunTrim") + DIM(" CI Check"));
10197
+ console.log("");
10198
+ const verdictColor = verdict === "PASS" ? chalk.green : verdict === "WARN" ? chalk.yellow : chalk.red;
10199
+ console.log(DIM(" Verdict: ") + verdictColor(verdict));
10200
+ console.log("");
10201
+ console.log(DIM(" Changed files:"));
10202
+ if (changedFiles.length === 0) console.log(chalk.white(" - (none detected)"));
10203
+ for (const f of changedFiles.slice(0, 20)) console.log(chalk.white(" - " + f));
10204
+ if (changedFiles.length > 20) console.log(DIM(` ... and ${changedFiles.length - 20} more`));
10205
+ console.log("");
10206
+ if (issues.length > 0) {
10207
+ console.log(DIM(" Issues:"));
10208
+ for (const i of issues) console.log(chalk.red(" - " + i));
10209
+ console.log("");
10210
+ }
10211
+ if (warnings.length > 0) {
10212
+ console.log(DIM(" Warnings:"));
10213
+ for (const w of warnings) console.log(chalk.yellow(" - " + w));
10214
+ console.log("");
10215
+ }
10216
+ console.log(DIM(" Next:"));
10217
+ for (const n of nextSteps) console.log(chalk.white(" - " + n));
10218
+ if (report) {
10219
+ console.log("");
10220
+ console.log(DIM(" Report:"));
10221
+ console.log(chalk.white(` - Base: ${detection.baseUsed}`));
10222
+ console.log(chalk.white(` - Head: ${detection.headUsed}`));
10223
+ console.log(chalk.white(` - Contract: ${contract.exists ? contract.active ? "active" : "present" : "none"}`));
10224
+ console.log(chalk.white(` - Latest run: ${latestRun ? latestRun.status : "none"}`));
10225
+ }
10226
+ console.log("");
10227
+ process.exit(exitCode);
10228
+ });
9636
10229
  var authCommand = program.command("auth").description("Configure sync token for dashboard sync");
9637
10230
  authCommand.command("set <token>").description("Set sync token and enable sync").action((token) => {
9638
10231
  const cwd = process.cwd();
@@ -9774,6 +10367,214 @@ repoCommand.command("unlink").description("Unlink tracked repo from local free-p
9774
10367
  console.log(DIM(" No tracked repo found."));
9775
10368
  console.log("");
9776
10369
  });
10370
+ program.command("clean").description("Clean old local RunTrim artifacts while preserving active project state").option("--dry-run", "Preview files that would be removed").option("--keep <n>", "Number of latest run-linked artifacts to keep", "10").action(async (options) => {
10371
+ var _a2;
10372
+ const cwd = process.cwd();
10373
+ const dryRun = (options == null ? void 0 : options.dryRun) === true;
10374
+ const keep = Math.max(1, Number.parseInt((_a2 = options == null ? void 0 : options.keep) != null ? _a2 : "10", 10) || 10);
10375
+ ensureInternalArtifactDirs(cwd);
10376
+ const runs = loadAllRuns(cwd);
10377
+ const keepRunIds = new Set(runs.slice(0, keep).map((r) => r.id));
10378
+ const files = listArtifactFiles(cwd);
10379
+ const removable = files.filter((filePath) => {
10380
+ const runId = parseRunIdFromArtifact(filePath);
10381
+ if (!runId) return true;
10382
+ return !keepRunIds.has(runId);
10383
+ });
10384
+ const byCategory = {
10385
+ runs: 0,
10386
+ previews: 0,
10387
+ restores: 0,
10388
+ archives: 0,
10389
+ other: 0
10390
+ };
10391
+ for (const filePath of removable) {
10392
+ const rel = path13.relative(cwd, filePath).replace(/\\/g, "/").toLowerCase();
10393
+ if (rel.includes(".runtrim/internal/runs/") || rel.includes(".runtrim/runs/")) byCategory.runs += 1;
10394
+ else if (rel.includes(".runtrim/internal/previews/") || rel.includes(".runtrim/previews/")) byCategory.previews += 1;
10395
+ else if (rel.includes(".runtrim/internal/restores/") || rel.includes(".runtrim/restores/")) byCategory.restores += 1;
10396
+ else if (rel.includes("archive")) byCategory.archives += 1;
10397
+ else byCategory.other += 1;
10398
+ }
10399
+ if (!dryRun) {
10400
+ for (const filePath of removable) {
10401
+ try {
10402
+ fs13.rmSync(filePath, { force: true });
10403
+ } catch (e) {
10404
+ }
10405
+ }
10406
+ }
10407
+ console.log("");
10408
+ console.log(BOLD("RunTrim") + DIM(" clean"));
10409
+ console.log("");
10410
+ console.log(DIM(" Mode ") + chalk.white(dryRun ? "dry-run" : "apply"));
10411
+ console.log(DIM(" Keep latest runs ") + chalk.white(String(keep)));
10412
+ console.log(DIM(" Candidate files ") + chalk.white(String(removable.length)));
10413
+ console.log("");
10414
+ console.log(DIM(" Cleanup summary"));
10415
+ console.log(chalk.white(` - runs: ${byCategory.runs}`));
10416
+ console.log(chalk.white(` - previews: ${byCategory.previews}`));
10417
+ console.log(chalk.white(` - restores: ${byCategory.restores}`));
10418
+ console.log(chalk.white(` - archives: ${byCategory.archives}`));
10419
+ if (byCategory.other > 0) console.log(chalk.white(` - other: ${byCategory.other}`));
10420
+ console.log("");
10421
+ if (removable.length > 0) {
10422
+ const sample = removable.slice(0, 10).map((p) => path13.relative(cwd, p).replace(/\\/g, "/"));
10423
+ console.log(DIM(" Sample files"));
10424
+ for (const rel of sample) console.log(chalk.white(` - ${rel}`));
10425
+ if (removable.length > sample.length) {
10426
+ console.log(DIM(` ... and ${removable.length - sample.length} more`));
10427
+ }
10428
+ console.log("");
10429
+ }
10430
+ if (dryRun) {
10431
+ console.log(chalk.white(" No files were removed."));
10432
+ console.log(chalk.white(" Re-run without --dry-run to apply cleanup."));
10433
+ } else {
10434
+ console.log(chalk.white(" Cleanup complete."));
10435
+ console.log(chalk.white(" Active contract, latest files, memory current, and MCP snippets were preserved."));
10436
+ }
10437
+ console.log("");
10438
+ });
10439
+ var restoreCommand = program.command("restore").description("Preview or apply local restore for guarded runs");
10440
+ restoreCommand.argument("[runId]", "Run ID to restore, or use 'last'").option("--preview", "Preview restore plan").option("--apply", "Apply restore plan").option("--force", "Apply even if unrelated new changes are detected").action(async (runIdArg, options) => {
10441
+ var _a2, _b, _c, _d;
10442
+ const cwd = process.cwd();
10443
+ const doPreview = (options == null ? void 0 : options.preview) === true;
10444
+ const doApply = (options == null ? void 0 : options.apply) === true;
10445
+ const force = (options == null ? void 0 : options.force) === true;
10446
+ if (!doPreview && !doApply) {
10447
+ console.log("");
10448
+ console.log(chalk.yellow("Choose --preview or --apply."));
10449
+ console.log("");
10450
+ return;
10451
+ }
10452
+ if (doPreview && doApply) {
10453
+ console.log("");
10454
+ console.log(chalk.yellow("Use either --preview or --apply, not both."));
10455
+ console.log("");
10456
+ return;
10457
+ }
10458
+ const runs = loadAllRuns(cwd);
10459
+ const runIdInput = (runIdArg != null ? runIdArg : "last").trim();
10460
+ let targetRunId = runIdInput;
10461
+ if (runIdInput === "last") {
10462
+ const latest = runs.find((r) => r.status === "completed" || r.status === "guarded" || r.status === "executed" || r.status === "checked");
10463
+ if (!latest) {
10464
+ console.log("");
10465
+ console.log(chalk.yellow("No runs available for restore."));
10466
+ console.log("");
10467
+ return;
10468
+ }
10469
+ targetRunId = latest.id;
10470
+ }
10471
+ const run = runs.find((r) => r.id === targetRunId);
10472
+ if (!run) {
10473
+ console.log("");
10474
+ console.log(chalk.yellow(`Run not found: ${targetRunId}`));
10475
+ console.log("");
10476
+ return;
10477
+ }
10478
+ const restore = loadRestorePoint(cwd, targetRunId);
10479
+ if (!restore) {
10480
+ console.log("");
10481
+ console.log(chalk.yellow("No restore point found for this run."));
10482
+ console.log("");
10483
+ return;
10484
+ }
10485
+ const postFiles = (_b = (_a2 = restore.postRun) == null ? void 0 : _a2.changedFiles) != null ? _b : [];
10486
+ const sensitive = postFiles.filter((f) => isSensitivePath(f) || isSecretLikePath(f));
10487
+ const safeFiles = postFiles.filter((f) => !sensitive.includes(f));
10488
+ const gitAvailable = await isGitRepo(cwd);
10489
+ const method = gitAvailable && restore.preRun.commit ? "git checkout from pre-run commit" : "metadata-only (limited)";
10490
+ const nowChanged = gitAvailable ? dedupeFiles((await getGitChangedFiles(cwd)).map((f) => f.path)) : [];
10491
+ const unrelated = nowChanged.filter((f) => !postFiles.includes(f));
10492
+ if (doPreview) {
10493
+ console.log("");
10494
+ console.log(BOLD("RunTrim") + DIM(" restore preview"));
10495
+ console.log("");
10496
+ console.log(DIM(" Run ID ") + chalk.white(targetRunId));
10497
+ console.log(DIM(" Task ") + chalk.white(truncate(run.task, 80)));
10498
+ console.log(DIM(" Method ") + chalk.white(method));
10499
+ console.log(DIM(" Verdict ") + chalk.white((_d = (_c = restore.postRun) == null ? void 0 : _c.finishVerdict) != null ? _d : "unknown"));
10500
+ console.log("");
10501
+ console.log(DIM(" Files to restore"));
10502
+ if (safeFiles.length === 0) console.log(chalk.white(" - (none)"));
10503
+ for (const f of safeFiles.slice(0, 20)) console.log(chalk.white(" - " + f));
10504
+ if (safeFiles.length > 20) console.log(DIM(` ... and ${safeFiles.length - 20} more`));
10505
+ if (sensitive.length > 0) {
10506
+ console.log("");
10507
+ console.log(DIM(" Sensitive files (listed by path only)"));
10508
+ for (const f of sensitive.slice(0, 20)) console.log(chalk.yellow(" - " + f));
10509
+ console.log(chalk.yellow(" Sensitive files are skipped by default and not auto-restored."));
10510
+ }
10511
+ if (unrelated.length > 0) {
10512
+ console.log("");
10513
+ console.log(chalk.yellow(" Warning: repo has new changes after this run."));
10514
+ console.log(chalk.yellow(" Use --apply --force or review manually before restore."));
10515
+ }
10516
+ console.log("");
10517
+ return;
10518
+ }
10519
+ if (!doApply) return;
10520
+ if (!gitAvailable || !restore.preRun.commit) {
10521
+ console.log("");
10522
+ console.log(chalk.red("Restore apply requires git and a pre-run commit restore point."));
10523
+ console.log("");
10524
+ process.exit(1);
10525
+ return;
10526
+ }
10527
+ if (unrelated.length > 0 && !force) {
10528
+ console.log("");
10529
+ console.log(chalk.red("Restore blocked: new unrelated changes detected after this run."));
10530
+ console.log(chalk.red("Re-run with --apply --force after manual review."));
10531
+ console.log("");
10532
+ process.exit(1);
10533
+ return;
10534
+ }
10535
+ const restored = [];
10536
+ const skippedSensitive = [...sensitive];
10537
+ const failed = [];
10538
+ for (const file of safeFiles) {
10539
+ try {
10540
+ let existedBefore = false;
10541
+ try {
10542
+ await execa3("git", ["cat-file", "-e", `${restore.preRun.commit}:${file}`], { cwd });
10543
+ existedBefore = true;
10544
+ } catch (e) {
10545
+ existedBefore = false;
10546
+ }
10547
+ if (existedBefore) {
10548
+ await execa3("git", ["checkout", restore.preRun.commit, "--", file], { cwd });
10549
+ } else if (fs13.existsSync(path13.join(cwd, file))) {
10550
+ fs13.rmSync(path13.join(cwd, file), { force: true });
10551
+ }
10552
+ restored.push(file);
10553
+ } catch (e) {
10554
+ failed.push(file);
10555
+ }
10556
+ }
10557
+ const report = {
10558
+ runId: targetRunId,
10559
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
10560
+ restored,
10561
+ skippedSensitive,
10562
+ failed,
10563
+ forced: force
10564
+ };
10565
+ const reportPath = path13.join(getRestoresDir(cwd), `${targetRunId}.report.${Date.now()}.json`);
10566
+ fs13.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
10567
+ console.log("");
10568
+ console.log(BOLD("RunTrim") + DIM(" restore apply"));
10569
+ console.log("");
10570
+ console.log(DIM(" Run ID ") + chalk.white(targetRunId));
10571
+ console.log(DIM(" Restored files ") + chalk.white(String(restored.length)));
10572
+ console.log(DIM(" Skipped sensitive ") + chalk.white(String(skippedSensitive.length)));
10573
+ console.log(DIM(" Failed ") + chalk.white(String(failed.length)));
10574
+ console.log(DIM(" Report ") + chalk.white(path13.relative(cwd, reportPath)));
10575
+ console.log("");
10576
+ if (failed.length > 0) process.exit(1);
10577
+ });
9777
10578
  program.command("guard <task>").description("Audit a task and generate a guarded run contract").action(async (task) => {
9778
10579
  var _a2, _b, _c;
9779
10580
  const cwd = process.cwd();
@@ -9861,7 +10662,7 @@ program.command("guard <task>").description("Audit a task and generate a guarded
9861
10662
  const run2 = saveRun(task, audit, contract, cwd);
9862
10663
  updateRun(run2.id, { status: "blocked" }, cwd);
9863
10664
  console.log("");
9864
- console.log(DIM(" Run saved: .runtrim/runs/" + run2.id + ".json (status: blocked)"));
10665
+ console.log(DIM(" Run saved: .runtrim/internal/runs/" + run2.id + ".json (status: blocked)"));
9865
10666
  console.log("");
9866
10667
  console.log(DIM(" Next: ") + chalk.white('runtrim guard "<one system at a time>"'));
9867
10668
  console.log("");
@@ -9941,7 +10742,7 @@ program.command("guard <task>").description("Audit a task and generate a guarded
9941
10742
  }
9942
10743
  const run = saveRun(task, audit, contract, cwd);
9943
10744
  console.log("");
9944
- console.log(DIM(" Run saved: .runtrim/runs/" + run.id + ".json"));
10745
+ console.log(DIM(" Run saved: .runtrim/internal/runs/" + run.id + ".json"));
9945
10746
  console.log("");
9946
10747
  console.log(DIM(" After your agent run: ") + chalk.white("runtrim check"));
9947
10748
  console.log("");
@@ -9991,6 +10792,7 @@ program.command("run <task>").description("Guard then run configured local agent
9991
10792
  },
9992
10793
  cwd
9993
10794
  );
10795
+ await captureRestorePoint(cwd, run.id, task);
9994
10796
  const copySavings = estimateSavingsFromTokens2(
9995
10797
  parseEstimatedNumber3(String(contract.estimatedTokensTrimmed))
9996
10798
  );
@@ -10071,6 +10873,7 @@ program.command("run <task>").description("Guard then run configured local agent
10071
10873
  },
10072
10874
  cwd
10073
10875
  );
10876
+ await captureRestorePoint(cwd, run.id, task);
10074
10877
  console.log(DIM(" Execution cancelled. Guarded contract copied to clipboard."));
10075
10878
  console.log("");
10076
10879
  return;
@@ -10127,6 +10930,7 @@ program.command("run <task>").description("Guard then run configured local agent
10127
10930
  },
10128
10931
  cwd
10129
10932
  );
10933
+ await captureRestorePoint(cwd, run.id, task);
10130
10934
  console.log("");
10131
10935
  console.log(chalk.yellow(" Agent command not found. Falling back to copy mode guidance."));
10132
10936
  console.log(DIM(" Configure with: npm run runtrim -- agent set claude|codex|custom"));
@@ -10160,6 +10964,7 @@ program.command("run <task>").description("Guard then run configured local agent
10160
10964
  },
10161
10965
  cwd
10162
10966
  );
10967
+ await captureRestorePoint(cwd, run.id, task);
10163
10968
  console.log("");
10164
10969
  console.log(chalk.yellow(" Agent command not found. Falling back to copy mode guidance."));
10165
10970
  console.log(DIM(" Configure with: npm run runtrim -- agent set claude|codex|custom"));
@@ -10203,7 +11008,7 @@ ${stderr}`, "utf-8");
10203
11008
  exitCode,
10204
11009
  stdoutPreview: stdout.slice(0, OUTPUT_PREVIEW_MAX),
10205
11010
  stderrPreview: stderr.slice(0, OUTPUT_PREVIEW_MAX),
10206
- outputPath: `.runtrim/runs/${run.id}.output.txt`
11011
+ outputPath: `.runtrim/internal/runs/${run.id}.output.txt`
10207
11012
  },
10208
11013
  evaluation
10209
11014
  },
@@ -10353,6 +11158,7 @@ program.command("go <task>").description("Bridge Mode: generate a scoped contrac
10353
11158
  memoryUsed,
10354
11159
  providerRouting
10355
11160
  }, cwd);
11161
+ await captureRestorePoint(cwd, run.id, task);
10356
11162
  const bridgeCtx = deriveBridgeContext(task, contract, runs, projectName);
10357
11163
  let bridgeWritten = [];
10358
11164
  let bridgeManagedPaths = [];
@@ -10411,7 +11217,7 @@ program.command("go <task>").description("Bridge Mode: generate a scoped contrac
10411
11217
  if (contract.contract.stopRules.length > 0) {
10412
11218
  console.log(DIM(" Stop rule ") + chalk.white(truncate((_g = contract.contract.stopRules[contract.contract.stopRules.length - 1]) != null ? _g : "", 60)));
10413
11219
  }
10414
- console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
11220
+ console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
10415
11221
  console.log(DIM(" Contract ") + chalk.white("created"));
10416
11222
  console.log("");
10417
11223
  if (bridgeWritten.length > 0) {
@@ -11766,6 +12572,18 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
11766
12572
  const blockedByExistingHard = scope.forbiddenFiles.length > 0 || scope.status === "limit_exceeded";
11767
12573
  const warnBySensitiveIgnored = sensitiveIgnored.length > 0;
11768
12574
  const finishVerdict = blockedBySensitive || blockedByContract || blockedByExistingHard ? "BLOCKED" : warnBySensitiveIgnored || scopeDriftStatus !== "passed" || evaluation.status === "needs_verification" || evaluation.status === "partial" ? "WARN" : "PASS";
12575
+ const restoreRecord = loadRestorePoint(cwd, activeRun.id);
12576
+ if (restoreRecord) {
12577
+ restoreRecord.postRun = {
12578
+ changedFiles,
12579
+ forbiddenFiles: [...scope.forbiddenFiles, ...forbiddenPathFiles],
12580
+ sensitiveFiles: scope.sensitiveFiles,
12581
+ outOfScopeFiles: [...outOfContractFiles, ...scope.outOfScopeFiles],
12582
+ finishVerdict,
12583
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
12584
+ };
12585
+ saveRestorePoint(cwd, restoreRecord);
12586
+ }
11769
12587
  const verdictColor = finishVerdict === "PASS" ? chalk.green : finishVerdict === "WARN" ? chalk.yellow : chalk.red;
11770
12588
  const scopeColor = scopeDriftStatus === "passed" ? chalk.green : scopeDriftStatus === "forbidden_touched" ? chalk.red : chalk.yellow;
11771
12589
  const riskAfter = (_m = activeRun.contract.wasteRiskAfter) != null ? _m : "medium";