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