getprismo 0.1.38 → 0.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/prismo-dev/agent.js +163 -6
- package/lib/prismo-dev/cli.js +46 -3
- package/lib/prismo-dev/cloud-sync.js +26 -13
- package/lib/prismo-dev/connector.js +4 -2
- package/lib/prismo-dev/help.js +39 -1
- package/lib/prismo-dev/repair-executors.js +543 -0
- package/lib/prismo-dev/repair-planner.js +307 -0
- package/lib/prismo-dev-scan.js +44 -0
- package/package.json +1 -1
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) {
|
|
@@ -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
|
|
192
|
-
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:
|
|
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
|
|
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:
|
|
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,
|
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,
|
|
@@ -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
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
lines.push(
|
|
416
|
-
|
|
417
|
-
|
|
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() {
|
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] [--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
|
|