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.
- package/README.md +104 -76
- package/dist-cli/runtrim.cjs +872 -51
- package/dist-cli/runtrim.js +872 -51
- package/package.json +1 -1
package/dist-cli/runtrim.js
CHANGED
|
@@ -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
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
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(
|
|
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
|
|
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
|
|
1578
|
-
|
|
1579
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
-
"-
|
|
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
|
-
"-
|
|
6911
|
-
"-
|
|
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
|
-
'-
|
|
6914
|
-
"- At the end, run
|
|
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
|
-
"-
|
|
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
|
|
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
|
-
|
|
8225
|
-
|
|
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("");
|
|
@@ -8448,6 +8799,9 @@ program.command("start").description("Guided RunTrim onboarding and daily loop")
|
|
|
8448
8799
|
console.log(chalk.white(" runtrim mcp config --print"));
|
|
8449
8800
|
console.log(chalk.white(" runtrim mcp start"));
|
|
8450
8801
|
console.log("");
|
|
8802
|
+
console.log(DIM(" Check readiness"));
|
|
8803
|
+
console.log(chalk.white(" runtrim doctor"));
|
|
8804
|
+
console.log("");
|
|
8451
8805
|
console.log(DIM(" Adapters"));
|
|
8452
8806
|
console.log(chalk.white(" runtrim adapters"));
|
|
8453
8807
|
console.log(chalk.white(" runtrim adapters status"));
|
|
@@ -8529,6 +8883,108 @@ program.command("start").description("Guided RunTrim onboarding and daily loop")
|
|
|
8529
8883
|
console.log("");
|
|
8530
8884
|
}
|
|
8531
8885
|
});
|
|
8886
|
+
program.command("doctor").description("Check whether the current project is RunTrim-ready for agents, MCP, memory and finish verification").action(async () => {
|
|
8887
|
+
const cwd = process.cwd();
|
|
8888
|
+
const repoCheck = await assertFreeRepoAllowed(cwd);
|
|
8889
|
+
const profilePath = path13.join(getConfigDir(cwd), "project-profile.json");
|
|
8890
|
+
const memoryPath = path13.join(getConfigDir(cwd), "memory", "current.md");
|
|
8891
|
+
const instructionsPath = path13.join(getConfigDir(cwd), "agent", "instructions.md");
|
|
8892
|
+
const snippetsDir = getProjectMcpDir(cwd);
|
|
8893
|
+
const snippetFiles = [
|
|
8894
|
+
path13.join(snippetsDir, "claude-desktop.json"),
|
|
8895
|
+
path13.join(snippetsDir, "cursor.json"),
|
|
8896
|
+
path13.join(snippetsDir, "generic.json")
|
|
8897
|
+
];
|
|
8898
|
+
const profileReady = fs13.existsSync(profilePath);
|
|
8899
|
+
const memoryReady = fs13.existsSync(memoryPath) && fs13.readFileSync(memoryPath, "utf-8").trim().length > 0;
|
|
8900
|
+
const instructionsReady = fs13.existsSync(instructionsPath) && fs13.readFileSync(instructionsPath, "utf-8").trim().length > 0;
|
|
8901
|
+
const claudeBlock = hasCurrentRuntrimBlock(path13.join(cwd, "CLAUDE.md"));
|
|
8902
|
+
const agentsBlock = hasCurrentRuntrimBlock(path13.join(cwd, "AGENTS.md"));
|
|
8903
|
+
const cursorRulePath = path13.join(cwd, ".cursor", "rules", "runtrim.mdc");
|
|
8904
|
+
const cursorRuleExists = fs13.existsSync(cursorRulePath);
|
|
8905
|
+
const cursorRuleCurrent = cursorRuleExists ? fs13.readFileSync(cursorRulePath, "utf-8").includes(`RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`) : false;
|
|
8906
|
+
const anySurfaceInstalled = claudeBlock.exists || agentsBlock.exists || cursorRuleExists;
|
|
8907
|
+
const runtrimBlockCurrent = [claudeBlock.current, agentsBlock.current, cursorRuleCurrent].some(Boolean);
|
|
8908
|
+
const snippetsGenerated = snippetFiles.every((p) => fs13.existsSync(p));
|
|
8909
|
+
const knownMcp = detectKnownMcpConfigPresence();
|
|
8910
|
+
const claudeMcpState = detectMcpConfigState(knownMcp.claudeConfigPath, knownMcp.claudeConfigFound);
|
|
8911
|
+
const cursorMcpState = detectMcpConfigState(knownMcp.cursorConfigPath, knownMcp.cursorConfigFound);
|
|
8912
|
+
const genericReady = snippetsGenerated ? "ready" : "missing";
|
|
8913
|
+
const tools = buildMcpTools();
|
|
8914
|
+
const hasContractTool = tools.some((t) => t.name === "runtrim_create_contract");
|
|
8915
|
+
const hasPathTool = tools.some((t) => t.name === "runtrim_check_path");
|
|
8916
|
+
const hasApprovalTool = tools.some((t) => t.name === "runtrim_suggest_approval");
|
|
8917
|
+
const hasFinishTool = tools.some((t) => t.name === "runtrim_finish_guidance");
|
|
8918
|
+
const contract = parseContractSummary(cwd);
|
|
8919
|
+
const lastMcp = readMcpLastUsed(cwd);
|
|
8920
|
+
const setupCorrupt = configExists(cwd) && (!profileReady || !memoryReady || !instructionsReady);
|
|
8921
|
+
const artifactFiles = listArtifactFiles(cwd);
|
|
8922
|
+
const artifactCount = artifactFiles.length;
|
|
8923
|
+
let readiness = "partial";
|
|
8924
|
+
if (!repoCheck.allowed || setupCorrupt) {
|
|
8925
|
+
readiness = "blocked";
|
|
8926
|
+
} else if (profileReady && memoryReady && instructionsReady && anySurfaceInstalled && snippetsGenerated && hasContractTool && hasPathTool && hasApprovalTool && hasFinishTool) {
|
|
8927
|
+
readiness = "ready";
|
|
8928
|
+
}
|
|
8929
|
+
console.log("");
|
|
8930
|
+
console.log(BOLD("RunTrim") + DIM(" doctor"));
|
|
8931
|
+
console.log("");
|
|
8932
|
+
console.log(BOLD("Project"));
|
|
8933
|
+
console.log(chalk.white(`- Project profile: ${profileReady ? "ready" : "missing"}`));
|
|
8934
|
+
console.log(chalk.white(`- Project memory: ${memoryReady ? "ready" : "missing"}`));
|
|
8935
|
+
console.log(chalk.white(`- Agent instructions: ${instructionsReady ? "ready" : "missing"}`));
|
|
8936
|
+
console.log(chalk.white(`- Active contract: ${contract.active ? "active" : "none"}`));
|
|
8937
|
+
console.log("");
|
|
8938
|
+
console.log(BOLD("Agent rules"));
|
|
8939
|
+
console.log(chalk.white(`- CLAUDE.md: ${claudeBlock.exists ? "installed" : "not found"}`));
|
|
8940
|
+
console.log(chalk.white(`- AGENTS.md: ${agentsBlock.exists ? "installed" : "not found"}`));
|
|
8941
|
+
console.log(chalk.white(`- Cursor rules: ${cursorRuleExists ? "installed" : "not found"}`));
|
|
8942
|
+
console.log(chalk.white(`- RunTrim instruction block: ${runtrimBlockCurrent ? "current" : anySurfaceInstalled ? "stale" : "missing"}`));
|
|
8943
|
+
console.log("");
|
|
8944
|
+
console.log(BOLD("MCP"));
|
|
8945
|
+
console.log(chalk.white("- MCP server: available"));
|
|
8946
|
+
console.log(chalk.white(`- Project snippets: ${snippetsGenerated ? "generated" : "missing"}`));
|
|
8947
|
+
console.log(chalk.white(`- Claude Desktop config: ${claudeMcpState}`));
|
|
8948
|
+
console.log(chalk.white(`- Cursor MCP config: ${cursorMcpState}`));
|
|
8949
|
+
console.log(chalk.white(`- Generic config: ${genericReady}`));
|
|
8950
|
+
if (lastMcp.tracked && lastMcp.tool && lastMcp.usedAt) {
|
|
8951
|
+
console.log(chalk.white(`- Last MCP tool call: ${lastMcp.tool}, ${lastMcp.usedAt}`));
|
|
8952
|
+
} else {
|
|
8953
|
+
console.log(chalk.white("- Last MCP tool call: not tracked yet"));
|
|
8954
|
+
}
|
|
8955
|
+
console.log("");
|
|
8956
|
+
console.log(BOLD("Automation readiness"));
|
|
8957
|
+
console.log(chalk.white(`- Contract creation tool: ${hasContractTool ? "available" : "missing"}`));
|
|
8958
|
+
console.log(chalk.white(`- Path check tool: ${hasPathTool ? "available" : "missing"}`));
|
|
8959
|
+
console.log(chalk.white(`- Approval tool: ${hasApprovalTool ? "available" : "missing"}`));
|
|
8960
|
+
console.log(chalk.white(`- Finish guidance tool: ${hasFinishTool ? "available" : "missing"}`));
|
|
8961
|
+
console.log(chalk.white(`- Local artifacts: ${artifactCount}`));
|
|
8962
|
+
console.log("");
|
|
8963
|
+
console.log(BOLD("Readiness"));
|
|
8964
|
+
console.log(chalk.white(`- State: ${readiness}`));
|
|
8965
|
+
console.log("");
|
|
8966
|
+
console.log(BOLD("Next"));
|
|
8967
|
+
if (repoCheck.status === "blocked_repair") {
|
|
8968
|
+
console.log(chalk.yellow("- RunTrim local state needs repair. Run runtrim repo repair."));
|
|
8969
|
+
} else if (repoCheck.status === "blocked_limit") {
|
|
8970
|
+
console.log(chalk.yellow("- Free includes 1 tracked repo. Continue in tracked repo, unlink with runtrim repo unlink --force, or upgrade to Builder."));
|
|
8971
|
+
} else if (readiness === "ready") {
|
|
8972
|
+
if (claudeMcpState !== "configured" && cursorMcpState !== "configured") {
|
|
8973
|
+
console.log(chalk.white("- Ready locally, MCP client not connected."));
|
|
8974
|
+
console.log(chalk.white("- Run runtrim mcp instructions or copy .runtrim/mcp/cursor.json into your MCP client."));
|
|
8975
|
+
} else {
|
|
8976
|
+
console.log(chalk.white("- Your project is RunTrim-aware. Open Cursor/Claude/Codex and use normal language."));
|
|
8977
|
+
}
|
|
8978
|
+
} else {
|
|
8979
|
+
console.log(chalk.white("- Run runtrim start."));
|
|
8980
|
+
console.log(chalk.white("- Run runtrim mcp instructions."));
|
|
8981
|
+
console.log(chalk.white("- Run runtrim mcp config --print."));
|
|
8982
|
+
}
|
|
8983
|
+
if (artifactCount > 25) {
|
|
8984
|
+
console.log(chalk.white(`- Local artifacts: ${artifactCount}. Run runtrim clean --dry-run to review cleanup.`));
|
|
8985
|
+
}
|
|
8986
|
+
console.log("");
|
|
8987
|
+
});
|
|
8532
8988
|
var PROTOCOL_BLOCK_START = "<!-- RUNTRIM_PROTOCOL_START -->";
|
|
8533
8989
|
var PROTOCOL_BLOCK_END = "<!-- RUNTRIM_PROTOCOL_END -->";
|
|
8534
8990
|
var PROTOCOL_POINTER_BLOCK = `
|
|
@@ -9633,6 +10089,146 @@ agentCommand.command("prompt-mode <mode>").description("Set how guarded contract
|
|
|
9633
10089
|
console.log(ACCENT.bold(" Agent prompt mode updated to: " + m));
|
|
9634
10090
|
console.log("");
|
|
9635
10091
|
});
|
|
10092
|
+
var ciCommand = program.command("ci").description("RunTrim CI checks for pull requests and merges");
|
|
10093
|
+
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) => {
|
|
10094
|
+
const cwd = process.cwd();
|
|
10095
|
+
const strict = options.strict === true;
|
|
10096
|
+
const allowWarn = options.allowWarn === true;
|
|
10097
|
+
const outputJson = options.json === true;
|
|
10098
|
+
const report = options.report === true;
|
|
10099
|
+
const repoCheck = await assertFreeRepoAllowed(cwd);
|
|
10100
|
+
const detection = await detectCiChangedFiles(cwd, options.base, options.head);
|
|
10101
|
+
const changedFiles = detection.files;
|
|
10102
|
+
const contract = parseContractSummary(cwd);
|
|
10103
|
+
const latestRun = loadLatestRun(cwd);
|
|
10104
|
+
const hasRuntrimContext = contract.exists || Boolean(latestRun);
|
|
10105
|
+
const issues = [];
|
|
10106
|
+
const warnings = [...detection.warnings];
|
|
10107
|
+
const nextSteps = [];
|
|
10108
|
+
let verdict = "PASS";
|
|
10109
|
+
if (repoCheck.status === "blocked_repair") {
|
|
10110
|
+
issues.push("RunTrim local state needs repair before CI can trust local guard state.");
|
|
10111
|
+
nextSteps.push("Run: runtrim repo repair");
|
|
10112
|
+
verdict = "BLOCKED";
|
|
10113
|
+
} else if (repoCheck.status === "blocked_limit") {
|
|
10114
|
+
issues.push("Free includes 1 tracked repo and this repo is not currently tracked.");
|
|
10115
|
+
nextSteps.push("Continue in tracked repo, unlink with runtrim repo unlink --force, or upgrade to Builder.");
|
|
10116
|
+
verdict = "BLOCKED";
|
|
10117
|
+
}
|
|
10118
|
+
if (changedFiles.length === 0) {
|
|
10119
|
+
warnings.push("No changed files detected for this diff.");
|
|
10120
|
+
}
|
|
10121
|
+
const secretFiles = changedFiles.filter(isSecretLikePath);
|
|
10122
|
+
if (secretFiles.length > 0) {
|
|
10123
|
+
issues.push(`Sensitive file path changed: ${secretFiles[0]}`);
|
|
10124
|
+
nextSteps.push("Remove secret/env/key/cert files from this PR before merge.");
|
|
10125
|
+
verdict = "BLOCKED";
|
|
10126
|
+
}
|
|
10127
|
+
const highRiskFiles = changedFiles.filter((f) => isHighRiskLogicPath(f) && !isSecretLikePath(f));
|
|
10128
|
+
if (highRiskFiles.length > 0) {
|
|
10129
|
+
if (!hasRuntrimContext) {
|
|
10130
|
+
issues.push(`High-risk path changed without RunTrim context: ${highRiskFiles[0]}`);
|
|
10131
|
+
nextSteps.push("Create a dedicated guarded run and finish it before merging.");
|
|
10132
|
+
verdict = "BLOCKED";
|
|
10133
|
+
} else if (contract.allowedPaths.length > 0 && !matchesAnyContractRule(highRiskFiles[0], contract.allowedPaths)) {
|
|
10134
|
+
issues.push(`High-risk path is outside contract allowed scope: ${highRiskFiles[0]}`);
|
|
10135
|
+
nextSteps.push(`Run: runtrim approve "Allow ${highRiskFiles[0]} for this run only"`);
|
|
10136
|
+
verdict = "BLOCKED";
|
|
10137
|
+
}
|
|
10138
|
+
}
|
|
10139
|
+
if (contract.forbiddenPaths.length > 0) {
|
|
10140
|
+
const forbiddenTouched = changedFiles.filter((f) => matchesAnyContractRule(f, contract.forbiddenPaths));
|
|
10141
|
+
if (forbiddenTouched.length > 0) {
|
|
10142
|
+
issues.push(`Contract-forbidden path touched: ${forbiddenTouched[0]}`);
|
|
10143
|
+
nextSteps.push("Split the task or get explicit scoped approval before merge.");
|
|
10144
|
+
verdict = "BLOCKED";
|
|
10145
|
+
}
|
|
10146
|
+
}
|
|
10147
|
+
if (contract.allowedPaths.length > 0) {
|
|
10148
|
+
const outOfScope = changedFiles.filter((f) => !matchesAnyContractRule(f, contract.allowedPaths));
|
|
10149
|
+
if (outOfScope.length > 0) {
|
|
10150
|
+
issues.push(`Changed file is outside latest contract scope: ${outOfScope[0]}`);
|
|
10151
|
+
nextSteps.push(`Run: runtrim approve "Allow ${outOfScope[0]} for this run only"`);
|
|
10152
|
+
verdict = "BLOCKED";
|
|
10153
|
+
}
|
|
10154
|
+
}
|
|
10155
|
+
const docsOnly = changedFiles.length > 0 && changedFiles.every((f) => isDocsLikePath(f));
|
|
10156
|
+
if (!hasRuntrimContext && verdict !== "BLOCKED") {
|
|
10157
|
+
if (docsOnly) {
|
|
10158
|
+
warnings.push("No RunTrim contract/report found. Docs-only change treated as low risk.");
|
|
10159
|
+
verdict = "WARN";
|
|
10160
|
+
} else {
|
|
10161
|
+
warnings.push("No RunTrim contract/report found. CI could not fully verify scope context.");
|
|
10162
|
+
verdict = "WARN";
|
|
10163
|
+
}
|
|
10164
|
+
}
|
|
10165
|
+
if (strict && !hasRuntrimContext && verdict !== "BLOCKED") {
|
|
10166
|
+
issues.push("Strict mode requires RunTrim contract/report context.");
|
|
10167
|
+
verdict = "BLOCKED";
|
|
10168
|
+
}
|
|
10169
|
+
if (verdict === "PASS" && warnings.length > 0) {
|
|
10170
|
+
verdict = "WARN";
|
|
10171
|
+
}
|
|
10172
|
+
if (nextSteps.length === 0) {
|
|
10173
|
+
if (verdict === "PASS") {
|
|
10174
|
+
nextSteps.push("Safe to merge under current RunTrim CI policy.");
|
|
10175
|
+
} else if (verdict === "WARN") {
|
|
10176
|
+
nextSteps.push("Run runtrim finish locally to strengthen verification context.");
|
|
10177
|
+
} else {
|
|
10178
|
+
nextSteps.push("Resolve blocked issues, then rerun runtrim ci check.");
|
|
10179
|
+
}
|
|
10180
|
+
}
|
|
10181
|
+
let exitCode = 0;
|
|
10182
|
+
if (verdict === "BLOCKED") exitCode = 1;
|
|
10183
|
+
if (verdict === "WARN" && strict && !allowWarn) exitCode = 1;
|
|
10184
|
+
const jsonPayload = {
|
|
10185
|
+
verdict: verdict.toLowerCase(),
|
|
10186
|
+
exitCode,
|
|
10187
|
+
changedFiles,
|
|
10188
|
+
issues,
|
|
10189
|
+
warnings,
|
|
10190
|
+
nextSteps
|
|
10191
|
+
};
|
|
10192
|
+
if (outputJson) {
|
|
10193
|
+
process.stdout.write(`${JSON.stringify(jsonPayload, null, 2)}
|
|
10194
|
+
`);
|
|
10195
|
+
process.exit(exitCode);
|
|
10196
|
+
return;
|
|
10197
|
+
}
|
|
10198
|
+
console.log("");
|
|
10199
|
+
console.log(BOLD("RunTrim") + DIM(" CI Check"));
|
|
10200
|
+
console.log("");
|
|
10201
|
+
const verdictColor = verdict === "PASS" ? chalk.green : verdict === "WARN" ? chalk.yellow : chalk.red;
|
|
10202
|
+
console.log(DIM(" Verdict: ") + verdictColor(verdict));
|
|
10203
|
+
console.log("");
|
|
10204
|
+
console.log(DIM(" Changed files:"));
|
|
10205
|
+
if (changedFiles.length === 0) console.log(chalk.white(" - (none detected)"));
|
|
10206
|
+
for (const f of changedFiles.slice(0, 20)) console.log(chalk.white(" - " + f));
|
|
10207
|
+
if (changedFiles.length > 20) console.log(DIM(` ... and ${changedFiles.length - 20} more`));
|
|
10208
|
+
console.log("");
|
|
10209
|
+
if (issues.length > 0) {
|
|
10210
|
+
console.log(DIM(" Issues:"));
|
|
10211
|
+
for (const i of issues) console.log(chalk.red(" - " + i));
|
|
10212
|
+
console.log("");
|
|
10213
|
+
}
|
|
10214
|
+
if (warnings.length > 0) {
|
|
10215
|
+
console.log(DIM(" Warnings:"));
|
|
10216
|
+
for (const w of warnings) console.log(chalk.yellow(" - " + w));
|
|
10217
|
+
console.log("");
|
|
10218
|
+
}
|
|
10219
|
+
console.log(DIM(" Next:"));
|
|
10220
|
+
for (const n of nextSteps) console.log(chalk.white(" - " + n));
|
|
10221
|
+
if (report) {
|
|
10222
|
+
console.log("");
|
|
10223
|
+
console.log(DIM(" Report:"));
|
|
10224
|
+
console.log(chalk.white(` - Base: ${detection.baseUsed}`));
|
|
10225
|
+
console.log(chalk.white(` - Head: ${detection.headUsed}`));
|
|
10226
|
+
console.log(chalk.white(` - Contract: ${contract.exists ? contract.active ? "active" : "present" : "none"}`));
|
|
10227
|
+
console.log(chalk.white(` - Latest run: ${latestRun ? latestRun.status : "none"}`));
|
|
10228
|
+
}
|
|
10229
|
+
console.log("");
|
|
10230
|
+
process.exit(exitCode);
|
|
10231
|
+
});
|
|
9636
10232
|
var authCommand = program.command("auth").description("Configure sync token for dashboard sync");
|
|
9637
10233
|
authCommand.command("set <token>").description("Set sync token and enable sync").action((token) => {
|
|
9638
10234
|
const cwd = process.cwd();
|
|
@@ -9774,6 +10370,214 @@ repoCommand.command("unlink").description("Unlink tracked repo from local free-p
|
|
|
9774
10370
|
console.log(DIM(" No tracked repo found."));
|
|
9775
10371
|
console.log("");
|
|
9776
10372
|
});
|
|
10373
|
+
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) => {
|
|
10374
|
+
var _a2;
|
|
10375
|
+
const cwd = process.cwd();
|
|
10376
|
+
const dryRun = (options == null ? void 0 : options.dryRun) === true;
|
|
10377
|
+
const keep = Math.max(1, Number.parseInt((_a2 = options == null ? void 0 : options.keep) != null ? _a2 : "10", 10) || 10);
|
|
10378
|
+
ensureInternalArtifactDirs(cwd);
|
|
10379
|
+
const runs = loadAllRuns(cwd);
|
|
10380
|
+
const keepRunIds = new Set(runs.slice(0, keep).map((r) => r.id));
|
|
10381
|
+
const files = listArtifactFiles(cwd);
|
|
10382
|
+
const removable = files.filter((filePath) => {
|
|
10383
|
+
const runId = parseRunIdFromArtifact(filePath);
|
|
10384
|
+
if (!runId) return true;
|
|
10385
|
+
return !keepRunIds.has(runId);
|
|
10386
|
+
});
|
|
10387
|
+
const byCategory = {
|
|
10388
|
+
runs: 0,
|
|
10389
|
+
previews: 0,
|
|
10390
|
+
restores: 0,
|
|
10391
|
+
archives: 0,
|
|
10392
|
+
other: 0
|
|
10393
|
+
};
|
|
10394
|
+
for (const filePath of removable) {
|
|
10395
|
+
const rel = path13.relative(cwd, filePath).replace(/\\/g, "/").toLowerCase();
|
|
10396
|
+
if (rel.includes(".runtrim/internal/runs/") || rel.includes(".runtrim/runs/")) byCategory.runs += 1;
|
|
10397
|
+
else if (rel.includes(".runtrim/internal/previews/") || rel.includes(".runtrim/previews/")) byCategory.previews += 1;
|
|
10398
|
+
else if (rel.includes(".runtrim/internal/restores/") || rel.includes(".runtrim/restores/")) byCategory.restores += 1;
|
|
10399
|
+
else if (rel.includes("archive")) byCategory.archives += 1;
|
|
10400
|
+
else byCategory.other += 1;
|
|
10401
|
+
}
|
|
10402
|
+
if (!dryRun) {
|
|
10403
|
+
for (const filePath of removable) {
|
|
10404
|
+
try {
|
|
10405
|
+
fs13.rmSync(filePath, { force: true });
|
|
10406
|
+
} catch (e) {
|
|
10407
|
+
}
|
|
10408
|
+
}
|
|
10409
|
+
}
|
|
10410
|
+
console.log("");
|
|
10411
|
+
console.log(BOLD("RunTrim") + DIM(" clean"));
|
|
10412
|
+
console.log("");
|
|
10413
|
+
console.log(DIM(" Mode ") + chalk.white(dryRun ? "dry-run" : "apply"));
|
|
10414
|
+
console.log(DIM(" Keep latest runs ") + chalk.white(String(keep)));
|
|
10415
|
+
console.log(DIM(" Candidate files ") + chalk.white(String(removable.length)));
|
|
10416
|
+
console.log("");
|
|
10417
|
+
console.log(DIM(" Cleanup summary"));
|
|
10418
|
+
console.log(chalk.white(` - runs: ${byCategory.runs}`));
|
|
10419
|
+
console.log(chalk.white(` - previews: ${byCategory.previews}`));
|
|
10420
|
+
console.log(chalk.white(` - restores: ${byCategory.restores}`));
|
|
10421
|
+
console.log(chalk.white(` - archives: ${byCategory.archives}`));
|
|
10422
|
+
if (byCategory.other > 0) console.log(chalk.white(` - other: ${byCategory.other}`));
|
|
10423
|
+
console.log("");
|
|
10424
|
+
if (removable.length > 0) {
|
|
10425
|
+
const sample = removable.slice(0, 10).map((p) => path13.relative(cwd, p).replace(/\\/g, "/"));
|
|
10426
|
+
console.log(DIM(" Sample files"));
|
|
10427
|
+
for (const rel of sample) console.log(chalk.white(` - ${rel}`));
|
|
10428
|
+
if (removable.length > sample.length) {
|
|
10429
|
+
console.log(DIM(` ... and ${removable.length - sample.length} more`));
|
|
10430
|
+
}
|
|
10431
|
+
console.log("");
|
|
10432
|
+
}
|
|
10433
|
+
if (dryRun) {
|
|
10434
|
+
console.log(chalk.white(" No files were removed."));
|
|
10435
|
+
console.log(chalk.white(" Re-run without --dry-run to apply cleanup."));
|
|
10436
|
+
} else {
|
|
10437
|
+
console.log(chalk.white(" Cleanup complete."));
|
|
10438
|
+
console.log(chalk.white(" Active contract, latest files, memory current, and MCP snippets were preserved."));
|
|
10439
|
+
}
|
|
10440
|
+
console.log("");
|
|
10441
|
+
});
|
|
10442
|
+
var restoreCommand = program.command("restore").description("Preview or apply local restore for guarded runs");
|
|
10443
|
+
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) => {
|
|
10444
|
+
var _a2, _b, _c, _d;
|
|
10445
|
+
const cwd = process.cwd();
|
|
10446
|
+
const doPreview = (options == null ? void 0 : options.preview) === true;
|
|
10447
|
+
const doApply = (options == null ? void 0 : options.apply) === true;
|
|
10448
|
+
const force = (options == null ? void 0 : options.force) === true;
|
|
10449
|
+
if (!doPreview && !doApply) {
|
|
10450
|
+
console.log("");
|
|
10451
|
+
console.log(chalk.yellow("Choose --preview or --apply."));
|
|
10452
|
+
console.log("");
|
|
10453
|
+
return;
|
|
10454
|
+
}
|
|
10455
|
+
if (doPreview && doApply) {
|
|
10456
|
+
console.log("");
|
|
10457
|
+
console.log(chalk.yellow("Use either --preview or --apply, not both."));
|
|
10458
|
+
console.log("");
|
|
10459
|
+
return;
|
|
10460
|
+
}
|
|
10461
|
+
const runs = loadAllRuns(cwd);
|
|
10462
|
+
const runIdInput = (runIdArg != null ? runIdArg : "last").trim();
|
|
10463
|
+
let targetRunId = runIdInput;
|
|
10464
|
+
if (runIdInput === "last") {
|
|
10465
|
+
const latest = runs.find((r) => r.status === "completed" || r.status === "guarded" || r.status === "executed" || r.status === "checked");
|
|
10466
|
+
if (!latest) {
|
|
10467
|
+
console.log("");
|
|
10468
|
+
console.log(chalk.yellow("No runs available for restore."));
|
|
10469
|
+
console.log("");
|
|
10470
|
+
return;
|
|
10471
|
+
}
|
|
10472
|
+
targetRunId = latest.id;
|
|
10473
|
+
}
|
|
10474
|
+
const run = runs.find((r) => r.id === targetRunId);
|
|
10475
|
+
if (!run) {
|
|
10476
|
+
console.log("");
|
|
10477
|
+
console.log(chalk.yellow(`Run not found: ${targetRunId}`));
|
|
10478
|
+
console.log("");
|
|
10479
|
+
return;
|
|
10480
|
+
}
|
|
10481
|
+
const restore = loadRestorePoint(cwd, targetRunId);
|
|
10482
|
+
if (!restore) {
|
|
10483
|
+
console.log("");
|
|
10484
|
+
console.log(chalk.yellow("No restore point found for this run."));
|
|
10485
|
+
console.log("");
|
|
10486
|
+
return;
|
|
10487
|
+
}
|
|
10488
|
+
const postFiles = (_b = (_a2 = restore.postRun) == null ? void 0 : _a2.changedFiles) != null ? _b : [];
|
|
10489
|
+
const sensitive = postFiles.filter((f) => isSensitivePath(f) || isSecretLikePath(f));
|
|
10490
|
+
const safeFiles = postFiles.filter((f) => !sensitive.includes(f));
|
|
10491
|
+
const gitAvailable = await isGitRepo(cwd);
|
|
10492
|
+
const method = gitAvailable && restore.preRun.commit ? "git checkout from pre-run commit" : "metadata-only (limited)";
|
|
10493
|
+
const nowChanged = gitAvailable ? dedupeFiles((await getGitChangedFiles(cwd)).map((f) => f.path)) : [];
|
|
10494
|
+
const unrelated = nowChanged.filter((f) => !postFiles.includes(f));
|
|
10495
|
+
if (doPreview) {
|
|
10496
|
+
console.log("");
|
|
10497
|
+
console.log(BOLD("RunTrim") + DIM(" restore preview"));
|
|
10498
|
+
console.log("");
|
|
10499
|
+
console.log(DIM(" Run ID ") + chalk.white(targetRunId));
|
|
10500
|
+
console.log(DIM(" Task ") + chalk.white(truncate(run.task, 80)));
|
|
10501
|
+
console.log(DIM(" Method ") + chalk.white(method));
|
|
10502
|
+
console.log(DIM(" Verdict ") + chalk.white((_d = (_c = restore.postRun) == null ? void 0 : _c.finishVerdict) != null ? _d : "unknown"));
|
|
10503
|
+
console.log("");
|
|
10504
|
+
console.log(DIM(" Files to restore"));
|
|
10505
|
+
if (safeFiles.length === 0) console.log(chalk.white(" - (none)"));
|
|
10506
|
+
for (const f of safeFiles.slice(0, 20)) console.log(chalk.white(" - " + f));
|
|
10507
|
+
if (safeFiles.length > 20) console.log(DIM(` ... and ${safeFiles.length - 20} more`));
|
|
10508
|
+
if (sensitive.length > 0) {
|
|
10509
|
+
console.log("");
|
|
10510
|
+
console.log(DIM(" Sensitive files (listed by path only)"));
|
|
10511
|
+
for (const f of sensitive.slice(0, 20)) console.log(chalk.yellow(" - " + f));
|
|
10512
|
+
console.log(chalk.yellow(" Sensitive files are skipped by default and not auto-restored."));
|
|
10513
|
+
}
|
|
10514
|
+
if (unrelated.length > 0) {
|
|
10515
|
+
console.log("");
|
|
10516
|
+
console.log(chalk.yellow(" Warning: repo has new changes after this run."));
|
|
10517
|
+
console.log(chalk.yellow(" Use --apply --force or review manually before restore."));
|
|
10518
|
+
}
|
|
10519
|
+
console.log("");
|
|
10520
|
+
return;
|
|
10521
|
+
}
|
|
10522
|
+
if (!doApply) return;
|
|
10523
|
+
if (!gitAvailable || !restore.preRun.commit) {
|
|
10524
|
+
console.log("");
|
|
10525
|
+
console.log(chalk.red("Restore apply requires git and a pre-run commit restore point."));
|
|
10526
|
+
console.log("");
|
|
10527
|
+
process.exit(1);
|
|
10528
|
+
return;
|
|
10529
|
+
}
|
|
10530
|
+
if (unrelated.length > 0 && !force) {
|
|
10531
|
+
console.log("");
|
|
10532
|
+
console.log(chalk.red("Restore blocked: new unrelated changes detected after this run."));
|
|
10533
|
+
console.log(chalk.red("Re-run with --apply --force after manual review."));
|
|
10534
|
+
console.log("");
|
|
10535
|
+
process.exit(1);
|
|
10536
|
+
return;
|
|
10537
|
+
}
|
|
10538
|
+
const restored = [];
|
|
10539
|
+
const skippedSensitive = [...sensitive];
|
|
10540
|
+
const failed = [];
|
|
10541
|
+
for (const file of safeFiles) {
|
|
10542
|
+
try {
|
|
10543
|
+
let existedBefore = false;
|
|
10544
|
+
try {
|
|
10545
|
+
await execa3("git", ["cat-file", "-e", `${restore.preRun.commit}:${file}`], { cwd });
|
|
10546
|
+
existedBefore = true;
|
|
10547
|
+
} catch (e) {
|
|
10548
|
+
existedBefore = false;
|
|
10549
|
+
}
|
|
10550
|
+
if (existedBefore) {
|
|
10551
|
+
await execa3("git", ["checkout", restore.preRun.commit, "--", file], { cwd });
|
|
10552
|
+
} else if (fs13.existsSync(path13.join(cwd, file))) {
|
|
10553
|
+
fs13.rmSync(path13.join(cwd, file), { force: true });
|
|
10554
|
+
}
|
|
10555
|
+
restored.push(file);
|
|
10556
|
+
} catch (e) {
|
|
10557
|
+
failed.push(file);
|
|
10558
|
+
}
|
|
10559
|
+
}
|
|
10560
|
+
const report = {
|
|
10561
|
+
runId: targetRunId,
|
|
10562
|
+
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10563
|
+
restored,
|
|
10564
|
+
skippedSensitive,
|
|
10565
|
+
failed,
|
|
10566
|
+
forced: force
|
|
10567
|
+
};
|
|
10568
|
+
const reportPath = path13.join(getRestoresDir(cwd), `${targetRunId}.report.${Date.now()}.json`);
|
|
10569
|
+
fs13.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
10570
|
+
console.log("");
|
|
10571
|
+
console.log(BOLD("RunTrim") + DIM(" restore apply"));
|
|
10572
|
+
console.log("");
|
|
10573
|
+
console.log(DIM(" Run ID ") + chalk.white(targetRunId));
|
|
10574
|
+
console.log(DIM(" Restored files ") + chalk.white(String(restored.length)));
|
|
10575
|
+
console.log(DIM(" Skipped sensitive ") + chalk.white(String(skippedSensitive.length)));
|
|
10576
|
+
console.log(DIM(" Failed ") + chalk.white(String(failed.length)));
|
|
10577
|
+
console.log(DIM(" Report ") + chalk.white(path13.relative(cwd, reportPath)));
|
|
10578
|
+
console.log("");
|
|
10579
|
+
if (failed.length > 0) process.exit(1);
|
|
10580
|
+
});
|
|
9777
10581
|
program.command("guard <task>").description("Audit a task and generate a guarded run contract").action(async (task) => {
|
|
9778
10582
|
var _a2, _b, _c;
|
|
9779
10583
|
const cwd = process.cwd();
|
|
@@ -9861,7 +10665,7 @@ program.command("guard <task>").description("Audit a task and generate a guarded
|
|
|
9861
10665
|
const run2 = saveRun(task, audit, contract, cwd);
|
|
9862
10666
|
updateRun(run2.id, { status: "blocked" }, cwd);
|
|
9863
10667
|
console.log("");
|
|
9864
|
-
console.log(DIM(" Run saved: .runtrim/runs/" + run2.id + ".json (status: blocked)"));
|
|
10668
|
+
console.log(DIM(" Run saved: .runtrim/internal/runs/" + run2.id + ".json (status: blocked)"));
|
|
9865
10669
|
console.log("");
|
|
9866
10670
|
console.log(DIM(" Next: ") + chalk.white('runtrim guard "<one system at a time>"'));
|
|
9867
10671
|
console.log("");
|
|
@@ -9941,7 +10745,7 @@ program.command("guard <task>").description("Audit a task and generate a guarded
|
|
|
9941
10745
|
}
|
|
9942
10746
|
const run = saveRun(task, audit, contract, cwd);
|
|
9943
10747
|
console.log("");
|
|
9944
|
-
console.log(DIM(" Run saved: .runtrim/runs/" + run.id + ".json"));
|
|
10748
|
+
console.log(DIM(" Run saved: .runtrim/internal/runs/" + run.id + ".json"));
|
|
9945
10749
|
console.log("");
|
|
9946
10750
|
console.log(DIM(" After your agent run: ") + chalk.white("runtrim check"));
|
|
9947
10751
|
console.log("");
|
|
@@ -9991,6 +10795,7 @@ program.command("run <task>").description("Guard then run configured local agent
|
|
|
9991
10795
|
},
|
|
9992
10796
|
cwd
|
|
9993
10797
|
);
|
|
10798
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
9994
10799
|
const copySavings = estimateSavingsFromTokens2(
|
|
9995
10800
|
parseEstimatedNumber3(String(contract.estimatedTokensTrimmed))
|
|
9996
10801
|
);
|
|
@@ -10071,6 +10876,7 @@ program.command("run <task>").description("Guard then run configured local agent
|
|
|
10071
10876
|
},
|
|
10072
10877
|
cwd
|
|
10073
10878
|
);
|
|
10879
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
10074
10880
|
console.log(DIM(" Execution cancelled. Guarded contract copied to clipboard."));
|
|
10075
10881
|
console.log("");
|
|
10076
10882
|
return;
|
|
@@ -10127,6 +10933,7 @@ program.command("run <task>").description("Guard then run configured local agent
|
|
|
10127
10933
|
},
|
|
10128
10934
|
cwd
|
|
10129
10935
|
);
|
|
10936
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
10130
10937
|
console.log("");
|
|
10131
10938
|
console.log(chalk.yellow(" Agent command not found. Falling back to copy mode guidance."));
|
|
10132
10939
|
console.log(DIM(" Configure with: npm run runtrim -- agent set claude|codex|custom"));
|
|
@@ -10160,6 +10967,7 @@ program.command("run <task>").description("Guard then run configured local agent
|
|
|
10160
10967
|
},
|
|
10161
10968
|
cwd
|
|
10162
10969
|
);
|
|
10970
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
10163
10971
|
console.log("");
|
|
10164
10972
|
console.log(chalk.yellow(" Agent command not found. Falling back to copy mode guidance."));
|
|
10165
10973
|
console.log(DIM(" Configure with: npm run runtrim -- agent set claude|codex|custom"));
|
|
@@ -10203,7 +11011,7 @@ ${stderr}`, "utf-8");
|
|
|
10203
11011
|
exitCode,
|
|
10204
11012
|
stdoutPreview: stdout.slice(0, OUTPUT_PREVIEW_MAX),
|
|
10205
11013
|
stderrPreview: stderr.slice(0, OUTPUT_PREVIEW_MAX),
|
|
10206
|
-
outputPath: `.runtrim/runs/${run.id}.output.txt`
|
|
11014
|
+
outputPath: `.runtrim/internal/runs/${run.id}.output.txt`
|
|
10207
11015
|
},
|
|
10208
11016
|
evaluation
|
|
10209
11017
|
},
|
|
@@ -10353,6 +11161,7 @@ program.command("go <task>").description("Bridge Mode: generate a scoped contrac
|
|
|
10353
11161
|
memoryUsed,
|
|
10354
11162
|
providerRouting
|
|
10355
11163
|
}, cwd);
|
|
11164
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
10356
11165
|
const bridgeCtx = deriveBridgeContext(task, contract, runs, projectName);
|
|
10357
11166
|
let bridgeWritten = [];
|
|
10358
11167
|
let bridgeManagedPaths = [];
|
|
@@ -10411,7 +11220,7 @@ program.command("go <task>").description("Bridge Mode: generate a scoped contrac
|
|
|
10411
11220
|
if (contract.contract.stopRules.length > 0) {
|
|
10412
11221
|
console.log(DIM(" Stop rule ") + chalk.white(truncate((_g = contract.contract.stopRules[contract.contract.stopRules.length - 1]) != null ? _g : "", 60)));
|
|
10413
11222
|
}
|
|
10414
|
-
console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
|
|
11223
|
+
console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
|
|
10415
11224
|
console.log(DIM(" Contract ") + chalk.white("created"));
|
|
10416
11225
|
console.log("");
|
|
10417
11226
|
if (bridgeWritten.length > 0) {
|
|
@@ -11766,6 +12575,18 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
|
|
|
11766
12575
|
const blockedByExistingHard = scope.forbiddenFiles.length > 0 || scope.status === "limit_exceeded";
|
|
11767
12576
|
const warnBySensitiveIgnored = sensitiveIgnored.length > 0;
|
|
11768
12577
|
const finishVerdict = blockedBySensitive || blockedByContract || blockedByExistingHard ? "BLOCKED" : warnBySensitiveIgnored || scopeDriftStatus !== "passed" || evaluation.status === "needs_verification" || evaluation.status === "partial" ? "WARN" : "PASS";
|
|
12578
|
+
const restoreRecord = loadRestorePoint(cwd, activeRun.id);
|
|
12579
|
+
if (restoreRecord) {
|
|
12580
|
+
restoreRecord.postRun = {
|
|
12581
|
+
changedFiles,
|
|
12582
|
+
forbiddenFiles: [...scope.forbiddenFiles, ...forbiddenPathFiles],
|
|
12583
|
+
sensitiveFiles: scope.sensitiveFiles,
|
|
12584
|
+
outOfScopeFiles: [...outOfContractFiles, ...scope.outOfScopeFiles],
|
|
12585
|
+
finishVerdict,
|
|
12586
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12587
|
+
};
|
|
12588
|
+
saveRestorePoint(cwd, restoreRecord);
|
|
12589
|
+
}
|
|
11769
12590
|
const verdictColor = finishVerdict === "PASS" ? chalk.green : finishVerdict === "WARN" ? chalk.yellow : chalk.red;
|
|
11770
12591
|
const scopeColor = scopeDriftStatus === "passed" ? chalk.green : scopeDriftStatus === "forbidden_touched" ? chalk.red : chalk.yellow;
|
|
11771
12592
|
const riskAfter = (_m = activeRun.contract.wasteRiskAfter) != null ? _m : "medium";
|