runtrim 0.1.18 → 0.1.20

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
  }
@@ -1573,35 +1610,38 @@ function saveRun(task, audit, contract, cwd = process.cwd()) {
1573
1610
  return record;
1574
1611
  }
1575
1612
  function loadLatestRun(cwd = process.cwd()) {
1576
- const runsDir = getRunsDir(cwd);
1577
- if (!import_fs3.default.existsSync(runsDir)) return null;
1578
- const files = import_fs3.default.readdirSync(runsDir).filter((f) => f.endsWith(".json")).map((f) => ({
1579
- name: f,
1580
- time: import_fs3.default.statSync(import_path3.default.join(runsDir, f)).mtime.getTime()
1581
- })).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);
1582
1621
  if (files.length === 0) return null;
1583
1622
  try {
1584
1623
  return JSON.parse(
1585
- 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")
1586
1625
  );
1587
1626
  } catch (e) {
1588
1627
  return null;
1589
1628
  }
1590
1629
  }
1591
1630
  function updateRun(runId, updates, cwd = process.cwd()) {
1592
- 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;
1593
1634
  if (!import_fs3.default.existsSync(filePath)) return;
1594
1635
  const existing = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1595
1636
  import_fs3.default.writeFileSync(filePath, JSON.stringify(__spreadValues(__spreadValues({}, existing), updates), null, 2));
1596
1637
  }
1597
1638
  function loadAllRuns(cwd = process.cwd()) {
1598
- const runsDir = getRunsDir(cwd);
1599
- if (!import_fs3.default.existsSync(runsDir)) return [];
1600
- 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) => {
1601
1643
  try {
1602
- return JSON.parse(
1603
- import_fs3.default.readFileSync(import_path3.default.join(runsDir, f), "utf-8")
1604
- );
1644
+ return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1605
1645
  } catch (e) {
1606
1646
  return null;
1607
1647
  }
@@ -2794,7 +2834,7 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
2794
2834
  "6. After editing, tell the user to run: `runtrim finish`",
2795
2835
  "",
2796
2836
  "---",
2797
- `Protocol: runtrim init. Updated: ${(/* @__PURE__ */ new Date()).toISOString()}`
2837
+ `Protocol: runtrim start. Updated: ${(/* @__PURE__ */ new Date()).toISOString()}`
2798
2838
  ];
2799
2839
  import_fs7.default.writeFileSync(import_path7.default.join(cwd, "RUNTRIM.md"), lines.join("\n"), "utf-8");
2800
2840
  }
@@ -2850,7 +2890,7 @@ function archiveContract(cwd, runId) {
2850
2890
  if (!import_fs7.default.existsSync(latestPath)) return;
2851
2891
  const content = import_fs7.default.readFileSync(latestPath, "utf-8");
2852
2892
  if (content.includes("Status: none")) return;
2853
- const archiveDir = import_path7.default.join(contractsDir, "archive");
2893
+ const archiveDir = getContractsArchiveDir(cwd);
2854
2894
  if (!import_fs7.default.existsSync(archiveDir)) import_fs7.default.mkdirSync(archiveDir, { recursive: true });
2855
2895
  import_fs7.default.writeFileSync(import_path7.default.join(archiveDir, `${runId}.md`), content, "utf-8");
2856
2896
  }
