getprismo 0.1.39 → 0.1.41
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 +127 -0
- package/lib/prismo-dev/cli.js +45 -2
- package/lib/prismo-dev/cloud-sync.js +1 -0
- package/lib/prismo-dev/help.js +40 -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
package/lib/prismo-dev/agent.js
CHANGED
|
@@ -13,6 +13,8 @@ module.exports = function createAgent(deps) {
|
|
|
13
13
|
runShield,
|
|
14
14
|
runOptimize,
|
|
15
15
|
openUrl,
|
|
16
|
+
repairExecutors,
|
|
17
|
+
repairPlanner,
|
|
16
18
|
} = deps;
|
|
17
19
|
|
|
18
20
|
const DEFAULT_WORKSPACE_URL = "https://getprismo.dev/dashboard/dev";
|
|
@@ -21,6 +23,15 @@ module.exports = function createAgent(deps) {
|
|
|
21
23
|
const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
|
|
22
24
|
const SAFE_SHIELD_COMMANDS = new Set(["npm", "pnpm", "yarn", "bun", "npx", "pytest", "python", "python3", "node"]);
|
|
23
25
|
const VALID_MODES = new Set(["observe", "suggest", "autopilot"]);
|
|
26
|
+
// Backend verification only measures these action types, so self-repairs
|
|
27
|
+
// are registered under the matching type for their cause.
|
|
28
|
+
const CAUSE_ACTION_TYPES = {
|
|
29
|
+
"repeated-file-reads": "doctor",
|
|
30
|
+
"tool-output-flood": "shield",
|
|
31
|
+
"generated-artifacts": "doctor",
|
|
32
|
+
"context-loop": "guard",
|
|
33
|
+
"long-session-buildup": "context",
|
|
34
|
+
};
|
|
24
35
|
|
|
25
36
|
function apiBase(config) {
|
|
26
37
|
return String(config?.apiUrl || DEFAULT_API_URL).replace(/\/$/, "");
|
|
@@ -166,6 +177,66 @@ module.exports = function createAgent(deps) {
|
|
|
166
177
|
} catch (_) {}
|
|
167
178
|
}
|
|
168
179
|
|
|
180
|
+
// Register an executed self-repair as a real workspace action row so the
|
|
181
|
+
// backend verification loop measures it like a dashboard-queued repair.
|
|
182
|
+
// Returns false when the endpoint is unavailable (older API) so the
|
|
183
|
+
// caller can fall back to the auto-detect channel.
|
|
184
|
+
async function registerSelfRepair(config, plannerResult, options = {}) {
|
|
185
|
+
if (!plannerResult || !plannerResult.decision || !plannerResult.outcome) return false;
|
|
186
|
+
const { cause, tier } = plannerResult.decision;
|
|
187
|
+
const endpoint = options.selfRepairEndpoint || `${apiBase(config)}/v1/dev/workspace/actions/agent`;
|
|
188
|
+
try {
|
|
189
|
+
const created = await requestJson("POST", endpoint, config.token, {
|
|
190
|
+
actionType: CAUSE_ACTION_TYPES[cause] || "doctor",
|
|
191
|
+
command: `${NPX_COMMAND} repair ${cause}`,
|
|
192
|
+
label: `Self-repair: ${cause} (${tier})`,
|
|
193
|
+
targetCause: cause,
|
|
194
|
+
}, options.timeoutMs || 10000);
|
|
195
|
+
const actionId = created.data && created.data.id;
|
|
196
|
+
if (!actionId) return false;
|
|
197
|
+
await updateAction(config, actionId, {
|
|
198
|
+
status: plannerResult.outcome.status,
|
|
199
|
+
statusMessage: plannerResult.outcome.statusMessage,
|
|
200
|
+
result: {
|
|
201
|
+
executor: "repair-planner",
|
|
202
|
+
targetCause: cause,
|
|
203
|
+
tier,
|
|
204
|
+
generatedFiles: plannerResult.outcome.generatedFiles || [],
|
|
205
|
+
},
|
|
206
|
+
}, options);
|
|
207
|
+
return true;
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Fallback reporting channel for planner activity (planned-only repairs,
|
|
214
|
+
// or APIs without the agent action endpoint).
|
|
215
|
+
async function reportPlanner(config, plannerResult, mode, options = {}) {
|
|
216
|
+
if (!plannerResult || (!plannerResult.decision && !plannerResult.executed)) return;
|
|
217
|
+
const findings = [];
|
|
218
|
+
if (plannerResult.decision) {
|
|
219
|
+
findings.push({
|
|
220
|
+
type: "self-repair",
|
|
221
|
+
cause: plannerResult.decision.cause,
|
|
222
|
+
tier: plannerResult.decision.tier,
|
|
223
|
+
message: plannerResult.outcome && plannerResult.outcome.statusMessage
|
|
224
|
+
? plannerResult.outcome.statusMessage
|
|
225
|
+
: plannerResult.decision.reason,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
await reportAutoDetect(config, {
|
|
229
|
+
startedAt: plannerResult.generatedAt,
|
|
230
|
+
completedAt: new Date().toISOString(),
|
|
231
|
+
mode,
|
|
232
|
+
score: null,
|
|
233
|
+
findings,
|
|
234
|
+
generatedFiles: plannerResult.outcome ? plannerResult.outcome.generatedFiles : [],
|
|
235
|
+
applied: Boolean(plannerResult.executed),
|
|
236
|
+
needsApproval: mode !== "autopilot" && Boolean(plannerResult.decision),
|
|
237
|
+
}, options);
|
|
238
|
+
}
|
|
239
|
+
|
|
169
240
|
function openWorkspace(config) {
|
|
170
241
|
const url = config?.workspaceUrl || DEFAULT_WORKSPACE_URL;
|
|
171
242
|
if (openUrl) {
|
|
@@ -192,6 +263,20 @@ module.exports = function createAgent(deps) {
|
|
|
192
263
|
const config = options._config || null;
|
|
193
264
|
const progress = config ? (step, detail) => reportProgress(config, action.id, step, detail, options) : async () => {};
|
|
194
265
|
|
|
266
|
+
// Cause-specific repair executors take precedence: an action that names a
|
|
267
|
+
// waste cause gets a targeted repair instead of the generic command run.
|
|
268
|
+
const targetCause = action.targetCause
|
|
269
|
+
|| (parsed.command === "repair" ? parsed.args.find((arg) => !arg.startsWith("-")) : null);
|
|
270
|
+
const causeExecutor = repairExecutors ? repairExecutors.forCause(targetCause) : null;
|
|
271
|
+
if (causeExecutor) {
|
|
272
|
+
// The backend escalates a repair by queueing "repair <cause> --tier
|
|
273
|
+
// aggressive" when the previous repair's verdict was no-change/regressed.
|
|
274
|
+
const tierIndex = parsed.args.indexOf("--tier");
|
|
275
|
+
const tierArg = tierIndex >= 0 ? parsed.args[tierIndex + 1] : null;
|
|
276
|
+
const executorOptions = tierArg ? { ...options, tier: tierArg } : options;
|
|
277
|
+
return causeExecutor(action, root, { progress, parsed, options: executorOptions });
|
|
278
|
+
}
|
|
279
|
+
|
|
195
280
|
if (parsed.command === "doctor" || action.actionType === "doctor") {
|
|
196
281
|
await progress("scanning", "Scanning repo for context issues");
|
|
197
282
|
const result = runDoctor(root, { limit: options.limit || 3, applySuggestions: true, json: true });
|
|
@@ -371,6 +456,23 @@ module.exports = function createAgent(deps) {
|
|
|
371
456
|
results.push({ id: action.id, label: action.label, ...result });
|
|
372
457
|
}
|
|
373
458
|
|
|
459
|
+
let plannerResult = null;
|
|
460
|
+
if (options.planRepairs && repairPlanner) {
|
|
461
|
+
try {
|
|
462
|
+
plannerResult = await repairPlanner.runPlannerOnce(rootDir, {
|
|
463
|
+
execute: mode === "autopilot",
|
|
464
|
+
sessionLimit: options.plannerSessionLimit,
|
|
465
|
+
});
|
|
466
|
+
const registered = plannerResult.executed
|
|
467
|
+
? await registerSelfRepair(config, plannerResult, options)
|
|
468
|
+
: false;
|
|
469
|
+
plannerResult.registered = registered;
|
|
470
|
+
if (!registered) await reportPlanner(config, plannerResult, mode, options);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
plannerResult = { error: error && error.message ? error.message : String(error) };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
374
476
|
let syncResult = null;
|
|
375
477
|
if (options.syncTelemetry) {
|
|
376
478
|
try {
|
|
@@ -400,6 +502,7 @@ module.exports = function createAgent(deps) {
|
|
|
400
502
|
actionsFailed: results.filter((item) => item.status === "failed").length,
|
|
401
503
|
actionsObserved: results.filter((item) => item.status === "observed" || item.status === "pending_approval").length,
|
|
402
504
|
autoDetect: autoDetectResult,
|
|
505
|
+
planner: plannerResult,
|
|
403
506
|
sync: syncResult,
|
|
404
507
|
results,
|
|
405
508
|
privacy: {
|
|
@@ -449,6 +552,25 @@ module.exports = function createAgent(deps) {
|
|
|
449
552
|
lines.push(` - ${f.message}`);
|
|
450
553
|
});
|
|
451
554
|
}
|
|
555
|
+
if (result.planner && !result.planner.error) {
|
|
556
|
+
lines.push("");
|
|
557
|
+
lines.push("Self-repair");
|
|
558
|
+
if (result.planner.decision) {
|
|
559
|
+
lines.push(` Cause: ${result.planner.decision.cause} (${result.planner.decision.tier} tier)`);
|
|
560
|
+
lines.push(` Why: ${result.planner.decision.reason}`);
|
|
561
|
+
if (result.planner.outcome) {
|
|
562
|
+
lines.push(` Status: ${result.planner.outcome.status}`);
|
|
563
|
+
if (result.planner.outcome.statusMessage) lines.push(` ${result.planner.outcome.statusMessage}`);
|
|
564
|
+
} else {
|
|
565
|
+
lines.push(` Status: planned (mode: ${result.mode}); not executed`);
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
lines.push(" Nothing to repair right now.");
|
|
569
|
+
}
|
|
570
|
+
(result.planner.skipped || []).forEach((item) => {
|
|
571
|
+
lines.push(` Held back: ${item.cause} (${item.reason})`);
|
|
572
|
+
});
|
|
573
|
+
}
|
|
452
574
|
if (result.sync) {
|
|
453
575
|
lines.push("");
|
|
454
576
|
lines.push("Sync");
|
|
@@ -479,11 +601,13 @@ module.exports = function createAgent(deps) {
|
|
|
479
601
|
const intervalMs = Math.max(5, Number(options.interval || 15)) * 1000;
|
|
480
602
|
const syncIntervalMs = Math.max(30, Number(options.syncInterval || 60)) * 1000;
|
|
481
603
|
const detectIntervalMs = Math.max(60, Number(options.detectInterval || 300)) * 1000;
|
|
604
|
+
const plannerIntervalMs = Math.max(60, Number(options.plannerInterval || 600)) * 1000;
|
|
482
605
|
let running = true;
|
|
483
606
|
let sleepResolve = null;
|
|
484
607
|
let firstRun = true;
|
|
485
608
|
let lastSyncAt = 0;
|
|
486
609
|
let lastDetectAt = 0;
|
|
610
|
+
let lastPlanAt = 0;
|
|
487
611
|
|
|
488
612
|
if (options.open) {
|
|
489
613
|
const config = loadConfig();
|
|
@@ -513,9 +637,12 @@ module.exports = function createAgent(deps) {
|
|
|
513
637
|
if (shouldSync) lastSyncAt = now;
|
|
514
638
|
const shouldDetect = options.autoDetect !== false && (firstRun || now - lastDetectAt >= detectIntervalMs);
|
|
515
639
|
if (shouldDetect) lastDetectAt = now;
|
|
640
|
+
const shouldPlan = options.planRepairs !== false && (firstRun || now - lastPlanAt >= plannerIntervalMs);
|
|
641
|
+
if (shouldPlan) lastPlanAt = now;
|
|
516
642
|
const runOptions = {
|
|
517
643
|
...options,
|
|
518
644
|
autoDetect: shouldDetect,
|
|
645
|
+
planRepairs: shouldPlan,
|
|
519
646
|
syncTelemetry: shouldSync,
|
|
520
647
|
};
|
|
521
648
|
firstRun = false;
|
package/lib/prismo-dev/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ const VALID_COMMANDS = new Set([
|
|
|
5
5
|
"dev", "init", "doctor", "firewall", "benchmark", "shield", "mcp",
|
|
6
6
|
"connect", "sync", "status", "disconnect", "agent", "connector", "setup", "scan",
|
|
7
7
|
"optimize", "context", "cc", "cursor", "receipt", "instructions",
|
|
8
|
-
"timeline", "replay", "boundaries", "usage", "guard", "watch", "demo",
|
|
8
|
+
"timeline", "replay", "boundaries", "usage", "guard", "watch", "demo", "repair",
|
|
9
9
|
]);
|
|
10
10
|
|
|
11
11
|
function parseTokenBudget(value) {
|
|
@@ -68,6 +68,11 @@ function createCli(deps) {
|
|
|
68
68
|
runSync,
|
|
69
69
|
renderGuardTerminal,
|
|
70
70
|
runGuard,
|
|
71
|
+
REPAIR_CAUSES,
|
|
72
|
+
renderRepairTerminal,
|
|
73
|
+
runRepair,
|
|
74
|
+
renderPlannerTerminal,
|
|
75
|
+
runPlannerOnce,
|
|
71
76
|
renderAgentTerminal,
|
|
72
77
|
runAgent,
|
|
73
78
|
renderConnectorTerminal,
|
|
@@ -425,6 +430,7 @@ function createCli(deps) {
|
|
|
425
430
|
const intervalIndex = rest.indexOf("--interval");
|
|
426
431
|
const syncIntervalIndex = rest.indexOf("--sync-interval");
|
|
427
432
|
const detectIntervalIndex = rest.indexOf("--detect-interval");
|
|
433
|
+
const plannerIntervalIndex = rest.indexOf("--planner-interval");
|
|
428
434
|
const limitIndex = rest.indexOf("--limit");
|
|
429
435
|
const budgetIndex = rest.indexOf("--budget");
|
|
430
436
|
const modeIndex = rest.indexOf("--mode");
|
|
@@ -432,7 +438,7 @@ function createCli(deps) {
|
|
|
432
438
|
if (!AGENT_VALID_MODES.has(modeValue)) {
|
|
433
439
|
throw new Error(`Invalid agent mode: ${modeValue}. Valid modes: observe, suggest, autopilot`);
|
|
434
440
|
}
|
|
435
|
-
const positional = getPositionals(rest, new Set(["--interval", "--sync-interval", "--detect-interval", "--limit", "--budget", "--mode"]));
|
|
441
|
+
const positional = getPositionals(rest, new Set(["--interval", "--sync-interval", "--detect-interval", "--planner-interval", "--limit", "--budget", "--mode"]));
|
|
436
442
|
const target = positional[0] || process.cwd();
|
|
437
443
|
const agentOptions = {
|
|
438
444
|
json,
|
|
@@ -440,10 +446,12 @@ function createCli(deps) {
|
|
|
440
446
|
watch: rest.includes("--watch") && !rest.includes("--once"),
|
|
441
447
|
open: rest.includes("--open"),
|
|
442
448
|
autoDetect: !rest.includes("--no-detect"),
|
|
449
|
+
planRepairs: !rest.includes("--no-planner"),
|
|
443
450
|
noSync: rest.includes("--no-sync"),
|
|
444
451
|
interval: parsePositiveInt(intervalIndex >= 0 ? rest[intervalIndex + 1] : null, 15),
|
|
445
452
|
syncInterval: parsePositiveInt(syncIntervalIndex >= 0 ? rest[syncIntervalIndex + 1] : null, 60),
|
|
446
453
|
detectInterval: parsePositiveInt(detectIntervalIndex >= 0 ? rest[detectIntervalIndex + 1] : null, 300),
|
|
454
|
+
plannerInterval: parsePositiveInt(plannerIntervalIndex >= 0 ? rest[plannerIntervalIndex + 1] : null, 600),
|
|
447
455
|
limit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 5),
|
|
448
456
|
syncLimit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 20),
|
|
449
457
|
tokenBudget: parseTokenBudget(budgetIndex >= 0 ? rest[budgetIndex + 1] : null) || 600000,
|
|
@@ -758,6 +766,41 @@ function createCli(deps) {
|
|
|
758
766
|
return;
|
|
759
767
|
}
|
|
760
768
|
|
|
769
|
+
if (command === "repair") {
|
|
770
|
+
const json = rest.includes("--json");
|
|
771
|
+
const separatorIndex = rest.indexOf("--");
|
|
772
|
+
const ownArgs = separatorIndex >= 0 ? rest.slice(0, separatorIndex) : rest;
|
|
773
|
+
const commandArgs = separatorIndex >= 0 ? rest.slice(separatorIndex + 1) : [];
|
|
774
|
+
const positional = getPositionals(ownArgs, new Set(["--limit", "--budget", "--scope", "--tier"]));
|
|
775
|
+
const cause = (positional[0] || "").toLowerCase();
|
|
776
|
+
const target = positional[1] || process.cwd();
|
|
777
|
+
const limitIndex = ownArgs.indexOf("--limit");
|
|
778
|
+
const budgetIndex = ownArgs.indexOf("--budget");
|
|
779
|
+
const scopeIndex = ownArgs.indexOf("--scope");
|
|
780
|
+
const tierIndex = ownArgs.indexOf("--tier");
|
|
781
|
+
if (cause === "auto") {
|
|
782
|
+
const result = await runPlannerOnce(target, {
|
|
783
|
+
execute: !ownArgs.includes("--dry-run"),
|
|
784
|
+
limit: parsePositiveInt(limitIndex >= 0 ? ownArgs[limitIndex + 1] : null, 5),
|
|
785
|
+
tokenBudget: parseTokenBudget(budgetIndex >= 0 ? ownArgs[budgetIndex + 1] : null),
|
|
786
|
+
});
|
|
787
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
788
|
+
else console.log(renderPlannerTerminal(result));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const result = await runRepair(target, cause, {
|
|
792
|
+
limit: parsePositiveInt(limitIndex >= 0 ? ownArgs[limitIndex + 1] : null, 5),
|
|
793
|
+
tokenBudget: parseTokenBudget(budgetIndex >= 0 ? ownArgs[budgetIndex + 1] : null),
|
|
794
|
+
scope: scopeIndex >= 0 ? ownArgs[scopeIndex + 1] : null,
|
|
795
|
+
tier: tierIndex >= 0 ? ownArgs[tierIndex + 1] : null,
|
|
796
|
+
commandArgs,
|
|
797
|
+
});
|
|
798
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
799
|
+
else console.log(renderRepairTerminal(result));
|
|
800
|
+
if (result.status === "failed") process.exitCode = 1;
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
761
804
|
if (command === "usage" || command === "watch") {
|
|
762
805
|
const json = rest.includes("--json");
|
|
763
806
|
const knownTools = new Set(["codex", "claude", "cursor", "all"]);
|
package/lib/prismo-dev/help.js
CHANGED
|
@@ -33,6 +33,7 @@ Usage:
|
|
|
33
33
|
prismo boundaries [codex|claude|cursor|all] [--json] [--limit N] [path]
|
|
34
34
|
prismo usage [codex|claude|cursor|all] [--json] [--limit N] [path]
|
|
35
35
|
prismo guard [codex|claude|cursor|all] [--json] [--watch] [--once] [--no-sync] [--dry-run] [--limit N] [--budget N] [--interval N] [path]
|
|
36
|
+
prismo repair <cause|auto> [--json] [--dry-run] [--tier mild|aggressive] [--limit N] [--budget N] [--scope SCOPE] [path] [-- <command ...>]
|
|
36
37
|
prismo watch [codex|claude|cursor|all] [--json] [--once] [--agents] [--report] [--rescue] [--guardrails] [--throttle] [--events] [--no-events] [--auto] [--budget N] [--redact-paths] [--interval N] [path]
|
|
37
38
|
prismo demo
|
|
38
39
|
|
|
@@ -62,6 +63,7 @@ Commands:
|
|
|
62
63
|
boundaries Check whether parallel agents are isolated or overlapping.
|
|
63
64
|
usage Read local Codex/Claude Code/Cursor session logs and summarize token usage.
|
|
64
65
|
guard Run proactive local guardrails and sync prevention events to Prismo.
|
|
66
|
+
repair Run the cause-specific repair for a detected waste cause; "auto" lets the planner pick.
|
|
65
67
|
watch Refresh local session usage in the terminal.
|
|
66
68
|
demo Show sample output without needing a messy repo.
|
|
67
69
|
setup Detect coding tools, tracking modes, local logs, and Prismo proxy readiness.
|
|
@@ -288,6 +290,37 @@ Examples:
|
|
|
288
290
|
Output:
|
|
289
291
|
Runs proactive local protection on top of Prismo watch: live guardrails, rescue prompt, context throttle, context firewall, guard event log, and dashboard-ready prevention events.
|
|
290
292
|
Guard never uploads prompts, source code, file contents, stdout, stderr, or full command logs.`,
|
|
293
|
+
repair: `PrismoDev Repair
|
|
294
|
+
|
|
295
|
+
Usage:
|
|
296
|
+
prismo repair <cause|auto> [--json] [--dry-run] [--tier mild|aggressive] [--limit N] [--budget N] [--scope SCOPE] [path] [-- <command ...>]
|
|
297
|
+
|
|
298
|
+
Causes:
|
|
299
|
+
repeated-file-reads Refresh ignore rules and context packs, and map hot files into .prismo/hot-files.md.
|
|
300
|
+
tool-output-flood Stage noisy commands behind shield in .prismo/noisy-commands.md; run a safe command shielded when passed after --.
|
|
301
|
+
generated-artifacts Append ignore rules for scan-detected build output plus artifact paths seen in recent sessions.
|
|
302
|
+
context-loop Run a tightened guard snapshot and write loop-breaking rules to .prismo/loop-breaker.md.
|
|
303
|
+
long-session-buildup Generate scoped context packs and a fresh-session restart routine in .prismo/session-restart.md.
|
|
304
|
+
auto Let the planner score recent sessions, pick the top cause, and run its repair.
|
|
305
|
+
|
|
306
|
+
Examples:
|
|
307
|
+
prismo repair auto
|
|
308
|
+
prismo repair auto --dry-run
|
|
309
|
+
prismo repair repeated-file-reads
|
|
310
|
+
prismo repair tool-output-flood -- npm test
|
|
311
|
+
prismo repair generated-artifacts --json
|
|
312
|
+
prismo repair context-loop --budget 400k
|
|
313
|
+
prismo repair long-session-buildup --scope frontend
|
|
314
|
+
|
|
315
|
+
Output:
|
|
316
|
+
Runs the targeted repair for one waste cause instead of the generic doctor flow.
|
|
317
|
+
These are the same executors the workspace agent uses when a queued action carries a targetCause.
|
|
318
|
+
"auto" applies cooldowns and verdict feedback from .prismo/repair-state.json: a repair that came back
|
|
319
|
+
no-change or regressed escalates to an aggressive tier (adds a context firewall policy and tighter budgets);
|
|
320
|
+
a cause that already failed both tiers is held for review instead of being retried forever.
|
|
321
|
+
--dry-run with auto prints the planner decision without executing.
|
|
322
|
+
--tier aggressive forces the stronger repair; cloud-queued actions carry it automatically after a no-change/regressed verdict.
|
|
323
|
+
Repairs only write .prismo/ files and append ignore rules with backups; they never overwrite CLAUDE.md, AGENTS.md, .gitignore, or source code.`,
|
|
291
324
|
watch: `Prismo Watch
|
|
292
325
|
|
|
293
326
|
Usage:
|
|
@@ -492,7 +525,7 @@ Output:
|
|
|
492
525
|
agent: `PrismoDev Agent
|
|
493
526
|
|
|
494
527
|
Usage:
|
|
495
|
-
prismo agent [--json] [--once] [--watch] [--open] [--no-detect] [--no-sync] [--interval N] [--sync-interval N] [--limit N] [--mode MODE] [path]
|
|
528
|
+
prismo agent [--json] [--once] [--watch] [--open] [--no-detect] [--no-planner] [--no-sync] [--interval N] [--sync-interval N] [--planner-interval N] [--limit N] [--mode MODE] [path]
|
|
496
529
|
|
|
497
530
|
Modes:
|
|
498
531
|
observe Watch and report actions without executing. No changes made.
|
|
@@ -511,6 +544,8 @@ Examples:
|
|
|
511
544
|
Options:
|
|
512
545
|
--open Open the Prismo workspace in the browser on start.
|
|
513
546
|
--no-detect Skip the initial auto-detect scan on first poll.
|
|
547
|
+
--no-planner Disable the self-repair planner (cause scoring, cooldowns, tier escalation).
|
|
548
|
+
--planner-interval N Seconds between self-repair planner runs in watch mode (default 600).
|
|
514
549
|
--no-sync Keep watch mode from continuously syncing aggregate telemetry.
|
|
515
550
|
|
|
516
551
|
Output:
|
|
@@ -520,6 +555,10 @@ Output:
|
|
|
520
555
|
Sends heartbeat to Prismo Cloud on each poll and syncs aggregate telemetry on a controlled interval so the dashboard stays fresh.
|
|
521
556
|
Handles SIGINT/SIGTERM gracefully and marks the agent offline before exiting.
|
|
522
557
|
Supported actions are doctor, sync, guard, context/optimize, and shield with a conservative command allowlist.
|
|
558
|
+
Actions queued with a targetCause run cause-specific repair executors instead of the generic command; see prismo repair --help.
|
|
559
|
+
In autopilot the self-repair planner also runs on its own interval: it scores waste causes from local sessions,
|
|
560
|
+
repairs the top cause above threshold, judges the result against later sessions, and escalates or backs off accordingly.
|
|
561
|
+
In observe/suggest modes the planner only reports what it would repair.
|
|
523
562
|
Agent does not upload prompts, source code, file contents, stdout, stderr, or full command logs.`,
|
|
524
563
|
ci: `Prismo CI
|
|
525
564
|
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
module.exports = function createRepairPlanner(deps) {
|
|
2
|
+
const {
|
|
3
|
+
fs,
|
|
4
|
+
path,
|
|
5
|
+
NPX_COMMAND,
|
|
6
|
+
getUsageSummary,
|
|
7
|
+
estimateWaste,
|
|
8
|
+
repairExecutors,
|
|
9
|
+
} = deps;
|
|
10
|
+
|
|
11
|
+
// Decision thresholds. Verdict math mirrors the backend's
|
|
12
|
+
// _measure_repair_impact (14-day baseline, >=2 post-repair sessions,
|
|
13
|
+
// 0.01 waste-rate epsilon) so local and cloud verdicts agree.
|
|
14
|
+
const DEFAULTS = {
|
|
15
|
+
sessionLimit: 10,
|
|
16
|
+
minWastedTokens: 15000,
|
|
17
|
+
minWasteRate: 0.05,
|
|
18
|
+
cooldownMs: 6 * 60 * 60 * 1000,
|
|
19
|
+
minSessionsToJudge: 2,
|
|
20
|
+
baselineDays: 14,
|
|
21
|
+
rateEpsilon: 0.01,
|
|
22
|
+
historyLimit: 20,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function nowIso() {
|
|
26
|
+
return new Date().toISOString();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function statePath(root) {
|
|
30
|
+
return path.join(root, ".prismo", "repair-state.json");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readState(root) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(statePath(root), "utf8");
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
return parsed && typeof parsed === "object" ? { causes: {}, history: [], ...parsed } : { causes: {}, history: [] };
|
|
38
|
+
} catch {
|
|
39
|
+
return { causes: {}, history: [] };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeState(root, state) {
|
|
44
|
+
const filePath = statePath(root);
|
|
45
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
46
|
+
fs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sessionStamp(session) {
|
|
50
|
+
const value = session.updatedAt || session.startedAt || null;
|
|
51
|
+
const parsed = value ? Date.parse(value) : NaN;
|
|
52
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function analyzeSessions(root, options = {}) {
|
|
56
|
+
let sessions = [];
|
|
57
|
+
try {
|
|
58
|
+
const summary = getUsageSummary({ tool: "all", cwd: root, limit: options.sessionLimit || DEFAULTS.sessionLimit });
|
|
59
|
+
sessions = summary.sessions || [];
|
|
60
|
+
} catch {
|
|
61
|
+
sessions = [];
|
|
62
|
+
}
|
|
63
|
+
return sessions
|
|
64
|
+
.map((session) => {
|
|
65
|
+
const stamp = sessionStamp(session);
|
|
66
|
+
if (stamp === null) return null;
|
|
67
|
+
const waste = estimateWaste(session);
|
|
68
|
+
return {
|
|
69
|
+
stamp,
|
|
70
|
+
tokens: Number(waste.tokens || 0),
|
|
71
|
+
wastedTokens: Number(waste.wastedTokens || 0),
|
|
72
|
+
topCause: waste.topCause,
|
|
73
|
+
};
|
|
74
|
+
})
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function scoreCauses(analyzed) {
|
|
79
|
+
const totals = new Map();
|
|
80
|
+
let observedTokens = 0;
|
|
81
|
+
for (const session of analyzed) {
|
|
82
|
+
observedTokens += session.tokens;
|
|
83
|
+
if (!session.topCause || session.topCause === "low-signal") continue;
|
|
84
|
+
const entry = totals.get(session.topCause) || { cause: session.topCause, wastedTokens: 0, sessions: 0 };
|
|
85
|
+
entry.wastedTokens += session.wastedTokens;
|
|
86
|
+
entry.sessions += 1;
|
|
87
|
+
totals.set(session.topCause, entry);
|
|
88
|
+
}
|
|
89
|
+
return Array.from(totals.values())
|
|
90
|
+
.map((entry) => ({
|
|
91
|
+
...entry,
|
|
92
|
+
wasteRate: observedTokens > 0 ? Math.round((entry.wastedTokens / observedTokens) * 10000) / 10000 : 0,
|
|
93
|
+
}))
|
|
94
|
+
.sort((a, b) => b.wastedTokens - a.wastedTokens);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Local mirror of the backend's repair verification: compare the cause's
|
|
98
|
+
// waste rate in sessions before vs. after the last repair.
|
|
99
|
+
function judgeRepair(cause, repairedAtMs, analyzed, config) {
|
|
100
|
+
const baselineStart = repairedAtMs - config.baselineDays * 24 * 60 * 60 * 1000;
|
|
101
|
+
const before = analyzed.filter((s) => s.stamp <= repairedAtMs && s.stamp >= baselineStart);
|
|
102
|
+
const after = analyzed.filter((s) => s.stamp > repairedAtMs);
|
|
103
|
+
|
|
104
|
+
const causeWasted = (group) => group.reduce((sum, s) => sum + (s.topCause === cause ? s.wastedTokens : 0), 0);
|
|
105
|
+
const observed = (group) => group.reduce((sum, s) => sum + s.tokens, 0);
|
|
106
|
+
|
|
107
|
+
const observedAfter = observed(after);
|
|
108
|
+
const observedBefore = observed(before);
|
|
109
|
+
const verdict = {
|
|
110
|
+
cause,
|
|
111
|
+
sessionsBefore: before.length,
|
|
112
|
+
sessionsAfter: after.length,
|
|
113
|
+
wasteRateBefore: observedBefore > 0 ? Math.round((causeWasted(before) / observedBefore) * 10000) / 10000 : null,
|
|
114
|
+
wasteRateAfter: observedAfter > 0 ? Math.round((causeWasted(after) / observedAfter) * 10000) / 10000 : null,
|
|
115
|
+
status: "measuring",
|
|
116
|
+
judgedAt: nowIso(),
|
|
117
|
+
};
|
|
118
|
+
if (after.length < config.minSessionsToJudge || observedAfter <= 0) return verdict;
|
|
119
|
+
if (!before.length || observedBefore <= 0) {
|
|
120
|
+
verdict.status = "no-baseline";
|
|
121
|
+
return verdict;
|
|
122
|
+
}
|
|
123
|
+
const delta = verdict.wasteRateBefore - verdict.wasteRateAfter;
|
|
124
|
+
if (delta > config.rateEpsilon) verdict.status = "improved";
|
|
125
|
+
else if (delta < -config.rateEpsilon) verdict.status = "regressed";
|
|
126
|
+
else verdict.status = "no-change";
|
|
127
|
+
return verdict;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Decide what (if anything) to repair next. Pure read: does not execute
|
|
131
|
+
// or modify state, so observe/suggest modes can call it safely.
|
|
132
|
+
function plan(root, options = {}) {
|
|
133
|
+
const config = { ...DEFAULTS, ...options };
|
|
134
|
+
const analyzed = analyzeSessions(root, config);
|
|
135
|
+
const causes = scoreCauses(analyzed);
|
|
136
|
+
const state = readState(root);
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
const skipped = [];
|
|
139
|
+
let decision = null;
|
|
140
|
+
|
|
141
|
+
for (const scored of causes) {
|
|
142
|
+
if (scored.wastedTokens < config.minWastedTokens || scored.wasteRate < config.minWasteRate) {
|
|
143
|
+
skipped.push({ cause: scored.cause, reason: "below-threshold" });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!repairExecutors.forCause(scored.cause)) {
|
|
147
|
+
skipped.push({ cause: scored.cause, reason: "no-executor" });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const entry = state.causes[scored.cause] || null;
|
|
151
|
+
let verdict = null;
|
|
152
|
+
let tier = "mild";
|
|
153
|
+
if (entry && entry.lastRepairAt) {
|
|
154
|
+
const repairedAtMs = Date.parse(entry.lastRepairAt);
|
|
155
|
+
if (Number.isFinite(repairedAtMs)) {
|
|
156
|
+
if (now - repairedAtMs < config.cooldownMs) {
|
|
157
|
+
skipped.push({ cause: scored.cause, reason: "cooldown" });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
verdict = judgeRepair(scored.cause, repairedAtMs, analyzed, config);
|
|
161
|
+
if (verdict.status === "measuring") {
|
|
162
|
+
skipped.push({ cause: scored.cause, reason: "measuring", verdict });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (verdict.status === "no-change" || verdict.status === "regressed") {
|
|
166
|
+
if (entry.lastTier === "aggressive") {
|
|
167
|
+
// Both tiers tried without improvement; stop auto-repairing
|
|
168
|
+
// this cause until a human looks at it or sessions change.
|
|
169
|
+
skipped.push({ cause: scored.cause, reason: "needs-review", verdict });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
tier = "aggressive";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!decision) {
|
|
177
|
+
decision = {
|
|
178
|
+
cause: scored.cause,
|
|
179
|
+
tier,
|
|
180
|
+
wastedTokens: scored.wastedTokens,
|
|
181
|
+
wasteRate: scored.wasteRate,
|
|
182
|
+
previousVerdict: verdict ? verdict.status : null,
|
|
183
|
+
reason: verdict && tier === "aggressive"
|
|
184
|
+
? `Last ${entry.lastTier} repair came back ${verdict.status}; escalating to aggressive.`
|
|
185
|
+
: `${scored.cause} is the top waste cause (${scored.wastedTokens.toLocaleString()} tokens, ${(scored.wasteRate * 100).toFixed(1)}% of observed).`,
|
|
186
|
+
};
|
|
187
|
+
} else {
|
|
188
|
+
skipped.push({ cause: scored.cause, reason: "lower-priority" });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
generatedAt: nowIso(),
|
|
194
|
+
sessionsAnalyzed: analyzed.length,
|
|
195
|
+
causes,
|
|
196
|
+
decision,
|
|
197
|
+
skipped,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function runPlannerOnce(rootDir = process.cwd(), options = {}) {
|
|
202
|
+
const root = path.resolve(rootDir);
|
|
203
|
+
const config = { ...DEFAULTS, ...options };
|
|
204
|
+
const planResult = plan(root, config);
|
|
205
|
+
const report = {
|
|
206
|
+
schemaVersion: 1,
|
|
207
|
+
command: "repair-planner",
|
|
208
|
+
...planResult,
|
|
209
|
+
executed: false,
|
|
210
|
+
outcome: null,
|
|
211
|
+
};
|
|
212
|
+
if (!planResult.decision || options.execute === false) return report;
|
|
213
|
+
|
|
214
|
+
const { cause, tier } = planResult.decision;
|
|
215
|
+
const executor = repairExecutors.forCause(cause);
|
|
216
|
+
const action = {
|
|
217
|
+
id: null,
|
|
218
|
+
actionType: "repair",
|
|
219
|
+
command: `${NPX_COMMAND} repair ${cause}`,
|
|
220
|
+
label: `Auto-repair ${cause} (${tier})`,
|
|
221
|
+
targetCause: cause,
|
|
222
|
+
};
|
|
223
|
+
let outcome;
|
|
224
|
+
try {
|
|
225
|
+
outcome = await executor(action, root, { options: { ...options, tier } });
|
|
226
|
+
} catch (error) {
|
|
227
|
+
outcome = {
|
|
228
|
+
status: "failed",
|
|
229
|
+
statusMessage: `Repair executor failed: ${error && error.message ? error.message : String(error)}`,
|
|
230
|
+
result: null,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const state = readState(root);
|
|
235
|
+
const previous = state.causes[cause] || { attempts: 0 };
|
|
236
|
+
state.causes[cause] = {
|
|
237
|
+
...previous,
|
|
238
|
+
lastRepairAt: nowIso(),
|
|
239
|
+
lastTier: tier,
|
|
240
|
+
lastStatus: outcome.status,
|
|
241
|
+
attempts: Number(previous.attempts || 0) + 1,
|
|
242
|
+
lastVerdict: planResult.decision.previousVerdict || previous.lastVerdict || null,
|
|
243
|
+
};
|
|
244
|
+
state.history = [
|
|
245
|
+
{ at: nowIso(), cause, tier, status: outcome.status, wastedTokens: planResult.decision.wastedTokens },
|
|
246
|
+
...(state.history || []),
|
|
247
|
+
].slice(0, config.historyLimit);
|
|
248
|
+
writeState(root, state);
|
|
249
|
+
|
|
250
|
+
report.executed = outcome.status === "completed";
|
|
251
|
+
report.outcome = {
|
|
252
|
+
status: outcome.status,
|
|
253
|
+
statusMessage: outcome.statusMessage,
|
|
254
|
+
tier,
|
|
255
|
+
generatedFiles: (outcome.result && outcome.result.generatedFiles) || [],
|
|
256
|
+
};
|
|
257
|
+
return report;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function renderPlannerTerminal(report) {
|
|
261
|
+
const lines = [];
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push("PrismoDev Repair Planner");
|
|
264
|
+
lines.push("");
|
|
265
|
+
lines.push(`Sessions analyzed: ${report.sessionsAnalyzed}`);
|
|
266
|
+
if (report.causes.length) {
|
|
267
|
+
lines.push("");
|
|
268
|
+
lines.push("Waste causes:");
|
|
269
|
+
report.causes.forEach((entry) => {
|
|
270
|
+
lines.push(`- ${entry.cause}: ${entry.wastedTokens.toLocaleString()} tokens (${(entry.wasteRate * 100).toFixed(1)}% of observed, ${entry.sessions} session(s))`);
|
|
271
|
+
});
|
|
272
|
+
} else {
|
|
273
|
+
lines.push("No attributable waste causes in recent sessions.");
|
|
274
|
+
}
|
|
275
|
+
if (report.decision) {
|
|
276
|
+
lines.push("");
|
|
277
|
+
lines.push(`Decision: repair ${report.decision.cause} (${report.decision.tier} tier)`);
|
|
278
|
+
lines.push(`Why: ${report.decision.reason}`);
|
|
279
|
+
} else {
|
|
280
|
+
lines.push("");
|
|
281
|
+
lines.push("Decision: nothing to repair right now.");
|
|
282
|
+
}
|
|
283
|
+
if (report.skipped.length) {
|
|
284
|
+
lines.push("");
|
|
285
|
+
lines.push("Held back:");
|
|
286
|
+
report.skipped.forEach((item) => lines.push(`- ${item.cause}: ${item.reason}`));
|
|
287
|
+
}
|
|
288
|
+
if (report.outcome) {
|
|
289
|
+
lines.push("");
|
|
290
|
+
lines.push(`Repair: ${report.outcome.status}`);
|
|
291
|
+
if (report.outcome.statusMessage) lines.push(report.outcome.statusMessage);
|
|
292
|
+
} else if (report.decision && !report.executed) {
|
|
293
|
+
lines.push("");
|
|
294
|
+
lines.push(`Not executed (planning only). Run: ${NPX_COMMAND} repair ${report.decision.cause}`);
|
|
295
|
+
}
|
|
296
|
+
return lines.join("\n");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
DEFAULTS,
|
|
301
|
+
judgeRepair,
|
|
302
|
+
plan,
|
|
303
|
+
readState,
|
|
304
|
+
renderPlannerTerminal,
|
|
305
|
+
runPlannerOnce,
|
|
306
|
+
};
|
|
307
|
+
};
|
package/lib/prismo-dev-scan.js
CHANGED
|
@@ -272,6 +272,7 @@ const {
|
|
|
272
272
|
|
|
273
273
|
const {
|
|
274
274
|
buildSyncPayload,
|
|
275
|
+
estimateWaste,
|
|
275
276
|
loadConfig,
|
|
276
277
|
renderConnectTerminal,
|
|
277
278
|
renderDisconnectTerminal,
|
|
@@ -313,6 +314,37 @@ const {
|
|
|
313
314
|
loadConfig,
|
|
314
315
|
});
|
|
315
316
|
|
|
317
|
+
const repairExecutors = require("./prismo-dev/repair-executors")({
|
|
318
|
+
fs,
|
|
319
|
+
path,
|
|
320
|
+
NPX_COMMAND,
|
|
321
|
+
runDoctor,
|
|
322
|
+
runOptimize,
|
|
323
|
+
runGuard,
|
|
324
|
+
runShield,
|
|
325
|
+
runFirewall,
|
|
326
|
+
getUsageSummary,
|
|
327
|
+
appendIgnoreSuggestions,
|
|
328
|
+
});
|
|
329
|
+
const {
|
|
330
|
+
REPAIR_CAUSES,
|
|
331
|
+
renderRepairTerminal,
|
|
332
|
+
runRepair,
|
|
333
|
+
} = repairExecutors;
|
|
334
|
+
|
|
335
|
+
const repairPlanner = require("./prismo-dev/repair-planner")({
|
|
336
|
+
fs,
|
|
337
|
+
path,
|
|
338
|
+
NPX_COMMAND,
|
|
339
|
+
getUsageSummary,
|
|
340
|
+
estimateWaste,
|
|
341
|
+
repairExecutors,
|
|
342
|
+
});
|
|
343
|
+
const {
|
|
344
|
+
renderPlannerTerminal,
|
|
345
|
+
runPlannerOnce,
|
|
346
|
+
} = repairPlanner;
|
|
347
|
+
|
|
316
348
|
const {
|
|
317
349
|
renderAgentTerminal,
|
|
318
350
|
runAgent,
|
|
@@ -331,6 +363,8 @@ const {
|
|
|
331
363
|
runShield,
|
|
332
364
|
runOptimize,
|
|
333
365
|
openUrl,
|
|
366
|
+
repairExecutors,
|
|
367
|
+
repairPlanner,
|
|
334
368
|
});
|
|
335
369
|
|
|
336
370
|
const {
|
|
@@ -452,6 +486,11 @@ const { runCli } = require("./prismo-dev/cli")({
|
|
|
452
486
|
runSync,
|
|
453
487
|
renderGuardTerminal,
|
|
454
488
|
runGuard,
|
|
489
|
+
REPAIR_CAUSES,
|
|
490
|
+
renderRepairTerminal,
|
|
491
|
+
runRepair,
|
|
492
|
+
renderPlannerTerminal,
|
|
493
|
+
runPlannerOnce,
|
|
455
494
|
renderAgentTerminal,
|
|
456
495
|
runAgent,
|
|
457
496
|
renderConnectorTerminal,
|
|
@@ -552,6 +591,11 @@ module.exports = {
|
|
|
552
591
|
runConnect,
|
|
553
592
|
runDisconnect,
|
|
554
593
|
runGuard,
|
|
594
|
+
runRepair,
|
|
595
|
+
renderRepairTerminal,
|
|
596
|
+
REPAIR_CAUSES,
|
|
597
|
+
runPlannerOnce,
|
|
598
|
+
renderPlannerTerminal,
|
|
555
599
|
runConnectorInstall,
|
|
556
600
|
runConnectorStart,
|
|
557
601
|
runConnectorStatus,
|
package/package.json
CHANGED