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.js
CHANGED
|
@@ -97,8 +97,45 @@ function getConfigPath(cwd = process.cwd()) {
|
|
|
97
97
|
return path.join(getConfigDir(cwd), "config.json");
|
|
98
98
|
}
|
|
99
99
|
function getRunsDir(cwd = process.cwd()) {
|
|
100
|
+
return path.join(getInternalDir(cwd), "runs");
|
|
101
|
+
}
|
|
102
|
+
function getLegacyRunsDir(cwd = process.cwd()) {
|
|
100
103
|
return path.join(getConfigDir(cwd), "runs");
|
|
101
104
|
}
|
|
105
|
+
function getInternalDir(cwd = process.cwd()) {
|
|
106
|
+
return path.join(getConfigDir(cwd), "internal");
|
|
107
|
+
}
|
|
108
|
+
function getPreviewsDir(cwd = process.cwd()) {
|
|
109
|
+
return path.join(getInternalDir(cwd), "previews");
|
|
110
|
+
}
|
|
111
|
+
function getLegacyPreviewsDir(cwd = process.cwd()) {
|
|
112
|
+
return path.join(getConfigDir(cwd), "previews");
|
|
113
|
+
}
|
|
114
|
+
function getRestoresDir(cwd = process.cwd()) {
|
|
115
|
+
return path.join(getInternalDir(cwd), "restores");
|
|
116
|
+
}
|
|
117
|
+
function getLegacyRestoresDir(cwd = process.cwd()) {
|
|
118
|
+
return path.join(getConfigDir(cwd), "restores");
|
|
119
|
+
}
|
|
120
|
+
function getContractsArchiveDir(cwd = process.cwd()) {
|
|
121
|
+
return path.join(getInternalDir(cwd), "contracts-archive");
|
|
122
|
+
}
|
|
123
|
+
function getAgentArchiveDir(cwd = process.cwd()) {
|
|
124
|
+
return path.join(getInternalDir(cwd), "agent-archive");
|
|
125
|
+
}
|
|
126
|
+
function ensureInternalArtifactDirs(cwd = process.cwd()) {
|
|
127
|
+
const dirs = [
|
|
128
|
+
getInternalDir(cwd),
|
|
129
|
+
getRunsDir(cwd),
|
|
130
|
+
getPreviewsDir(cwd),
|
|
131
|
+
getRestoresDir(cwd),
|
|
132
|
+
getContractsArchiveDir(cwd),
|
|
133
|
+
getAgentArchiveDir(cwd)
|
|
134
|
+
];
|
|
135
|
+
for (const dir of dirs) {
|
|
136
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
102
139
|
function configExists(cwd = process.cwd()) {
|
|
103
140
|
return fs.existsSync(getConfigPath(cwd));
|
|
104
141
|
}
|
|
@@ -666,6 +703,28 @@ function buildCategoryScope(category, hasSrc, hasApp, hasPages) {
|
|
|
666
703
|
"Check no regression in adjacent routes"
|
|
667
704
|
]
|
|
668
705
|
};
|
|
706
|
+
case "docs":
|
|
707
|
+
return {
|
|
708
|
+
allowedHints: [
|
|
709
|
+
"README.md - project documentation",
|
|
710
|
+
"docs/ - documentation files",
|
|
711
|
+
"CHANGELOG.md or CONTRIBUTING.md if task-specific"
|
|
712
|
+
],
|
|
713
|
+
forbiddenAdditions: [
|
|
714
|
+
"Do not touch auth internals, session logic, or JWT handling",
|
|
715
|
+
"Do not touch billing, subscription, payment, or webhook logic",
|
|
716
|
+
"Do not touch database schema or migrations",
|
|
717
|
+
"Do not touch .env files or secrets"
|
|
718
|
+
],
|
|
719
|
+
stopRules: [
|
|
720
|
+
"Stop if the requested change requires code-path behavior changes outside docs",
|
|
721
|
+
"Stop if sensitive files or secrets are referenced"
|
|
722
|
+
],
|
|
723
|
+
verificationSteps: [
|
|
724
|
+
"Confirm documentation text matches the requested task",
|
|
725
|
+
"Check markdown formatting renders correctly"
|
|
726
|
+
]
|
|
727
|
+
};
|
|
669
728
|
default:
|
|
670
729
|
return {
|
|
671
730
|
allowedHints: [],
|
|
@@ -1530,35 +1589,38 @@ function saveRun(task, audit, contract, cwd = process.cwd()) {
|
|
|
1530
1589
|
return record;
|
|
1531
1590
|
}
|
|
1532
1591
|
function loadLatestRun(cwd = process.cwd()) {
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1592
|
+
const candidateDirs = [getRunsDir(cwd), getLegacyRunsDir(cwd)].filter((dir, idx, arr) => arr.indexOf(dir) === idx);
|
|
1593
|
+
const files = candidateDirs.filter((dir) => fs3.existsSync(dir)).flatMap(
|
|
1594
|
+
(dir) => fs3.readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => ({
|
|
1595
|
+
dir,
|
|
1596
|
+
name: f,
|
|
1597
|
+
time: fs3.statSync(path3.join(dir, f)).mtime.getTime()
|
|
1598
|
+
}))
|
|
1599
|
+
).sort((a, b) => b.time - a.time);
|
|
1539
1600
|
if (files.length === 0) return null;
|
|
1540
1601
|
try {
|
|
1541
1602
|
return JSON.parse(
|
|
1542
|
-
fs3.readFileSync(path3.join(
|
|
1603
|
+
fs3.readFileSync(path3.join(files[0].dir, files[0].name), "utf-8")
|
|
1543
1604
|
);
|
|
1544
1605
|
} catch (e) {
|
|
1545
1606
|
return null;
|
|
1546
1607
|
}
|
|
1547
1608
|
}
|
|
1548
1609
|
function updateRun(runId, updates, cwd = process.cwd()) {
|
|
1549
|
-
const
|
|
1610
|
+
const preferredPath = path3.join(getRunsDir(cwd), `${runId}.json`);
|
|
1611
|
+
const legacyPath = path3.join(getLegacyRunsDir(cwd), `${runId}.json`);
|
|
1612
|
+
const filePath = fs3.existsSync(preferredPath) ? preferredPath : legacyPath;
|
|
1550
1613
|
if (!fs3.existsSync(filePath)) return;
|
|
1551
1614
|
const existing = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
1552
1615
|
fs3.writeFileSync(filePath, JSON.stringify(__spreadValues(__spreadValues({}, existing), updates), null, 2));
|
|
1553
1616
|
}
|
|
1554
1617
|
function loadAllRuns(cwd = process.cwd()) {
|
|
1555
|
-
const
|
|
1556
|
-
|
|
1557
|
-
|
|
1618
|
+
const candidateDirs = [getRunsDir(cwd), getLegacyRunsDir(cwd)].filter((dir, idx, arr) => arr.indexOf(dir) === idx);
|
|
1619
|
+
const files = candidateDirs.filter((dir) => fs3.existsSync(dir)).flatMap((dir) => fs3.readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => path3.join(dir, f)));
|
|
1620
|
+
const deduped = [...new Set(files)];
|
|
1621
|
+
return deduped.map((filePath) => {
|
|
1558
1622
|
try {
|
|
1559
|
-
return JSON.parse(
|
|
1560
|
-
fs3.readFileSync(path3.join(runsDir, f), "utf-8")
|
|
1561
|
-
);
|
|
1623
|
+
return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
1562
1624
|
} catch (e) {
|
|
1563
1625
|
return null;
|
|
1564
1626
|
}
|
|
@@ -2577,7 +2639,7 @@ function buildSyncPayload(input) {
|
|
|
2577
2639
|
var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t;
|
|
2578
2640
|
const { cwd, projectName, config, projectAudit, memoryMarkdown, runs } = input;
|
|
2579
2641
|
const latest = runs[0];
|
|
2580
|
-
const
|
|
2642
|
+
const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2581
2643
|
const localProjectId = buildLocalProjectId(cwd);
|
|
2582
2644
|
const latestPromptText = readTextFileIfExists(path6.join(cwd, ".runtrim", "latest-prompt.md"));
|
|
2583
2645
|
const continuationPromptText = readTextFileIfExists(
|
|
@@ -2639,7 +2701,7 @@ function buildSyncPayload(input) {
|
|
|
2639
2701
|
name: resolveProjectName2(cwd, projectName, projectAudit == null ? void 0 : projectAudit.projectName),
|
|
2640
2702
|
stack: (_a2 = projectAudit == null ? void 0 : projectAudit.detectedStack) != null ? _a2 : config.stack ? config.stack.split(",").map((s) => s.trim()).filter(Boolean) : ["auto"],
|
|
2641
2703
|
packageManager: (_c = (_b = projectAudit == null ? void 0 : projectAudit.packageManager) != null ? _b : config.packageManager) != null ? _c : null,
|
|
2642
|
-
lastUpdated:
|
|
2704
|
+
lastUpdated: nowIso2
|
|
2643
2705
|
},
|
|
2644
2706
|
memory: {
|
|
2645
2707
|
markdown: memoryMarkdown,
|
|
@@ -2721,7 +2783,8 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
|
|
|
2721
2783
|
"## How to start an AI coding task",
|
|
2722
2784
|
"",
|
|
2723
2785
|
"```",
|
|
2724
|
-
|
|
2786
|
+
"runtrim start",
|
|
2787
|
+
'runtrim agent "Your task" --copy',
|
|
2725
2788
|
"```",
|
|
2726
2789
|
"",
|
|
2727
2790
|
"RunTrim creates a scoped contract, loads project memory, and generates a guarded prompt.",
|
|
@@ -2742,7 +2805,7 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
|
|
|
2742
2805
|
"",
|
|
2743
2806
|
"1. Read `.runtrim/contracts/latest.md`.",
|
|
2744
2807
|
" - If `Status: active` \u2014 a live task exists. Follow the contract strictly.",
|
|
2745
|
-
' - If `Status: none` \u2014 no active task. Ask the user to run `runtrim
|
|
2808
|
+
' - If `Status: none` \u2014 no active task. Ask the user to run `runtrim start` then `runtrim agent "Your task" --copy`.',
|
|
2746
2809
|
"2. Do not assume any prior task is still active.",
|
|
2747
2810
|
"3. Stay inside the allowed scope defined in the contract.",
|
|
2748
2811
|
"4. Stop and ask before touching any forbidden area.",
|
|
@@ -2750,7 +2813,7 @@ function writeCanonicalRuntrimMd(cwd = process.cwd(), projectName) {
|
|
|
2750
2813
|
"6. After editing, tell the user to run: `runtrim finish`",
|
|
2751
2814
|
"",
|
|
2752
2815
|
"---",
|
|
2753
|
-
`Protocol: runtrim
|
|
2816
|
+
`Protocol: runtrim start. Updated: ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
2754
2817
|
];
|
|
2755
2818
|
fs7.writeFileSync(path7.join(cwd, "RUNTRIM.md"), lines.join("\n"), "utf-8");
|
|
2756
2819
|
}
|
|
@@ -2767,7 +2830,8 @@ function writeRestingContract(cwd = process.cwd()) {
|
|
|
2767
2830
|
"Start one with:",
|
|
2768
2831
|
"",
|
|
2769
2832
|
"```",
|
|
2770
|
-
|
|
2833
|
+
"runtrim start",
|
|
2834
|
+
'runtrim agent "Your task" --copy',
|
|
2771
2835
|
"```",
|
|
2772
2836
|
"",
|
|
2773
2837
|
"---",
|
|
@@ -2790,7 +2854,8 @@ function writeRestingMemory(cwd = process.cwd()) {
|
|
|
2790
2854
|
"Start a new session with:",
|
|
2791
2855
|
"",
|
|
2792
2856
|
"```",
|
|
2793
|
-
|
|
2857
|
+
"runtrim start",
|
|
2858
|
+
'runtrim agent "Your task" --copy',
|
|
2794
2859
|
"```",
|
|
2795
2860
|
"",
|
|
2796
2861
|
"---",
|
|
@@ -2804,7 +2869,7 @@ function archiveContract(cwd, runId) {
|
|
|
2804
2869
|
if (!fs7.existsSync(latestPath)) return;
|
|
2805
2870
|
const content = fs7.readFileSync(latestPath, "utf-8");
|
|
2806
2871
|
if (content.includes("Status: none")) return;
|
|
2807
|
-
const archiveDir =
|
|
2872
|
+
const archiveDir = getContractsArchiveDir(cwd);
|
|
2808
2873
|
if (!fs7.existsSync(archiveDir)) fs7.mkdirSync(archiveDir, { recursive: true });
|
|
2809
2874
|
fs7.writeFileSync(path7.join(archiveDir, `${runId}.md`), content, "utf-8");
|
|
2810
2875
|
}
|
|
@@ -2911,7 +2976,7 @@ function writeBridgeInstructions(cwd = process.cwd()) {
|
|
|
2911
2976
|
"1. Read `RUNTRIM.md`.",
|
|
2912
2977
|
"2. Read `.runtrim/contracts/latest.md`.",
|
|
2913
2978
|
" - If `Status: active` \u2014 follow the contract strictly.",
|
|
2914
|
-
' - If `Status: none` \u2014 stop. Ask the user to run `runtrim
|
|
2979
|
+
' - If `Status: none` \u2014 stop. Ask the user to run `runtrim start` then `runtrim agent "Your task" --copy`.',
|
|
2915
2980
|
"3. If the contract is active, read `.runtrim/memory/current.md` for session context.",
|
|
2916
2981
|
" If no active session, read `.runtrim/memory/baseline.md` for project baseline.",
|
|
2917
2982
|
"",
|
|
@@ -2985,6 +3050,28 @@ function buildBridgePrompt(contractText, ctx) {
|
|
|
2985
3050
|
// src/lib/run-watch.ts
|
|
2986
3051
|
function normalizeScopeKeywords2(scope) {
|
|
2987
3052
|
var _a2;
|
|
3053
|
+
const genericStopwords = /* @__PURE__ */ new Set([
|
|
3054
|
+
"read",
|
|
3055
|
+
"write",
|
|
3056
|
+
"reference",
|
|
3057
|
+
"touch",
|
|
3058
|
+
"modify",
|
|
3059
|
+
"change",
|
|
3060
|
+
"update",
|
|
3061
|
+
"allow",
|
|
3062
|
+
"scope",
|
|
3063
|
+
"paths",
|
|
3064
|
+
"path",
|
|
3065
|
+
"files",
|
|
3066
|
+
"file",
|
|
3067
|
+
"only",
|
|
3068
|
+
"with",
|
|
3069
|
+
"without",
|
|
3070
|
+
"before",
|
|
3071
|
+
"after",
|
|
3072
|
+
"inside",
|
|
3073
|
+
"outside"
|
|
3074
|
+
]);
|
|
2988
3075
|
const words = /* @__PURE__ */ new Set();
|
|
2989
3076
|
for (const line of scope) {
|
|
2990
3077
|
const lower = line.toLowerCase();
|
|
@@ -2994,7 +3081,7 @@ function normalizeScopeKeywords2(scope) {
|
|
|
2994
3081
|
}
|
|
2995
3082
|
const cleaned = lower.replace(/[^a-z0-9_./\s-]/g, " ").split(/\s+/).filter(Boolean);
|
|
2996
3083
|
for (const token of cleaned) {
|
|
2997
|
-
if (token.length >= 4) words.add(token);
|
|
3084
|
+
if (token.length >= 4 && !genericStopwords.has(token)) words.add(token);
|
|
2998
3085
|
}
|
|
2999
3086
|
}
|
|
3000
3087
|
return [...words];
|
|
@@ -3091,15 +3178,7 @@ import fs8 from "fs";
|
|
|
3091
3178
|
import os from "os";
|
|
3092
3179
|
import path8 from "path";
|
|
3093
3180
|
import { execa as execa2 } from "execa";
|
|
3094
|
-
var
|
|
3095
|
-
version: 1,
|
|
3096
|
-
plan: "free",
|
|
3097
|
-
trackedRepos: [],
|
|
3098
|
-
telemetry: {
|
|
3099
|
-
enabled: false,
|
|
3100
|
-
anonymousId: ""
|
|
3101
|
-
}
|
|
3102
|
-
};
|
|
3181
|
+
var EMPTY_TELEMETRY = { enabled: false, anonymousId: "" };
|
|
3103
3182
|
function normalizeRepoPath(input) {
|
|
3104
3183
|
const resolved = path8.resolve(input);
|
|
3105
3184
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
@@ -3107,41 +3186,208 @@ function normalizeRepoPath(input) {
|
|
|
3107
3186
|
function hashValue(value) {
|
|
3108
3187
|
return crypto.createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
3109
3188
|
}
|
|
3189
|
+
function nowIso() {
|
|
3190
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3191
|
+
}
|
|
3192
|
+
function randomId(prefix) {
|
|
3193
|
+
return `${prefix}_${crypto.randomBytes(12).toString("hex")}`;
|
|
3194
|
+
}
|
|
3110
3195
|
function getGlobalRunTrimDir() {
|
|
3111
3196
|
return path8.join(os.homedir(), ".runtrim");
|
|
3112
3197
|
}
|
|
3113
3198
|
function getGlobalRegistryPath() {
|
|
3114
3199
|
return path8.join(getGlobalRunTrimDir(), "global.json");
|
|
3115
3200
|
}
|
|
3116
|
-
function
|
|
3201
|
+
function getInstallStatePath() {
|
|
3202
|
+
return path8.join(getGlobalRunTrimDir(), "install-state.json");
|
|
3203
|
+
}
|
|
3204
|
+
function buildSealInput(registry) {
|
|
3205
|
+
const tracked = [...registry.trackedRepos].map((r) => ({
|
|
3206
|
+
id: r.id,
|
|
3207
|
+
name: r.name,
|
|
3208
|
+
path: normalizeRepoPath(r.path),
|
|
3209
|
+
gitRemote: r.gitRemote,
|
|
3210
|
+
createdAt: r.createdAt,
|
|
3211
|
+
lastSeenAt: r.lastSeenAt
|
|
3212
|
+
})).sort((a, b) => `${a.id}:${a.path}`.localeCompare(`${b.id}:${b.path}`));
|
|
3213
|
+
const payload = {
|
|
3214
|
+
version: registry.version,
|
|
3215
|
+
stateVersion: registry.stateVersion,
|
|
3216
|
+
plan: registry.plan,
|
|
3217
|
+
machineInstallId: registry.machineInstallId,
|
|
3218
|
+
createdAt: registry.createdAt,
|
|
3219
|
+
updatedAt: registry.updatedAt,
|
|
3220
|
+
trackedRepos: tracked,
|
|
3221
|
+
lastKnownRepo: registry.lastKnownRepo ? __spreadProps(__spreadValues({}, registry.lastKnownRepo), {
|
|
3222
|
+
path: normalizeRepoPath(registry.lastKnownRepo.path)
|
|
3223
|
+
}) : null
|
|
3224
|
+
};
|
|
3225
|
+
return JSON.stringify(payload);
|
|
3226
|
+
}
|
|
3227
|
+
function computeSeal(registry) {
|
|
3228
|
+
return crypto.createHash("sha256").update(buildSealInput(registry)).digest("hex");
|
|
3229
|
+
}
|
|
3230
|
+
function sanitizeTrackedRepoEntry(input) {
|
|
3231
|
+
var _a2, _b, _c, _d, _e, _f;
|
|
3232
|
+
const id = String((_a2 = input.id) != null ? _a2 : "").trim();
|
|
3233
|
+
const rawPath = String((_b = input.path) != null ? _b : "").trim();
|
|
3234
|
+
if (!id || !rawPath) return null;
|
|
3235
|
+
return {
|
|
3236
|
+
id,
|
|
3237
|
+
name: String((_c = input.name) != null ? _c : "").trim(),
|
|
3238
|
+
path: normalizeRepoPath(rawPath),
|
|
3239
|
+
gitRemote: String((_d = input.gitRemote) != null ? _d : "").trim(),
|
|
3240
|
+
createdAt: String((_e = input.createdAt) != null ? _e : "").trim(),
|
|
3241
|
+
lastSeenAt: String((_f = input.lastSeenAt) != null ? _f : "").trim()
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
function readInstallStateRaw() {
|
|
3245
|
+
var _a2, _b, _c;
|
|
3246
|
+
const p = getInstallStatePath();
|
|
3247
|
+
if (!fs8.existsSync(p)) return { exists: false, state: null };
|
|
3248
|
+
try {
|
|
3249
|
+
const parsed = JSON.parse(fs8.readFileSync(p, "utf-8"));
|
|
3250
|
+
const machineInstallId = String((_a2 = parsed.machineInstallId) != null ? _a2 : "").trim();
|
|
3251
|
+
if (!machineInstallId) return { exists: true, state: null };
|
|
3252
|
+
return {
|
|
3253
|
+
exists: true,
|
|
3254
|
+
state: {
|
|
3255
|
+
machineInstallId,
|
|
3256
|
+
createdAt: String((_b = parsed.createdAt) != null ? _b : "").trim() || nowIso(),
|
|
3257
|
+
updatedAt: String((_c = parsed.updatedAt) != null ? _c : "").trim() || nowIso()
|
|
3258
|
+
}
|
|
3259
|
+
};
|
|
3260
|
+
} catch (e) {
|
|
3261
|
+
return { exists: true, state: null };
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
function writeInstallState(state) {
|
|
3265
|
+
const dir = getGlobalRunTrimDir();
|
|
3266
|
+
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
3267
|
+
fs8.writeFileSync(getInstallStatePath(), JSON.stringify(state, null, 2), "utf-8");
|
|
3268
|
+
}
|
|
3269
|
+
function ensureInstallState() {
|
|
3270
|
+
const raw = readInstallStateRaw();
|
|
3271
|
+
if (raw.exists && raw.state) return raw.state;
|
|
3272
|
+
const created = {
|
|
3273
|
+
machineInstallId: randomId("rt_install"),
|
|
3274
|
+
createdAt: nowIso(),
|
|
3275
|
+
updatedAt: nowIso()
|
|
3276
|
+
};
|
|
3277
|
+
writeInstallState(created);
|
|
3278
|
+
return created;
|
|
3279
|
+
}
|
|
3280
|
+
function buildDefaultRegistry(install) {
|
|
3281
|
+
const base = {
|
|
3282
|
+
version: 2,
|
|
3283
|
+
stateVersion: 2,
|
|
3284
|
+
plan: "free",
|
|
3285
|
+
machineInstallId: install.machineInstallId,
|
|
3286
|
+
createdAt: nowIso(),
|
|
3287
|
+
updatedAt: nowIso(),
|
|
3288
|
+
trackedRepos: [],
|
|
3289
|
+
lastKnownRepo: null,
|
|
3290
|
+
telemetry: __spreadValues({}, EMPTY_TELEMETRY)
|
|
3291
|
+
};
|
|
3292
|
+
return __spreadProps(__spreadValues({}, base), {
|
|
3293
|
+
integrity: {
|
|
3294
|
+
algorithm: "sha256-local-seal-v1",
|
|
3295
|
+
seal: computeSeal(base)
|
|
3296
|
+
}
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
function saveRegistryWithSeal(registry) {
|
|
3300
|
+
var _a2;
|
|
3301
|
+
const dir = getGlobalRunTrimDir();
|
|
3302
|
+
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
3303
|
+
const normalizedBase = __spreadProps(__spreadValues({}, registry), {
|
|
3304
|
+
version: 2,
|
|
3305
|
+
stateVersion: 2,
|
|
3306
|
+
trackedRepos: registry.trackedRepos.map((r) => __spreadProps(__spreadValues({}, r), { path: normalizeRepoPath(r.path) })),
|
|
3307
|
+
telemetry: (_a2 = registry.telemetry) != null ? _a2 : __spreadValues({}, EMPTY_TELEMETRY)
|
|
3308
|
+
});
|
|
3309
|
+
const sealed = __spreadProps(__spreadValues({}, normalizedBase), {
|
|
3310
|
+
integrity: {
|
|
3311
|
+
algorithm: "sha256-local-seal-v1",
|
|
3312
|
+
seal: computeSeal(normalizedBase)
|
|
3313
|
+
}
|
|
3314
|
+
});
|
|
3315
|
+
fs8.writeFileSync(getGlobalRegistryPath(), JSON.stringify(sealed, null, 2), "utf-8");
|
|
3316
|
+
}
|
|
3317
|
+
function inspectGlobalRegistry() {
|
|
3318
|
+
var _a2, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
3319
|
+
const installRaw = readInstallStateRaw();
|
|
3320
|
+
const install = (_a2 = installRaw.state) != null ? _a2 : ensureInstallState();
|
|
3117
3321
|
const registryPath = getGlobalRegistryPath();
|
|
3118
|
-
|
|
3322
|
+
const defaultRegistry = buildDefaultRegistry(install);
|
|
3323
|
+
if (!fs8.existsSync(registryPath)) {
|
|
3324
|
+
if (installRaw.exists) {
|
|
3325
|
+
return {
|
|
3326
|
+
registry: defaultRegistry,
|
|
3327
|
+
needsRepair: true,
|
|
3328
|
+
repairReason: "missing_registry_after_initialization"
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
3331
|
+
return { registry: defaultRegistry, needsRepair: false, repairReason: null };
|
|
3332
|
+
}
|
|
3119
3333
|
try {
|
|
3120
3334
|
const raw = JSON.parse(fs8.readFileSync(registryPath, "utf-8"));
|
|
3121
|
-
|
|
3122
|
-
|
|
3335
|
+
const trackedRepos = Array.isArray(raw.trackedRepos) ? raw.trackedRepos.map((item) => sanitizeTrackedRepoEntry(item)).filter((item) => Boolean(item)) : [];
|
|
3336
|
+
const base = {
|
|
3337
|
+
version: 2,
|
|
3338
|
+
stateVersion: 2,
|
|
3123
3339
|
plan: raw.plan === "free" ? "free" : "free",
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3340
|
+
machineInstallId: String((_b = raw.machineInstallId) != null ? _b : "").trim() || install.machineInstallId,
|
|
3341
|
+
createdAt: String((_c = raw.createdAt) != null ? _c : "").trim() || nowIso(),
|
|
3342
|
+
updatedAt: String((_d = raw.updatedAt) != null ? _d : "").trim() || nowIso(),
|
|
3343
|
+
trackedRepos,
|
|
3344
|
+
lastKnownRepo: raw.lastKnownRepo && typeof raw.lastKnownRepo === "object" ? {
|
|
3345
|
+
id: String((_e = raw.lastKnownRepo.id) != null ? _e : "").trim(),
|
|
3346
|
+
name: String((_f = raw.lastKnownRepo.name) != null ? _f : "").trim(),
|
|
3347
|
+
path: normalizeRepoPath(String((_g = raw.lastKnownRepo.path) != null ? _g : "")),
|
|
3348
|
+
gitRemote: String((_h = raw.lastKnownRepo.gitRemote) != null ? _h : "").trim(),
|
|
3349
|
+
lastSeenAt: String((_i = raw.lastKnownRepo.lastSeenAt) != null ? _i : "").trim() || nowIso()
|
|
3350
|
+
} : null,
|
|
3132
3351
|
telemetry: {
|
|
3133
3352
|
enabled: typeof raw.telemetry === "object" && raw.telemetry !== null && Boolean(raw.telemetry.enabled),
|
|
3134
3353
|
anonymousId: typeof raw.telemetry === "object" && raw.telemetry !== null && typeof raw.telemetry.anonymousId === "string" ? String(raw.telemetry.anonymousId).slice(0, 120) : ""
|
|
3135
3354
|
}
|
|
3136
3355
|
};
|
|
3356
|
+
const normalized = __spreadProps(__spreadValues({}, base), {
|
|
3357
|
+
integrity: {
|
|
3358
|
+
algorithm: "sha256-local-seal-v1",
|
|
3359
|
+
seal: raw.integrity && typeof raw.integrity === "object" && typeof raw.integrity.seal === "string" ? String(raw.integrity.seal) : ""
|
|
3360
|
+
}
|
|
3361
|
+
});
|
|
3362
|
+
if (normalized.machineInstallId !== install.machineInstallId) {
|
|
3363
|
+
return {
|
|
3364
|
+
registry: normalized,
|
|
3365
|
+
needsRepair: true,
|
|
3366
|
+
repairReason: "machine_install_id_mismatch"
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
const expectedSeal = computeSeal(base);
|
|
3370
|
+
if (!normalized.integrity.seal || normalized.integrity.seal !== expectedSeal) {
|
|
3371
|
+
return {
|
|
3372
|
+
registry: normalized,
|
|
3373
|
+
needsRepair: true,
|
|
3374
|
+
repairReason: "integrity_seal_mismatch"
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
return { registry: normalized, needsRepair: false, repairReason: null };
|
|
3137
3378
|
} catch (e) {
|
|
3138
|
-
return
|
|
3379
|
+
return {
|
|
3380
|
+
registry: defaultRegistry,
|
|
3381
|
+
needsRepair: true,
|
|
3382
|
+
repairReason: "registry_corrupt"
|
|
3383
|
+
};
|
|
3139
3384
|
}
|
|
3140
3385
|
}
|
|
3386
|
+
function loadGlobalRegistry() {
|
|
3387
|
+
return inspectGlobalRegistry().registry;
|
|
3388
|
+
}
|
|
3141
3389
|
function saveGlobalRegistry(registry) {
|
|
3142
|
-
|
|
3143
|
-
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
3144
|
-
fs8.writeFileSync(getGlobalRegistryPath(), JSON.stringify(registry, null, 2), "utf-8");
|
|
3390
|
+
saveRegistryWithSeal(registry);
|
|
3145
3391
|
}
|
|
3146
3392
|
async function getCurrentRepoIdentity(cwd = process.cwd()) {
|
|
3147
3393
|
const normalizedPath = normalizeRepoPath(cwd);
|
|
@@ -3171,36 +3417,71 @@ function findTrackedRepo(trackedRepos, currentRepo) {
|
|
|
3171
3417
|
return byPath != null ? byPath : null;
|
|
3172
3418
|
}
|
|
3173
3419
|
async function assertFreeRepoAllowed(cwd = process.cwd()) {
|
|
3174
|
-
const
|
|
3420
|
+
const inspected = inspectGlobalRegistry();
|
|
3421
|
+
const registry = inspected.registry;
|
|
3175
3422
|
const currentRepo = await getCurrentRepoIdentity(cwd);
|
|
3176
3423
|
const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
|
|
3177
|
-
|
|
3178
|
-
|
|
3424
|
+
const base = {
|
|
3425
|
+
plan: registry.plan,
|
|
3426
|
+
currentRepo,
|
|
3427
|
+
trackedRepo,
|
|
3428
|
+
registryPath: getGlobalRegistryPath()
|
|
3429
|
+
};
|
|
3430
|
+
if (inspected.needsRepair) {
|
|
3431
|
+
return __spreadProps(__spreadValues({}, base), {
|
|
3432
|
+
allowed: false,
|
|
3433
|
+
status: "blocked_repair",
|
|
3434
|
+
repairRequired: true,
|
|
3435
|
+
repairReason: inspected.repairReason
|
|
3436
|
+
});
|
|
3179
3437
|
}
|
|
3180
|
-
if (
|
|
3181
|
-
return {
|
|
3438
|
+
if (registry.plan !== "free") {
|
|
3439
|
+
return __spreadProps(__spreadValues({}, base), {
|
|
3440
|
+
allowed: true,
|
|
3441
|
+
status: "allowed",
|
|
3442
|
+
repairRequired: false,
|
|
3443
|
+
repairReason: null
|
|
3444
|
+
});
|
|
3182
3445
|
}
|
|
3183
|
-
if (registry.trackedRepos.length === 0) {
|
|
3184
|
-
return {
|
|
3446
|
+
if (trackedRepo || registry.trackedRepos.length === 0) {
|
|
3447
|
+
return __spreadProps(__spreadValues({}, base), {
|
|
3448
|
+
allowed: true,
|
|
3449
|
+
status: "allowed",
|
|
3450
|
+
repairRequired: false,
|
|
3451
|
+
repairReason: null
|
|
3452
|
+
});
|
|
3185
3453
|
}
|
|
3186
|
-
return {
|
|
3454
|
+
return __spreadProps(__spreadValues({}, base), {
|
|
3187
3455
|
allowed: false,
|
|
3188
|
-
|
|
3189
|
-
|
|
3456
|
+
status: "blocked_limit",
|
|
3457
|
+
repairRequired: false,
|
|
3458
|
+
repairReason: null,
|
|
3190
3459
|
trackedRepo: registry.trackedRepos[0]
|
|
3191
|
-
};
|
|
3460
|
+
});
|
|
3192
3461
|
}
|
|
3193
3462
|
async function registerCurrentRepo(cwd = process.cwd()) {
|
|
3463
|
+
const check = await assertFreeRepoAllowed(cwd);
|
|
3464
|
+
if (!check.allowed && check.status === "blocked_repair") {
|
|
3465
|
+
throw new Error("runtrim_local_state_repair_required");
|
|
3466
|
+
}
|
|
3194
3467
|
const registry = loadGlobalRegistry();
|
|
3195
3468
|
const currentRepo = await getCurrentRepoIdentity(cwd);
|
|
3196
|
-
const now = (
|
|
3469
|
+
const now = nowIso();
|
|
3197
3470
|
const existing = findTrackedRepo(registry.trackedRepos, currentRepo);
|
|
3198
3471
|
if (existing) {
|
|
3199
3472
|
existing.lastSeenAt = now;
|
|
3200
3473
|
existing.name = currentRepo.name;
|
|
3201
3474
|
existing.path = currentRepo.path;
|
|
3202
3475
|
existing.gitRemote = currentRepo.gitRemote;
|
|
3203
|
-
|
|
3476
|
+
registry.updatedAt = now;
|
|
3477
|
+
registry.lastKnownRepo = {
|
|
3478
|
+
id: currentRepo.id,
|
|
3479
|
+
name: currentRepo.name,
|
|
3480
|
+
path: currentRepo.path,
|
|
3481
|
+
gitRemote: currentRepo.gitRemote,
|
|
3482
|
+
lastSeenAt: now
|
|
3483
|
+
};
|
|
3484
|
+
saveRegistryWithSeal(registry);
|
|
3204
3485
|
return existing;
|
|
3205
3486
|
}
|
|
3206
3487
|
const entry = {
|
|
@@ -3211,24 +3492,110 @@ async function registerCurrentRepo(cwd = process.cwd()) {
|
|
|
3211
3492
|
createdAt: now,
|
|
3212
3493
|
lastSeenAt: now
|
|
3213
3494
|
};
|
|
3214
|
-
registry.trackedRepos
|
|
3215
|
-
|
|
3495
|
+
registry.trackedRepos = [entry];
|
|
3496
|
+
registry.updatedAt = now;
|
|
3497
|
+
registry.lastKnownRepo = {
|
|
3498
|
+
id: entry.id,
|
|
3499
|
+
name: entry.name,
|
|
3500
|
+
path: entry.path,
|
|
3501
|
+
gitRemote: entry.gitRemote,
|
|
3502
|
+
lastSeenAt: now
|
|
3503
|
+
};
|
|
3504
|
+
saveRegistryWithSeal(registry);
|
|
3216
3505
|
return entry;
|
|
3217
3506
|
}
|
|
3507
|
+
async function repairGlobalRegistry(cwd = process.cwd(), options = {}) {
|
|
3508
|
+
const before = await assertFreeRepoAllowed(cwd);
|
|
3509
|
+
if (!before.repairRequired) {
|
|
3510
|
+
return { repaired: false, check: before };
|
|
3511
|
+
}
|
|
3512
|
+
const install = ensureInstallState();
|
|
3513
|
+
const now = nowIso();
|
|
3514
|
+
const repaired = buildDefaultRegistry(install);
|
|
3515
|
+
repaired.createdAt = now;
|
|
3516
|
+
repaired.updatedAt = now;
|
|
3517
|
+
if (options.useCurrentRepo) {
|
|
3518
|
+
const currentRepo = await getCurrentRepoIdentity(cwd);
|
|
3519
|
+
repaired.trackedRepos = [
|
|
3520
|
+
{
|
|
3521
|
+
id: currentRepo.id,
|
|
3522
|
+
name: currentRepo.name,
|
|
3523
|
+
path: currentRepo.path,
|
|
3524
|
+
gitRemote: currentRepo.gitRemote,
|
|
3525
|
+
createdAt: now,
|
|
3526
|
+
lastSeenAt: now
|
|
3527
|
+
}
|
|
3528
|
+
];
|
|
3529
|
+
repaired.lastKnownRepo = {
|
|
3530
|
+
id: currentRepo.id,
|
|
3531
|
+
name: currentRepo.name,
|
|
3532
|
+
path: currentRepo.path,
|
|
3533
|
+
gitRemote: currentRepo.gitRemote,
|
|
3534
|
+
lastSeenAt: now
|
|
3535
|
+
};
|
|
3536
|
+
}
|
|
3537
|
+
saveRegistryWithSeal(repaired);
|
|
3538
|
+
const check = await assertFreeRepoAllowed(cwd);
|
|
3539
|
+
return { repaired: true, check };
|
|
3540
|
+
}
|
|
3218
3541
|
async function unlinkCurrentRepo(cwd = process.cwd(), force = false) {
|
|
3219
3542
|
var _a2;
|
|
3543
|
+
const check = await assertFreeRepoAllowed(cwd);
|
|
3544
|
+
if (check.status === "blocked_repair") {
|
|
3545
|
+
if (!force) {
|
|
3546
|
+
return {
|
|
3547
|
+
removed: false,
|
|
3548
|
+
forced: false,
|
|
3549
|
+
currentRepo: check.currentRepo,
|
|
3550
|
+
trackedRepo: null
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
const install = ensureInstallState();
|
|
3554
|
+
const repaired = buildDefaultRegistry(install);
|
|
3555
|
+
repaired.updatedAt = nowIso();
|
|
3556
|
+
repaired.lastKnownRepo = {
|
|
3557
|
+
id: check.currentRepo.id,
|
|
3558
|
+
name: check.currentRepo.name,
|
|
3559
|
+
path: check.currentRepo.path,
|
|
3560
|
+
gitRemote: check.currentRepo.gitRemote,
|
|
3561
|
+
lastSeenAt: repaired.updatedAt
|
|
3562
|
+
};
|
|
3563
|
+
saveRegistryWithSeal(repaired);
|
|
3564
|
+
return {
|
|
3565
|
+
removed: true,
|
|
3566
|
+
forced: true,
|
|
3567
|
+
currentRepo: check.currentRepo,
|
|
3568
|
+
trackedRepo: null
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3220
3571
|
const registry = loadGlobalRegistry();
|
|
3221
3572
|
const currentRepo = await getCurrentRepoIdentity(cwd);
|
|
3222
3573
|
const trackedRepo = findTrackedRepo(registry.trackedRepos, currentRepo);
|
|
3223
3574
|
if (trackedRepo) {
|
|
3224
3575
|
registry.trackedRepos = registry.trackedRepos.filter((repo) => repo.id !== trackedRepo.id);
|
|
3225
|
-
|
|
3576
|
+
registry.updatedAt = nowIso();
|
|
3577
|
+
registry.lastKnownRepo = {
|
|
3578
|
+
id: trackedRepo.id,
|
|
3579
|
+
name: trackedRepo.name,
|
|
3580
|
+
path: trackedRepo.path,
|
|
3581
|
+
gitRemote: trackedRepo.gitRemote,
|
|
3582
|
+
lastSeenAt: registry.updatedAt
|
|
3583
|
+
};
|
|
3584
|
+
saveRegistryWithSeal(registry);
|
|
3226
3585
|
return { removed: true, forced: false, currentRepo, trackedRepo };
|
|
3227
3586
|
}
|
|
3228
3587
|
if (force && registry.trackedRepos.length > 0) {
|
|
3229
3588
|
const first = registry.trackedRepos[0];
|
|
3230
3589
|
registry.trackedRepos = [];
|
|
3231
|
-
|
|
3590
|
+
registry.updatedAt = nowIso();
|
|
3591
|
+
registry.lastKnownRepo = {
|
|
3592
|
+
id: first.id,
|
|
3593
|
+
name: first.name,
|
|
3594
|
+
path: first.path,
|
|
3595
|
+
gitRemote: first.gitRemote,
|
|
3596
|
+
lastSeenAt: registry.updatedAt
|
|
3597
|
+
};
|
|
3598
|
+
saveRegistryWithSeal(registry);
|
|
3232
3599
|
return { removed: true, forced: true, currentRepo, trackedRepo: first };
|
|
3233
3600
|
}
|
|
3234
3601
|
return {
|
|
@@ -3832,9 +4199,15 @@ async function getPanelState(cwd, monitorMode) {
|
|
|
3832
4199
|
let runs = [];
|
|
3833
4200
|
let latest = null;
|
|
3834
4201
|
let registry = {
|
|
3835
|
-
version:
|
|
4202
|
+
version: 2,
|
|
4203
|
+
stateVersion: 2,
|
|
3836
4204
|
plan: "free",
|
|
4205
|
+
machineInstallId: "",
|
|
4206
|
+
createdAt: "",
|
|
4207
|
+
updatedAt: "",
|
|
3837
4208
|
trackedRepos: [],
|
|
4209
|
+
lastKnownRepo: null,
|
|
4210
|
+
integrity: { algorithm: "sha256-local-seal-v1", seal: "" },
|
|
3838
4211
|
telemetry: {
|
|
3839
4212
|
enabled: false,
|
|
3840
4213
|
anonymousId: ""
|
|
@@ -3876,9 +4249,15 @@ async function getPanelState(cwd, monitorMode) {
|
|
|
3876
4249
|
} catch (e) {
|
|
3877
4250
|
warnings.push("global_registry_failed");
|
|
3878
4251
|
registry = {
|
|
3879
|
-
version:
|
|
4252
|
+
version: 2,
|
|
4253
|
+
stateVersion: 2,
|
|
3880
4254
|
plan: "free",
|
|
4255
|
+
machineInstallId: "",
|
|
4256
|
+
createdAt: "",
|
|
4257
|
+
updatedAt: "",
|
|
3881
4258
|
trackedRepos: [],
|
|
4259
|
+
lastKnownRepo: null,
|
|
4260
|
+
integrity: { algorithm: "sha256-local-seal-v1", seal: "" },
|
|
3882
4261
|
telemetry: {
|
|
3883
4262
|
enabled: false,
|
|
3884
4263
|
anonymousId: ""
|
|
@@ -4346,7 +4725,7 @@ Before editing code:
|
|
|
4346
4725
|
If no active RunTrim contract exists:
|
|
4347
4726
|
- Do not edit code without one.
|
|
4348
4727
|
- Ask the user to start a guarded run:
|
|
4349
|
-
runtrim
|
|
4728
|
+
runtrim agent "<task>" --copy
|
|
4350
4729
|
|
|
4351
4730
|
If the task requires leaving the current scope:
|
|
4352
4731
|
- Stop.
|
|
@@ -4442,7 +4821,7 @@ ${BASE_PROTOCOL}
|
|
|
4442
4821
|
Before using any tool or executing any command:
|
|
4443
4822
|
1. Confirm a RunTrim contract is active at .runtrim/contracts/latest.md.
|
|
4444
4823
|
2. If no active contract exists, do not proceed. Ask for:
|
|
4445
|
-
runtrim
|
|
4824
|
+
runtrim agent "<task>" --copy
|
|
4446
4825
|
3. Do not call shell commands, write files, or read env vars outside the contract.
|
|
4447
4826
|
`.trim(),
|
|
4448
4827
|
custom: `
|
|
@@ -4495,7 +4874,7 @@ function getCursorMdcContent() {
|
|
|
4495
4874
|
"## If no active contract",
|
|
4496
4875
|
"",
|
|
4497
4876
|
"Ask the user to start a guarded run:",
|
|
4498
|
-
'`runtrim
|
|
4877
|
+
'`runtrim agent "<task>" --copy`',
|
|
4499
4878
|
"",
|
|
4500
4879
|
"Any agent. One run boundary."
|
|
4501
4880
|
].join("\n");
|
|
@@ -4757,7 +5136,7 @@ Before editing code:
|
|
|
4757
5136
|
- If the task touches auth, billing, payments, webhooks, database, middleware,
|
|
4758
5137
|
env vars, secrets, or subscriptions, stop and require an active RunTrim contract.
|
|
4759
5138
|
Ask the user to run:
|
|
4760
|
-
runtrim
|
|
5139
|
+
runtrim agent "<task>" --copy
|
|
4761
5140
|
- For low-risk work (UI polish, copy, docs, isolated component styling):
|
|
4762
5141
|
Fast Path is allowed if no unfinished changes exist.
|
|
4763
5142
|
Keep the change minimal.
|
|
@@ -4780,7 +5159,7 @@ No active RunTrim contract means no code edits.
|
|
|
4780
5159
|
If no active contract exists at .runtrim/contracts/latest.md:
|
|
4781
5160
|
- Do not edit any file.
|
|
4782
5161
|
- Ask the user to start a guarded run:
|
|
4783
|
-
runtrim
|
|
5162
|
+
runtrim agent "<task>" --copy
|
|
4784
5163
|
|
|
4785
5164
|
After every editing session:
|
|
4786
5165
|
- Ask the user to run:
|
|
@@ -4794,7 +5173,7 @@ Fast Path is allowed for low and medium risk work.
|
|
|
4794
5173
|
|
|
4795
5174
|
Critical systems (auth, billing, payments, webhooks, database, middleware,
|
|
4796
5175
|
env vars, secrets, subscriptions) still require a RunTrim contract:
|
|
4797
|
-
runtrim
|
|
5176
|
+
runtrim agent "<task>" --copy
|
|
4798
5177
|
|
|
4799
5178
|
After any edits:
|
|
4800
5179
|
- runtrim finish is required before continuing to another task.
|
|
@@ -4805,7 +5184,7 @@ RunTrim Auto-guard: Off
|
|
|
4805
5184
|
|
|
4806
5185
|
Auto-guard is disabled for this project.
|
|
4807
5186
|
RunTrim can still be used manually:
|
|
4808
|
-
runtrim
|
|
5187
|
+
runtrim agent "<task>" --copy
|
|
4809
5188
|
runtrim finish
|
|
4810
5189
|
`.trim();
|
|
4811
5190
|
}
|
|
@@ -4831,7 +5210,7 @@ function saveFastRunRecord(cwd, changedFiles, risk) {
|
|
|
4831
5210
|
reportParts.push(`${sensitive.length} sensitive path${sensitive.length === 1 ? "" : "s"} touched.`);
|
|
4832
5211
|
}
|
|
4833
5212
|
reportParts.push("No pre-run contract was captured for this run.");
|
|
4834
|
-
const nextSafeAction = changedFiles.length > 0 ? 'Create a contract before the next change: runtrim
|
|
5213
|
+
const nextSafeAction = changedFiles.length > 0 ? 'Create a contract before the next change: runtrim agent "<task>" --copy' : 'Start a guarded run: runtrim agent "<task>" --copy';
|
|
4835
5214
|
const summary = {
|
|
4836
5215
|
id,
|
|
4837
5216
|
task,
|
|
@@ -5351,7 +5730,7 @@ function recommendProviderRouting(ctx) {
|
|
|
5351
5730
|
} else if (route === "split-required") {
|
|
5352
5731
|
routingReason = "This spans multiple critical systems, so RunTrim should split into audit, implementation, and verification.";
|
|
5353
5732
|
}
|
|
5354
|
-
let nextCommand = `runtrim
|
|
5733
|
+
let nextCommand = `runtrim agent "${ctx.task}" --copy`;
|
|
5355
5734
|
if (route === "split-required") {
|
|
5356
5735
|
nextCommand = "split into:\n1. audit only\n2. implementation only\n3. verification only";
|
|
5357
5736
|
} else if (route === "preview-only") {
|
|
@@ -5373,6 +5752,7 @@ var _a;
|
|
|
5373
5752
|
var oraFactory = typeof ora === "function" ? ora : (_a = ora.default) != null ? _a : ora;
|
|
5374
5753
|
var ACCENT = chalk.hex("#C8901A");
|
|
5375
5754
|
var GO_ACCENT = chalk.hex("#8B7CFF");
|
|
5755
|
+
var RUNTRIM_AGENT_INSTRUCTIONS_VERSION = "2";
|
|
5376
5756
|
var DIM = chalk.gray;
|
|
5377
5757
|
var BOLD = chalk.white.bold;
|
|
5378
5758
|
var program = new Command();
|
|
@@ -5508,6 +5888,98 @@ async function copyToClipboardSafe(value) {
|
|
|
5508
5888
|
function dedupeFiles(files) {
|
|
5509
5889
|
return [...new Set(files.filter(Boolean).map((f) => f.replace(/\\/g, "/")))];
|
|
5510
5890
|
}
|
|
5891
|
+
var RUNTRIM_GITIGNORE_BLOCK_START = "# BEGIN RUNTRIM_ARTIFACTS";
|
|
5892
|
+
var RUNTRIM_GITIGNORE_BLOCK_END = "# END RUNTRIM_ARTIFACTS";
|
|
5893
|
+
function ensureRuntrimReadme(cwd) {
|
|
5894
|
+
const readmePath = path13.join(getConfigDir(cwd), "README.md");
|
|
5895
|
+
const content = [
|
|
5896
|
+
"# RunTrim Local Files",
|
|
5897
|
+
"",
|
|
5898
|
+
"RunTrim stores local metadata in this folder.",
|
|
5899
|
+
"",
|
|
5900
|
+
"Human-facing files:",
|
|
5901
|
+
"- `agent/instructions.md` and `agent/latest.md`",
|
|
5902
|
+
"- `contracts/latest.md`",
|
|
5903
|
+
"- `memory/current.md` and `memory/baseline.md`",
|
|
5904
|
+
"- `mcp/*.json`",
|
|
5905
|
+
"- `config.json`",
|
|
5906
|
+
"",
|
|
5907
|
+
"Internal artifacts:",
|
|
5908
|
+
"- `.runtrim/internal/runs/`",
|
|
5909
|
+
"- `.runtrim/internal/previews/`",
|
|
5910
|
+
"- `.runtrim/internal/restores/`",
|
|
5911
|
+
"- `.runtrim/internal/contracts-archive/`",
|
|
5912
|
+
"- `.runtrim/internal/agent-archive/`",
|
|
5913
|
+
"",
|
|
5914
|
+
"Notes:",
|
|
5915
|
+
"- Artifacts are local-first.",
|
|
5916
|
+
"- Source code is not uploaded by local storage.",
|
|
5917
|
+
"- Restore metadata is path-only and does not store secret contents."
|
|
5918
|
+
].join("\n");
|
|
5919
|
+
fs13.writeFileSync(readmePath, content + "\n", "utf-8");
|
|
5920
|
+
}
|
|
5921
|
+
function ensureRuntrimGitignoreGuidance(cwd) {
|
|
5922
|
+
const gitignorePath = path13.join(cwd, ".gitignore");
|
|
5923
|
+
if (!fs13.existsSync(gitignorePath)) return;
|
|
5924
|
+
const desired = [
|
|
5925
|
+
RUNTRIM_GITIGNORE_BLOCK_START,
|
|
5926
|
+
"# RunTrim local artifacts",
|
|
5927
|
+
".runtrim/internal/",
|
|
5928
|
+
".runtrim/runs/",
|
|
5929
|
+
".runtrim/previews/",
|
|
5930
|
+
".runtrim/restores/",
|
|
5931
|
+
".runtrim/contracts/archive/",
|
|
5932
|
+
".runtrim/agent/*.json",
|
|
5933
|
+
RUNTRIM_GITIGNORE_BLOCK_END
|
|
5934
|
+
].join("\n");
|
|
5935
|
+
const current = fs13.readFileSync(gitignorePath, "utf-8");
|
|
5936
|
+
const start = current.indexOf(RUNTRIM_GITIGNORE_BLOCK_START);
|
|
5937
|
+
const end = current.indexOf(RUNTRIM_GITIGNORE_BLOCK_END);
|
|
5938
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
5939
|
+
const next = current.slice(0, start).trimEnd() + "\n\n" + desired + "\n" + current.slice(end + RUNTRIM_GITIGNORE_BLOCK_END.length).replace(/^\n+/, "\n");
|
|
5940
|
+
if (next !== current) fs13.writeFileSync(gitignorePath, next, "utf-8");
|
|
5941
|
+
return;
|
|
5942
|
+
}
|
|
5943
|
+
if (!current.includes(".runtrim/internal/")) {
|
|
5944
|
+
fs13.writeFileSync(gitignorePath, current.trimEnd() + "\n\n" + desired + "\n", "utf-8");
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5947
|
+
function listFilesIfExists(dir) {
|
|
5948
|
+
if (!fs13.existsSync(dir)) return [];
|
|
5949
|
+
return fs13.readdirSync(dir).map((name) => path13.join(dir, name)).filter((p) => fs13.existsSync(p) && fs13.statSync(p).isFile());
|
|
5950
|
+
}
|
|
5951
|
+
function listArtifactFiles(cwd) {
|
|
5952
|
+
const dirs = [
|
|
5953
|
+
getRunsDir(cwd),
|
|
5954
|
+
getPreviewsDir(cwd),
|
|
5955
|
+
getRestoresDir(cwd),
|
|
5956
|
+
getContractsArchiveDir(cwd),
|
|
5957
|
+
path13.join(getConfigDir(cwd), "internal", "agent-archive"),
|
|
5958
|
+
getLegacyRunsDir(cwd),
|
|
5959
|
+
getLegacyPreviewsDir(cwd),
|
|
5960
|
+
getLegacyRestoresDir(cwd),
|
|
5961
|
+
path13.join(getConfigDir(cwd), "contracts", "archive"),
|
|
5962
|
+
path13.join(getConfigDir(cwd), "agent")
|
|
5963
|
+
];
|
|
5964
|
+
const files = dirs.flatMap((dir) => listFilesIfExists(dir));
|
|
5965
|
+
return files.filter((filePath) => {
|
|
5966
|
+
const base = path13.basename(filePath).toLowerCase();
|
|
5967
|
+
const rel = path13.relative(cwd, filePath).replace(/\\/g, "/");
|
|
5968
|
+
if (rel === ".runtrim/previews/latest.md") return false;
|
|
5969
|
+
if (base === "latest.md" || base === "instructions.md" || base === "current.md" || base === "baseline.md") return false;
|
|
5970
|
+
return true;
|
|
5971
|
+
});
|
|
5972
|
+
}
|
|
5973
|
+
function parseRunIdFromArtifact(filePath) {
|
|
5974
|
+
const base = path13.basename(filePath);
|
|
5975
|
+
const direct = base.match(/^([a-zA-Z0-9_-]{6,})\.json$/);
|
|
5976
|
+
if (direct) return direct[1];
|
|
5977
|
+
const report = base.match(/^([a-zA-Z0-9_-]{6,})\.report\.\d+\.json$/);
|
|
5978
|
+
if (report) return report[1];
|
|
5979
|
+
const out = base.match(/^([a-zA-Z0-9_-]{6,})\.output\.txt$/);
|
|
5980
|
+
if (out) return out[1];
|
|
5981
|
+
return null;
|
|
5982
|
+
}
|
|
5511
5983
|
function normalizeContractPathPattern(pattern) {
|
|
5512
5984
|
let p = pattern.trim().replace(/\\/g, "/");
|
|
5513
5985
|
if (!p || p === "-" || p.toLowerCase() === "none") return "";
|
|
@@ -5677,10 +6149,11 @@ function buildRecommendedNextCommand(task, approval, filesToInspect) {
|
|
|
5677
6149
|
return `runtrim go "${task}"`;
|
|
5678
6150
|
}
|
|
5679
6151
|
function writePreviewArtifacts(cwd, preview) {
|
|
5680
|
-
const previewsDir =
|
|
6152
|
+
const previewsDir = getPreviewsDir(cwd);
|
|
5681
6153
|
if (!fs13.existsSync(previewsDir)) fs13.mkdirSync(previewsDir, { recursive: true });
|
|
5682
6154
|
const jsonPath = path13.join(previewsDir, `${preview.id}.json`);
|
|
5683
|
-
const markdownPath = path13.join(
|
|
6155
|
+
const markdownPath = path13.join(getLegacyPreviewsDir(cwd), "latest.md");
|
|
6156
|
+
if (!fs13.existsSync(path13.dirname(markdownPath))) fs13.mkdirSync(path13.dirname(markdownPath), { recursive: true });
|
|
5684
6157
|
fs13.writeFileSync(jsonPath, JSON.stringify(preview, null, 2), "utf-8");
|
|
5685
6158
|
const lines = [
|
|
5686
6159
|
"RunTrim Agent Preview",
|
|
@@ -5719,7 +6192,7 @@ function writePreviewArtifacts(cwd, preview) {
|
|
|
5719
6192
|
"Next:",
|
|
5720
6193
|
preview.recommendedNextCommand,
|
|
5721
6194
|
"",
|
|
5722
|
-
`Preview JSON: .runtrim/previews/${preview.id}.json`
|
|
6195
|
+
`Preview JSON: .runtrim/internal/previews/${preview.id}.json`
|
|
5723
6196
|
];
|
|
5724
6197
|
fs13.writeFileSync(markdownPath, lines.join("\n"), "utf-8");
|
|
5725
6198
|
return { jsonPath, markdownPath };
|
|
@@ -6101,6 +6574,7 @@ async function runAgentApply(task, mode) {
|
|
|
6101
6574
|
const copied = mode.copy ? await copyToClipboardSafe(fs13.readFileSync(handoff.markdownPath, "utf-8")) : false;
|
|
6102
6575
|
const run = saveRun(task, previewResult.audit, previewResult.contract, cwd);
|
|
6103
6576
|
updateRun(run.id, { status: "guarded" }, cwd);
|
|
6577
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
6104
6578
|
const riskColor = (_a2 = { low: chalk.green, medium: chalk.yellow, high: chalk.hex("#FF8C00"), critical: chalk.red }[risk]) != null ? _a2 : chalk.white;
|
|
6105
6579
|
if (mode.primary) {
|
|
6106
6580
|
console.log("");
|
|
@@ -6287,6 +6761,7 @@ async function runControlledExecution(task, mode) {
|
|
|
6287
6761
|
controlledExecutionId: id,
|
|
6288
6762
|
controlledExecutionStatus: status
|
|
6289
6763
|
}, cwd);
|
|
6764
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
6290
6765
|
}
|
|
6291
6766
|
if (status === "blocked") {
|
|
6292
6767
|
console.log("");
|
|
@@ -6577,14 +7052,18 @@ ${block}
|
|
|
6577
7052
|
function buildRuntrimAgentBlockContent() {
|
|
6578
7053
|
return [
|
|
6579
7054
|
"RunTrim project instructions",
|
|
7055
|
+
`RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`,
|
|
6580
7056
|
"- Before editing, read RunTrim project memory and profile context.",
|
|
6581
|
-
"-
|
|
7057
|
+
"- Before editing, create or verify an active RunTrim contract for the current user task.",
|
|
7058
|
+
"- If MCP is available and no active contract exists, call runtrim_create_contract with the user's task.",
|
|
6582
7059
|
'- If MCP is unavailable, ask the user to run: runtrim agent "task" --copy',
|
|
6583
|
-
"-
|
|
6584
|
-
"-
|
|
7060
|
+
"- Before touching risky paths (auth, billing, middleware/proxy, migrations, env/sensitive files, broad app-wide refactors), call runtrim_check_path when available.",
|
|
7061
|
+
"- Use RunTrim project memory and stay inside active contract scope.",
|
|
7062
|
+
"- Do not silently expand scope. Preserve existing project style and logic.",
|
|
6585
7063
|
"- Never read or print env file contents.",
|
|
6586
|
-
'-
|
|
6587
|
-
"- At the end, run
|
|
7064
|
+
'- If scope must expand, stop and request approval: runtrim approve "Allow <path/scope> for this run only"',
|
|
7065
|
+
"- At the end, run runtrim finish if terminal execution is available, otherwise ask the user to run runtrim finish.",
|
|
7066
|
+
"- Do not claim completion before finish verification when RunTrim is active.",
|
|
6588
7067
|
"",
|
|
6589
7068
|
"RunTrim MCP:",
|
|
6590
7069
|
"- runtrim mcp instructions",
|
|
@@ -6744,12 +7223,16 @@ function updateStartAgentInstructions(cwd) {
|
|
|
6744
7223
|
const existing = fs13.existsSync(instructionsPath) ? fs13.readFileSync(instructionsPath, "utf-8") : "";
|
|
6745
7224
|
const body = [
|
|
6746
7225
|
"RunTrim agent instructions:",
|
|
7226
|
+
`RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`,
|
|
6747
7227
|
"- Use RunTrim project memory and profile before editing.",
|
|
6748
|
-
"-
|
|
7228
|
+
"- Create or verify an active RunTrim contract before edits.",
|
|
7229
|
+
"- If MCP is available and contract is missing, call runtrim_create_contract with the user's task.",
|
|
6749
7230
|
'- If MCP is unavailable, ask user to run: runtrim agent "task" --copy',
|
|
6750
|
-
"- For high-risk paths (auth, billing, middleware/proxy, migrations, sensitive files, broad app-wide changes), check scope first. If MCP is available, call runtrim_check_path.",
|
|
7231
|
+
"- For high-risk paths (auth, billing, middleware/proxy, migrations, env/sensitive files, broad app-wide changes), check scope first. If MCP is available, call runtrim_check_path.",
|
|
7232
|
+
"- Stay inside active contract scope and preserve existing project style and logic.",
|
|
6751
7233
|
'- If scope must expand, request: runtrim approve "Allow <path/scope> for this run only"',
|
|
6752
|
-
"- Run finish
|
|
7234
|
+
"- Run runtrim finish when terminal execution is available, otherwise ask the user to run runtrim finish.",
|
|
7235
|
+
"- Do not claim completion before runtrim finish verification when RunTrim is active.",
|
|
6753
7236
|
"- Never read or print env file contents.",
|
|
6754
7237
|
"",
|
|
6755
7238
|
"RunTrim MCP:",
|
|
@@ -6822,6 +7305,202 @@ function detectKnownMcpConfigPresence() {
|
|
|
6822
7305
|
cursorConfigFound: Boolean(cursorMatch)
|
|
6823
7306
|
};
|
|
6824
7307
|
}
|
|
7308
|
+
function getRestorePointPath(cwd, runId) {
|
|
7309
|
+
return path13.join(getRestoresDir(cwd), `${runId}.json`);
|
|
7310
|
+
}
|
|
7311
|
+
function getLegacyRestorePointPath(cwd, runId) {
|
|
7312
|
+
return path13.join(getLegacyRestoresDir(cwd), `${runId}.json`);
|
|
7313
|
+
}
|
|
7314
|
+
async function isGitRepo(cwd) {
|
|
7315
|
+
try {
|
|
7316
|
+
await execa3("git", ["rev-parse", "--is-inside-work-tree"], { cwd });
|
|
7317
|
+
return true;
|
|
7318
|
+
} catch (e) {
|
|
7319
|
+
return false;
|
|
7320
|
+
}
|
|
7321
|
+
}
|
|
7322
|
+
async function captureRestorePoint(cwd, runId, task) {
|
|
7323
|
+
const dir = getRestoresDir(cwd);
|
|
7324
|
+
if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
|
|
7325
|
+
const existingPath = getRestorePointPath(cwd, runId);
|
|
7326
|
+
if (fs13.existsSync(existingPath) || fs13.existsSync(getLegacyRestorePointPath(cwd, runId))) return;
|
|
7327
|
+
let commit = null;
|
|
7328
|
+
let changedBeforeRun = [];
|
|
7329
|
+
if (await isGitRepo(cwd)) {
|
|
7330
|
+
try {
|
|
7331
|
+
const { stdout } = await execa3("git", ["rev-parse", "HEAD"], { cwd });
|
|
7332
|
+
commit = stdout.trim() || null;
|
|
7333
|
+
} catch (e) {
|
|
7334
|
+
commit = null;
|
|
7335
|
+
}
|
|
7336
|
+
try {
|
|
7337
|
+
const changed = await getGitChangedFiles(cwd);
|
|
7338
|
+
changedBeforeRun = dedupeFiles(changed.map((c) => c.path));
|
|
7339
|
+
} catch (e) {
|
|
7340
|
+
changedBeforeRun = [];
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
const record = {
|
|
7344
|
+
runId,
|
|
7345
|
+
task,
|
|
7346
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7347
|
+
preRun: {
|
|
7348
|
+
commit,
|
|
7349
|
+
dirty: changedBeforeRun.length > 0,
|
|
7350
|
+
changedBeforeRun
|
|
7351
|
+
}
|
|
7352
|
+
};
|
|
7353
|
+
fs13.writeFileSync(existingPath, JSON.stringify(record, null, 2), "utf-8");
|
|
7354
|
+
}
|
|
7355
|
+
function loadRestorePoint(cwd, runId) {
|
|
7356
|
+
const preferred = getRestorePointPath(cwd, runId);
|
|
7357
|
+
const legacy = getLegacyRestorePointPath(cwd, runId);
|
|
7358
|
+
const p = fs13.existsSync(preferred) ? preferred : legacy;
|
|
7359
|
+
if (!fs13.existsSync(p)) return null;
|
|
7360
|
+
try {
|
|
7361
|
+
return JSON.parse(fs13.readFileSync(p, "utf-8"));
|
|
7362
|
+
} catch (e) {
|
|
7363
|
+
return null;
|
|
7364
|
+
}
|
|
7365
|
+
}
|
|
7366
|
+
function saveRestorePoint(cwd, record) {
|
|
7367
|
+
const dir = getRestoresDir(cwd);
|
|
7368
|
+
if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
|
|
7369
|
+
fs13.writeFileSync(getRestorePointPath(cwd, record.runId), JSON.stringify(record, null, 2), "utf-8");
|
|
7370
|
+
}
|
|
7371
|
+
function isSecretLikePath(file) {
|
|
7372
|
+
const n = file.replace(/\\/g, "/").toLowerCase();
|
|
7373
|
+
return n.includes(".env") || n.endsWith(".env") || n.endsWith(".pem") || n.endsWith(".key") || n.includes("id_rsa") || n.includes("id_ed25519") || n.includes("private-key");
|
|
7374
|
+
}
|
|
7375
|
+
function isDocsLikePath(file) {
|
|
7376
|
+
const n = file.replace(/\\/g, "/").toLowerCase();
|
|
7377
|
+
return n === "readme.md" || n.startsWith("docs/") || n.endsWith(".md") || n.endsWith(".mdx") || n.includes("changelog");
|
|
7378
|
+
}
|
|
7379
|
+
function isUiCheckoutPath(file) {
|
|
7380
|
+
const n = file.replace(/\\/g, "/").toLowerCase();
|
|
7381
|
+
return n.includes("/checkout/") && !n.includes("/api/") && !n.includes("webhook") && !n.includes("stripe") && !n.includes("dodo") && !n.includes("provider") && !n.includes("session");
|
|
7382
|
+
}
|
|
7383
|
+
function isHighRiskLogicPath(file) {
|
|
7384
|
+
const n = file.replace(/\\/g, "/").toLowerCase();
|
|
7385
|
+
if (isSecretLikePath(n)) return true;
|
|
7386
|
+
if (n.endsWith("middleware.ts") || n.endsWith("middleware.js") || n.endsWith("proxy.ts") || n.endsWith("proxy.js")) return true;
|
|
7387
|
+
if (n.includes("supabase/migrations") || n.includes("prisma/migrations") || n.includes("/migrations/")) return true;
|
|
7388
|
+
if (n.includes("/auth/") || n.includes("session") || n.includes("jwt")) return true;
|
|
7389
|
+
if (n.includes("webhook")) return true;
|
|
7390
|
+
if ((n.includes("billing") || n.includes("payment") || n.includes("stripe") || n.includes("dodo")) && (n.includes("/api/") || n.includes("/lib/") || n.includes("route.ts") || n.includes("route.js") || n.includes("server"))) return true;
|
|
7391
|
+
if (n.includes("/checkout/") && !isUiCheckoutPath(n) && (n.includes("/api/") || n.includes("provider") || n.includes("session") || n.includes("route.ts") || n.includes("route.js"))) return true;
|
|
7392
|
+
return false;
|
|
7393
|
+
}
|
|
7394
|
+
function matchesAnyContractRule(file, rules) {
|
|
7395
|
+
return rules.some((rule) => matchesContractPattern(file, rule));
|
|
7396
|
+
}
|
|
7397
|
+
async function detectCiChangedFiles(cwd, base, head) {
|
|
7398
|
+
var _a2;
|
|
7399
|
+
const warnings = [];
|
|
7400
|
+
let baseUsed = (base == null ? void 0 : base.trim()) || "";
|
|
7401
|
+
let headUsed = (head == null ? void 0 : head.trim()) || "";
|
|
7402
|
+
if (!headUsed) {
|
|
7403
|
+
try {
|
|
7404
|
+
const { stdout } = await execa3("git", ["rev-parse", "HEAD"], { cwd });
|
|
7405
|
+
headUsed = stdout.trim();
|
|
7406
|
+
} catch (e) {
|
|
7407
|
+
headUsed = "HEAD";
|
|
7408
|
+
}
|
|
7409
|
+
}
|
|
7410
|
+
if (!baseUsed) {
|
|
7411
|
+
const ghBase = (_a2 = process.env.GITHUB_BASE_REF) == null ? void 0 : _a2.trim();
|
|
7412
|
+
if (ghBase) {
|
|
7413
|
+
baseUsed = `origin/${ghBase}`;
|
|
7414
|
+
}
|
|
7415
|
+
}
|
|
7416
|
+
if (!baseUsed) {
|
|
7417
|
+
for (const candidate of ["origin/main", "origin/master", "main", "master", "HEAD~1"]) {
|
|
7418
|
+
try {
|
|
7419
|
+
await execa3("git", ["rev-parse", "--verify", candidate], { cwd });
|
|
7420
|
+
baseUsed = candidate;
|
|
7421
|
+
break;
|
|
7422
|
+
} catch (e) {
|
|
7423
|
+
continue;
|
|
7424
|
+
}
|
|
7425
|
+
}
|
|
7426
|
+
}
|
|
7427
|
+
if (!baseUsed) {
|
|
7428
|
+
warnings.push("Could not infer a base ref. Using working tree changes only.");
|
|
7429
|
+
}
|
|
7430
|
+
let diffFiles = [];
|
|
7431
|
+
if (baseUsed) {
|
|
7432
|
+
try {
|
|
7433
|
+
const { stdout } = await execa3("git", ["diff", "--name-only", `${baseUsed}...${headUsed}`], { cwd });
|
|
7434
|
+
diffFiles = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
7435
|
+
} catch (e) {
|
|
7436
|
+
warnings.push(`Could not diff ${baseUsed}...${headUsed}. Falling back to local changes.`);
|
|
7437
|
+
}
|
|
7438
|
+
}
|
|
7439
|
+
if (diffFiles.length === 0) {
|
|
7440
|
+
try {
|
|
7441
|
+
const { stdout } = await execa3("git", ["diff", "--name-only"], { cwd });
|
|
7442
|
+
diffFiles = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
7443
|
+
} catch (e) {
|
|
7444
|
+
}
|
|
7445
|
+
}
|
|
7446
|
+
try {
|
|
7447
|
+
const { stdout } = await execa3("git", ["status", "--porcelain"], { cwd });
|
|
7448
|
+
const untracked = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("?? ")).map((line) => line.slice(3).trim());
|
|
7449
|
+
diffFiles = dedupeFiles([...diffFiles, ...untracked]);
|
|
7450
|
+
} catch (e) {
|
|
7451
|
+
}
|
|
7452
|
+
return {
|
|
7453
|
+
files: dedupeFiles(diffFiles.map((f) => f.replace(/\\/g, "/"))),
|
|
7454
|
+
baseUsed: baseUsed || "(local)",
|
|
7455
|
+
headUsed,
|
|
7456
|
+
warnings
|
|
7457
|
+
};
|
|
7458
|
+
}
|
|
7459
|
+
function detectMcpConfigState(configPath, found) {
|
|
7460
|
+
if (!found || !configPath) return "not found";
|
|
7461
|
+
try {
|
|
7462
|
+
const raw = JSON.parse(fs13.readFileSync(configPath, "utf-8"));
|
|
7463
|
+
const servers = raw.mcpServers && typeof raw.mcpServers === "object" ? raw.mcpServers : {};
|
|
7464
|
+
return servers.runtrim ? "configured" : "missing runtrim";
|
|
7465
|
+
} catch (e) {
|
|
7466
|
+
return "missing runtrim";
|
|
7467
|
+
}
|
|
7468
|
+
}
|
|
7469
|
+
function hasCurrentRuntrimBlock(filePath) {
|
|
7470
|
+
if (!fs13.existsSync(filePath)) return { exists: false, current: false };
|
|
7471
|
+
const content = fs13.readFileSync(filePath, "utf-8");
|
|
7472
|
+
const hasBlock = content.includes("<!-- RUNTRIM:START -->") && content.includes("<!-- RUNTRIM:END -->");
|
|
7473
|
+
const hasVersion = content.includes(`RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`);
|
|
7474
|
+
return { exists: hasBlock, current: hasBlock && hasVersion };
|
|
7475
|
+
}
|
|
7476
|
+
function readMcpLastUsed(cwd) {
|
|
7477
|
+
const p = path13.join(getProjectMcpDir(cwd), "last-used.json");
|
|
7478
|
+
if (!fs13.existsSync(p)) return { tracked: false, tool: null, usedAt: null };
|
|
7479
|
+
try {
|
|
7480
|
+
const parsed = JSON.parse(fs13.readFileSync(p, "utf-8"));
|
|
7481
|
+
return {
|
|
7482
|
+
tracked: true,
|
|
7483
|
+
tool: typeof parsed.tool === "string" ? parsed.tool : null,
|
|
7484
|
+
usedAt: typeof parsed.usedAt === "string" ? parsed.usedAt : null
|
|
7485
|
+
};
|
|
7486
|
+
} catch (e) {
|
|
7487
|
+
return { tracked: false, tool: null, usedAt: null };
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
function writeMcpLastUsed(cwd, tool) {
|
|
7491
|
+
try {
|
|
7492
|
+
const dir = getProjectMcpDir(cwd);
|
|
7493
|
+
if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
|
|
7494
|
+
const payload = {
|
|
7495
|
+
tool,
|
|
7496
|
+
usedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7497
|
+
projectPath: cwd
|
|
7498
|
+
};
|
|
7499
|
+
fs13.writeFileSync(path13.join(dir, "last-used.json"), `${JSON.stringify(payload, null, 2)}
|
|
7500
|
+
`, "utf-8");
|
|
7501
|
+
} catch (e) {
|
|
7502
|
+
}
|
|
7503
|
+
}
|
|
6825
7504
|
function appendContractAmendment(cwd, approvalText) {
|
|
6826
7505
|
const p = path13.join(cwd, ".runtrim", "contracts", "latest.md");
|
|
6827
7506
|
if (!fs13.existsSync(p)) return { ok: false, reason: "missing_contract" };
|
|
@@ -7129,6 +7808,24 @@ async function buildRuntrimCreateContractMcp(cwd, args) {
|
|
|
7129
7808
|
isError: true
|
|
7130
7809
|
};
|
|
7131
7810
|
}
|
|
7811
|
+
const repoCheck = await assertFreeRepoAllowed(cwd);
|
|
7812
|
+
if (!repoCheck.allowed) {
|
|
7813
|
+
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.";
|
|
7814
|
+
const blockedPayload = {
|
|
7815
|
+
contract_created: false,
|
|
7816
|
+
task: taskRaw,
|
|
7817
|
+
error: repoCheck.status === "blocked_repair" ? "repo_registry_repair_required" : "repo_limit_blocked",
|
|
7818
|
+
guidance,
|
|
7819
|
+
next_action: guidance,
|
|
7820
|
+
finish_command: "runtrim finish",
|
|
7821
|
+
approval_command_example: 'runtrim approve "Allow <path> for this run only"'
|
|
7822
|
+
};
|
|
7823
|
+
return {
|
|
7824
|
+
content: [{ type: "text", text: JSON.stringify(blockedPayload, null, 2) }],
|
|
7825
|
+
structuredContent: blockedPayload,
|
|
7826
|
+
isError: true
|
|
7827
|
+
};
|
|
7828
|
+
}
|
|
7132
7829
|
const latest = loadLatestRun(cwd);
|
|
7133
7830
|
if ((latest == null ? void 0 : latest.status) === "guarded") {
|
|
7134
7831
|
const blockedPayload = {
|
|
@@ -7176,6 +7873,7 @@ async function buildRuntrimCreateContractMcp(cwd, args) {
|
|
|
7176
7873
|
const handoff = writeAgentHandoffArtifacts(cwd, apply, path13.relative(cwd, previewPath));
|
|
7177
7874
|
const run = saveRun(mergedTask, previewResult.audit, previewResult.contract, cwd);
|
|
7178
7875
|
updateRun(run.id, { status: "guarded" }, cwd);
|
|
7876
|
+
await captureRestorePoint(cwd, run.id, mergedTask);
|
|
7179
7877
|
const payload = {
|
|
7180
7878
|
contract_created: true,
|
|
7181
7879
|
task: taskRaw,
|
|
@@ -7370,6 +8068,7 @@ async function startMcpServerStdio(cwd) {
|
|
|
7370
8068
|
});
|
|
7371
8069
|
return;
|
|
7372
8070
|
}
|
|
8071
|
+
writeMcpLastUsed(cwd, name);
|
|
7373
8072
|
send({
|
|
7374
8073
|
jsonrpc: "2.0",
|
|
7375
8074
|
id,
|
|
@@ -7790,6 +8489,21 @@ function isInteractiveTerminal() {
|
|
|
7790
8489
|
async function ensureRepoAllowedForFree(cwd) {
|
|
7791
8490
|
var _a2, _b;
|
|
7792
8491
|
const check = await assertFreeRepoAllowed(cwd);
|
|
8492
|
+
if (check.status === "blocked_repair") {
|
|
8493
|
+
console.log(chalk.yellow(" RunTrim local state needs repair."));
|
|
8494
|
+
console.log(chalk.yellow(" Free includes 1 tracked repo."));
|
|
8495
|
+
console.log(chalk.yellow(" Your local repo registry changed unexpectedly."));
|
|
8496
|
+
console.log("");
|
|
8497
|
+
console.log(DIM(" Next:"));
|
|
8498
|
+
console.log(chalk.white(" - runtrim repo status"));
|
|
8499
|
+
console.log(chalk.white(" - runtrim repo repair"));
|
|
8500
|
+
console.log(chalk.white(" - runtrim repo repair --use-current"));
|
|
8501
|
+
console.log(chalk.white(" - runtrim repo unlink --force"));
|
|
8502
|
+
console.log(chalk.white(" - upgrade to Builder for unlimited repos"));
|
|
8503
|
+
console.log(chalk.white(" - sign in to restore cloud entitlements"));
|
|
8504
|
+
console.log("");
|
|
8505
|
+
return false;
|
|
8506
|
+
}
|
|
7793
8507
|
if (check.allowed) {
|
|
7794
8508
|
await registerCurrentRepo(cwd);
|
|
7795
8509
|
return true;
|
|
@@ -7803,9 +8517,12 @@ async function ensureRepoAllowedForFree(cwd) {
|
|
|
7803
8517
|
console.log(chalk.white(` ${check.currentRepo.path}`));
|
|
7804
8518
|
console.log("");
|
|
7805
8519
|
console.log(DIM(" Next:"));
|
|
7806
|
-
console.log(
|
|
7807
|
-
|
|
7808
|
-
|
|
8520
|
+
console.log(
|
|
8521
|
+
chalk.white(
|
|
8522
|
+
" 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."
|
|
8523
|
+
)
|
|
8524
|
+
);
|
|
8525
|
+
console.log(chalk.white(" Agent instructions were not installed because this repo is not tracked."));
|
|
7809
8526
|
console.log("");
|
|
7810
8527
|
console.log(
|
|
7811
8528
|
DIM(
|
|
@@ -7839,6 +8556,7 @@ async function initializeRunTrim(cwd, options = {}) {
|
|
|
7839
8556
|
const runsDir = getRunsDir(cwd);
|
|
7840
8557
|
if (!fs13.existsSync(configDir)) fs13.mkdirSync(configDir, { recursive: true });
|
|
7841
8558
|
if (!fs13.existsSync(runsDir)) fs13.mkdirSync(runsDir, { recursive: true });
|
|
8559
|
+
ensureInternalArtifactDirs(cwd);
|
|
7842
8560
|
const existingConfig = hadConfig ? loadConfig(cwd) : null;
|
|
7843
8561
|
const baseConfig = __spreadValues(__spreadValues({}, DEFAULT_CONFIG), detectProjectInfo(cwd));
|
|
7844
8562
|
const nextConfig = options.refresh && existingConfig ? __spreadValues({}, existingConfig) : baseConfig;
|
|
@@ -7858,13 +8576,8 @@ async function initializeRunTrim(cwd, options = {}) {
|
|
|
7858
8576
|
fs13.writeFileSync(memoryPath, buildBaselineMemoryMarkdown(baseline), "utf-8");
|
|
7859
8577
|
}
|
|
7860
8578
|
ensureStarterPromptIfMissing(cwd);
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
const content = fs13.readFileSync(gitignorePath, "utf-8");
|
|
7864
|
-
if (!content.includes(".runtrim/runs")) {
|
|
7865
|
-
fs13.appendFileSync(gitignorePath, "\n# RunTrim run history\n.runtrim/runs/\n");
|
|
7866
|
-
}
|
|
7867
|
-
}
|
|
8579
|
+
ensureRuntrimReadme(cwd);
|
|
8580
|
+
ensureRuntrimGitignoreGuidance(cwd);
|
|
7868
8581
|
return { ok: true };
|
|
7869
8582
|
}
|
|
7870
8583
|
async function runPrepareTask(task, options) {
|
|
@@ -7899,7 +8612,7 @@ async function runPrepareTask(task, options) {
|
|
|
7899
8612
|
console.log("");
|
|
7900
8613
|
console.log(DIM(" Task ") + chalk.white(truncate(task, 70)));
|
|
7901
8614
|
console.log(DIM(" Prompt ") + chalk.white(promptPath2));
|
|
7902
|
-
console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
|
|
8615
|
+
console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
|
|
7903
8616
|
console.log("");
|
|
7904
8617
|
printPrepareAgentInstructions(selectedAgent, config.lastPromptPath);
|
|
7905
8618
|
console.log("");
|
|
@@ -7919,6 +8632,7 @@ async function runPrepareTask(task, options) {
|
|
|
7919
8632
|
const promptPath = writeLatestPromptFile(contract.contractText, config, cwd);
|
|
7920
8633
|
if (options.copy !== false) await copyToClipboardSafe(contract.contractText);
|
|
7921
8634
|
updateRun(run.id, { status: "guarded" }, cwd);
|
|
8635
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
7922
8636
|
const riskColors = {
|
|
7923
8637
|
low: chalk.green,
|
|
7924
8638
|
medium: chalk.yellow,
|
|
@@ -7941,7 +8655,7 @@ async function runPrepareTask(task, options) {
|
|
|
7941
8655
|
);
|
|
7942
8656
|
console.log(DIM(" Reduction ") + chalk.white(contract.riskReductionPercent + "%"));
|
|
7943
8657
|
console.log(DIM(" Prompt ") + chalk.white(promptPath));
|
|
7944
|
-
console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
|
|
8658
|
+
console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
|
|
7945
8659
|
console.log("");
|
|
7946
8660
|
printPrepareAgentInstructions(selectedAgent, config.lastPromptPath);
|
|
7947
8661
|
console.log("");
|
|
@@ -8166,13 +8880,116 @@ program.command("start").description("Guided RunTrim onboarding and daily loop")
|
|
|
8166
8880
|
console.log("");
|
|
8167
8881
|
}
|
|
8168
8882
|
});
|
|
8883
|
+
program.command("doctor").description("Check project readiness for RunTrim agent auto-control").action(async () => {
|
|
8884
|
+
const cwd = process.cwd();
|
|
8885
|
+
const repoCheck = await assertFreeRepoAllowed(cwd);
|
|
8886
|
+
const profilePath = path13.join(getConfigDir(cwd), "project-profile.json");
|
|
8887
|
+
const memoryPath = path13.join(getConfigDir(cwd), "memory", "current.md");
|
|
8888
|
+
const instructionsPath = path13.join(getConfigDir(cwd), "agent", "instructions.md");
|
|
8889
|
+
const snippetsDir = getProjectMcpDir(cwd);
|
|
8890
|
+
const snippetFiles = [
|
|
8891
|
+
path13.join(snippetsDir, "claude-desktop.json"),
|
|
8892
|
+
path13.join(snippetsDir, "cursor.json"),
|
|
8893
|
+
path13.join(snippetsDir, "generic.json")
|
|
8894
|
+
];
|
|
8895
|
+
const profileReady = fs13.existsSync(profilePath);
|
|
8896
|
+
const memoryReady = fs13.existsSync(memoryPath) && fs13.readFileSync(memoryPath, "utf-8").trim().length > 0;
|
|
8897
|
+
const instructionsReady = fs13.existsSync(instructionsPath) && fs13.readFileSync(instructionsPath, "utf-8").trim().length > 0;
|
|
8898
|
+
const claudeBlock = hasCurrentRuntrimBlock(path13.join(cwd, "CLAUDE.md"));
|
|
8899
|
+
const agentsBlock = hasCurrentRuntrimBlock(path13.join(cwd, "AGENTS.md"));
|
|
8900
|
+
const cursorRulePath = path13.join(cwd, ".cursor", "rules", "runtrim.mdc");
|
|
8901
|
+
const cursorRuleExists = fs13.existsSync(cursorRulePath);
|
|
8902
|
+
const cursorRuleCurrent = cursorRuleExists ? fs13.readFileSync(cursorRulePath, "utf-8").includes(`RUNTRIM_AGENT_INSTRUCTIONS_VERSION: ${RUNTRIM_AGENT_INSTRUCTIONS_VERSION}`) : false;
|
|
8903
|
+
const anySurfaceInstalled = claudeBlock.exists || agentsBlock.exists || cursorRuleExists;
|
|
8904
|
+
const runtrimBlockCurrent = [claudeBlock.current, agentsBlock.current, cursorRuleCurrent].some(Boolean);
|
|
8905
|
+
const snippetsGenerated = snippetFiles.every((p) => fs13.existsSync(p));
|
|
8906
|
+
const knownMcp = detectKnownMcpConfigPresence();
|
|
8907
|
+
const claudeMcpState = detectMcpConfigState(knownMcp.claudeConfigPath, knownMcp.claudeConfigFound);
|
|
8908
|
+
const cursorMcpState = detectMcpConfigState(knownMcp.cursorConfigPath, knownMcp.cursorConfigFound);
|
|
8909
|
+
const genericReady = snippetsGenerated ? "ready" : "missing";
|
|
8910
|
+
const tools = buildMcpTools();
|
|
8911
|
+
const hasContractTool = tools.some((t) => t.name === "runtrim_create_contract");
|
|
8912
|
+
const hasPathTool = tools.some((t) => t.name === "runtrim_check_path");
|
|
8913
|
+
const hasApprovalTool = tools.some((t) => t.name === "runtrim_suggest_approval");
|
|
8914
|
+
const hasFinishTool = tools.some((t) => t.name === "runtrim_finish_guidance");
|
|
8915
|
+
const contract = parseContractSummary(cwd);
|
|
8916
|
+
const lastMcp = readMcpLastUsed(cwd);
|
|
8917
|
+
const setupCorrupt = configExists(cwd) && (!profileReady || !memoryReady || !instructionsReady);
|
|
8918
|
+
const artifactFiles = listArtifactFiles(cwd);
|
|
8919
|
+
const artifactCount = artifactFiles.length;
|
|
8920
|
+
let readiness = "partial";
|
|
8921
|
+
if (!repoCheck.allowed || setupCorrupt) {
|
|
8922
|
+
readiness = "blocked";
|
|
8923
|
+
} else if (profileReady && memoryReady && instructionsReady && anySurfaceInstalled && snippetsGenerated && hasContractTool && hasPathTool && hasApprovalTool && hasFinishTool) {
|
|
8924
|
+
readiness = "ready";
|
|
8925
|
+
}
|
|
8926
|
+
console.log("");
|
|
8927
|
+
console.log(BOLD("RunTrim") + DIM(" doctor"));
|
|
8928
|
+
console.log("");
|
|
8929
|
+
console.log(BOLD("Project"));
|
|
8930
|
+
console.log(chalk.white(`- Project profile: ${profileReady ? "ready" : "missing"}`));
|
|
8931
|
+
console.log(chalk.white(`- Project memory: ${memoryReady ? "ready" : "missing"}`));
|
|
8932
|
+
console.log(chalk.white(`- Agent instructions: ${instructionsReady ? "ready" : "missing"}`));
|
|
8933
|
+
console.log(chalk.white(`- Active contract: ${contract.active ? "active" : "none"}`));
|
|
8934
|
+
console.log("");
|
|
8935
|
+
console.log(BOLD("Agent rules"));
|
|
8936
|
+
console.log(chalk.white(`- CLAUDE.md: ${claudeBlock.exists ? "installed" : "not found"}`));
|
|
8937
|
+
console.log(chalk.white(`- AGENTS.md: ${agentsBlock.exists ? "installed" : "not found"}`));
|
|
8938
|
+
console.log(chalk.white(`- Cursor rules: ${cursorRuleExists ? "installed" : "not found"}`));
|
|
8939
|
+
console.log(chalk.white(`- RunTrim instruction block: ${runtrimBlockCurrent ? "current" : anySurfaceInstalled ? "stale" : "missing"}`));
|
|
8940
|
+
console.log("");
|
|
8941
|
+
console.log(BOLD("MCP"));
|
|
8942
|
+
console.log(chalk.white("- MCP server: available"));
|
|
8943
|
+
console.log(chalk.white(`- Project snippets: ${snippetsGenerated ? "generated" : "missing"}`));
|
|
8944
|
+
console.log(chalk.white(`- Claude Desktop config: ${claudeMcpState}`));
|
|
8945
|
+
console.log(chalk.white(`- Cursor MCP config: ${cursorMcpState}`));
|
|
8946
|
+
console.log(chalk.white(`- Generic config: ${genericReady}`));
|
|
8947
|
+
if (lastMcp.tracked && lastMcp.tool && lastMcp.usedAt) {
|
|
8948
|
+
console.log(chalk.white(`- Last MCP tool call: ${lastMcp.tool}, ${lastMcp.usedAt}`));
|
|
8949
|
+
} else {
|
|
8950
|
+
console.log(chalk.white("- Last MCP tool call: not tracked yet"));
|
|
8951
|
+
}
|
|
8952
|
+
console.log("");
|
|
8953
|
+
console.log(BOLD("Automation readiness"));
|
|
8954
|
+
console.log(chalk.white(`- Contract creation tool: ${hasContractTool ? "available" : "missing"}`));
|
|
8955
|
+
console.log(chalk.white(`- Path check tool: ${hasPathTool ? "available" : "missing"}`));
|
|
8956
|
+
console.log(chalk.white(`- Approval tool: ${hasApprovalTool ? "available" : "missing"}`));
|
|
8957
|
+
console.log(chalk.white(`- Finish guidance tool: ${hasFinishTool ? "available" : "missing"}`));
|
|
8958
|
+
console.log(chalk.white(`- Local artifacts: ${artifactCount}`));
|
|
8959
|
+
console.log("");
|
|
8960
|
+
console.log(BOLD("Readiness"));
|
|
8961
|
+
console.log(chalk.white(`- State: ${readiness}`));
|
|
8962
|
+
console.log("");
|
|
8963
|
+
console.log(BOLD("Next"));
|
|
8964
|
+
if (repoCheck.status === "blocked_repair") {
|
|
8965
|
+
console.log(chalk.yellow("- RunTrim local state needs repair. Run runtrim repo repair."));
|
|
8966
|
+
} else if (repoCheck.status === "blocked_limit") {
|
|
8967
|
+
console.log(chalk.yellow("- Free includes 1 tracked repo. Continue in tracked repo, unlink with runtrim repo unlink --force, or upgrade to Builder."));
|
|
8968
|
+
} else if (readiness === "ready") {
|
|
8969
|
+
if (claudeMcpState !== "configured" && cursorMcpState !== "configured") {
|
|
8970
|
+
console.log(chalk.white("- Ready locally, MCP client not connected."));
|
|
8971
|
+
console.log(chalk.white("- Run runtrim mcp instructions or copy .runtrim/mcp/cursor.json into your MCP client."));
|
|
8972
|
+
} else {
|
|
8973
|
+
console.log(chalk.white("- Your project is RunTrim-aware. Open Cursor/Claude/Codex and use normal language."));
|
|
8974
|
+
}
|
|
8975
|
+
} else {
|
|
8976
|
+
console.log(chalk.white("- Run runtrim start."));
|
|
8977
|
+
console.log(chalk.white("- Run runtrim mcp instructions."));
|
|
8978
|
+
console.log(chalk.white("- Run runtrim mcp config --print."));
|
|
8979
|
+
}
|
|
8980
|
+
if (artifactCount > 25) {
|
|
8981
|
+
console.log(chalk.white(`- Local artifacts: ${artifactCount}. Run runtrim clean --dry-run to review cleanup.`));
|
|
8982
|
+
}
|
|
8983
|
+
console.log("");
|
|
8984
|
+
});
|
|
8169
8985
|
var PROTOCOL_BLOCK_START = "<!-- RUNTRIM_PROTOCOL_START -->";
|
|
8170
8986
|
var PROTOCOL_BLOCK_END = "<!-- RUNTRIM_PROTOCOL_END -->";
|
|
8171
8987
|
var PROTOCOL_POINTER_BLOCK = `
|
|
8172
8988
|
${PROTOCOL_BLOCK_START}
|
|
8173
8989
|
This repo uses RunTrim as the guarded AI coding protocol.
|
|
8174
8990
|
Before editing code, read RUNTRIM.md.
|
|
8175
|
-
Start every task with: runtrim
|
|
8991
|
+
Start every task with: runtrim start
|
|
8992
|
+
Then run: runtrim agent "Your task" --copy
|
|
8176
8993
|
Stay inside .runtrim/contracts/latest.md.
|
|
8177
8994
|
After edits, ask the user to run: runtrim finish
|
|
8178
8995
|
${PROTOCOL_BLOCK_END}
|
|
@@ -8868,6 +9685,8 @@ program.command("execute <task>").description("Create a controlled execution pac
|
|
|
8868
9685
|
var agentCommand = program.command("agent").description("Start a guarded AI coding run with contract, scope, memory, and handoff");
|
|
8869
9686
|
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) => {
|
|
8870
9687
|
if (task == null ? void 0 : task.trim()) {
|
|
9688
|
+
const allowed = await ensureRepoAllowedForFree(process.cwd());
|
|
9689
|
+
if (!allowed) return;
|
|
8871
9690
|
const normalizedTask = (task != null ? task : "").trim();
|
|
8872
9691
|
if (options == null ? void 0 : options.bridge) {
|
|
8873
9692
|
const bridge = await ensureBridgeRunningForAgent(process.cwd());
|
|
@@ -9267,6 +10086,146 @@ agentCommand.command("prompt-mode <mode>").description("Set how guarded contract
|
|
|
9267
10086
|
console.log(ACCENT.bold(" Agent prompt mode updated to: " + m));
|
|
9268
10087
|
console.log("");
|
|
9269
10088
|
});
|
|
10089
|
+
var ciCommand = program.command("ci").description("RunTrim CI checks for pull requests and merges");
|
|
10090
|
+
ciCommand.command("check").description("Evaluate changed files and RunTrim context for CI-safe PASS/WARN/BLOCKED verdict").option("--base <ref>", "Base ref to diff from").option("--head <ref>", "Head ref to diff to").option("--strict", "Treat WARN as failing and require RunTrim context").option("--allow-warn", "Allow WARN even when --strict is set").option("--json", "Print machine-readable JSON output").option("--report", "Include extra diagnostic context in output").action(async (options) => {
|
|
10091
|
+
const cwd = process.cwd();
|
|
10092
|
+
const strict = options.strict === true;
|
|
10093
|
+
const allowWarn = options.allowWarn === true;
|
|
10094
|
+
const outputJson = options.json === true;
|
|
10095
|
+
const report = options.report === true;
|
|
10096
|
+
const repoCheck = await assertFreeRepoAllowed(cwd);
|
|
10097
|
+
const detection = await detectCiChangedFiles(cwd, options.base, options.head);
|
|
10098
|
+
const changedFiles = detection.files;
|
|
10099
|
+
const contract = parseContractSummary(cwd);
|
|
10100
|
+
const latestRun = loadLatestRun(cwd);
|
|
10101
|
+
const hasRuntrimContext = contract.exists || Boolean(latestRun);
|
|
10102
|
+
const issues = [];
|
|
10103
|
+
const warnings = [...detection.warnings];
|
|
10104
|
+
const nextSteps = [];
|
|
10105
|
+
let verdict = "PASS";
|
|
10106
|
+
if (repoCheck.status === "blocked_repair") {
|
|
10107
|
+
issues.push("RunTrim local state needs repair before CI can trust local guard state.");
|
|
10108
|
+
nextSteps.push("Run: runtrim repo repair");
|
|
10109
|
+
verdict = "BLOCKED";
|
|
10110
|
+
} else if (repoCheck.status === "blocked_limit") {
|
|
10111
|
+
issues.push("Free includes 1 tracked repo and this repo is not currently tracked.");
|
|
10112
|
+
nextSteps.push("Continue in tracked repo, unlink with runtrim repo unlink --force, or upgrade to Builder.");
|
|
10113
|
+
verdict = "BLOCKED";
|
|
10114
|
+
}
|
|
10115
|
+
if (changedFiles.length === 0) {
|
|
10116
|
+
warnings.push("No changed files detected for this diff.");
|
|
10117
|
+
}
|
|
10118
|
+
const secretFiles = changedFiles.filter(isSecretLikePath);
|
|
10119
|
+
if (secretFiles.length > 0) {
|
|
10120
|
+
issues.push(`Sensitive file path changed: ${secretFiles[0]}`);
|
|
10121
|
+
nextSteps.push("Remove secret/env/key/cert files from this PR before merge.");
|
|
10122
|
+
verdict = "BLOCKED";
|
|
10123
|
+
}
|
|
10124
|
+
const highRiskFiles = changedFiles.filter((f) => isHighRiskLogicPath(f) && !isSecretLikePath(f));
|
|
10125
|
+
if (highRiskFiles.length > 0) {
|
|
10126
|
+
if (!hasRuntrimContext) {
|
|
10127
|
+
issues.push(`High-risk path changed without RunTrim context: ${highRiskFiles[0]}`);
|
|
10128
|
+
nextSteps.push("Create a dedicated guarded run and finish it before merging.");
|
|
10129
|
+
verdict = "BLOCKED";
|
|
10130
|
+
} else if (contract.allowedPaths.length > 0 && !matchesAnyContractRule(highRiskFiles[0], contract.allowedPaths)) {
|
|
10131
|
+
issues.push(`High-risk path is outside contract allowed scope: ${highRiskFiles[0]}`);
|
|
10132
|
+
nextSteps.push(`Run: runtrim approve "Allow ${highRiskFiles[0]} for this run only"`);
|
|
10133
|
+
verdict = "BLOCKED";
|
|
10134
|
+
}
|
|
10135
|
+
}
|
|
10136
|
+
if (contract.forbiddenPaths.length > 0) {
|
|
10137
|
+
const forbiddenTouched = changedFiles.filter((f) => matchesAnyContractRule(f, contract.forbiddenPaths));
|
|
10138
|
+
if (forbiddenTouched.length > 0) {
|
|
10139
|
+
issues.push(`Contract-forbidden path touched: ${forbiddenTouched[0]}`);
|
|
10140
|
+
nextSteps.push("Split the task or get explicit scoped approval before merge.");
|
|
10141
|
+
verdict = "BLOCKED";
|
|
10142
|
+
}
|
|
10143
|
+
}
|
|
10144
|
+
if (contract.allowedPaths.length > 0) {
|
|
10145
|
+
const outOfScope = changedFiles.filter((f) => !matchesAnyContractRule(f, contract.allowedPaths));
|
|
10146
|
+
if (outOfScope.length > 0) {
|
|
10147
|
+
issues.push(`Changed file is outside latest contract scope: ${outOfScope[0]}`);
|
|
10148
|
+
nextSteps.push(`Run: runtrim approve "Allow ${outOfScope[0]} for this run only"`);
|
|
10149
|
+
verdict = "BLOCKED";
|
|
10150
|
+
}
|
|
10151
|
+
}
|
|
10152
|
+
const docsOnly = changedFiles.length > 0 && changedFiles.every((f) => isDocsLikePath(f));
|
|
10153
|
+
if (!hasRuntrimContext && verdict !== "BLOCKED") {
|
|
10154
|
+
if (docsOnly) {
|
|
10155
|
+
warnings.push("No RunTrim contract/report found. Docs-only change treated as low risk.");
|
|
10156
|
+
verdict = "WARN";
|
|
10157
|
+
} else {
|
|
10158
|
+
warnings.push("No RunTrim contract/report found. CI could not fully verify scope context.");
|
|
10159
|
+
verdict = "WARN";
|
|
10160
|
+
}
|
|
10161
|
+
}
|
|
10162
|
+
if (strict && !hasRuntrimContext && verdict !== "BLOCKED") {
|
|
10163
|
+
issues.push("Strict mode requires RunTrim contract/report context.");
|
|
10164
|
+
verdict = "BLOCKED";
|
|
10165
|
+
}
|
|
10166
|
+
if (verdict === "PASS" && warnings.length > 0) {
|
|
10167
|
+
verdict = "WARN";
|
|
10168
|
+
}
|
|
10169
|
+
if (nextSteps.length === 0) {
|
|
10170
|
+
if (verdict === "PASS") {
|
|
10171
|
+
nextSteps.push("Safe to merge under current RunTrim CI policy.");
|
|
10172
|
+
} else if (verdict === "WARN") {
|
|
10173
|
+
nextSteps.push("Run runtrim finish locally to strengthen verification context.");
|
|
10174
|
+
} else {
|
|
10175
|
+
nextSteps.push("Resolve blocked issues, then rerun runtrim ci check.");
|
|
10176
|
+
}
|
|
10177
|
+
}
|
|
10178
|
+
let exitCode = 0;
|
|
10179
|
+
if (verdict === "BLOCKED") exitCode = 1;
|
|
10180
|
+
if (verdict === "WARN" && strict && !allowWarn) exitCode = 1;
|
|
10181
|
+
const jsonPayload = {
|
|
10182
|
+
verdict: verdict.toLowerCase(),
|
|
10183
|
+
exitCode,
|
|
10184
|
+
changedFiles,
|
|
10185
|
+
issues,
|
|
10186
|
+
warnings,
|
|
10187
|
+
nextSteps
|
|
10188
|
+
};
|
|
10189
|
+
if (outputJson) {
|
|
10190
|
+
process.stdout.write(`${JSON.stringify(jsonPayload, null, 2)}
|
|
10191
|
+
`);
|
|
10192
|
+
process.exit(exitCode);
|
|
10193
|
+
return;
|
|
10194
|
+
}
|
|
10195
|
+
console.log("");
|
|
10196
|
+
console.log(BOLD("RunTrim") + DIM(" CI Check"));
|
|
10197
|
+
console.log("");
|
|
10198
|
+
const verdictColor = verdict === "PASS" ? chalk.green : verdict === "WARN" ? chalk.yellow : chalk.red;
|
|
10199
|
+
console.log(DIM(" Verdict: ") + verdictColor(verdict));
|
|
10200
|
+
console.log("");
|
|
10201
|
+
console.log(DIM(" Changed files:"));
|
|
10202
|
+
if (changedFiles.length === 0) console.log(chalk.white(" - (none detected)"));
|
|
10203
|
+
for (const f of changedFiles.slice(0, 20)) console.log(chalk.white(" - " + f));
|
|
10204
|
+
if (changedFiles.length > 20) console.log(DIM(` ... and ${changedFiles.length - 20} more`));
|
|
10205
|
+
console.log("");
|
|
10206
|
+
if (issues.length > 0) {
|
|
10207
|
+
console.log(DIM(" Issues:"));
|
|
10208
|
+
for (const i of issues) console.log(chalk.red(" - " + i));
|
|
10209
|
+
console.log("");
|
|
10210
|
+
}
|
|
10211
|
+
if (warnings.length > 0) {
|
|
10212
|
+
console.log(DIM(" Warnings:"));
|
|
10213
|
+
for (const w of warnings) console.log(chalk.yellow(" - " + w));
|
|
10214
|
+
console.log("");
|
|
10215
|
+
}
|
|
10216
|
+
console.log(DIM(" Next:"));
|
|
10217
|
+
for (const n of nextSteps) console.log(chalk.white(" - " + n));
|
|
10218
|
+
if (report) {
|
|
10219
|
+
console.log("");
|
|
10220
|
+
console.log(DIM(" Report:"));
|
|
10221
|
+
console.log(chalk.white(` - Base: ${detection.baseUsed}`));
|
|
10222
|
+
console.log(chalk.white(` - Head: ${detection.headUsed}`));
|
|
10223
|
+
console.log(chalk.white(` - Contract: ${contract.exists ? contract.active ? "active" : "present" : "none"}`));
|
|
10224
|
+
console.log(chalk.white(` - Latest run: ${latestRun ? latestRun.status : "none"}`));
|
|
10225
|
+
}
|
|
10226
|
+
console.log("");
|
|
10227
|
+
process.exit(exitCode);
|
|
10228
|
+
});
|
|
9270
10229
|
var authCommand = program.command("auth").description("Configure sync token for dashboard sync");
|
|
9271
10230
|
authCommand.command("set <token>").description("Set sync token and enable sync").action((token) => {
|
|
9272
10231
|
const cwd = process.cwd();
|
|
@@ -9336,12 +10295,54 @@ repoCommand.command("status").description("Show local tracked repo status").acti
|
|
|
9336
10295
|
console.log(DIM(" Current repo ") + chalk.white(identity.path));
|
|
9337
10296
|
console.log(DIM(" Tracked repo ") + chalk.white((_b = tracked == null ? void 0 : tracked.path) != null ? _b : "(none)"));
|
|
9338
10297
|
console.log(DIM(" Allowed ") + chalk.white(check.allowed ? "yes" : "no"));
|
|
10298
|
+
console.log(DIM(" State ") + chalk.white(check.status));
|
|
9339
10299
|
console.log("");
|
|
10300
|
+
if (check.status === "blocked_repair") {
|
|
10301
|
+
console.log(chalk.yellow(" RunTrim local state needs repair."));
|
|
10302
|
+
console.log(chalk.yellow(" Free includes 1 tracked repo."));
|
|
10303
|
+
console.log(chalk.yellow(" The local repo registry changed unexpectedly."));
|
|
10304
|
+
console.log(DIM(" Run: runtrim repo repair"));
|
|
10305
|
+
console.log("");
|
|
10306
|
+
}
|
|
9340
10307
|
if (tracked) {
|
|
9341
10308
|
console.log(DIM(" A tracked repo is one codebase with its own .runtrim workspace."));
|
|
9342
10309
|
console.log("");
|
|
9343
10310
|
}
|
|
9344
10311
|
});
|
|
10312
|
+
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) => {
|
|
10313
|
+
const cwd = process.cwd();
|
|
10314
|
+
const before = await assertFreeRepoAllowed(cwd);
|
|
10315
|
+
console.log("");
|
|
10316
|
+
console.log(BOLD("RunTrim") + DIM(" repo repair"));
|
|
10317
|
+
console.log("");
|
|
10318
|
+
if (!before.repairRequired) {
|
|
10319
|
+
console.log(DIM(" Local state is healthy. No repair required."));
|
|
10320
|
+
console.log("");
|
|
10321
|
+
return;
|
|
10322
|
+
}
|
|
10323
|
+
if (!options.useCurrent) {
|
|
10324
|
+
console.log(chalk.yellow(" RunTrim local state needs repair."));
|
|
10325
|
+
console.log(chalk.yellow(" Free includes 1 tracked repo."));
|
|
10326
|
+
console.log(chalk.yellow(" Your local repo registry changed unexpectedly."));
|
|
10327
|
+
console.log("");
|
|
10328
|
+
console.log(DIM(" Safe next actions:"));
|
|
10329
|
+
console.log(chalk.white(" - runtrim repo repair --use-current"));
|
|
10330
|
+
console.log(chalk.white(" - runtrim repo unlink --force"));
|
|
10331
|
+
console.log(chalk.white(" - upgrade to Builder for unlimited repos"));
|
|
10332
|
+
console.log(chalk.white(" - sign in to restore cloud entitlements"));
|
|
10333
|
+
console.log("");
|
|
10334
|
+
return;
|
|
10335
|
+
}
|
|
10336
|
+
const result = await repairGlobalRegistry(cwd, { useCurrentRepo: true });
|
|
10337
|
+
if (result.repaired) {
|
|
10338
|
+
console.log(ACCENT.bold(" Local registry repaired."));
|
|
10339
|
+
console.log(DIM(" Current repo is now the tracked Free repo."));
|
|
10340
|
+
console.log("");
|
|
10341
|
+
return;
|
|
10342
|
+
}
|
|
10343
|
+
console.log(DIM(" No repair changes applied."));
|
|
10344
|
+
console.log("");
|
|
10345
|
+
});
|
|
9345
10346
|
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) => {
|
|
9346
10347
|
const cwd = process.cwd();
|
|
9347
10348
|
const result = await unlinkCurrentRepo(cwd, Boolean(options.force));
|
|
@@ -9366,6 +10367,214 @@ repoCommand.command("unlink").description("Unlink tracked repo from local free-p
|
|
|
9366
10367
|
console.log(DIM(" No tracked repo found."));
|
|
9367
10368
|
console.log("");
|
|
9368
10369
|
});
|
|
10370
|
+
program.command("clean").description("Clean old local RunTrim artifacts while preserving active project state").option("--dry-run", "Preview files that would be removed").option("--keep <n>", "Number of latest run-linked artifacts to keep", "10").action(async (options) => {
|
|
10371
|
+
var _a2;
|
|
10372
|
+
const cwd = process.cwd();
|
|
10373
|
+
const dryRun = (options == null ? void 0 : options.dryRun) === true;
|
|
10374
|
+
const keep = Math.max(1, Number.parseInt((_a2 = options == null ? void 0 : options.keep) != null ? _a2 : "10", 10) || 10);
|
|
10375
|
+
ensureInternalArtifactDirs(cwd);
|
|
10376
|
+
const runs = loadAllRuns(cwd);
|
|
10377
|
+
const keepRunIds = new Set(runs.slice(0, keep).map((r) => r.id));
|
|
10378
|
+
const files = listArtifactFiles(cwd);
|
|
10379
|
+
const removable = files.filter((filePath) => {
|
|
10380
|
+
const runId = parseRunIdFromArtifact(filePath);
|
|
10381
|
+
if (!runId) return true;
|
|
10382
|
+
return !keepRunIds.has(runId);
|
|
10383
|
+
});
|
|
10384
|
+
const byCategory = {
|
|
10385
|
+
runs: 0,
|
|
10386
|
+
previews: 0,
|
|
10387
|
+
restores: 0,
|
|
10388
|
+
archives: 0,
|
|
10389
|
+
other: 0
|
|
10390
|
+
};
|
|
10391
|
+
for (const filePath of removable) {
|
|
10392
|
+
const rel = path13.relative(cwd, filePath).replace(/\\/g, "/").toLowerCase();
|
|
10393
|
+
if (rel.includes(".runtrim/internal/runs/") || rel.includes(".runtrim/runs/")) byCategory.runs += 1;
|
|
10394
|
+
else if (rel.includes(".runtrim/internal/previews/") || rel.includes(".runtrim/previews/")) byCategory.previews += 1;
|
|
10395
|
+
else if (rel.includes(".runtrim/internal/restores/") || rel.includes(".runtrim/restores/")) byCategory.restores += 1;
|
|
10396
|
+
else if (rel.includes("archive")) byCategory.archives += 1;
|
|
10397
|
+
else byCategory.other += 1;
|
|
10398
|
+
}
|
|
10399
|
+
if (!dryRun) {
|
|
10400
|
+
for (const filePath of removable) {
|
|
10401
|
+
try {
|
|
10402
|
+
fs13.rmSync(filePath, { force: true });
|
|
10403
|
+
} catch (e) {
|
|
10404
|
+
}
|
|
10405
|
+
}
|
|
10406
|
+
}
|
|
10407
|
+
console.log("");
|
|
10408
|
+
console.log(BOLD("RunTrim") + DIM(" clean"));
|
|
10409
|
+
console.log("");
|
|
10410
|
+
console.log(DIM(" Mode ") + chalk.white(dryRun ? "dry-run" : "apply"));
|
|
10411
|
+
console.log(DIM(" Keep latest runs ") + chalk.white(String(keep)));
|
|
10412
|
+
console.log(DIM(" Candidate files ") + chalk.white(String(removable.length)));
|
|
10413
|
+
console.log("");
|
|
10414
|
+
console.log(DIM(" Cleanup summary"));
|
|
10415
|
+
console.log(chalk.white(` - runs: ${byCategory.runs}`));
|
|
10416
|
+
console.log(chalk.white(` - previews: ${byCategory.previews}`));
|
|
10417
|
+
console.log(chalk.white(` - restores: ${byCategory.restores}`));
|
|
10418
|
+
console.log(chalk.white(` - archives: ${byCategory.archives}`));
|
|
10419
|
+
if (byCategory.other > 0) console.log(chalk.white(` - other: ${byCategory.other}`));
|
|
10420
|
+
console.log("");
|
|
10421
|
+
if (removable.length > 0) {
|
|
10422
|
+
const sample = removable.slice(0, 10).map((p) => path13.relative(cwd, p).replace(/\\/g, "/"));
|
|
10423
|
+
console.log(DIM(" Sample files"));
|
|
10424
|
+
for (const rel of sample) console.log(chalk.white(` - ${rel}`));
|
|
10425
|
+
if (removable.length > sample.length) {
|
|
10426
|
+
console.log(DIM(` ... and ${removable.length - sample.length} more`));
|
|
10427
|
+
}
|
|
10428
|
+
console.log("");
|
|
10429
|
+
}
|
|
10430
|
+
if (dryRun) {
|
|
10431
|
+
console.log(chalk.white(" No files were removed."));
|
|
10432
|
+
console.log(chalk.white(" Re-run without --dry-run to apply cleanup."));
|
|
10433
|
+
} else {
|
|
10434
|
+
console.log(chalk.white(" Cleanup complete."));
|
|
10435
|
+
console.log(chalk.white(" Active contract, latest files, memory current, and MCP snippets were preserved."));
|
|
10436
|
+
}
|
|
10437
|
+
console.log("");
|
|
10438
|
+
});
|
|
10439
|
+
var restoreCommand = program.command("restore").description("Preview or apply local restore for guarded runs");
|
|
10440
|
+
restoreCommand.argument("[runId]", "Run ID to restore, or use 'last'").option("--preview", "Preview restore plan").option("--apply", "Apply restore plan").option("--force", "Apply even if unrelated new changes are detected").action(async (runIdArg, options) => {
|
|
10441
|
+
var _a2, _b, _c, _d;
|
|
10442
|
+
const cwd = process.cwd();
|
|
10443
|
+
const doPreview = (options == null ? void 0 : options.preview) === true;
|
|
10444
|
+
const doApply = (options == null ? void 0 : options.apply) === true;
|
|
10445
|
+
const force = (options == null ? void 0 : options.force) === true;
|
|
10446
|
+
if (!doPreview && !doApply) {
|
|
10447
|
+
console.log("");
|
|
10448
|
+
console.log(chalk.yellow("Choose --preview or --apply."));
|
|
10449
|
+
console.log("");
|
|
10450
|
+
return;
|
|
10451
|
+
}
|
|
10452
|
+
if (doPreview && doApply) {
|
|
10453
|
+
console.log("");
|
|
10454
|
+
console.log(chalk.yellow("Use either --preview or --apply, not both."));
|
|
10455
|
+
console.log("");
|
|
10456
|
+
return;
|
|
10457
|
+
}
|
|
10458
|
+
const runs = loadAllRuns(cwd);
|
|
10459
|
+
const runIdInput = (runIdArg != null ? runIdArg : "last").trim();
|
|
10460
|
+
let targetRunId = runIdInput;
|
|
10461
|
+
if (runIdInput === "last") {
|
|
10462
|
+
const latest = runs.find((r) => r.status === "completed" || r.status === "guarded" || r.status === "executed" || r.status === "checked");
|
|
10463
|
+
if (!latest) {
|
|
10464
|
+
console.log("");
|
|
10465
|
+
console.log(chalk.yellow("No runs available for restore."));
|
|
10466
|
+
console.log("");
|
|
10467
|
+
return;
|
|
10468
|
+
}
|
|
10469
|
+
targetRunId = latest.id;
|
|
10470
|
+
}
|
|
10471
|
+
const run = runs.find((r) => r.id === targetRunId);
|
|
10472
|
+
if (!run) {
|
|
10473
|
+
console.log("");
|
|
10474
|
+
console.log(chalk.yellow(`Run not found: ${targetRunId}`));
|
|
10475
|
+
console.log("");
|
|
10476
|
+
return;
|
|
10477
|
+
}
|
|
10478
|
+
const restore = loadRestorePoint(cwd, targetRunId);
|
|
10479
|
+
if (!restore) {
|
|
10480
|
+
console.log("");
|
|
10481
|
+
console.log(chalk.yellow("No restore point found for this run."));
|
|
10482
|
+
console.log("");
|
|
10483
|
+
return;
|
|
10484
|
+
}
|
|
10485
|
+
const postFiles = (_b = (_a2 = restore.postRun) == null ? void 0 : _a2.changedFiles) != null ? _b : [];
|
|
10486
|
+
const sensitive = postFiles.filter((f) => isSensitivePath(f) || isSecretLikePath(f));
|
|
10487
|
+
const safeFiles = postFiles.filter((f) => !sensitive.includes(f));
|
|
10488
|
+
const gitAvailable = await isGitRepo(cwd);
|
|
10489
|
+
const method = gitAvailable && restore.preRun.commit ? "git checkout from pre-run commit" : "metadata-only (limited)";
|
|
10490
|
+
const nowChanged = gitAvailable ? dedupeFiles((await getGitChangedFiles(cwd)).map((f) => f.path)) : [];
|
|
10491
|
+
const unrelated = nowChanged.filter((f) => !postFiles.includes(f));
|
|
10492
|
+
if (doPreview) {
|
|
10493
|
+
console.log("");
|
|
10494
|
+
console.log(BOLD("RunTrim") + DIM(" restore preview"));
|
|
10495
|
+
console.log("");
|
|
10496
|
+
console.log(DIM(" Run ID ") + chalk.white(targetRunId));
|
|
10497
|
+
console.log(DIM(" Task ") + chalk.white(truncate(run.task, 80)));
|
|
10498
|
+
console.log(DIM(" Method ") + chalk.white(method));
|
|
10499
|
+
console.log(DIM(" Verdict ") + chalk.white((_d = (_c = restore.postRun) == null ? void 0 : _c.finishVerdict) != null ? _d : "unknown"));
|
|
10500
|
+
console.log("");
|
|
10501
|
+
console.log(DIM(" Files to restore"));
|
|
10502
|
+
if (safeFiles.length === 0) console.log(chalk.white(" - (none)"));
|
|
10503
|
+
for (const f of safeFiles.slice(0, 20)) console.log(chalk.white(" - " + f));
|
|
10504
|
+
if (safeFiles.length > 20) console.log(DIM(` ... and ${safeFiles.length - 20} more`));
|
|
10505
|
+
if (sensitive.length > 0) {
|
|
10506
|
+
console.log("");
|
|
10507
|
+
console.log(DIM(" Sensitive files (listed by path only)"));
|
|
10508
|
+
for (const f of sensitive.slice(0, 20)) console.log(chalk.yellow(" - " + f));
|
|
10509
|
+
console.log(chalk.yellow(" Sensitive files are skipped by default and not auto-restored."));
|
|
10510
|
+
}
|
|
10511
|
+
if (unrelated.length > 0) {
|
|
10512
|
+
console.log("");
|
|
10513
|
+
console.log(chalk.yellow(" Warning: repo has new changes after this run."));
|
|
10514
|
+
console.log(chalk.yellow(" Use --apply --force or review manually before restore."));
|
|
10515
|
+
}
|
|
10516
|
+
console.log("");
|
|
10517
|
+
return;
|
|
10518
|
+
}
|
|
10519
|
+
if (!doApply) return;
|
|
10520
|
+
if (!gitAvailable || !restore.preRun.commit) {
|
|
10521
|
+
console.log("");
|
|
10522
|
+
console.log(chalk.red("Restore apply requires git and a pre-run commit restore point."));
|
|
10523
|
+
console.log("");
|
|
10524
|
+
process.exit(1);
|
|
10525
|
+
return;
|
|
10526
|
+
}
|
|
10527
|
+
if (unrelated.length > 0 && !force) {
|
|
10528
|
+
console.log("");
|
|
10529
|
+
console.log(chalk.red("Restore blocked: new unrelated changes detected after this run."));
|
|
10530
|
+
console.log(chalk.red("Re-run with --apply --force after manual review."));
|
|
10531
|
+
console.log("");
|
|
10532
|
+
process.exit(1);
|
|
10533
|
+
return;
|
|
10534
|
+
}
|
|
10535
|
+
const restored = [];
|
|
10536
|
+
const skippedSensitive = [...sensitive];
|
|
10537
|
+
const failed = [];
|
|
10538
|
+
for (const file of safeFiles) {
|
|
10539
|
+
try {
|
|
10540
|
+
let existedBefore = false;
|
|
10541
|
+
try {
|
|
10542
|
+
await execa3("git", ["cat-file", "-e", `${restore.preRun.commit}:${file}`], { cwd });
|
|
10543
|
+
existedBefore = true;
|
|
10544
|
+
} catch (e) {
|
|
10545
|
+
existedBefore = false;
|
|
10546
|
+
}
|
|
10547
|
+
if (existedBefore) {
|
|
10548
|
+
await execa3("git", ["checkout", restore.preRun.commit, "--", file], { cwd });
|
|
10549
|
+
} else if (fs13.existsSync(path13.join(cwd, file))) {
|
|
10550
|
+
fs13.rmSync(path13.join(cwd, file), { force: true });
|
|
10551
|
+
}
|
|
10552
|
+
restored.push(file);
|
|
10553
|
+
} catch (e) {
|
|
10554
|
+
failed.push(file);
|
|
10555
|
+
}
|
|
10556
|
+
}
|
|
10557
|
+
const report = {
|
|
10558
|
+
runId: targetRunId,
|
|
10559
|
+
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10560
|
+
restored,
|
|
10561
|
+
skippedSensitive,
|
|
10562
|
+
failed,
|
|
10563
|
+
forced: force
|
|
10564
|
+
};
|
|
10565
|
+
const reportPath = path13.join(getRestoresDir(cwd), `${targetRunId}.report.${Date.now()}.json`);
|
|
10566
|
+
fs13.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
10567
|
+
console.log("");
|
|
10568
|
+
console.log(BOLD("RunTrim") + DIM(" restore apply"));
|
|
10569
|
+
console.log("");
|
|
10570
|
+
console.log(DIM(" Run ID ") + chalk.white(targetRunId));
|
|
10571
|
+
console.log(DIM(" Restored files ") + chalk.white(String(restored.length)));
|
|
10572
|
+
console.log(DIM(" Skipped sensitive ") + chalk.white(String(skippedSensitive.length)));
|
|
10573
|
+
console.log(DIM(" Failed ") + chalk.white(String(failed.length)));
|
|
10574
|
+
console.log(DIM(" Report ") + chalk.white(path13.relative(cwd, reportPath)));
|
|
10575
|
+
console.log("");
|
|
10576
|
+
if (failed.length > 0) process.exit(1);
|
|
10577
|
+
});
|
|
9369
10578
|
program.command("guard <task>").description("Audit a task and generate a guarded run contract").action(async (task) => {
|
|
9370
10579
|
var _a2, _b, _c;
|
|
9371
10580
|
const cwd = process.cwd();
|
|
@@ -9453,7 +10662,7 @@ program.command("guard <task>").description("Audit a task and generate a guarded
|
|
|
9453
10662
|
const run2 = saveRun(task, audit, contract, cwd);
|
|
9454
10663
|
updateRun(run2.id, { status: "blocked" }, cwd);
|
|
9455
10664
|
console.log("");
|
|
9456
|
-
console.log(DIM(" Run saved: .runtrim/runs/" + run2.id + ".json (status: blocked)"));
|
|
10665
|
+
console.log(DIM(" Run saved: .runtrim/internal/runs/" + run2.id + ".json (status: blocked)"));
|
|
9457
10666
|
console.log("");
|
|
9458
10667
|
console.log(DIM(" Next: ") + chalk.white('runtrim guard "<one system at a time>"'));
|
|
9459
10668
|
console.log("");
|
|
@@ -9533,7 +10742,7 @@ program.command("guard <task>").description("Audit a task and generate a guarded
|
|
|
9533
10742
|
}
|
|
9534
10743
|
const run = saveRun(task, audit, contract, cwd);
|
|
9535
10744
|
console.log("");
|
|
9536
|
-
console.log(DIM(" Run saved: .runtrim/runs/" + run.id + ".json"));
|
|
10745
|
+
console.log(DIM(" Run saved: .runtrim/internal/runs/" + run.id + ".json"));
|
|
9537
10746
|
console.log("");
|
|
9538
10747
|
console.log(DIM(" After your agent run: ") + chalk.white("runtrim check"));
|
|
9539
10748
|
console.log("");
|
|
@@ -9583,6 +10792,7 @@ program.command("run <task>").description("Guard then run configured local agent
|
|
|
9583
10792
|
},
|
|
9584
10793
|
cwd
|
|
9585
10794
|
);
|
|
10795
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
9586
10796
|
const copySavings = estimateSavingsFromTokens2(
|
|
9587
10797
|
parseEstimatedNumber3(String(contract.estimatedTokensTrimmed))
|
|
9588
10798
|
);
|
|
@@ -9663,6 +10873,7 @@ program.command("run <task>").description("Guard then run configured local agent
|
|
|
9663
10873
|
},
|
|
9664
10874
|
cwd
|
|
9665
10875
|
);
|
|
10876
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
9666
10877
|
console.log(DIM(" Execution cancelled. Guarded contract copied to clipboard."));
|
|
9667
10878
|
console.log("");
|
|
9668
10879
|
return;
|
|
@@ -9719,6 +10930,7 @@ program.command("run <task>").description("Guard then run configured local agent
|
|
|
9719
10930
|
},
|
|
9720
10931
|
cwd
|
|
9721
10932
|
);
|
|
10933
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
9722
10934
|
console.log("");
|
|
9723
10935
|
console.log(chalk.yellow(" Agent command not found. Falling back to copy mode guidance."));
|
|
9724
10936
|
console.log(DIM(" Configure with: npm run runtrim -- agent set claude|codex|custom"));
|
|
@@ -9752,6 +10964,7 @@ program.command("run <task>").description("Guard then run configured local agent
|
|
|
9752
10964
|
},
|
|
9753
10965
|
cwd
|
|
9754
10966
|
);
|
|
10967
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
9755
10968
|
console.log("");
|
|
9756
10969
|
console.log(chalk.yellow(" Agent command not found. Falling back to copy mode guidance."));
|
|
9757
10970
|
console.log(DIM(" Configure with: npm run runtrim -- agent set claude|codex|custom"));
|
|
@@ -9795,7 +11008,7 @@ ${stderr}`, "utf-8");
|
|
|
9795
11008
|
exitCode,
|
|
9796
11009
|
stdoutPreview: stdout.slice(0, OUTPUT_PREVIEW_MAX),
|
|
9797
11010
|
stderrPreview: stderr.slice(0, OUTPUT_PREVIEW_MAX),
|
|
9798
|
-
outputPath: `.runtrim/runs/${run.id}.output.txt`
|
|
11011
|
+
outputPath: `.runtrim/internal/runs/${run.id}.output.txt`
|
|
9799
11012
|
},
|
|
9800
11013
|
evaluation
|
|
9801
11014
|
},
|
|
@@ -9945,6 +11158,7 @@ program.command("go <task>").description("Bridge Mode: generate a scoped contrac
|
|
|
9945
11158
|
memoryUsed,
|
|
9946
11159
|
providerRouting
|
|
9947
11160
|
}, cwd);
|
|
11161
|
+
await captureRestorePoint(cwd, run.id, task);
|
|
9948
11162
|
const bridgeCtx = deriveBridgeContext(task, contract, runs, projectName);
|
|
9949
11163
|
let bridgeWritten = [];
|
|
9950
11164
|
let bridgeManagedPaths = [];
|
|
@@ -10003,7 +11217,7 @@ program.command("go <task>").description("Bridge Mode: generate a scoped contrac
|
|
|
10003
11217
|
if (contract.contract.stopRules.length > 0) {
|
|
10004
11218
|
console.log(DIM(" Stop rule ") + chalk.white(truncate((_g = contract.contract.stopRules[contract.contract.stopRules.length - 1]) != null ? _g : "", 60)));
|
|
10005
11219
|
}
|
|
10006
|
-
console.log(DIM(" Run saved ") + chalk.white(`.runtrim/runs/${run.id}.json`));
|
|
11220
|
+
console.log(DIM(" Run saved ") + chalk.white(`.runtrim/internal/runs/${run.id}.json`));
|
|
10007
11221
|
console.log(DIM(" Contract ") + chalk.white("created"));
|
|
10008
11222
|
console.log("");
|
|
10009
11223
|
if (bridgeWritten.length > 0) {
|
|
@@ -10508,7 +11722,7 @@ program.command("continue").description("Create a safe continuation prompt from
|
|
|
10508
11722
|
const audit = (_a2 = loadProjectAudit(cwd)) != null ? _a2 : performBaselineProjectAudit(cwd, null);
|
|
10509
11723
|
const reason = normalizeContinuationReason(options.reason);
|
|
10510
11724
|
const agent = normalizeContinuationAgent(options.agent, config.defaultAgent);
|
|
10511
|
-
const
|
|
11725
|
+
const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
10512
11726
|
const continuationPath = resolveContinuationPath(cwd);
|
|
10513
11727
|
const latestPromptPath = resolvePromptPath(config, cwd);
|
|
10514
11728
|
const latestPrompt = fs13.existsSync(latestPromptPath) ? fs13.readFileSync(latestPromptPath, "utf-8").trim() : "";
|
|
@@ -10698,14 +11912,14 @@ program.command("continue").description("Create a safe continuation prompt from
|
|
|
10698
11912
|
fs13.writeFileSync(continuationPath, continuationPrompt, "utf-8");
|
|
10699
11913
|
const copied = await copyToClipboardSafe(continuationPrompt);
|
|
10700
11914
|
if (memory) {
|
|
10701
|
-
const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath,
|
|
11915
|
+
const memoryWithContinuation = updateMemoryWithContinuation(memory, reason, continuationPath, nowIso2);
|
|
10702
11916
|
fs13.writeFileSync(path13.join(getConfigDir(cwd), "memory.md"), memoryWithContinuation, "utf-8");
|
|
10703
11917
|
}
|
|
10704
11918
|
if (hasConfig) {
|
|
10705
11919
|
const nextConfig = __spreadProps(__spreadValues({}, config), {
|
|
10706
11920
|
lastContinuationReason: reason,
|
|
10707
11921
|
continuationPromptPath: continuationPath.replace(/\\/g, "/"),
|
|
10708
|
-
continuationCreatedAt:
|
|
11922
|
+
continuationCreatedAt: nowIso2
|
|
10709
11923
|
});
|
|
10710
11924
|
saveConfig(nextConfig, cwd);
|
|
10711
11925
|
}
|
|
@@ -11358,6 +12572,18 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
|
|
|
11358
12572
|
const blockedByExistingHard = scope.forbiddenFiles.length > 0 || scope.status === "limit_exceeded";
|
|
11359
12573
|
const warnBySensitiveIgnored = sensitiveIgnored.length > 0;
|
|
11360
12574
|
const finishVerdict = blockedBySensitive || blockedByContract || blockedByExistingHard ? "BLOCKED" : warnBySensitiveIgnored || scopeDriftStatus !== "passed" || evaluation.status === "needs_verification" || evaluation.status === "partial" ? "WARN" : "PASS";
|
|
12575
|
+
const restoreRecord = loadRestorePoint(cwd, activeRun.id);
|
|
12576
|
+
if (restoreRecord) {
|
|
12577
|
+
restoreRecord.postRun = {
|
|
12578
|
+
changedFiles,
|
|
12579
|
+
forbiddenFiles: [...scope.forbiddenFiles, ...forbiddenPathFiles],
|
|
12580
|
+
sensitiveFiles: scope.sensitiveFiles,
|
|
12581
|
+
outOfScopeFiles: [...outOfContractFiles, ...scope.outOfScopeFiles],
|
|
12582
|
+
finishVerdict,
|
|
12583
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12584
|
+
};
|
|
12585
|
+
saveRestorePoint(cwd, restoreRecord);
|
|
12586
|
+
}
|
|
11361
12587
|
const verdictColor = finishVerdict === "PASS" ? chalk.green : finishVerdict === "WARN" ? chalk.yellow : chalk.red;
|
|
11362
12588
|
const scopeColor = scopeDriftStatus === "passed" ? chalk.green : scopeDriftStatus === "forbidden_touched" ? chalk.red : chalk.yellow;
|
|
11363
12589
|
const riskAfter = (_m = activeRun.contract.wasteRiskAfter) != null ? _m : "medium";
|
|
@@ -11497,6 +12723,8 @@ program.command("finish").description("Bridge Mode: evaluate agent output, check
|
|
|
11497
12723
|
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) => {
|
|
11498
12724
|
var _a2, _b;
|
|
11499
12725
|
const cwd = process.cwd();
|
|
12726
|
+
const allowed = await ensureRepoAllowedForFree(cwd);
|
|
12727
|
+
if (!allowed) return;
|
|
11500
12728
|
console.log("");
|
|
11501
12729
|
console.log(BOLD("RunTrim") + DIM(" cloud sync"));
|
|
11502
12730
|
console.log("");
|