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.
@@ -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) {
@@ -174,13 +245,41 @@ module.exports = function createAgent(deps) {
174
245
  return url;
175
246
  }
176
247
 
248
+ async function reportProgress(config, actionId, step, detail, options = {}) {
249
+ const endpoint = options.progressEndpoint || `${apiBase(config)}/v1/dev/workspace/actions/${actionId}/progress`;
250
+ try {
251
+ await requestJson("POST", endpoint, config.token, {
252
+ step,
253
+ detail: detail || null,
254
+ timestamp: new Date().toISOString(),
255
+ }, options.timeoutMs || 5000);
256
+ } catch (_) {}
257
+ }
258
+
177
259
  async function executeAction(action, rootDir, options = {}) {
178
260
  const root = repoRoot(path.resolve(rootDir || process.cwd()), action);
179
261
  const parsed = parseCommand(action.command);
180
262
  const startedAt = new Date().toISOString();
263
+ const config = options._config || null;
264
+ const progress = config ? (step, detail) => reportProgress(config, action.id, step, detail, options) : async () => {};
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
+ return causeExecutor(action, root, { progress, parsed, options });
273
+ }
181
274
 
182
275
  if (parsed.command === "doctor" || action.actionType === "doctor") {
276
+ await progress("scanning", "Scanning repo for context issues");
183
277
  const result = runDoctor(root, { limit: options.limit || 3, applySuggestions: true, json: true });
278
+ const score = result.after?.score ?? result.scan?.score ?? null;
279
+ const issueCount = result.scan?.issues?.length || 0;
280
+ const generatedFiles = result.generatedFiles || result.optimize?.generatedFiles || [];
281
+ if (issueCount > 0) await progress("fixing", `Found ${issueCount} issue${issueCount === 1 ? "" : "s"}, applying fixes`);
282
+ await progress("done", `Score: ${score}/100, generated ${generatedFiles.length} file${generatedFiles.length === 1 ? "" : "s"}`);
184
283
  return {
185
284
  status: "completed",
186
285
  statusMessage: "Doctor completed and applied safe ignore/context fixes.",
@@ -188,14 +287,16 @@ module.exports = function createAgent(deps) {
188
287
  command: "doctor",
189
288
  startedAt,
190
289
  completedAt: new Date().toISOString(),
191
- score: result.after?.score ?? result.scan?.score ?? null,
192
- generatedFiles: result.generatedFiles || result.optimize?.generatedFiles || [],
290
+ score,
291
+ generatedFiles,
193
292
  },
194
293
  };
195
294
  }
196
295
 
197
296
  if (parsed.command === "sync" || action.actionType === "sync") {
297
+ await progress("syncing", "Syncing local telemetry to workspace");
198
298
  const result = await runSync(root, { limit: options.limit || 20 });
299
+ await progress("done", result.synced ? "Sync completed" : "Sync failed");
199
300
  return {
200
301
  status: result.synced ? "completed" : "failed",
201
302
  statusMessage: result.synced ? "Sync completed." : "Sync could not run because this machine is not connected.",
@@ -211,6 +312,7 @@ module.exports = function createAgent(deps) {
211
312
  }
212
313
 
213
314
  if (parsed.command === "guard" || action.actionType === "guard") {
315
+ await progress("guarding", "Running guardrail snapshot across sessions");
214
316
  const result = await runGuard(root, {
215
317
  tool: "all",
216
318
  limit: options.limit || 5,
@@ -218,6 +320,8 @@ module.exports = function createAgent(deps) {
218
320
  noSync: false,
219
321
  watch: false,
220
322
  });
323
+ const eventCount = result.events?.length || 0;
324
+ await progress("done", `Guard complete, ${eventCount} event${eventCount === 1 ? "" : "s"} recorded`);
221
325
  return {
222
326
  status: "completed",
223
327
  statusMessage: "Guard snapshot completed. Start agent watch mode for continuous protection.",
@@ -226,14 +330,17 @@ module.exports = function createAgent(deps) {
226
330
  startedAt,
227
331
  completedAt: new Date().toISOString(),
228
332
  guardRunning: Boolean(result.guardRunning),
229
- events: result.events?.length || 0,
333
+ events: eventCount,
230
334
  },
231
335
  };
232
336
  }
233
337
 
234
338
  if (parsed.command === "context" || parsed.command === "optimize" || action.actionType === "context") {
235
339
  const scope = parsed.args.find((arg) => !arg.startsWith("-")) || null;
340
+ await progress("generating", `Generating context pack${scope ? ` for ${scope}` : ""}`);
236
341
  const result = runOptimize(root, { scope });
342
+ const generatedFiles = result.generatedFiles || [];
343
+ await progress("done", `Generated ${generatedFiles.length} file${generatedFiles.length === 1 ? "" : "s"}`);
237
344
  return {
238
345
  status: "completed",
239
346
  statusMessage: "Context pack generated.",
@@ -242,7 +349,7 @@ module.exports = function createAgent(deps) {
242
349
  startedAt,
243
350
  completedAt: new Date().toISOString(),
244
351
  scope,
245
- generatedFiles: result.generatedFiles || [],
352
+ generatedFiles,
246
353
  },
247
354
  };
248
355
  }
@@ -250,13 +357,16 @@ module.exports = function createAgent(deps) {
250
357
  if (parsed.command === "shield" || action.actionType === "shield") {
251
358
  const commandArgs = parseShieldArgs(parsed.args);
252
359
  if (!commandArgs) {
360
+ await progress("rejected", "Command not on safe allowlist");
253
361
  return {
254
362
  status: "failed",
255
363
  statusMessage: "Shield action was rejected because the command is not on the safe allowlist.",
256
364
  result: { command: "shield", rejected: true, reason: "unsafe-shield-command" },
257
365
  };
258
366
  }
367
+ await progress("shielding", `Running shielded command: ${commandArgs.join(" ")}`);
259
368
  const result = runShield(root, commandArgs);
369
+ await progress("done", result.exitCode === 0 ? "Command completed" : "Command failed");
260
370
  return {
261
371
  status: result.exitCode === 0 ? "completed" : "failed",
262
372
  statusMessage: result.exitCode === 0 ? "Shielded command completed." : "Shielded command exited with an error.",
@@ -336,11 +446,28 @@ module.exports = function createAgent(deps) {
336
446
  status: "running",
337
447
  statusMessage: "Running locally through PrismoDev agent.",
338
448
  }, options);
339
- const result = await executeAction(action, rootDir, options);
449
+ const result = await executeAction(action, rootDir, { ...options, _config: config });
340
450
  await updateAction(config, action.id, result, options);
341
451
  results.push({ id: action.id, label: action.label, ...result });
342
452
  }
343
453
 
454
+ let plannerResult = null;
455
+ if (options.planRepairs && repairPlanner) {
456
+ try {
457
+ plannerResult = await repairPlanner.runPlannerOnce(rootDir, {
458
+ execute: mode === "autopilot",
459
+ sessionLimit: options.plannerSessionLimit,
460
+ });
461
+ const registered = plannerResult.executed
462
+ ? await registerSelfRepair(config, plannerResult, options)
463
+ : false;
464
+ plannerResult.registered = registered;
465
+ if (!registered) await reportPlanner(config, plannerResult, mode, options);
466
+ } catch (error) {
467
+ plannerResult = { error: error && error.message ? error.message : String(error) };
468
+ }
469
+ }
470
+
344
471
  let syncResult = null;
345
472
  if (options.syncTelemetry) {
346
473
  try {
@@ -370,6 +497,7 @@ module.exports = function createAgent(deps) {
370
497
  actionsFailed: results.filter((item) => item.status === "failed").length,
371
498
  actionsObserved: results.filter((item) => item.status === "observed" || item.status === "pending_approval").length,
372
499
  autoDetect: autoDetectResult,
500
+ planner: plannerResult,
373
501
  sync: syncResult,
374
502
  results,
375
503
  privacy: {
@@ -419,6 +547,25 @@ module.exports = function createAgent(deps) {
419
547
  lines.push(` - ${f.message}`);
420
548
  });
421
549
  }
550
+ if (result.planner && !result.planner.error) {
551
+ lines.push("");
552
+ lines.push("Self-repair");
553
+ if (result.planner.decision) {
554
+ lines.push(` Cause: ${result.planner.decision.cause} (${result.planner.decision.tier} tier)`);
555
+ lines.push(` Why: ${result.planner.decision.reason}`);
556
+ if (result.planner.outcome) {
557
+ lines.push(` Status: ${result.planner.outcome.status}`);
558
+ if (result.planner.outcome.statusMessage) lines.push(` ${result.planner.outcome.statusMessage}`);
559
+ } else {
560
+ lines.push(` Status: planned (mode: ${result.mode}); not executed`);
561
+ }
562
+ } else {
563
+ lines.push(" Nothing to repair right now.");
564
+ }
565
+ (result.planner.skipped || []).forEach((item) => {
566
+ lines.push(` Held back: ${item.cause} (${item.reason})`);
567
+ });
568
+ }
422
569
  if (result.sync) {
423
570
  lines.push("");
424
571
  lines.push("Sync");
@@ -448,10 +595,14 @@ module.exports = function createAgent(deps) {
448
595
 
449
596
  const intervalMs = Math.max(5, Number(options.interval || 15)) * 1000;
450
597
  const syncIntervalMs = Math.max(30, Number(options.syncInterval || 60)) * 1000;
598
+ const detectIntervalMs = Math.max(60, Number(options.detectInterval || 300)) * 1000;
599
+ const plannerIntervalMs = Math.max(60, Number(options.plannerInterval || 600)) * 1000;
451
600
  let running = true;
452
601
  let sleepResolve = null;
453
602
  let firstRun = true;
454
603
  let lastSyncAt = 0;
604
+ let lastDetectAt = 0;
605
+ let lastPlanAt = 0;
455
606
 
456
607
  if (options.open) {
457
608
  const config = loadConfig();
@@ -479,9 +630,14 @@ module.exports = function createAgent(deps) {
479
630
  const now = Date.now();
480
631
  const shouldSync = options.noSync !== true && (lastSyncAt === 0 || now - lastSyncAt >= syncIntervalMs);
481
632
  if (shouldSync) lastSyncAt = now;
633
+ const shouldDetect = options.autoDetect !== false && (firstRun || now - lastDetectAt >= detectIntervalMs);
634
+ if (shouldDetect) lastDetectAt = now;
635
+ const shouldPlan = options.planRepairs !== false && (firstRun || now - lastPlanAt >= plannerIntervalMs);
636
+ if (shouldPlan) lastPlanAt = now;
482
637
  const runOptions = {
483
638
  ...options,
484
- autoDetect: firstRun && options.autoDetect !== false,
639
+ autoDetect: shouldDetect,
640
+ planRepairs: shouldPlan,
485
641
  syncTelemetry: shouldSync,
486
642
  };
487
643
  firstRun = false;
@@ -506,6 +662,7 @@ module.exports = function createAgent(deps) {
506
662
  parseCommand,
507
663
  renderAgentTerminal,
508
664
  reportAutoDetect,
665
+ reportProgress,
509
666
  runAgent,
510
667
  runAgentOnce,
511
668
  runAutoDetect,
@@ -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,
@@ -345,7 +350,7 @@ function createCli(deps) {
345
350
  dryRun: rest.includes("--dry-run"),
346
351
  });
347
352
  result.next = result.connector?.started
348
- ? [`${NPX_COMMAND} status`, "Refresh Prismo Workspace"]
353
+ ? [`${NPX_COMMAND} status`]
349
354
  : [`${NPX_COMMAND} connector install`, `${NPX_COMMAND} status`];
350
355
  }
351
356
  if (result.connected && (rest.includes("--open") || !rest.includes("--no-open"))) {
@@ -424,6 +429,8 @@ function createCli(deps) {
424
429
  const json = rest.includes("--json");
425
430
  const intervalIndex = rest.indexOf("--interval");
426
431
  const syncIntervalIndex = rest.indexOf("--sync-interval");
432
+ const detectIntervalIndex = rest.indexOf("--detect-interval");
433
+ const plannerIntervalIndex = rest.indexOf("--planner-interval");
427
434
  const limitIndex = rest.indexOf("--limit");
428
435
  const budgetIndex = rest.indexOf("--budget");
429
436
  const modeIndex = rest.indexOf("--mode");
@@ -431,7 +438,7 @@ function createCli(deps) {
431
438
  if (!AGENT_VALID_MODES.has(modeValue)) {
432
439
  throw new Error(`Invalid agent mode: ${modeValue}. Valid modes: observe, suggest, autopilot`);
433
440
  }
434
- const positional = getPositionals(rest, new Set(["--interval", "--sync-interval", "--limit", "--budget", "--mode"]));
441
+ const positional = getPositionals(rest, new Set(["--interval", "--sync-interval", "--detect-interval", "--planner-interval", "--limit", "--budget", "--mode"]));
435
442
  const target = positional[0] || process.cwd();
436
443
  const agentOptions = {
437
444
  json,
@@ -439,9 +446,12 @@ function createCli(deps) {
439
446
  watch: rest.includes("--watch") && !rest.includes("--once"),
440
447
  open: rest.includes("--open"),
441
448
  autoDetect: !rest.includes("--no-detect"),
449
+ planRepairs: !rest.includes("--no-planner"),
442
450
  noSync: rest.includes("--no-sync"),
443
451
  interval: parsePositiveInt(intervalIndex >= 0 ? rest[intervalIndex + 1] : null, 15),
444
452
  syncInterval: parsePositiveInt(syncIntervalIndex >= 0 ? rest[syncIntervalIndex + 1] : null, 60),
453
+ detectInterval: parsePositiveInt(detectIntervalIndex >= 0 ? rest[detectIntervalIndex + 1] : null, 300),
454
+ plannerInterval: parsePositiveInt(plannerIntervalIndex >= 0 ? rest[plannerIntervalIndex + 1] : null, 600),
445
455
  limit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 5),
446
456
  syncLimit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 20),
447
457
  tokenBudget: parseTokenBudget(budgetIndex >= 0 ? rest[budgetIndex + 1] : null) || 600000,
@@ -756,6 +766,39 @@ function createCli(deps) {
756
766
  return;
757
767
  }
758
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"]));
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
+ if (cause === "auto") {
781
+ const result = await runPlannerOnce(target, {
782
+ execute: !ownArgs.includes("--dry-run"),
783
+ limit: parsePositiveInt(limitIndex >= 0 ? ownArgs[limitIndex + 1] : null, 5),
784
+ tokenBudget: parseTokenBudget(budgetIndex >= 0 ? ownArgs[budgetIndex + 1] : null),
785
+ });
786
+ if (json) console.log(JSON.stringify(result, null, 2));
787
+ else console.log(renderPlannerTerminal(result));
788
+ return;
789
+ }
790
+ const result = await runRepair(target, cause, {
791
+ limit: parsePositiveInt(limitIndex >= 0 ? ownArgs[limitIndex + 1] : null, 5),
792
+ tokenBudget: parseTokenBudget(budgetIndex >= 0 ? ownArgs[budgetIndex + 1] : null),
793
+ scope: scopeIndex >= 0 ? ownArgs[scopeIndex + 1] : null,
794
+ commandArgs,
795
+ });
796
+ if (json) console.log(JSON.stringify(result, null, 2));
797
+ else console.log(renderRepairTerminal(result));
798
+ if (result.status === "failed") process.exitCode = 1;
799
+ return;
800
+ }
801
+
759
802
  if (command === "usage" || command === "watch") {
760
803
  const json = rest.includes("--json");
761
804
  const knownTools = new Set(["codex", "claude", "cursor", "all"]);
@@ -405,20 +405,32 @@ module.exports = function createCloudSync(deps) {
405
405
  function renderConnectTerminal(result) {
406
406
  const lines = [];
407
407
  lines.push("");
408
- lines.push("PrismoDev Connect");
409
- lines.push("");
410
- lines.push(`Status: ${result.connected ? "connected" : "token needed"}`);
411
- lines.push(`Config: ${result.configPath}`);
412
- lines.push(`API: ${result.apiUrl}`);
413
- lines.push(`Device: ${result.device.name}`);
414
- if (result.connector) {
415
- lines.push(`Connector: ${result.connector.started ? "started" : result.connector.installed ? "installed" : "not started"}`);
416
- if (result.connector.reason) lines.push(`Connector note: ${result.connector.reason}`);
417
- if (result.connector.error) lines.push(`Connector error: ${result.connector.error}`);
408
+ if (result.connected && result.connector?.started) {
409
+ lines.push("Prismo agent is running.");
410
+ lines.push("");
411
+ lines.push(`Device: ${result.device.name}`);
412
+ lines.push(`Mode: ${result.connector.mode || "autopilot"}`);
413
+ lines.push(`Poll: every ${result.connector.interval || 15}s`);
414
+ lines.push(`Sync: every ${result.connector.syncInterval || 60}s`);
415
+ lines.push("");
416
+ lines.push("Your agent will continuously scan, repair, and guard this repo.");
417
+ lines.push("Open the dashboard to see what it's doing.");
418
+ } else {
419
+ lines.push("PrismoDev Connect");
420
+ lines.push("");
421
+ lines.push(`Status: ${result.connected ? "connected" : "token needed"}`);
422
+ lines.push(`Config: ${result.configPath}`);
423
+ lines.push(`API: ${result.apiUrl}`);
424
+ lines.push(`Device: ${result.device.name}`);
425
+ if (result.connector) {
426
+ lines.push(`Connector: ${result.connector.started ? "started" : result.connector.installed ? "installed" : "not started"}`);
427
+ if (result.connector.reason) lines.push(`Note: ${result.connector.reason}`);
428
+ if (result.connector.error) lines.push(`Error: ${result.connector.error}`);
429
+ }
430
+ lines.push("");
431
+ lines.push("Next");
432
+ result.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
418
433
  }
419
- lines.push("");
420
- lines.push("Next");
421
- result.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
422
434
  return lines.join("\n");
423
435
  }
424
436
 
@@ -484,6 +496,7 @@ module.exports = function createCloudSync(deps) {
484
496
  return {
485
497
  buildSyncPayload,
486
498
  configPath,
499
+ estimateWaste,
487
500
  loadConfig,
488
501
  renderConnectTerminal,
489
502
  renderDisconnectTerminal,
@@ -80,9 +80,10 @@ module.exports = function createConnector(deps) {
80
80
  const root = path.resolve(rootDir || process.cwd());
81
81
  const interval = Math.max(5, Number(options.interval || 15));
82
82
  const syncInterval = Math.max(30, Number(options.syncInterval || 60));
83
+ const detectInterval = Math.max(60, Number(options.detectInterval || 300));
83
84
  const mode = options.mode || "autopilot";
84
85
  fs.mkdirSync(connectorDir(), { recursive: true });
85
- const command = `${BACKGROUND_COMMAND} agent --watch --interval ${interval} --sync-interval ${syncInterval} --mode ${shellEscape(mode)} ${shellEscape(root)}`;
86
+ const command = `${BACKGROUND_COMMAND} agent --watch --interval ${interval} --sync-interval ${syncInterval} --detect-interval ${detectInterval} --mode ${shellEscape(mode)} ${shellEscape(root)}`;
86
87
  const contents = [
87
88
  "#!/bin/sh",
88
89
  "set -eu",
@@ -97,6 +98,7 @@ module.exports = function createConnector(deps) {
97
98
  root,
98
99
  interval,
99
100
  syncInterval,
101
+ detectInterval,
100
102
  mode,
101
103
  command,
102
104
  platform: process.platform,
@@ -104,7 +106,7 @@ module.exports = function createConnector(deps) {
104
106
  logPath: logPath(),
105
107
  errorLogPath: errorLogPath(),
106
108
  });
107
- return { root, interval, syncInterval, mode, command };
109
+ return { root, interval, syncInterval, detectInterval, mode, command };
108
110
  }
109
111
 
110
112
  function writePlist() {
@@ -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] [--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,36 @@ 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] [--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
+ Repairs only write .prismo/ files and append ignore rules with backups; they never overwrite CLAUDE.md, AGENTS.md, .gitignore, or source code.`,
291
323
  watch: `Prismo Watch
292
324
 
293
325
  Usage:
@@ -492,7 +524,7 @@ Output:
492
524
  agent: `PrismoDev Agent
493
525
 
494
526
  Usage:
495
- prismo agent [--json] [--once] [--watch] [--open] [--no-detect] [--no-sync] [--interval N] [--sync-interval N] [--limit N] [--mode MODE] [path]
527
+ 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
528
 
497
529
  Modes:
498
530
  observe Watch and report actions without executing. No changes made.
@@ -511,6 +543,8 @@ Examples:
511
543
  Options:
512
544
  --open Open the Prismo workspace in the browser on start.
513
545
  --no-detect Skip the initial auto-detect scan on first poll.
546
+ --no-planner Disable the self-repair planner (cause scoring, cooldowns, tier escalation).
547
+ --planner-interval N Seconds between self-repair planner runs in watch mode (default 600).
514
548
  --no-sync Keep watch mode from continuously syncing aggregate telemetry.
515
549
 
516
550
  Output:
@@ -520,6 +554,10 @@ Output:
520
554
  Sends heartbeat to Prismo Cloud on each poll and syncs aggregate telemetry on a controlled interval so the dashboard stays fresh.
521
555
  Handles SIGINT/SIGTERM gracefully and marks the agent offline before exiting.
522
556
  Supported actions are doctor, sync, guard, context/optimize, and shield with a conservative command allowlist.
557
+ Actions queued with a targetCause run cause-specific repair executors instead of the generic command; see prismo repair --help.
558
+ In autopilot the self-repair planner also runs on its own interval: it scores waste causes from local sessions,
559
+ repairs the top cause above threshold, judges the result against later sessions, and escalates or backs off accordingly.
560
+ In observe/suggest modes the planner only reports what it would repair.
523
561
  Agent does not upload prompts, source code, file contents, stdout, stderr, or full command logs.`,
524
562
  ci: `Prismo CI
525
563