@@ -4180,9 +4220,15 @@ async function getPanelState(cwd, monitorMode) {
4180
4220
  let runs = [];
4181
4221
  let latest = null;
4182
4222
  let registry = {
4183
- version: 1,
4223
+ version: 2,
4224
+ stateVersion: 2,
4184
4225
  plan: "free",
4226
+ machineInstallId: "",
4227
+ createdAt: "",
4228
+ updatedAt: "",
4185
4229
  trackedRepos: [],
4230
+ lastKnownRepo: null,
4231
+ integrity: { algorithm: "sha256-local-seal-v1", seal: "" },
4186
4232
  telemetry: {
4187
4233
  enabled: false,
4188
4234
  anonymousId: ""
@@ -4224,9 +4270,15 @@ async function getPanelState(cwd, monitorMode) {
4224
4270
  } catch (e) {
4225
4271
  warnings.push("global_registry_failed");
4226
4272
  registry = {
4227
- version: 1,
4273
+ version: 2,
4274
+ stateVersion: 2,
4228
4275
  plan: "free",
4276
+ machineInstallId: "",
4277
+ createdAt: "",
4278
+ updatedAt: "",
4229
4279
  trackedRepos: [],
4280
+ lastKnownRepo: null,
4281
+ integrity: { algorithm: "sha256-local-seal-v1", seal: "" },
4230
4282
  telemetry: {
4231
4283
  enabled: false,
4232
4284
  anonymousId: ""
@@ -4694,7 +4746,7 @@ Before editing code:
4694
4746
  If no active RunTrim contract exists:
4695
4747
  - Do not edit code without one.
4696
4748
  - Ask the user to start a guarded run:
4697
- runtrim go "<task>"
4749
+ runtrim agent "<task>" --copy
4698
4750
 
4699
4751
  If the task requires leaving the current scope:
4700
4752
  - Stop.
@@ -4790,7 +4842,7 @@ ${BASE_PROTOCOL}
4790
4842
  Before using any tool or executing any command:
4791
4843
  1. Confirm a RunTrim contract is active at .runtrim/contracts/latest.md.
4792
4844
  2. If no active contract exists, do not proceed. Ask for:
4793
- runtrim go "<task>"
4845
+ runtrim agent "<task>" --copy
4794
4846
  3. Do not call shell commands, write files, or read env vars outside the contract.
4795
4847
  `.trim(),
4796
4848
  custom: `
@@ -4843,7 +4895,7 @@ function getCursorMdcContent() {
4843
4895
  "## If no active contract",
4844
4896
  "",
4845
4897
  "Ask the user to start a guarded run:",
4846
- '`runtrim go "<task>"`',
4898
+ '`runtrim agent "<task>" --copy`',
4847
4899
  "",
4848
4900
  "Any agent. One run boundary."
4849
4901
  ].join("\n");
@@ -5105,7 +5157,7 @@ Before editing code:
5105
5157
  - If the task touches auth, billing, payments, webhooks, database, middleware,
5106
5158
  env vars, secrets, or subscriptions, stop and require an active RunTrim contract.
5107
5159
  Ask the user to run:
5108
- runtrim go "<task>"
5160
+ runtrim agent "<task>" --copy
5109
5161
  - For low-risk work (UI polish, copy, docs, isolated component styling):
5110
5162
  Fast Path is allowed if no unfinished changes exist.
5111
5163
  Keep the change minimal.
@@ -5128,7 +5180,7 @@ No active RunTrim contract means no code edits.
5128
5180
  If no active contract exists at .runtrim/contracts/latest.md:
5129
5181
  - Do not edit any file.
5130
5182
  - Ask the user to start a guarded run:
5131
- runtrim go "<task>"
5183
+ runtrim agent "<task>" --copy
5132
5184
 
5133
5185
  After every editing session:
5134
5186
  - Ask the user to run:
@@ -5142,7 +5194,7 @@ Fast Path is allowed for low and medium risk work.
5142
5194
 
5143
5195
  Critical systems (auth, billing, payments, webhooks, database, middleware,
5144
5196
  env vars, secrets, subscriptions) still require a RunTrim contract:
5145
- runtrim go "<task>"
5197
+ runtrim agent "<task>" --copy
5146
5198
 
5147
5199
  After any edits:
5148
5200
  - runtrim finish is required before continuing to another task.
@@ -5153,7 +5205,7 @@ RunTrim Auto-guard: Off
5153
5205
 
5154
5206
  Auto-guard is disabled for this project.
5155
5207
  RunTrim can still be used manually:
5156
- runtrim go "<task>"
5208
+ runtrim agent "<task>" --copy
5157
5209
  runtrim finish
5158
5210
  `.trim();
5159
5211
  }
@@ -5179,7 +5231,7 @@ function saveFastRunRecord(cwd, changedFiles, risk) {
5179
5231
  reportParts.push(`${sensitive.length} sensitive path${sensitive.length === 1 ? "" : "s"} touched.`);
5180
5232
  }
5181
5233
  reportParts.push("No pre-run contract was captured for this run.");
5182
- 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';
5183
5235
  const summary = {
5184
5236
  id,
5185
5237
  task,
@@ -5699,7 +5751,7 @@ function recommendProviderRouting(ctx) {
5699
5751
  } else if (route === "split-required") {
5700
5752
  routingReason = "This spans multiple critical systems, so RunTrim should split into audit, implementation, and verification.";
5701
5753
  }
5702
- let nextCommand = `runtrim go "${ctx.task}"`;
5754
+ let nextCommand = `runtrim agent "${ctx.task}" --copy`;
5703
5755
  if (route === "split-required") {
5704
5756
  nextCommand = "split into:\n1. audit only\n2. implementation only\n3. verification only";
5705
5757
  } else if (route === "preview-only") {
@@ -5721,6 +5773,7 @@ var _a;
5721
5773
  var oraFactory = typeof import_ora.default === "function" ? import_ora.default : (_a = import_ora.default.default) != null ? _a : import_ora.default;
5722
5774
  var ACCENT = chalk.hex("#C8901A");
5723
5775
  var GO_ACCENT = chalk.hex("#8B7CFF");
5776
+ var RUNTRIM_AGENT_INSTRUCTIONS_VERSION = "2";
5724
5777
  var DIM = chalk.gray;
5725
5778
  var BOLD = chalk.white.bold;
5726
5779
  var program = new import_commander.Command();
@@ -5856,6 +5909,98 @@ async function copyToClipboardSafe(value) {
5856
5909
  function dedupeFiles(files) {
5857
5910
  return [...new Set(files.filter(Boolean).map((f) => f.replace(/\\/g, "/")))];
5858
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
+ }
5859
6004
  function normalizeContractPathPattern(pattern) {
5860
6005
  let p = pattern.trim().replace(/\\/g, "/");
5861
6006
  if (!p || p === "-" || p.toLowerCase() === "none") return "";
@@ -6025,10 +6170,11 @@ function buildRecommendedNextCommand(task, approval, filesToInspect) {
6025
6170
  return `runtrim go "${task}"`;
6026
6171
  }
6027
6172
  function writePreviewArtifacts(cwd, preview) {
6028
- const previewsDir = import_path13.default.join(cwd, ".runtrim", "previews");
6173
+ const previewsDir = getPreviewsDir(cwd);
6029
6174
  if (!import_fs13.default.existsSync(previewsDir)) import_fs13.default.mkdirSync(previewsDir, { recursive: true });
6030
6175
  const jsonPath = import_path13.default.join(previewsDir, `${preview.id}.json`);
6031
- 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 });
6032
6178
  import_fs13.default.writeFileSync(jsonPath, JSON.stringify(preview, null, 2), "utf-8");
6033
6179
  const lines = [
6034
6180
  "RunTrim Agent Preview",
@@ -6067,7 +6213,7 @@ function writePreviewArtifacts(cwd, preview) {
6067
6213
  "Next:",
6068
6214
  preview.recommendedNextCommand,
6069
6215
  "",
6070
- `Preview JSON: .runtrim/previews/${preview.id}.json`
6216
+ `Preview JSON: .runtrim/internal/previews/${preview.id}.json`
6071
6217
  ];
6072
6218
  import_fs13.default.writeFileSync(markdownPath, lines.join("\n"), "utf-8");
6073
6219
  return { jsonPath, markdownPath };
@@ -6449,6 +6595,7 @@ async function runAgentApply(task, mode) {
6449
6595
  const copied = mode.copy ? await copyToClipboardSafe(import_fs13.default.readFileSync(handoff.markdownPath, "utf-8")) : false;
6450
6596
  const run = saveRun(task, previewResult.audit, previewResult.contract, cwd);
6451
6597
  updateRun(run.id, { status: "guarded" }, cwd);
6598
+ await captureRestorePoint(cwd, run.id, task);
6452
6599
  const riskColor = (_a2 = { low: chalk.green, medium: chalk.yellow, high: chalk.hex("#FF8C00"), critical: chalk.red }[risk]) != null ? _a2 : chalk.white;
6453
6600
  if (mode.primary) {
6454
6601
  console.log("");
@@ -6635,6 +6782,7 @@ async function runControlledExecution(task, mode) {
6635
6782
  controlledExecutionId: id,
6636
6783
  controlledExecutionStatus: status
6637
6784
  }, cwd);
6785
+ await captureRestorePoint(cwd, run.id, task);
6638
6786
  }
6639
6787
  if (status === "blocked") {
6640
6788
  console.log("");
@@ -6925,14 +7073,18 @@ ${block}
6925
7073
  function buildRuntrimAgentBlockContent() {
6926
7074
  return [
6927
7075
  "RunTrim project instructions",
7076
+ `RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`,
6928
7077
  "- Before editing, read RunTrim project memory and profile context.",
6929
- "- 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.",
6930
7080
  '- If MCP is unavailable, ask the user to run: runtrim agent "task" --copy',
6931
- "- Stay inside active contract scope.",
6932
- "- 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.",
6933
7084
  "- Never read or print env file contents.",
6934
- '- Do not continue outside scope silently. Request: runtrim approve "Allow <path/scope> for this run only"',
6935
- "- 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.",
6936
7088
  "",
6937
7089
  "RunTrim MCP:",
6938
7090
  "- runtrim mcp instructions",
@@ -7092,12 +7244,16 @@ function updateStartAgentInstructions(cwd) {
7092
7244
  const existing = import_fs13.default.existsSync(instructionsPath) ? import_fs13.default.readFileSync(instructionsPath, "utf-8") : "";
7093
7245
  const body = [
7094
7246
  "RunTrim agent instructions:",
7247
+ `RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`,
7095
7248
  "- Use RunTrim project memory and profile before editing.",
7096
- "- 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.",
7097
7251
  '- If MCP is unavailable, ask user to run: runtrim agent "task" --copy',
7098
- "- 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.",
7099
7254
  '- If scope must expand, request: runtrim approve "Allow <path/scope> for this run only"',
7100
- "- 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.",
7101
7257
  "- Never read or print env file contents.",
7102
7258
  "",
7103
7259
  "RunTrim MCP:",
@@ -7170,6 +7326,202 @@ function detectKnownMcpConfigPresence() {
7170
7326
  cursorConfigFound: Boolean(cursorMatch)
7171
7327
  };
7172
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
+ }
7173
7525
  function appendContractAmendment(cwd, approvalText) {
7174
7526
  const p = import_path13.default.join(cwd, ".runtrim", "contracts", "latest.md");
7175
7527
  if (!import_fs13.default.existsSync(p)) return { ok: false, reason: "missing_contract" };
@@ -7542,6 +7894,7 @@ async function buildRuntrimCreateContractMcp(cwd, args) {
7542
7894
  const handoff = writeAgentHandoffArtifacts(cwd, apply, import_path13.default.relative(cwd, previewPath));
7543
7895
  const run = saveRun(mergedTask, previewResult.audit, previewResult.contract, cwd);
7544
7896
  updateRun(run.id, { status: "guarded" }, cwd);
7897
+ await captureRestorePoint(cwd, run.id, mergedTask);
7545
7898
  const payload = {
7546
7899
  contract_created: true,
7547
7900
  task: taskRaw,
@@ -7736,6 +8089,7 @@ async function startMcpServerStdio(cwd) {
7736
8089
  });
7737
8090
  return;
7738
8091
  }
8092
+ writeMcpLastUsed(cwd, name);
7739
8093
  send({
7740
8094
  jsonrpc: "2.0",
7741
8095
  id,
@@ -8223,6 +8577,7 @@ async function initializeRunTrim(cwd, options = {}) {
8223
8577
  const runsDir = getRunsDir(cwd);
8224
8578
  if (!import_fs13.default.existsSync(configDir)) import_fs13.default.mkdirSync(configDir, { recursive: true });
8225
8579
  if (!import_fs13.default.existsSync(runsDir)) import_fs13.default.mkdirSync(runsDir, { recursive: true });
8580
+ ensureInternalArtifactDirs(cwd);
8226
8581
  const existingConfig = hadConfig ? loadConfig(cwd) : null;
8227
8582
  const baseConfig = __spreadValues(__spreadValues({}, DEFAULT_CONFIG), detectProjectInfo(cwd));
8228
8583
  const nextConfig = options.refresh && existingConfig ? __spreadValues({}, existingConfig) : baseConfig;
@@ -8242,13 +8597,8 @@ async function initializeRunTrim(cwd, options = {}) {
8242
8597
  import_fs13.default.writeFileSync(memoryPath, buildBaselineMemoryMarkdown(baseline), "utf-8");
8243
8598
  }
8244
8599
  ensureStarterPromptIfMissing(cwd);
8245
- const gitignorePath = import_path13.default.join(cwd, ".gitignore");
8246
- if (import_fs13.default.existsSync(gitignorePath)) {
8247
- const content = import_fs13.default.readFileSync(gitignorePath, "utf-8");
8248
- if (!content.includes(".runtrim/runs")) {
8249
- import_fs13.default.appendFileSync(gitignorePath, "\n# RunTrim run history\n.runtrim/runs/\n");
8250
- }
8251
- }
8600
+ ensureRuntrimReadme(cwd);
8601
+ ensureRuntrimGitignoreGuidance(cwd);
8252
8602
  return { ok: true };
8253
8603
  }
8254
8604
  async function runPrepareTask(task, options) {
@@ -8283,7 +8633,7 @@ async function runPrepareTask(task, options) {
8283
8633
  console.log("");
8284
8634
  console.log(DIM(" Task ") + chalk.white(truncate(task, 70)));
8285
8635
  console.log(DIM(" Prompt ") + chalk.white(promptPath2));
8286
- 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`));
8287
8637
  console.log("");
8288
8638
  printPrepareAgentInstructions(selectedAgent, config.lastPromptPath);
8289
8639
  console.log("");
@@ -8303,6 +8653,7 @@ async function runPrepareTask(task, options) {
8303
8653
  const promptPath = writeLatestPromptFile(contract.contractText, config, cwd);
8304
8654
  if (options.copy !== false) await copyToClipboardSafe(contract.contractText);
8305
8655
  updateRun(run.id, { status: "guarded" }, cwd);
8656
+ await captureRestorePoint(cwd, run.id, task);
8306
8657
  const riskColors = {
8307
8658
  low: chalk.green,
8308
8659
  medium: chalk.yellow,
@@ -8325,7 +8676,7 @@ async function runPrepareTask(task, options) {
8325
8676
  );
8326
8677
  console.log(DIM(" Reduction ") + chalk.white(contract.riskReductionPercent + "%"));
8327
8678
  console.log(DIM(" Prompt ") + chalk.white(promptPath));
8328
- 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`));
8329
8680
  console.log("");
8330
8681
  printPrepareAgentInstructions(selectedAgent, config.lastPromptPath);
8331
8682
  console.log("");
@@ -8469,6 +8820,9 @@ program.command("start").description("Guided RunTrim onboarding and daily loop")
8469
8820
  console.log(chalk.white(" runtrim mcp config --print"));
8470
8821
  console.log(chalk.white(" runtrim mcp start"));
8471
8822
  console.log("");
8823
+ console.log(DIM(" Check readiness"));
8824
+ console.log(chalk.white(" runtrim doctor"));
8825
+ console.log("");
8472
8826
  console.log(DIM(" Adapters"));
8473
8827
  console.log(chalk.white(" runtrim adapters"));
8474
8828
  console.log(chalk.white(" runtrim adapters status"));
@@ -8550,6 +8904,108 @@ program.command("start").description("Guided RunTrim onboarding and daily loop")
8550
8904
  console.log("");
8551
8905
  }
8552
8906
  });
8907
+ program.command("doctor").description("Check whether the current project is RunTrim-ready for agents, MCP, memory and finish verification").action(async () => {
8908
+ const cwd = process.cwd();
8909
+ const repoCheck = await assertFreeRepoAllowed(cwd);
8910
+ const profilePath = import_path13.default.join(getConfigDir(cwd), "project-profile.json");
8911
+ const memoryPath = import_path13.default.join(getConfigDir(cwd), "memory", "current.md");
8912
+ const instructionsPath = import_path13.default.join(getConfigDir(cwd), "agent", "instructions.md");
8913
+ const snippetsDir = getProjectMcpDir(cwd);
8914
+ const snippetFiles = [
8915
+ import_path13.default.join(snippetsDir, "claude-desktop.json"),
8916
+ import_path13.default.join(snippetsDir, "cursor.json"),
8917
+ import_path13.default.join(snippetsDir, "generic.json")
8918
+ ];
8919
+ const profileReady = import_fs13.default.existsSync(profilePath);
8920
+ const memoryReady = import_fs13.default.existsSync(memoryPath) && import_fs13.default.readFileSync(memoryPath, "utf-8").trim().length > 0;
8921
+ const instructionsReady = import_fs13.default.existsSync(instructionsPath) && import_fs13.default.readFileSync(instructionsPath, "utf-8").trim().length > 0;
8922
+ const claudeBlock = hasCurrentRuntrimBlock(import_path13.default.join(cwd, "CLAUDE.md"));
8923
+ const agentsBlock = hasCurrentRuntrimBlock(import_path13.default.join(cwd, "AGENTS.md"));
8924
+ const cursorRulePath = import_path13.default.join(cwd, ".cursor", "rules", "runtrim.mdc");
8925
+ const cursorRuleExists = import_fs13.default.existsSync(cursorRulePath);
8926
+ const cursorRuleCurrent = cursorRuleExists ? import_fs13.default.readFileSync(cursorRulePath, "utf-8").includes(`RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`) : false;
8927
+ const anySurfaceInstalled = claudeBlock.exists || agentsBlock.exists || cursorRuleExists;
8928
+ const runtrimBlockCurrent = [claudeBlock.current, agentsBlock.current, cursorRuleCurrent].some(Boolean);
8929
+ const snippetsGenerated = snippetFiles.every((p) => import_fs13.default.existsSync(p));
8930
+ const knownMcp = detectKnownMcpConfigPresence();
8931
+ const claudeMcpState = detectMcpConfigState(knownMcp.claudeConfigPath, knownMcp.claudeConfigFound);
8932
+ const cursorMcpState = detectMcpConfigState(knownMcp.cursorConfigPath, knownMcp.cursorConfigFound);
8933
+ const genericReady = snippetsGenerated ? "ready" : "missing";
8934
+ const tools = buildMcpTools();
8935
+ const hasContractTool = tools.some((t) => t.name === "runtrim_create_contract");
8936
+ const hasPathTool = tools.some((t) => t.name === "runtrim_check_path");
8937
+ const hasApprovalTool = tools.some((t) => t.name === "runtrim_suggest_approval");
8938
+ const hasFinishTool = tools.some((t) => t.name === "runtrim_finish_guidance");
8939
+ const contract = parseContractSummary(cwd);
8940
+ const lastMcp = readMcpLastUsed(cwd);
8941
+ const setupCorrupt = configExists(cwd) && (!profileReady || !memoryReady || !instructionsReady);
8942
+ const artifactFiles = listArtifactFiles(cwd);
8943
+ const artifactCount = artifactFiles.length;
8944
+ let readiness = "partial";
8945
+ if (!repoCheck.allowed || setupCorrupt) {
8946
+ readiness = "blocked";
8947
+ } else if (profileReady && memoryReady && instructionsReady && anySurfaceInstalled && snippetsGenerated && hasContractTool && hasPathTool && hasApprovalTool && hasFinishTool) {
8948
+ readiness = "ready";
8949
+ }
8950
+ console.log("");
8951
+ console.log(BOLD("RunTrim") + DIM(" doctor"));
8952
+ console.log("");
8953
+ console.log(BOLD("Project"));
8954
+ console.log(chalk.white(`- Project profile: ${profileReady ? "ready" : "missing"}`));
8955
+ console.log(chalk.white(`- Project memory: ${memoryReady ? "ready" : "missing"}`));
8956
+ console.log(chalk.white(`- Agent instructions: ${instructionsReady ? "ready" : "missing"}`));
8957
+ console.log(chalk.white(`- Active contract: ${contract.active ? "active" : "none"}`));
8958
+ console.log("");
8959
+ console.log(BOLD("Agent rules"));
8960
+ console.log(chalk.white(`- CLAUDE.md: ${claudeBlock.exists ? "installed" : "not found"}`));
8961
+ console.log(chalk.white(`- AGENTS.md: ${agentsBlock.exists ? "installed" : "not found"}`));
8962
+ console.log(chalk.white(`- Cursor rules: ${cursorRuleExists ? "installed" : "not found"}`));
8963
+ console.log(chalk.white(`- RunTrim instruction block: ${runtrimBlockCurrent ? "current" : anySurfaceInstalled ? "stale" : "missing"}`));
8964
+ console.log("");
8965
+ console.log(BOLD("MCP"));
8966
+ console.log(chalk.white("- MCP server: available"));
8967
+ console.log(chalk.white(`- Project snippets: ${snippetsGenerated ? "generated" : "missing"}`));
8968
+ console.log(chalk.white(`- Claude Desktop config: ${claudeMcpState}`));
8969
+ console.log(chalk.white(`- Cursor MCP config: ${cursorMcpState}`));
8970
+ console.log(chalk.white(`- Generic config: ${genericReady}`));
8971
+ if (lastMcp.tracked && lastMcp.tool && lastMcp.usedAt) {
8972
+ console.log(chalk.white(`- Last MCP tool call: ${lastMcp.tool}, ${lastMcp.usedAt}`));
8973
+ } else {
8974
+ console.log(chalk.white("- Last MCP tool call: not tracked yet"));
8975
+ }
8976
+ console.log("");
8977
+ console.log(BOLD("Automation readiness"));
8978
+ console.log(chalk.white(`- Contract creation tool: ${hasContractTool ? "available" : "missing"}`));
8979
+ console.log(chalk.white(`- Path check tool: ${hasPathTool ? "available" : "missing"}`));
8980
+ console.log(chalk.white(`- Approval tool: ${hasApprovalTool ? "available" : "missing"}`));
8981
+ console.log(chalk.white(`- Finish guidance tool: ${hasFinishTool ? "available" : "missing"}`));
8982
+ console.log(chalk.white(`- Local artifacts: ${artifactCount}`));
8983
+ console.log("");
8984
+ console.log(BOLD("Readiness"));
8985
+ console.log(chalk.white(`- State: ${readiness}`));
8986
+ console.log("");
8987
+ console.log(BOLD("Next"));
8988
+ if (repoCheck.status === "blocked_repair") {
8989
+ console.log(chalk.yellow("- RunTrim local state needs repair. Run runtrim repo repair."));
8990
+ } else if (repoCheck.status === "blocked_limit") {
8991
+ console.log(chalk.yellow("- Free includes 1 tracked repo. Continue in tracked repo, unlink with runtrim repo unlink --force, or upgrade to Builder."));
8992
+ } else if (readiness === "ready") {
8993
+ if (claudeMcpState !== "configured" && cursorMcpState !== "configured") {
8994
+ console.log(chalk.white("- Ready locally, MCP client not connected."));
8995
+ console.log(chalk.white("- Run runtrim mcp instructions or copy .runtrim/mcp/cursor.json into your MCP client."));
8996
+ } else {
8997
+ console.log(chalk.white("- Your project is RunTrim-aware. Open Cursor/Claude/Codex and use normal language."));
8998
+ }
8999
+ } else {
9000
+ console.log(chalk.white("- Run runtrim start."));
9001
+ console.log(chalk.white("- Run runtrim mcp instructions."));
9002
+ console.log(chalk.white("- Run runtrim mcp config --print."));
9003
+ }
9004
+ if (artifactCount > 25) {
9005
+ console.log(chalk.white(`- Local artifacts: ${artifactCount}. Run runtrim clean --dry-run to review cleanup.`));
9006
+ }
9007
+ console.log("");
9008
+ });
8553
9009
  var PROTOCOL_BLOCK_START = "<!-- RUNTRIM_PROTOCOL_START -->";
8554
9010
  var PROTOCOL_BLOCK_END = "<!-- RUNTRIM_PROTOCOL_END -->";
8555
9011
  var PROTOCOL_POINTER_BLOCK = `
@@ -9654,6 +10110,146 @@ agentCommand.command("prompt-mode <mode>").description("Set how guarded contract
9654
10110
  console.log(ACCENT.bold(" Agent prompt mode updated to: " + m));
9655
10111
  console.log("");
9656
10112
  });
10113
+ var ciCommand = program.command("ci").description("RunTrim CI checks for pull requests and merges");
10114
+ 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) => {
10115
+ const cwd = process.cwd();
10116
+ const strict = options.strict === true;
10117
+ const allowWarn = options.allowWarn === true;
10118
+ const outputJson = options.json === true;
10119
+ const report = options.report === true;
10120
+ const repoCheck = await assertFreeRepoAllowed(cwd);
10121
+ const detection = await detectCiChangedFiles(cwd, options.base, options.head);
10122
+ const changedFiles = detection.files;
10123
+ const contract = parseContractSummary(cwd);
10124
+ const latestRun = loadLatestRun(cwd);
10125
+ const hasRuntrimContext = contract.exists || Boolean(latestRun);
10126
+ const issues = [];
10127
+ const warnings = [...detection.warnings];
10128
+ const nextSteps = [];
10129
+ let verdict = "PASS";
10130
+ if (repoCheck.status === "blocked_repair") {
10131
+ issues.push("RunTrim local state needs repair before CI can trust local guard state.");
10132
+ nextSteps.push("Run: runtrim repo repair");
10133
+ verdict = "BLOCKED";
10134
+ } else if (repoCheck.status === "blocked_limit") {
10135
+ issues.push("Free includes 1 tracked repo and this repo is not currently tracked.");
10136
+ nextSteps.push("Continue in tracked repo, unlink with runtrim repo unlink --force, or upgrade to Builder.");
10137
+ verdict = "BLOCKED";
10138
+ }
10139
+ if (changedFiles.length === 0) {
10140
+ warnings.push("No changed files detected for this diff.");
10141
+ }
10142
+ const secretFiles = changedFiles.filter(isSecretLikePath);
10143
+ if (secretFiles.length > 0) {
10144
+ issues.push(`Sensitive file path changed: ${secretFiles[0]}`);
10145
+ nextSteps.push("Remove secret/env/key/cert files from this PR before merge.");
10146
+ verdict = "BLOCKED";
10147
+ }
10148
+ const highRiskFiles = changedFiles.filter((f) => isHighRiskLogicPath(f) && !isSecretLikePath(f));
10149
+ if (highRiskFiles.length > 0) {
10150
+ if (!hasRuntrimContext) {
10151
+ issues.push(`High-risk path changed without RunTrim context: ${highRiskFiles[0]}`);
10152
+ nextSteps.push("Create a dedicated guarded run and finish it before merging.");
10153
+ verdict = "BLOCKED";
10154
+ } else if (contract.allowedPaths.length > 0 && !matchesAnyContractRule(highRiskFiles[0], contract.allowedPaths)) {
10155
+ issues.push(`High-risk path is outside contract allowed scope: ${highRiskFiles[0]}`);
10156
+ nextSteps.push(`Run: runtrim approve "Allow ${highRiskFiles[0]} for this run only"`);
10157
+ verdict = "BLOCKED";
10158
+ }
10159
+ }
10160
+ if (contract.forbiddenPaths.length > 0) {
10161
+ const forbiddenTouched = changedFiles.filter((f) => matchesAnyContractRule(f, contract.forbiddenPaths));
10162
+ if (forbiddenTouched.length > 0) {
10163
+ issues.push(`Contract-forbidden path touched: ${forbiddenTouched[0]}`);
10164
+ nextSteps.push("Split the task or get explicit scoped approval before merge.");
10165
+ verdict = "BLOCKED";
10166
+ }
10167
+ }
10168
+ if (contract.allowedPaths.length > 0) {
10169
+ const outOfScope = changedFiles.filter((f) => !matchesAnyContractRule(f, contract.allowedPaths));
10170
+ if (outOfScope.length > 0) {
10171
+ issues.push(`Changed file is outside latest contract scope: ${outOfScope[0]}`);
10172
+ nextSteps.push(`Run: runtrim approve "Allow ${outOfScope[0]} for this run only"`);
10173
+ verdict = "BLOCKED";
10174
+ }
10175
+ }
10176
+ const docsOnly = changedFiles.length > 0 && changedFiles.every((f) => isDocsLikePath(f));
10177
+ if (!hasRuntrimContext && verdict !== "BLOCKED") {
10178
+ if (docsOnly) {
10179
+ warnings.push("No RunTrim contract/report found. Docs-only change treated as low risk.");
10180
+ verdict = "WARN";
10181
+ } else {
10182
+ warnings.push("No RunTrim contract/report found. CI could not fully verify scope context.");
10183
+ verdict = "WARN";
10184
+ }
10185
+ }
10186
+ if (strict && !hasRuntrimContext && verdict !== "BLOCKED") {
10187
+ issues.push("Strict mode requires RunTrim contract/report context.");
10188
+ verdict = "BLOCKED";
10189
+ }
10190
+ if (verdict === "PASS" && warnings.length > 0) {
10191
+ verdict = "WARN";
10192
+ }
10193
+ if (nextSteps.length === 0) {
10194
+ if (verdict === "PASS") {
10195
+ nextSteps.push("Safe to merge under current RunTrim CI policy.");
10196
+ } else if (verdict === "WARN") {
10197
+ nextSteps.push("Run runtrim finish locally to strengthen verification context.");
10198
+ } else {
10199
+ nextSteps.push("Resolve blocked issues, then rerun runtrim ci check.");
10200
+ }
10201
+ }
10202
+ let exitCode = 0;
10203
+ if (verdict === "BLOCKED") exitCode = 1;
10204
+ if (verdict === "WARN" && strict && !allowWarn) exitCode = 1;
10205
+ const jsonPayload = {
10206
+ verdict: verdict.toLowerCase(),
10207
+ exitCode,
10208
+ changedFiles,
10209
+ issues,
10210
+ warnings,
10211
+ nextSteps
10212
+ };
10213
+ if (outputJson) {
10214
+ process.stdout.write(`${JSON.stringify(jsonPayload, null, 2)}
10215
+ `);
10216
+ process.exit(exitCode);
10217
+ return;
10218
+ }
10219
+ console.log("");
10220
+ console.log(BOLD("RunTrim") + DIM(" CI Check"));
10221
+ console.log("");
10222
+ const verdictColor = verdict === "PASS" ? chalk.green : verdict === "WARN" ? chalk.yellow : chalk.red;
10223
+ console.log(DIM(" Verdict: ") + verdictColor(verdict));
10224
+ console.log("");
10225
+ console.log(DIM(" Changed files:"));
10226
+ if (changedFiles.length === 0) console.log(chalk.white(" - (none detected)"));
10227
+ for (const f of changedFiles.slice(0, 20)) console.log(chalk.white(" - " + f));
10228
+ if (changedFiles.length > 20) console.log(DIM(` ... and ${changedFiles.length - 20} more`));
10229
+ console.log("");
10230
+ if (issues.length > 0) {
10231
+ console.log(DIM(" Issues:"));
10232
+ for (const i of issues) console.log(chalk.red(" - " + i));
10233
+ console.log("");
10234
+ }
10235
+ if (warnings.length > 0) {
10236
+ console.log(DIM(" Warnings:"));
10237
+ for (const w of warnings) console.log(chalk.yellow(" - " + w));
10238
+ console.log("");
10239
+ }
10240
+ console.log(DIM(" Next:"));
10241
+ for (const n of nextSteps) console.log(chalk.white(" - " + n));
10242
+ if (report) {
10243
+ console.log("");
10244
+ console.log(DIM(" Report:"));
10245
+ console.log(chalk.white(` - Base: ${detection.baseUsed}`));
10246
+ console.log(chalk.white(` - Head: ${detection.headUsed}`));
10247
+ console.log(chalk.white(` - Contract: ${contract.exists ? contract.active ? "active" : "present" : "none"}`));
10248
+ console.log(chalk.white(` - Latest run: ${latestRun ? latestRun.status : "none"}`));
10249
+ }
10250
+ console.log("");
10251
+ process.exit(exitCode);
10252
+ });
9657
10253
  var authCommand = program.command("auth").description("Configure sync token for dashboard sync");
9658
10254
  authCommand.command("set <token>").description("Set sync token and enable sync").action((token) => {
9659
10255
  const cwd = process.cwd();
@@ -9795,6 +10391,214 @@ repoCommand.command("unlink").description("Unlink tracked repo from local free-p
9795
10391
  console.log(DIM(" No tracked repo found."));
9796
10392
  console.log("");
9797
10393
  });
10394
+ 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) => {
10395
+ var _a2;
10396
+ const cwd = process.cwd();
10397
+ const dryRun = (options == null ? void 0 : options.dryRun) === true;
10398
+ const keep = Math.max(1, Number.parseInt((_a2 = options == null ? void 0 : options.keep) != null ? _a2 : "10", 10) || 10);
10399
+ ensureInternalArtifactDirs(cwd);
10400
+ const runs = loadAllRuns(cwd);
10401
+ const keepRunIds = new Set(runs.slice(0, keep).map((r) => r.id));
10402
+ const files = listArtifactFiles(cwd);
10403
+ const removable = files.filter((filePath) => {
10404
+ const runId = parseRunIdFromArtifact(filePath);
10405
+ if (!runId) return true;
10406
+ return !keepRunIds.has(runId);
10407
+ });
10408
+ const byCategory = {
10409
+ runs: 0,
10410
+ previews: 0,
10411
+ restores: 0,
10412
+ archives: 0,
10413
+ other: 0
10414
+ };
10415
+ for (const filePath of removable) {
10416
+ const rel = import_path13.default.relative(cwd, filePath).replace(/\\/g, "/").toLowerCase();
10417
+ if (rel.includes(".runtrim/internal/runs/") || rel.includes(".runtrim/runs/")) byCategory.runs += 1;
10418
+ else if (rel.includes(".runtrim/internal/previews/") || rel.includes(".runtrim/previews/")) byCategory.previews += 1;
10419
+ else if (rel.includes(".runtrim/internal/restores/") || rel.includes(".runtrim/restores/")) byCategory.restores += 1;
10420
+ else if (rel.includes("archive")) byCategory.archives += 1;
10421
+ else byCategory.other += 1;
10422
+ }
10423
+ if (!dryRun) {
10424
+ for (const filePath of removable) {
10425
+ try {
10426
+ import_fs13.default.rmSync(filePath, { force: true });
10427
+ } catch (e) {
10428
+ }
10429
+ }
10430
+ }
10431
+ console.log("");
10432
+ console.log(BOLD("RunTrim") + DIM(" clean"));
10433
+ console.log("");
10434
+ console.log(DIM(" Mode ") + chalk.white(dryRun ? "dry-run" : "apply"));
10435
+ console.log(DIM(" Keep latest runs ") + chalk.white(String(keep)));
10436
+ console.log(DIM(" Candidate files ") + chalk.white(String(removable.length)));
10437
+ console.log("");
10438
+ console.log(DIM(" Cleanup summary"));
10439
+ console.log(chalk.white(` - runs: ${byCategory.runs}`));
10440
+ console.log(chalk.white(` - previews: ${byCategory.previews}`));
10441
+ console.log(chalk.white(` - restores: ${byCategory.restores}`));
10442
+ console.log(chalk.white(` - archives: ${byCategory.archives}`));
10443
+ if (byCategory.other > 0) console.log(chalk.white(` - other: ${byCategory.other}`));
10444
+ console.log("");
10445
+ if (removable.length > 0) {
10446
+ const sample = removable.slice(0, 10).map((p) => import_path13.default.relative(cwd, p).replace(/\\/g, "/"));
10447
+ console.log(DIM(" Sample files"));
10448
+ for (const rel of sample) console.log(chalk.white(` - ${rel}`));
10449
+ if (removable.length > sample.length) {
10450
+ console.log(DIM(` ... and ${removable.length - sample.length} more`));
10451
+ }
10452
+ console.log("");
10453
+ }
10454
+ if (dryRun) {
10455
+ console.log(chalk.white(" No files were removed."));
10456
+ console.log(chalk.white(" Re-run without --dry-run to apply cleanup."));
10457
+ } else {
10458
+ console.log(chalk.white(" Cleanup complete."));
10459
+ console.log(chalk.white(" Active contract, latest files, memory current, and MCP snippets were preserved."));
10460
+ }
10461
+ console.log("");
10462
+ });
10463
+ var restoreCommand = program.command("restore").description("Preview or apply local restore for guarded runs");
10464
+ 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) => {
10465
+ var _a2, _b, _c, _d;
10466
+ const cwd = process.cwd();
10467
+ const doPreview = (options == null ? void 0 : options.preview) === true;
10468
+ const doApply = (options == null ? void 0 : options.apply) === true;
10469
+ const force = (options == null ? void 0 : options.force) === true;
10470
+ if (!doPreview && !doApply) {
10471
+ console.log("");
10472
+ console.log(chalk.yellow("Choose --preview or --apply."));
10473
+ console.log("");
10474
+ return;
10475
+ }
10476
+ if (doPreview && doApply) {
10477
+ console.log("");
10478
+ console.log(chalk.yellow("Use either --preview or --apply, not both."));
10479
+ console.log("");
10480
+ return;
10481
+ }
10482
+ const runs = loadAllRuns(cwd);
10483
+ const runIdInput = (runIdArg != null ? runIdArg : "last").trim();
10484
+ let targetRunId = runIdInput;
10485
+ if (runIdInput === "last") {
10486
+ const latest = runs.find((r) => r.status === "completed" || r.status === "guarded" || r.status === "executed" || r.status === "checked");
10487
+ if (!latest) {
10488
+ console.log("");
10489
+ console.log(chalk.yellow("No runs available for restore."));
10490
+ console.log("");
10491
+ return;
10492
+ }
10493
+ targetRunId = latest.id;
10494
+ }
10495
+ const run = runs.find((r) => r.id === targetRunId);
10496
+ if (!run) {
10497
+ console.log("");
10498
+ console.log(chalk.yellow(`Run not found: ${targetRunId}`));
10499
+ console.log("");
10500
+ return;
10501
+ }
10502
+ const restore = loadRestorePoint(cwd, targetRunId);
10503
+ if (!restore) {
10504
+ console.log("");
10505
+ console.log(chalk.yellow("No restore point found for this run."));
10506
+ console.log("");
10507
+ return;
10508
+ }
10509
+ const postFiles = (_b = (_a2 = restore.postRun) == null ? void 0 : _a2.changedFiles) != null ? _b : [];
10510
+ const sensitive = postFiles.filter((f) => isSensitivePath(f) || isSecretLikePath(f));
10511
+ const safeFiles = postFiles.filter((f) => !sensitive.includes(f));
10512
+ const gitAvailable = await isGitRepo(cwd);
10513
+ const method = gitAvailable && restore.preRun.commit ? "git checkout from pre-run commit" : "metadata-only (limited)";
10514
+ const nowChanged = gitAvailable ? dedupeFiles((await getGitChangedFiles(cwd)).map((f) => f.path)) : [];
10515
+ const unrelated = nowChanged.filter((f) => !postFiles.includes(f));
10516
+ if (doPreview) {
10517
+ console.log("");
10518
+ console.log(BOLD("RunTrim") + DIM(" restore preview"));
10519
+ console.log("");
10520
+ console.log(DIM(" Run ID ") + chalk.white(targetRunId));
10521
+ console.log(DIM(" Task ") + chalk.white(truncate(run.task, 80)));
10522
+ console.log(DIM(" Method ") + chalk.white(method));
10523
+ console.log(DIM(" Verdict ") + chalk.white((_d = (_c = restore.postRun) == null ? void 0 : _c.finishVerdict) != null ? _d : "unknown"));
10524
+ console.log("");
10525
+ console.log(DIM(" Files to restore"));
10526
+ if (safeFiles.length === 0) console.log(chalk.white(" - (none)"));
10527
+ for (const f of safeFiles.slice(0, 20)) console.log(chalk.white(" - " + f));
10528
+ if (safeFiles.length > 20) console.log(DIM(` ... and ${safeFiles.length - 20} more`));
10529
+ if (sensitive.length > 0) {
10530
+ console.log("");
10531
+ console.log(DIM(" Sensitive files (listed by path only)"));
10532
+ for (const f of sensitive.slice(0, 20)) console.log(chalk.yellow(" - " + f));
10533
+ console.log(chalk.yellow(" Sensitive files are skipped by default and not auto-restored."));
10534
+ }
10535
+ if (unrelated.length > 0) {
10536
+ console.log("");
10537
+ console.log(chalk.yellow(" Warning: repo has new changes after this run."));
10538
+ console.log(chalk.yellow(" Use --apply --force or review manually before restore."));
10539
+ }
10540
+ console.log("");
10541
+ return;
10542
+ }
10543
+ if (!doApply) return;
10544
+ if (!gitAvailable || !restore.preRun.commit) {
10545
+ console.log("");
10546
+ console.log(chalk.red("Restore apply requires git and a pre-run commit restore point."));
10547
+ console.log("");
10548
+ process.exit(1);
10549
+ return;
10550
+ }
10551
+ if (unrelated.length > 0 && !force) {
10552
+ console.log("");
10553
+ console.log(chalk.red("Restore blocked: new unrelated changes detected after this run."));
10554
+ console.log(chalk.red("Re-run with --apply --force after manual review."));
10555
+ console.log("");
10556
+ process.exit(1);
10557
+ return;
10558
+ }
10559
+ const restored = [];
10560
+ const skippedSensitive = [...sensitive];
10561
+ const failed = [];
10562
+ for (const file of safeFiles) {
10563
+ try {
10564
+ let existedBefore = false;
10565
+ try {
10566
+ await (0, import_execa3.execa)("git", ["cat-file", "-e", `${restore.preRun.commit}:${file}`], { cwd });
10567
+ existedBefore = true;
10568
+ } catch (e) {
10569
+ existedBefore = false;
10570
+ }
10571
+ if (existedBefore) {
10572
+ await (0, import_execa3.execa)("git", ["checkout", restore.preRun.commit, "--", file], { cwd });
10573
+ } else if (import_fs13.default.existsSync(import_path13.default.join(cwd, file))) {
10574
+ import_fs13.default.rmSync(import_path13.default.join(cwd, file), { force: true });
10575
+ }
10576
+ restored.push(file);
10577
+ } catch (e) {
10578
+ failed.push(file);
10579
+ }
10580
+ }
10581
+ const report = {
10582
+ runId: targetRunId,
10583
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
10584
+ restored,
10585
+ skippedSensitive,
10586
+ failed,
10587
+ forced: force
10588
+ };
10589
+ const reportPath = import_path13.default.join(getRestoresDir(cwd), `${targetRunId}.report.${Date.now()}.json`);
10590
+ import_fs13.default.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
10591
+ console.log("");
10592
+ console.log(BOLD("RunTrim") + DIM(" restore apply"));
10593
+ console.log("");
10594
+ console.log(DIM(" Run ID ") + chalk.white(targetRunId));
10595
+ console.log(DIM(" Restored files ") + chalk.white(String(restored.length)));
10596
+ console.log(DIM(" Skipped sensitive ") + chalk.white(String(skippedSensitive.length)));
10597
+ console.log(DIM(" Failed ") + chalk.white(String(failed.length)));
10598
+ console.log(DIM(" Report ") + chalk.white(import_path13.default.relative(cwd, reportPath)));
10599
+ console.log("");
10600
+ if (failed.length > 0) process.exit(1);
10601
+ });
9798
10602
  program.command("guard <task>").description("Audit a task and generate a guarded run contract").action(async (task) => {
9799
10603
  var _a2, _b, _c;
9800
10604
  const cwd = process.cwd();
@@ -9882,7 +10686,7 @@ program.command("guard <task>").description("Audit a task and generate a guarded
9882
10686
  const run2 = saveRun(task, audit, contract, cwd);
9883
10687
  updateRun(run2.id, { status: "blocked" }, cwd);
9884
10688
  console.log("");
9885
- console.log(DIM(" Run saved: .runtrim/runs/" + run2.id + ".json (status: blocked)"));
10689
+ console.log(DIM(" Run saved: .runtrim/internal/runs/" + run2.id + ".json (status: blocked)"));
9886
10690
  console.log("");
9887
10691
  console.log(DIM(" Next: ") + chalk.white('runtrim guard "<one system at a time>"'));
9888
10692
  console.log("");
@@ -9962,7 +10766,7 @@ program.command("guard <task>").description("Audit a task and generate a guarded
9962
10766
  }
9963
10767
  const run = saveRun(task, audit, contract, cwd);
9964
10768
  console.log("");
9965
- console.log(DIM(" Run saved: .runtrim/runs/" + run.id + ".json"));
10769
+ console.log(DIM(" Run saved: .runtrim/internal/runs/" + run.id + ".json"));
9966
10770
  console.log("");
9967
10771
  console.log(DIM(" After your agent run: ") + chalk.white("runtrim check"));
9968
10772
  console.log("");
@@ -10012,6 +10816,7 @@ program.command("run <task>").description("Guard then run configured local agent
10012
10816
  },
10013
10817
  cwd
10014
10818
  );
10819
+ await captureRestorePoint(cwd, run.id, task);
10015
10820
  const copySavings = estimateSavingsFromTokens2(
10016
10821
  parseEstimatedNumber3(String(contract.estimatedTokensTrimmed))
10017
10822
  );
@@ -10092,6 +10897,7 @@ program.command("run <task>").description("Guard then run configured local agent
10092
10897
  },
10093
10898
  cwd
10094
10899
  );
10900
+ await captureRestorePoint(cwd, run.id, task);
10095
10901
  console.log(DIM(" Execution cancelled. Guarded contract copied to clipboard."));
10096
10902
  console.log("");
10097
10903
  return;
@@ -10148,6 +10954,7 @@ program.command("run <task>").description("Guard then run configured local agent
10148
10954
  },
10149
10955
  cwd
10150
10956
  );
