getprismo 0.1.38 → 0.1.40
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/lib/prismo-dev/agent.js +163 -6
- package/lib/prismo-dev/cli.js +46 -3
- package/lib/prismo-dev/cloud-sync.js +26 -13
- package/lib/prismo-dev/connector.js +4 -2
- package/lib/prismo-dev/help.js +39 -1
- package/lib/prismo-dev/repair-executors.js +543 -0
- package/lib/prismo-dev/repair-planner.js +307 -0
- package/lib/prismo-dev-scan.js +44 -0
- package/package.json +1 -1
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
module.exports = function createRepairExecutors(deps) {
|
|
2
|
+
const {
|
|
3
|
+
fs,
|
|
4
|
+
path,
|
|
5
|
+
NPX_COMMAND,
|
|
6
|
+
runDoctor,
|
|
7
|
+
runOptimize,
|
|
8
|
+
runGuard,
|
|
9
|
+
runShield,
|
|
10
|
+
runFirewall,
|
|
11
|
+
getUsageSummary,
|
|
12
|
+
appendIgnoreSuggestions,
|
|
13
|
+
} = deps;
|
|
14
|
+
|
|
15
|
+
const REPAIR_CAUSES = [
|
|
16
|
+
"repeated-file-reads",
|
|
17
|
+
"tool-output-flood",
|
|
18
|
+
"generated-artifacts",
|
|
19
|
+
"context-loop",
|
|
20
|
+
"long-session-buildup",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const SAFE_SHIELD_COMMANDS = new Set(["npm", "pnpm", "yarn", "bun", "npx", "pytest", "python", "python3", "node"]);
|
|
24
|
+
const EXPECTED_REPEATED_PATHS = new Set(["claude.md", "agents.md", "readme.md"]);
|
|
25
|
+
|
|
26
|
+
function nowIso() {
|
|
27
|
+
return new Date().toISOString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function noopProgress() {
|
|
31
|
+
return Promise.resolve();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeRepairFile(root, name, contents) {
|
|
35
|
+
const relPath = path.join(".prismo", name);
|
|
36
|
+
const fullPath = path.join(root, relPath);
|
|
37
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
38
|
+
fs.writeFileSync(fullPath, contents, "utf8");
|
|
39
|
+
return relPath;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function collectSessions(root, limit) {
|
|
43
|
+
try {
|
|
44
|
+
const summary = getUsageSummary({ tool: "all", cwd: root, limit: limit || 5 });
|
|
45
|
+
return summary.sessions || [];
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function aggregateEntries(sessions, pick) {
|
|
52
|
+
const totals = new Map();
|
|
53
|
+
for (const session of sessions) {
|
|
54
|
+
for (const entry of pick(session) || []) {
|
|
55
|
+
if (!entry || !entry.value) continue;
|
|
56
|
+
totals.set(entry.value, (totals.get(entry.value) || 0) + Number(entry.count || 0));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return Array.from(totals, ([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isExpectedRepeatedPath(value) {
|
|
63
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
64
|
+
return EXPECTED_REPEATED_PATHS.has(normalized) || normalized.endsWith("/readme.md");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseSafeCommandArgs(command) {
|
|
68
|
+
const parts = String(command || "").trim().split(/\s+/).filter(Boolean);
|
|
69
|
+
const separatorIndex = parts.indexOf("--");
|
|
70
|
+
if (separatorIndex < 0) return null;
|
|
71
|
+
const commandArgs = parts.slice(separatorIndex + 1);
|
|
72
|
+
if (!commandArgs.length) return null;
|
|
73
|
+
if (!SAFE_SHIELD_COMMANDS.has(commandArgs[0])) return null;
|
|
74
|
+
if (commandArgs.some((arg) => /[;&|`$<>]/.test(arg))) return null;
|
|
75
|
+
return commandArgs;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function repairTier(helpers) {
|
|
79
|
+
return helpers.options && helpers.options.tier === "aggressive" ? "aggressive" : "mild";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Aggressive tier adds a context firewall policy on top of the mild repair,
|
|
83
|
+
// so the cause is fenced off by an allow/block file instead of guidance only.
|
|
84
|
+
function applyAggressiveFirewall(root, cause) {
|
|
85
|
+
if (!runFirewall) return [];
|
|
86
|
+
try {
|
|
87
|
+
const firewall = runFirewall(root, { task: cause, dryRun: false });
|
|
88
|
+
return firewall.generatedFiles || [];
|
|
89
|
+
} catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseScope(action, helpers) {
|
|
95
|
+
if (helpers.options && helpers.options.scope) return helpers.options.scope;
|
|
96
|
+
const args = helpers.parsed ? helpers.parsed.args : String(action.command || "").trim().split(/\s+/).slice(1);
|
|
97
|
+
const scope = (args || []).find((arg) => arg && !arg.startsWith("-"));
|
|
98
|
+
return ["frontend", "backend"].includes(String(scope || "").toLowerCase()) ? String(scope).toLowerCase() : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function looksLikeIgnoreRule(value) {
|
|
102
|
+
const rule = String(value || "").trim();
|
|
103
|
+
if (!rule || rule.length > 200) return false;
|
|
104
|
+
if (rule.startsWith("/") || rule.startsWith("-") || rule.includes("..")) return false;
|
|
105
|
+
return /^[A-Za-z0-9_.*/@-]+$/.test(rule);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function renderHotFilesGuide(hotPaths, contextFile) {
|
|
109
|
+
return [
|
|
110
|
+
"# Prismo Repair: Repeated File Reads",
|
|
111
|
+
"",
|
|
112
|
+
"These files were read repeatedly across recent coding-agent sessions.",
|
|
113
|
+
"Read each one once, then rely on the compact context packs below instead of re-opening the file.",
|
|
114
|
+
"",
|
|
115
|
+
"## Hot Files",
|
|
116
|
+
"",
|
|
117
|
+
...(hotPaths.length
|
|
118
|
+
? hotPaths.map((entry) => `- ${entry.value} (${entry.count} reads)`)
|
|
119
|
+
: ["- No repeated reads detected in recent sessions."]),
|
|
120
|
+
"",
|
|
121
|
+
"## Session Rules",
|
|
122
|
+
"",
|
|
123
|
+
`- Start sessions from ${contextFile || ".prismo/architecture-summary.md"} instead of broad exploration.`,
|
|
124
|
+
"- Quote the relevant section from a context pack instead of re-reading a hot file.",
|
|
125
|
+
"- If a hot file changed during the session, re-read only the changed region.",
|
|
126
|
+
"",
|
|
127
|
+
].join("\n");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderNoisyCommandsGuide(noisyCommands, shieldedRun) {
|
|
131
|
+
return [
|
|
132
|
+
"# Prismo Repair: Tool-Output Flood",
|
|
133
|
+
"",
|
|
134
|
+
"These commands repeatedly flooded agent context with raw output.",
|
|
135
|
+
`Run them through \`${NPX_COMMAND} shield -- <command>\` so the full output stays on disk and only a compact summary enters context.`,
|
|
136
|
+
"",
|
|
137
|
+
"## Noisy Commands",
|
|
138
|
+
"",
|
|
139
|
+
...(noisyCommands.length
|
|
140
|
+
? noisyCommands.map((entry) => `- \`${entry.value}\` (${entry.count} runs) -> \`${NPX_COMMAND} shield -- ${entry.value}\``)
|
|
141
|
+
: ["- No repeated noisy commands detected in recent sessions."]),
|
|
142
|
+
"",
|
|
143
|
+
...(shieldedRun
|
|
144
|
+
? [
|
|
145
|
+
"## Last Shielded Run",
|
|
146
|
+
"",
|
|
147
|
+
`- Command: \`${shieldedRun.command}\``,
|
|
148
|
+
`- Exit code: ${shieldedRun.exitCode}`,
|
|
149
|
+
shieldedRun.runDir ? `- Full output stored in: ${shieldedRun.runDir}` : null,
|
|
150
|
+
"",
|
|
151
|
+
].filter((line) => line !== null)
|
|
152
|
+
: []),
|
|
153
|
+
"## Session Rules",
|
|
154
|
+
"",
|
|
155
|
+
"- Never paste full test, build, or install output into the conversation.",
|
|
156
|
+
"- Use shield summaries; search stored runs with `shield search` when details are needed.",
|
|
157
|
+
"",
|
|
158
|
+
].join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function renderLoopBreakerGuide(repeatedCommands, loopSessionCount) {
|
|
162
|
+
return [
|
|
163
|
+
"# Prismo Repair: Context Loop",
|
|
164
|
+
"",
|
|
165
|
+
loopSessionCount > 0
|
|
166
|
+
? `${loopSessionCount} recent session(s) showed loop behavior: the same command retried with growing context.`
|
|
167
|
+
: "No active loop detected, but guardrails below prevent retry loops from building context waste.",
|
|
168
|
+
"",
|
|
169
|
+
"## Repeated Commands",
|
|
170
|
+
"",
|
|
171
|
+
...(repeatedCommands.length
|
|
172
|
+
? repeatedCommands.map((entry) => `- \`${entry.value}\` (${entry.count} runs)`)
|
|
173
|
+
: ["- No repeated commands detected in recent sessions."]),
|
|
174
|
+
"",
|
|
175
|
+
"## Loop-Breaking Rules",
|
|
176
|
+
"",
|
|
177
|
+
"- After 2 failed attempts of the same command, stop and change the approach instead of retrying.",
|
|
178
|
+
`- Route retry-heavy commands through \`${NPX_COMMAND} shield -- <command>\` so each retry costs a summary, not full output.`,
|
|
179
|
+
"- If the session keeps circling, start a fresh session from the .prismo context packs and state what already failed.",
|
|
180
|
+
"- Follow .prismo/live-guardrails.md while the session is active.",
|
|
181
|
+
"",
|
|
182
|
+
].join("\n");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderSessionRestartGuide(scope, starterPrompt, heavySessionCount) {
|
|
186
|
+
return [
|
|
187
|
+
"# Prismo Repair: Long-Session Buildup",
|
|
188
|
+
"",
|
|
189
|
+
heavySessionCount > 0
|
|
190
|
+
? `${heavySessionCount} recent session(s) carried High/Medium context risk from accumulated history.`
|
|
191
|
+
: "Recent sessions look healthy; the restart routine below keeps them that way.",
|
|
192
|
+
"",
|
|
193
|
+
"## Restart Routine",
|
|
194
|
+
"",
|
|
195
|
+
"- Split work at task boundaries: one task, one session.",
|
|
196
|
+
`- Start each new session from the scoped context pack${scope ? ` for ${scope}` : ""} instead of carrying the old conversation forward.`,
|
|
197
|
+
"- When a session crosses ~60% of its budget, finish the current task and restart.",
|
|
198
|
+
"",
|
|
199
|
+
...(starterPrompt
|
|
200
|
+
? ["## Paste-Ready Starter Prompt", "", "```", starterPrompt, "```", ""]
|
|
201
|
+
: []),
|
|
202
|
+
].join("\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Repeated file reads: refresh ignore rules and context packs via doctor,
|
|
206
|
+
// then map the hot files into a guide agents can read instead of the files.
|
|
207
|
+
async function repairRepeatedFileReads(action, root, helpers = {}) {
|
|
208
|
+
const progress = helpers.progress || noopProgress;
|
|
209
|
+
const options = helpers.options || {};
|
|
210
|
+
const startedAt = nowIso();
|
|
211
|
+
|
|
212
|
+
await progress("analyzing", "Measuring repeated file reads in recent sessions");
|
|
213
|
+
const sessions = collectSessions(root, options.limit || 5);
|
|
214
|
+
const hotPaths = aggregateEntries(sessions, (session) => session.repeatedPathMentions)
|
|
215
|
+
.filter((entry) => !isExpectedRepeatedPath(entry.value))
|
|
216
|
+
.slice(0, 10);
|
|
217
|
+
|
|
218
|
+
await progress("fixing", hotPaths.length
|
|
219
|
+
? `Found ${hotPaths.length} hot file(s); refreshing ignore rules and context packs`
|
|
220
|
+
: "No hot files found; refreshing ignore rules and context packs as prevention");
|
|
221
|
+
const doctor = runDoctor(root, { limit: options.limit || 3, applySuggestions: true, json: true });
|
|
222
|
+
const generatedFiles = doctor.generatedFiles || [];
|
|
223
|
+
const guidePath = writeRepairFile(root, "hot-files.md", renderHotFilesGuide(hotPaths, doctor.contextFile));
|
|
224
|
+
const tier = repairTier(helpers);
|
|
225
|
+
const firewallFiles = tier === "aggressive" ? applyAggressiveFirewall(root, "repeated-file-reads") : [];
|
|
226
|
+
|
|
227
|
+
await progress("done", `Mapped ${hotPaths.length} hot file(s) into ${guidePath}`);
|
|
228
|
+
return {
|
|
229
|
+
status: "completed",
|
|
230
|
+
statusMessage: (hotPaths.length
|
|
231
|
+
? `Repaired repeated file reads: ${hotPaths.length} hot file(s) mapped into ${guidePath}; context packs refreshed so agents read summaries once.`
|
|
232
|
+
: `No repeated-read hot files detected; refreshed ignore rules and context packs, and wrote ${guidePath} as prevention.`)
|
|
233
|
+
+ (firewallFiles.length ? " Aggressive tier: context firewall policy added." : ""),
|
|
234
|
+
result: {
|
|
235
|
+
command: "repair",
|
|
236
|
+
targetCause: "repeated-file-reads",
|
|
237
|
+
tier,
|
|
238
|
+
startedAt,
|
|
239
|
+
completedAt: nowIso(),
|
|
240
|
+
hotPaths,
|
|
241
|
+
fixActions: doctor.fixActions || [],
|
|
242
|
+
generatedFiles: [...generatedFiles, guidePath, ...firewallFiles],
|
|
243
|
+
score: doctor.after && typeof doctor.after.score === "number" ? doctor.after.score : null,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Tool-output flood: stage the noisy commands behind shield and, when the
|
|
249
|
+
// action carries a safe command, run it shielded so output stays on disk.
|
|
250
|
+
async function repairToolOutputFlood(action, root, helpers = {}) {
|
|
251
|
+
const progress = helpers.progress || noopProgress;
|
|
252
|
+
const options = helpers.options || {};
|
|
253
|
+
const startedAt = nowIso();
|
|
254
|
+
|
|
255
|
+
await progress("analyzing", "Finding commands that flood agent context");
|
|
256
|
+
const sessions = collectSessions(root, options.limit || 5);
|
|
257
|
+
const noisyCommands = aggregateEntries(sessions, (session) => session.repeatedCommands).slice(0, 8);
|
|
258
|
+
|
|
259
|
+
const commandArgs = Array.isArray(options.commandArgs) && options.commandArgs.length
|
|
260
|
+
? (SAFE_SHIELD_COMMANDS.has(options.commandArgs[0]) && !options.commandArgs.some((arg) => /[;&|`$<>]/.test(arg)) ? options.commandArgs : null)
|
|
261
|
+
: parseSafeCommandArgs(action.command);
|
|
262
|
+
|
|
263
|
+
let shieldedRun = null;
|
|
264
|
+
if (commandArgs) {
|
|
265
|
+
await progress("shielding", `Running shielded command: ${commandArgs.join(" ")}`);
|
|
266
|
+
const shield = runShield(root, commandArgs);
|
|
267
|
+
shieldedRun = {
|
|
268
|
+
command: commandArgs.join(" "),
|
|
269
|
+
exitCode: shield.exitCode,
|
|
270
|
+
summary: shield.summary || null,
|
|
271
|
+
runDir: shield.runDir || null,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const guidePath = writeRepairFile(root, "noisy-commands.md", renderNoisyCommandsGuide(noisyCommands, shieldedRun));
|
|
276
|
+
const tier = repairTier(helpers);
|
|
277
|
+
const firewallFiles = tier === "aggressive" ? applyAggressiveFirewall(root, "tool-output-flood") : [];
|
|
278
|
+
await progress("done", shieldedRun
|
|
279
|
+
? `Shielded run stored; ${noisyCommands.length} noisy command(s) staged in ${guidePath}`
|
|
280
|
+
: `${noisyCommands.length} noisy command(s) staged in ${guidePath}`);
|
|
281
|
+
|
|
282
|
+
const statusMessage = (shieldedRun
|
|
283
|
+
? `Repaired tool-output flood: ran \`${shieldedRun.command}\` shielded (exit ${shieldedRun.exitCode}); full output stays on disk and ${guidePath} routes noisy commands through shield.`
|
|
284
|
+
: `Repaired tool-output flood: staged ${noisyCommands.length} noisy command(s) behind shield in ${guidePath}. Queue a \`shield -- <command>\` action to capture a run.`)
|
|
285
|
+
+ (firewallFiles.length ? " Aggressive tier: context firewall policy added." : "");
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
status: "completed",
|
|
289
|
+
statusMessage,
|
|
290
|
+
result: {
|
|
291
|
+
command: "repair",
|
|
292
|
+
targetCause: "tool-output-flood",
|
|
293
|
+
tier,
|
|
294
|
+
startedAt,
|
|
295
|
+
completedAt: nowIso(),
|
|
296
|
+
noisyCommands,
|
|
297
|
+
shieldedRun,
|
|
298
|
+
generatedFiles: [guidePath, ...firewallFiles],
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Generated artifacts: append ignore rules for scan-detected risks plus the
|
|
304
|
+
// specific artifact paths observed entering recent sessions.
|
|
305
|
+
async function repairGeneratedArtifacts(action, root, helpers = {}) {
|
|
306
|
+
const progress = helpers.progress || noopProgress;
|
|
307
|
+
const options = helpers.options || {};
|
|
308
|
+
const startedAt = nowIso();
|
|
309
|
+
|
|
310
|
+
await progress("analyzing", "Finding generated artifacts leaking into context");
|
|
311
|
+
const sessions = collectSessions(root, options.limit || 5);
|
|
312
|
+
const artifacts = aggregateEntries(sessions, (session) => session.generatedArtifacts).slice(0, 10);
|
|
313
|
+
|
|
314
|
+
await progress("fixing", "Appending ignore rules for generated artifacts");
|
|
315
|
+
const doctor = runDoctor(root, { limit: options.limit || 3, applySuggestions: true, noContextPacks: true, json: true });
|
|
316
|
+
const fixActions = [...(doctor.fixActions || [])];
|
|
317
|
+
|
|
318
|
+
const sessionRules = Array.from(new Set(artifacts.map((entry) => entry.value).filter(looksLikeIgnoreRule)));
|
|
319
|
+
if (sessionRules.length) {
|
|
320
|
+
const hasClaudeIgnore = fs.existsSync(path.join(root, ".claudeignore"));
|
|
321
|
+
const hasCursorIgnore = fs.existsSync(path.join(root, ".cursorignore"));
|
|
322
|
+
fixActions.push(...appendIgnoreSuggestions({
|
|
323
|
+
root,
|
|
324
|
+
hasClaudeIgnore,
|
|
325
|
+
recommendedClaudeIgnore: sessionRules,
|
|
326
|
+
missingClaudeIgnoreSuggestions: sessionRules,
|
|
327
|
+
hasCursorIgnore,
|
|
328
|
+
recommendedCursorIgnore: sessionRules,
|
|
329
|
+
missingCursorIgnoreSuggestions: sessionRules,
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const tier = repairTier(helpers);
|
|
334
|
+
const firewallFiles = tier === "aggressive" ? applyAggressiveFirewall(root, "generated-artifacts") : [];
|
|
335
|
+
|
|
336
|
+
await progress("done", `Ignore rules updated; ${artifacts.length} session-observed artifact(s) covered`);
|
|
337
|
+
return {
|
|
338
|
+
status: "completed",
|
|
339
|
+
statusMessage: (artifacts.length
|
|
340
|
+
? `Repaired generated artifacts: ignore rules now cover ${artifacts.length} artifact path(s) seen in recent sessions plus scan-detected build output.`
|
|
341
|
+
: "No artifact mentions found in recent sessions; refreshed scan-detected ignore rules as prevention.")
|
|
342
|
+
+ (firewallFiles.length ? " Aggressive tier: context firewall policy added." : ""),
|
|
343
|
+
result: {
|
|
344
|
+
command: "repair",
|
|
345
|
+
targetCause: "generated-artifacts",
|
|
346
|
+
tier,
|
|
347
|
+
startedAt,
|
|
348
|
+
completedAt: nowIso(),
|
|
349
|
+
artifacts,
|
|
350
|
+
fixActions,
|
|
351
|
+
generatedFiles: firewallFiles,
|
|
352
|
+
score: doctor.after && typeof doctor.after.score === "number" ? doctor.after.score : null,
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Context loop: run a guard snapshot with a tighter budget and write a
|
|
358
|
+
// loop-breaker guide built from the actual repeated commands.
|
|
359
|
+
async function repairContextLoop(action, root, helpers = {}) {
|
|
360
|
+
const progress = helpers.progress || noopProgress;
|
|
361
|
+
const options = helpers.options || {};
|
|
362
|
+
const startedAt = nowIso();
|
|
363
|
+
|
|
364
|
+
await progress("analyzing", "Looking for retry loops in recent sessions");
|
|
365
|
+
const sessions = collectSessions(root, options.limit || 5);
|
|
366
|
+
const loopSessionCount = sessions.filter((session) => session.loopSuspicion).length;
|
|
367
|
+
const repeatedCommands = aggregateEntries(sessions, (session) => session.repeatedCommands).slice(0, 8);
|
|
368
|
+
|
|
369
|
+
const tier = repairTier(helpers);
|
|
370
|
+
await progress("guarding", "Running guard snapshot with a tightened token budget");
|
|
371
|
+
const guard = await runGuard(root, {
|
|
372
|
+
tool: "all",
|
|
373
|
+
limit: options.limit || 5,
|
|
374
|
+
tokenBudget: options.tokenBudget || (tier === "aggressive" ? 250000 : 400000),
|
|
375
|
+
noSync: false,
|
|
376
|
+
watch: false,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const guidePath = writeRepairFile(root, "loop-breaker.md", renderLoopBreakerGuide(repeatedCommands, loopSessionCount));
|
|
380
|
+
const firewallFiles = tier === "aggressive" ? applyAggressiveFirewall(root, "context-loop") : [];
|
|
381
|
+
const eventCount = guard.events ? guard.events.length : 0;
|
|
382
|
+
await progress("done", `Guard recorded ${eventCount} event(s); loop-breaker rules written to ${guidePath}`);
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
status: "completed",
|
|
386
|
+
statusMessage: (loopSessionCount > 0
|
|
387
|
+
? `Repaired context loop: ${loopSessionCount} looping session(s) found; guard tightened and loop-breaker rules written to ${guidePath}.`
|
|
388
|
+
: `No active loop found; guard tightened and loop-breaker rules written to ${guidePath} as prevention.`)
|
|
389
|
+
+ (firewallFiles.length ? " Aggressive tier: context firewall policy added and budget tightened." : ""),
|
|
390
|
+
result: {
|
|
391
|
+
command: "repair",
|
|
392
|
+
targetCause: "context-loop",
|
|
393
|
+
tier,
|
|
394
|
+
startedAt,
|
|
395
|
+
completedAt: nowIso(),
|
|
396
|
+
loopSessions: loopSessionCount,
|
|
397
|
+
repeatedCommands,
|
|
398
|
+
guardEvents: eventCount,
|
|
399
|
+
generatedFiles: [guidePath, ...firewallFiles],
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Long-session buildup: generate scoped context packs and a restart routine
|
|
405
|
+
// so new sessions start small instead of inheriting stale history.
|
|
406
|
+
async function repairLongSessionBuildup(action, root, helpers = {}) {
|
|
407
|
+
const progress = helpers.progress || noopProgress;
|
|
408
|
+
const options = helpers.options || {};
|
|
409
|
+
const startedAt = nowIso();
|
|
410
|
+
|
|
411
|
+
await progress("analyzing", "Checking recent sessions for context buildup");
|
|
412
|
+
const sessions = collectSessions(root, options.limit || 5);
|
|
413
|
+
const heavySessionCount = sessions.filter((session) => session.contextRisk === "High" || session.contextRisk === "Medium").length;
|
|
414
|
+
|
|
415
|
+
const scope = parseScope(action, helpers);
|
|
416
|
+
await progress("generating", `Generating ${scope ? `${scope} ` : ""}context packs for fresh-session starts`);
|
|
417
|
+
const optimize = runOptimize(root, { scope });
|
|
418
|
+
const generatedFiles = optimize.generatedFiles || [];
|
|
419
|
+
const guidePath = writeRepairFile(root, "session-restart.md", renderSessionRestartGuide(optimize.scope || scope, optimize.starterPrompt, heavySessionCount));
|
|
420
|
+
const tier = repairTier(helpers);
|
|
421
|
+
const firewallFiles = tier === "aggressive" ? applyAggressiveFirewall(root, "long-session-buildup") : [];
|
|
422
|
+
|
|
423
|
+
await progress("done", `Generated ${generatedFiles.length} context file(s); restart routine in ${guidePath}`);
|
|
424
|
+
return {
|
|
425
|
+
status: "completed",
|
|
426
|
+
statusMessage: (heavySessionCount > 0
|
|
427
|
+
? `Repaired long-session buildup: ${heavySessionCount} heavy session(s) found; scoped context packs and a restart routine are in place so new sessions start small.`
|
|
428
|
+
: `Recent sessions look healthy; generated scoped context packs and a restart routine in ${guidePath} to keep buildup down.`)
|
|
429
|
+
+ (firewallFiles.length ? " Aggressive tier: context firewall policy added." : ""),
|
|
430
|
+
result: {
|
|
431
|
+
command: "repair",
|
|
432
|
+
targetCause: "long-session-buildup",
|
|
433
|
+
tier,
|
|
434
|
+
startedAt,
|
|
435
|
+
completedAt: nowIso(),
|
|
436
|
+
heavySessions: heavySessionCount,
|
|
437
|
+
scope: optimize.scope || scope,
|
|
438
|
+
starterPrompt: optimize.starterPrompt || null,
|
|
439
|
+
generatedFiles: [...generatedFiles, guidePath, ...firewallFiles],
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const executors = {
|
|
445
|
+
"repeated-file-reads": repairRepeatedFileReads,
|
|
446
|
+
"tool-output-flood": repairToolOutputFlood,
|
|
447
|
+
"generated-artifacts": repairGeneratedArtifacts,
|
|
448
|
+
"context-loop": repairContextLoop,
|
|
449
|
+
"long-session-buildup": repairLongSessionBuildup,
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
function forCause(cause) {
|
|
453
|
+
return executors[String(cause || "").trim().toLowerCase()] || null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function runRepair(rootDir = process.cwd(), cause, options = {}) {
|
|
457
|
+
const root = path.resolve(rootDir);
|
|
458
|
+
const executor = forCause(cause);
|
|
459
|
+
if (!executor) {
|
|
460
|
+
return {
|
|
461
|
+
schemaVersion: 1,
|
|
462
|
+
command: "repair",
|
|
463
|
+
cause: cause || null,
|
|
464
|
+
status: "failed",
|
|
465
|
+
statusMessage: `Unknown repair cause: ${cause || "(none)"}. Valid causes: ${REPAIR_CAUSES.join(", ")}.`,
|
|
466
|
+
result: null,
|
|
467
|
+
generatedAt: nowIso(),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const normalizedCause = String(cause).trim().toLowerCase();
|
|
471
|
+
const action = {
|
|
472
|
+
id: null,
|
|
473
|
+
actionType: "repair",
|
|
474
|
+
command: `${NPX_COMMAND} repair ${normalizedCause}${options.commandArgs && options.commandArgs.length ? ` -- ${options.commandArgs.join(" ")}` : ""}`,
|
|
475
|
+
label: `Repair ${normalizedCause}`,
|
|
476
|
+
targetCause: normalizedCause,
|
|
477
|
+
};
|
|
478
|
+
const outcome = await executor(action, root, { options });
|
|
479
|
+
return {
|
|
480
|
+
schemaVersion: 1,
|
|
481
|
+
command: "repair",
|
|
482
|
+
cause: normalizedCause,
|
|
483
|
+
status: outcome.status,
|
|
484
|
+
statusMessage: outcome.statusMessage,
|
|
485
|
+
result: outcome.result,
|
|
486
|
+
generatedAt: nowIso(),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function renderRepairTerminal(report) {
|
|
491
|
+
const lines = [];
|
|
492
|
+
lines.push("");
|
|
493
|
+
lines.push("PrismoDev Repair");
|
|
494
|
+
lines.push("");
|
|
495
|
+
lines.push(`Cause: ${report.cause || "unknown"}`);
|
|
496
|
+
lines.push(`Status: ${report.status}`);
|
|
497
|
+
if (report.statusMessage) lines.push(report.statusMessage);
|
|
498
|
+
const result = report.result || {};
|
|
499
|
+
if (result.hotPaths && result.hotPaths.length) {
|
|
500
|
+
lines.push("");
|
|
501
|
+
lines.push("Hot files:");
|
|
502
|
+
result.hotPaths.forEach((entry) => lines.push(`- ${entry.value} (${entry.count} reads)`));
|
|
503
|
+
}
|
|
504
|
+
if (result.noisyCommands && result.noisyCommands.length) {
|
|
505
|
+
lines.push("");
|
|
506
|
+
lines.push("Noisy commands:");
|
|
507
|
+
result.noisyCommands.forEach((entry) => lines.push(`- ${entry.value} (${entry.count} runs)`));
|
|
508
|
+
}
|
|
509
|
+
if (result.artifacts && result.artifacts.length) {
|
|
510
|
+
lines.push("");
|
|
511
|
+
lines.push("Artifacts seen in sessions:");
|
|
512
|
+
result.artifacts.forEach((entry) => lines.push(`- ${entry.value} (${entry.count} mentions)`));
|
|
513
|
+
}
|
|
514
|
+
if (result.repeatedCommands && result.repeatedCommands.length) {
|
|
515
|
+
lines.push("");
|
|
516
|
+
lines.push("Repeated commands:");
|
|
517
|
+
result.repeatedCommands.forEach((entry) => lines.push(`- ${entry.value} (${entry.count} runs)`));
|
|
518
|
+
}
|
|
519
|
+
if (result.fixActions && result.fixActions.length) {
|
|
520
|
+
lines.push("");
|
|
521
|
+
lines.push("Fixes:");
|
|
522
|
+
result.fixActions.forEach((item) => lines.push(`- ${item}`));
|
|
523
|
+
}
|
|
524
|
+
if (result.generatedFiles && result.generatedFiles.length) {
|
|
525
|
+
lines.push("");
|
|
526
|
+
lines.push("Generated:");
|
|
527
|
+
result.generatedFiles.forEach((file) => lines.push(`- ${file}`));
|
|
528
|
+
}
|
|
529
|
+
if (report.status === "failed" && !result.generatedFiles) {
|
|
530
|
+
lines.push("");
|
|
531
|
+
lines.push("Valid causes:");
|
|
532
|
+
REPAIR_CAUSES.forEach((item) => lines.push(`- ${item}`));
|
|
533
|
+
}
|
|
534
|
+
return lines.join("\n");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
REPAIR_CAUSES,
|
|
539
|
+
forCause,
|
|
540
|
+
renderRepairTerminal,
|
|
541
|
+
runRepair,
|
|
542
|
+
};
|
|
543
|
+
};
|