10957
+ await captureRestorePoint(cwd, run.id, task);
10151
10958
  console.log("");
10152
10959
  console.log(chalk.yellow(" Agent command not found. Falling back to copy mode guidance."));
10153
10960
  console.log(DIM(" Configure with: npm run runtrim -- agent set claude|codex|custom"));
@@ -10181,6 +10988,7 @@ program.command("run <task>").description("Guard then run configured local agent
10181
10988
  },
10182
10989
  cwd
10183
10990
  );
10991
+ await captureRestorePoint(cwd, run.id, task);
10184
10992
  console.log("");
10185
10993
  console.log(chalk.yellow(" Agent command not found. Falling back to copy mode guidance."));
10186
10994
  console.log(DIM(" Configure with: npm run runtrim -- agent set claude|codex|custom"));
@@ -10224,7 +11032,7 @@ ${stderr}`, "utf-8");
10224
11032
  exitCode,
10225
11033
  stdoutPreview: stdout.slice(0, OUTPUT_PREVIEW_MAX),
10226
11034
  stderrPreview: stderr.slice(0, OUTPUT_PREVIEW_MAX),
10227
- outputPath: `.runtrim/runs/${run.id}.output.txt`
11035
+ outputPath: `.runtrim/internal/runs/${run.id}.output.txt`
10228
11036
  },
10229
11037
  evaluation
10230
11038
  },
@@ -10374,6 +11182,7 @@ program.command("go <task>").description("Bridge Mode: generate a scoped contrac
10374
11182
  memoryUsed,
10375
11183
  providerRouting
10376
11184
  }, cwd);
11185
+ await captureRestorePoint(cwd, run.id, task);
10377
11186
  const bridgeCtx = deriveBridgeContext(task, contract, runs, projectName);
10378
11187
  let bridgeWritten = [];
10379
11188
  let bridgeManagedPaths = [];
@@ -10432,7 +11241,7 @@ program.command("go <task>").description("Bridge Mode: generate a scoped contrac
10432
11241
  if (contract.contract.stopRules.length > 0) {
10433
11242
  console.log(DIM(" Stop rule ") + chalk.white(truncate((_g = contract.contract.stopRules[contract.contract.stopRules.length - 1]) != null ? _g : "", 60)));
10434
11243
  }
10435
- console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
11244
+ console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
10436
11245
  console.log(DIM(" Contract ") + chalk.white("created"));
10437
11246
  console.log("");
10438
11247
  if (bridgeWritten.length > 0) {
@@ -11787,6 +12596,18 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
11787
12596
  const blockedByExistingHard = scope.forbiddenFiles.length > 0 || scope.status === "limit_exceeded";
11788
12597
  const warnBySensitiveIgnored = sensitiveIgnored.length > 0;
11789
12598
  const finishVerdict = blockedBySensitive || blockedByContract || blockedByExistingHard ? "BLOCKED" : warnBySensitiveIgnored || scopeDriftStatus !== "passed" || evaluation.status === "needs_verification" || evaluation.status === "partial" ? "WARN" : "PASS";
12599
+ const restoreRecord = loadRestorePoint(cwd, activeRun.id);
12600
+ if (restoreRecord) {
12601
+ restoreRecord.postRun = {
12602
+ changedFiles,
12603
+ forbiddenFiles: [...scope.forbiddenFiles, ...forbiddenPathFiles],
12604
+ sensitiveFiles: scope.sensitiveFiles,
12605
+ outOfScopeFiles: [...outOfContractFiles, ...scope.outOfScopeFiles],
12606
+ finishVerdict,
12607
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
12608
+ };
12609
+ saveRestorePoint(cwd, restoreRecord);
12610
+ }
11790
12611
  const verdictColor = finishVerdict === "PASS" ? chalk.green : finishVerdict === "WARN" ? chalk.yellow : chalk.red;
11791
12612
  const scopeColor = scopeDriftStatus === "passed" ? chalk.green : scopeDriftStatus === "forbidden_touched" ? chalk.red : chalk.yellow;
11792
12613
  const riskAfter = (_m = activeRun.contract.wasteRiskAfter) != null ? _m : "medium";