@kodrunhq/opencode-autopilot 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -13
- package/assets/commands/oc-update-docs.md +1 -1
- package/package.json +1 -1
- package/src/agents/index.ts +0 -12
- package/src/agents/pipeline/index.ts +0 -4
- package/src/autonomy/completion.ts +52 -0
- package/src/autonomy/controller.ts +144 -0
- package/src/autonomy/index.ts +25 -0
- package/src/autonomy/injector.ts +49 -0
- package/src/autonomy/state.ts +91 -0
- package/src/autonomy/types.ts +30 -0
- package/src/autonomy/verification.ts +86 -0
- package/src/background/database.ts +170 -0
- package/src/background/executor.ts +174 -0
- package/src/background/index.ts +8 -0
- package/src/background/manager.ts +232 -0
- package/src/background/repository.ts +174 -0
- package/src/background/schema.ts +24 -0
- package/src/background/sdk-runner.ts +40 -0
- package/src/background/slot-manager.ts +41 -0
- package/src/background/state-machine.ts +19 -0
- package/src/context/budget.ts +45 -0
- package/src/context/compaction-handler.ts +58 -0
- package/src/context/discovery.ts +94 -0
- package/src/context/index.ts +14 -0
- package/src/context/injector.ts +119 -0
- package/src/context/types.ts +24 -0
- package/src/health/checks.ts +145 -2
- package/src/health/index.ts +7 -1
- package/src/health/runner.ts +6 -0
- package/src/index.ts +113 -6
- package/src/installer.ts +13 -0
- package/src/kernel/index.ts +6 -0
- package/src/kernel/migrations.ts +50 -0
- package/src/kernel/retry.ts +49 -0
- package/src/kernel/schema.ts +9 -1
- package/src/kernel/transaction.ts +40 -12
- package/src/logging/forensic-writer.ts +6 -2
- package/src/logging/index.ts +2 -0
- package/src/mcp/index.ts +34 -0
- package/src/mcp/manager.ts +206 -0
- package/src/mcp/scope-filter.ts +44 -0
- package/src/mcp/types.ts +38 -0
- package/src/orchestrator/arena.ts +7 -1
- package/src/orchestrator/fallback/event-handler.ts +12 -1
- package/src/orchestrator/handlers/challenge.ts +8 -1
- package/src/orchestrator/handlers/plan.ts +8 -1
- package/src/orchestrator/handlers/recon.ts +8 -1
- package/src/orchestrator/handlers/types.ts +2 -2
- package/src/orchestrator/lesson-memory.ts +6 -1
- package/src/orchestrator/orchestration-logger.ts +15 -3
- package/src/orchestrator/skill-injection.ts +7 -1
- package/src/orchestrator/state.ts +6 -1
- package/src/recovery/classifier.ts +127 -0
- package/src/recovery/event-handler.ts +263 -0
- package/src/recovery/index.ts +20 -0
- package/src/recovery/orchestrator.ts +180 -0
- package/src/recovery/persistence.ts +87 -0
- package/src/recovery/strategies.ts +107 -0
- package/src/recovery/types.ts +31 -0
- package/src/registry/model-groups.ts +2 -19
- package/src/registry/resolver.ts +38 -9
- package/src/review/agent-catalog.ts +83 -251
- package/src/review/agents/architecture-verifier.ts +41 -0
- package/src/review/agents/code-hygiene-auditor.ts +40 -0
- package/src/review/agents/correctness-auditor.ts +41 -0
- package/src/review/agents/frontend-auditor.ts +39 -0
- package/src/review/agents/index.ts +15 -42
- package/src/review/agents/language-idioms-auditor.ts +39 -0
- package/src/review/agents/security-auditor.ts +12 -8
- package/src/review/stack-gate.ts +2 -6
- package/src/routing/categories.ts +111 -0
- package/src/routing/classifier.ts +152 -0
- package/src/routing/engine.ts +89 -0
- package/src/routing/index.ts +4 -0
- package/src/routing/types.ts +14 -0
- package/src/skills/adaptive-injector.ts +34 -3
- package/src/skills/loader.ts +4 -0
- package/src/tools/background.ts +196 -0
- package/src/tools/delegate.ts +205 -0
- package/src/tools/loop.ts +94 -0
- package/src/tools/recover.ts +172 -0
- package/src/types/recovery.ts +10 -0
- package/src/ux/context-warnings.ts +81 -0
- package/src/ux/error-hints.ts +38 -0
- package/src/ux/index.ts +7 -0
- package/src/ux/notifications.ts +67 -0
- package/src/ux/progress.ts +77 -0
- package/src/ux/session-summary.ts +67 -0
- package/src/ux/task-status.ts +109 -0
- package/src/ux/types.ts +24 -0
- package/src/agents/db-specialist.ts +0 -295
- package/src/agents/devops.ts +0 -352
- package/src/agents/documenter.ts +0 -44
- package/src/agents/frontend-engineer.ts +0 -541
- package/src/agents/pipeline/oc-explorer.ts +0 -46
- package/src/agents/pipeline/oc-retrospector.ts +0 -42
- package/src/review/agents/auth-flow-verifier.ts +0 -47
- package/src/review/agents/concurrency-checker.ts +0 -47
- package/src/review/agents/dead-code-scanner.ts +0 -47
- package/src/review/agents/go-idioms-auditor.ts +0 -46
- package/src/review/agents/python-django-auditor.ts +0 -46
- package/src/review/agents/react-patterns-auditor.ts +0 -46
- package/src/review/agents/rust-safety-auditor.ts +0 -46
- package/src/review/agents/scope-intent-verifier.ts +0 -45
- package/src/review/agents/silent-failure-hunter.ts +0 -45
- package/src/review/agents/spec-checker.ts +0 -45
- package/src/review/agents/state-mgmt-auditor.ts +0 -46
- package/src/review/agents/type-soundness.ts +0 -46
- package/src/review/agents/wiring-inspector.ts +0 -46
package/src/health/checks.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import { access, readFile, stat } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
4
|
import type { Config } from "@opencode-ai/plugin";
|
|
5
5
|
import { parse } from "yaml";
|
|
6
6
|
import { loadConfig } from "../config";
|
|
7
|
+
import { getGlobalMcpManager } from "../mcp";
|
|
7
8
|
import { AGENT_NAMES } from "../orchestrator/handlers/types";
|
|
9
|
+
import { ALL_GROUP_IDS } from "../registry/model-groups";
|
|
10
|
+
import { getAllCategories } from "../routing";
|
|
8
11
|
import { detectProjectStackTags, filterSkillsByStack } from "../skills/adaptive-injector";
|
|
9
12
|
import { loadAllSkills } from "../skills/loader";
|
|
10
13
|
import {
|
|
@@ -15,6 +18,8 @@ import {
|
|
|
15
18
|
} from "../utils/paths";
|
|
16
19
|
import type { HealthResult } from "./types";
|
|
17
20
|
|
|
21
|
+
const VALID_CATEGORY_NAMES = new Set(getAllCategories().map((definition) => definition.category));
|
|
22
|
+
|
|
18
23
|
/**
|
|
19
24
|
* Check that the plugin config file exists and passes Zod validation.
|
|
20
25
|
* loadConfig returns null when the file is missing, and throws on invalid JSON/schema.
|
|
@@ -213,9 +218,13 @@ export async function configV7FieldsCheck(configPath?: string): Promise<HealthRe
|
|
|
213
218
|
const STANDARD_AGENT_NAMES: readonly string[] = Object.freeze([
|
|
214
219
|
"researcher",
|
|
215
220
|
"metaprompter",
|
|
216
|
-
"documenter",
|
|
217
221
|
"pr-reviewer",
|
|
218
222
|
"autopilot",
|
|
223
|
+
"coder",
|
|
224
|
+
"debugger",
|
|
225
|
+
"planner",
|
|
226
|
+
"reviewer",
|
|
227
|
+
"security-auditor",
|
|
219
228
|
]);
|
|
220
229
|
|
|
221
230
|
/** Pipeline agent names, derived from AGENT_NAMES in the orchestrator. */
|
|
@@ -410,6 +419,88 @@ export async function skillHealthCheck(
|
|
|
410
419
|
}
|
|
411
420
|
}
|
|
412
421
|
|
|
422
|
+
export async function mcpHealthCheck(configPath?: string): Promise<HealthResult> {
|
|
423
|
+
try {
|
|
424
|
+
const config = await loadConfig(configPath);
|
|
425
|
+
if (config === null) {
|
|
426
|
+
return Object.freeze({
|
|
427
|
+
name: "mcp-health",
|
|
428
|
+
status: "fail" as const,
|
|
429
|
+
message: "Plugin config file not found",
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!config.mcp.enabled) {
|
|
434
|
+
return Object.freeze({
|
|
435
|
+
name: "mcp-health",
|
|
436
|
+
status: "pass" as const,
|
|
437
|
+
message: "MCP disabled in config",
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const manager = getGlobalMcpManager();
|
|
442
|
+
if (manager) {
|
|
443
|
+
const healthResults = await manager.healthCheckAll();
|
|
444
|
+
if (healthResults.length === 0) {
|
|
445
|
+
return Object.freeze({
|
|
446
|
+
name: "mcp-health",
|
|
447
|
+
status: "pass" as const,
|
|
448
|
+
message: "MCP enabled, no servers running",
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const unhealthy = healthResults.filter((result) => result.state !== "healthy");
|
|
453
|
+
const details = Object.freeze(
|
|
454
|
+
healthResults.map((result) => {
|
|
455
|
+
const status = result.state === "healthy" ? "ok" : result.state;
|
|
456
|
+
const errorSuffix = result.error ? ` - ${result.error}` : "";
|
|
457
|
+
return `${result.serverName} (${result.skillName}): ${status}${errorSuffix}`;
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
if (unhealthy.length > 0) {
|
|
462
|
+
return Object.freeze({
|
|
463
|
+
name: "mcp-health",
|
|
464
|
+
status: "warn" as const,
|
|
465
|
+
message: `MCP enabled: ${healthResults.length} server(s), ${unhealthy.length} unhealthy`,
|
|
466
|
+
details,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return Object.freeze({
|
|
471
|
+
name: "mcp-health",
|
|
472
|
+
status: "pass" as const,
|
|
473
|
+
message: `MCP enabled: ${healthResults.length} server(s) healthy`,
|
|
474
|
+
details,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const skillsDir = join(configPath ? dirname(configPath) : getGlobalConfigDir(), "skills");
|
|
479
|
+
const skills = await loadAllSkills(skillsDir);
|
|
480
|
+
const mcpSkills = [...skills.values()].filter((skill) => skill.frontmatter.mcp !== null);
|
|
481
|
+
const details = Object.freeze(
|
|
482
|
+
mcpSkills.map((skill) => {
|
|
483
|
+
const mcp = skill.frontmatter.mcp;
|
|
484
|
+
return `${skill.frontmatter.name}: ${mcp?.serverName} (${mcp?.transport})`;
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
return Object.freeze({
|
|
489
|
+
name: "mcp-health",
|
|
490
|
+
status: "pass" as const,
|
|
491
|
+
message: `MCP enabled with ${mcpSkills.length} MCP-capable skill${mcpSkills.length === 1 ? "" : "s"} (manager not initialized)`,
|
|
492
|
+
details,
|
|
493
|
+
});
|
|
494
|
+
} catch (error: unknown) {
|
|
495
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
496
|
+
return Object.freeze({
|
|
497
|
+
name: "mcp-health",
|
|
498
|
+
status: "fail" as const,
|
|
499
|
+
message: `MCP health check failed: ${msg}`,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
413
504
|
/**
|
|
414
505
|
* Check memory DB health: existence, readability, observation count.
|
|
415
506
|
* Does NOT call getMemoryDb() to avoid creating an empty DB as a side effect.
|
|
@@ -573,3 +664,55 @@ export async function commandHealthCheck(targetDir?: string): Promise<HealthResu
|
|
|
573
664
|
details: Object.freeze(issues),
|
|
574
665
|
});
|
|
575
666
|
}
|
|
667
|
+
|
|
668
|
+
export async function routingHealthCheck(configPath?: string): Promise<HealthResult> {
|
|
669
|
+
try {
|
|
670
|
+
const invalidDefinitions = getAllCategories().flatMap((definition) => {
|
|
671
|
+
const issues: string[] = [];
|
|
672
|
+
if (!VALID_CATEGORY_NAMES.has(definition.category)) {
|
|
673
|
+
issues.push(`${definition.category}: unknown category`);
|
|
674
|
+
}
|
|
675
|
+
if (!ALL_GROUP_IDS.includes(definition.modelGroup as (typeof ALL_GROUP_IDS)[number])) {
|
|
676
|
+
issues.push(`${definition.category}: invalid model group '${definition.modelGroup}'`);
|
|
677
|
+
}
|
|
678
|
+
if (definition.maxIterations < 1) {
|
|
679
|
+
issues.push(`${definition.category}: maxIterations must be >= 1`);
|
|
680
|
+
}
|
|
681
|
+
if (definition.timeoutSeconds < 1) {
|
|
682
|
+
issues.push(`${definition.category}: timeoutSeconds must be >= 1`);
|
|
683
|
+
}
|
|
684
|
+
return issues;
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
const config = await loadConfig(configPath);
|
|
688
|
+
const invalidOverrides = Object.keys(config?.routing.categories ?? {}).flatMap(
|
|
689
|
+
(categoryName) =>
|
|
690
|
+
VALID_CATEGORY_NAMES.has(categoryName as import("../types/routing").Category)
|
|
691
|
+
? []
|
|
692
|
+
: [`Invalid routing override category '${categoryName}'`],
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
const issues = [...invalidDefinitions, ...invalidOverrides];
|
|
696
|
+
if (issues.length > 0) {
|
|
697
|
+
return Object.freeze({
|
|
698
|
+
name: "routing-health",
|
|
699
|
+
status: "fail" as const,
|
|
700
|
+
message: `${issues.length} routing issue(s) found`,
|
|
701
|
+
details: Object.freeze(issues),
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return Object.freeze({
|
|
706
|
+
name: "routing-health",
|
|
707
|
+
status: "pass" as const,
|
|
708
|
+
message: `All ${getAllCategories().length} routing categories and overrides are valid`,
|
|
709
|
+
});
|
|
710
|
+
} catch (error: unknown) {
|
|
711
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
712
|
+
return Object.freeze({
|
|
713
|
+
name: "routing-health",
|
|
714
|
+
status: "fail" as const,
|
|
715
|
+
message: `Routing health check failed: ${msg}`,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
package/src/health/index.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
agentHealthCheck,
|
|
3
|
+
assetHealthCheck,
|
|
4
|
+
configHealthCheck,
|
|
5
|
+
mcpHealthCheck,
|
|
6
|
+
routingHealthCheck,
|
|
7
|
+
} from "./checks";
|
|
2
8
|
export { runHealthChecks } from "./runner";
|
|
3
9
|
export type { HealthReport, HealthResult } from "./types";
|
package/src/health/runner.ts
CHANGED
|
@@ -5,8 +5,10 @@ import {
|
|
|
5
5
|
commandHealthCheck,
|
|
6
6
|
configHealthCheck,
|
|
7
7
|
configV7FieldsCheck,
|
|
8
|
+
mcpHealthCheck,
|
|
8
9
|
memoryHealthCheck,
|
|
9
10
|
nativeAgentSuppressionHealthCheck,
|
|
11
|
+
routingHealthCheck,
|
|
10
12
|
skillHealthCheck,
|
|
11
13
|
} from "./checks";
|
|
12
14
|
import type { HealthReport, HealthResult } from "./types";
|
|
@@ -54,6 +56,8 @@ export async function runHealthChecks(options?: {
|
|
|
54
56
|
memoryHealthCheck(options?.targetDir),
|
|
55
57
|
commandHealthCheck(options?.targetDir),
|
|
56
58
|
configV7FieldsCheck(options?.configPath),
|
|
59
|
+
routingHealthCheck(options?.configPath),
|
|
60
|
+
mcpHealthCheck(options?.configPath),
|
|
57
61
|
]);
|
|
58
62
|
|
|
59
63
|
const allSettled = [...configOutcome, ...settled];
|
|
@@ -67,6 +71,8 @@ export async function runHealthChecks(options?: {
|
|
|
67
71
|
"memory-db",
|
|
68
72
|
"command-accessibility",
|
|
69
73
|
"config-v7-fields",
|
|
74
|
+
"routing-health",
|
|
75
|
+
"mcp-health",
|
|
70
76
|
];
|
|
71
77
|
const results: readonly HealthResult[] = Object.freeze(
|
|
72
78
|
allSettled.map((outcome, i) => settledToResult(outcome, fallbackNames[i])),
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { Config, Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
import { configHook } from "./agents";
|
|
3
|
+
import { getLoopController } from "./autonomy";
|
|
4
|
+
import { createLoopInjector } from "./autonomy/injector";
|
|
3
5
|
import { isFirstLoad, loadConfig } from "./config";
|
|
6
|
+
import { createCompactionHandler, createContextInjector } from "./context";
|
|
4
7
|
import { runHealthChecks } from "./health/runner";
|
|
5
8
|
import { createAntiSlopHandler } from "./hooks/anti-slop";
|
|
6
9
|
import { installAssets } from "./installer";
|
|
10
|
+
import { openKernelDb } from "./kernel/database";
|
|
7
11
|
import { getLogger, initLoggers } from "./logging/domains";
|
|
12
|
+
import { McpLifecycleManager, setGlobalMcpManager } from "./mcp";
|
|
8
13
|
import {
|
|
9
14
|
createMemoryCaptureHandler,
|
|
10
15
|
createMemoryChatMessageHandler,
|
|
@@ -30,6 +35,12 @@ import {
|
|
|
30
35
|
} from "./orchestrator/fallback";
|
|
31
36
|
import { fallbackDefaults } from "./orchestrator/fallback/fallback-config";
|
|
32
37
|
import { resolveChain } from "./orchestrator/fallback/resolve-chain";
|
|
38
|
+
import {
|
|
39
|
+
createRecoveryEventHandler,
|
|
40
|
+
createRecoveryOrchestratorWithDb,
|
|
41
|
+
getDefaultRecoveryOrchestrator,
|
|
42
|
+
} from "./recovery/index";
|
|
43
|
+
import { ocBackground, setBackgroundSdkOperations } from "./tools/background";
|
|
33
44
|
import { ocConfidence } from "./tools/confidence";
|
|
34
45
|
import {
|
|
35
46
|
ocConfigure,
|
|
@@ -40,10 +51,12 @@ import {
|
|
|
40
51
|
import { ocCreateAgent } from "./tools/create-agent";
|
|
41
52
|
import { ocCreateCommand } from "./tools/create-command";
|
|
42
53
|
import { ocCreateSkill } from "./tools/create-skill";
|
|
54
|
+
import { ocDelegate, setDelegateSdkOperations } from "./tools/delegate";
|
|
43
55
|
import { ocDoctor, setOpenCodeConfig as setDoctorOpenCodeConfig } from "./tools/doctor";
|
|
44
56
|
import { ocForensics } from "./tools/forensics";
|
|
45
57
|
import { ocHashlineEdit } from "./tools/hashline-edit";
|
|
46
58
|
import { ocLogs } from "./tools/logs";
|
|
59
|
+
import { ocLoop } from "./tools/loop";
|
|
47
60
|
import { ocMemoryPreferences } from "./tools/memory-preferences";
|
|
48
61
|
import { ocMemoryStatus } from "./tools/memory-status";
|
|
49
62
|
import { ocMockFallback } from "./tools/mock-fallback";
|
|
@@ -52,12 +65,17 @@ import { ocPhase } from "./tools/phase";
|
|
|
52
65
|
import { ocPipelineReport } from "./tools/pipeline-report";
|
|
53
66
|
import { ocPlan } from "./tools/plan";
|
|
54
67
|
import { ocQuick } from "./tools/quick";
|
|
68
|
+
import { ocRecover } from "./tools/recover";
|
|
55
69
|
import { ocReview } from "./tools/review";
|
|
56
70
|
import { ocSessionStats } from "./tools/session-stats";
|
|
57
71
|
import { ocState } from "./tools/state";
|
|
58
72
|
import { ocStocktake } from "./tools/stocktake";
|
|
59
73
|
import { ocSummary } from "./tools/summary";
|
|
60
74
|
import { ocUpdateDocs } from "./tools/update-docs";
|
|
75
|
+
import { ContextWarningMonitor } from "./ux/context-warnings";
|
|
76
|
+
import { getRemediationHint } from "./ux/error-hints";
|
|
77
|
+
import { NotificationManager } from "./ux/notifications";
|
|
78
|
+
import { ProgressTracker } from "./ux/progress";
|
|
61
79
|
|
|
62
80
|
let openCodeConfig: Config | null = null;
|
|
63
81
|
|
|
@@ -127,6 +145,20 @@ const plugin: Plugin = async (input) => {
|
|
|
127
145
|
});
|
|
128
146
|
});
|
|
129
147
|
|
|
148
|
+
// --- UX notification manager (rate-limited, best-effort) ---
|
|
149
|
+
const notificationManager = new NotificationManager({
|
|
150
|
+
sink: {
|
|
151
|
+
showToast: async (title, message, variant, duration) => {
|
|
152
|
+
await sdkOps.showToast(title, message, variant as "info" | "warning" | "error");
|
|
153
|
+
void duration;
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// --- UX surfaces: context warnings, progress tracking, error hints ---
|
|
159
|
+
const contextWarningMonitor = new ContextWarningMonitor({ notificationManager });
|
|
160
|
+
const _progressTracker = new ProgressTracker({ notificationManager });
|
|
161
|
+
|
|
130
162
|
// --- Fallback subsystem initialization ---
|
|
131
163
|
const sdkOps: SdkOperations = {
|
|
132
164
|
abortSession: async (sessionID) => {
|
|
@@ -160,6 +192,24 @@ const plugin: Plugin = async (input) => {
|
|
|
160
192
|
},
|
|
161
193
|
};
|
|
162
194
|
|
|
195
|
+
// --- Background task SDK wiring (enables real dispatch via promptAsync) ---
|
|
196
|
+
const backgroundSdkOps = {
|
|
197
|
+
promptAsync: async (
|
|
198
|
+
sessionId: string,
|
|
199
|
+
model: string | undefined,
|
|
200
|
+
parts: ReadonlyArray<{ type: "text"; text: string }>,
|
|
201
|
+
) => {
|
|
202
|
+
const modelSpec = model ? { providerID: "", modelID: model } : undefined;
|
|
203
|
+
await sdkOps.promptAsync(
|
|
204
|
+
sessionId,
|
|
205
|
+
modelSpec as { readonly providerID: string; readonly modelID: string },
|
|
206
|
+
parts as readonly import("./orchestrator/fallback").MessagePart[],
|
|
207
|
+
);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
setBackgroundSdkOperations(backgroundSdkOps);
|
|
211
|
+
setDelegateSdkOperations(backgroundSdkOps);
|
|
212
|
+
|
|
163
213
|
const manager = new FallbackManager({
|
|
164
214
|
config: fallbackConfig,
|
|
165
215
|
resolveFallbackChain: (_sessionID, agentName) => {
|
|
@@ -202,6 +252,23 @@ const plugin: Plugin = async (input) => {
|
|
|
202
252
|
});
|
|
203
253
|
const chatMessageHandler = createChatMessageHandler(manager);
|
|
204
254
|
const toolExecuteAfterHandler = createToolExecuteAfterHandler(manager);
|
|
255
|
+
const recoveryEventHandler = (() => {
|
|
256
|
+
try {
|
|
257
|
+
const kernelDb = openKernelDb();
|
|
258
|
+
const orchestrator = createRecoveryOrchestratorWithDb(kernelDb);
|
|
259
|
+
return createRecoveryEventHandler({
|
|
260
|
+
orchestrator,
|
|
261
|
+
db: kernelDb,
|
|
262
|
+
sdk: {
|
|
263
|
+
abortSession: sdkOps.abortSession,
|
|
264
|
+
showToast: (title, message, variant) =>
|
|
265
|
+
sdkOps.showToast(title, message, variant as "info" | "warning" | "error"),
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
} catch {
|
|
269
|
+
return createRecoveryEventHandler(getDefaultRecoveryOrchestrator());
|
|
270
|
+
}
|
|
271
|
+
})();
|
|
205
272
|
|
|
206
273
|
// --- Anti-slop hook initialization ---
|
|
207
274
|
const antiSlopHandler = createAntiSlopHandler({ showToast: sdkOps.showToast });
|
|
@@ -228,6 +295,16 @@ const plugin: Plugin = async (input) => {
|
|
|
228
295
|
getDb: () => getMemoryDb(),
|
|
229
296
|
})
|
|
230
297
|
: null;
|
|
298
|
+
const contextInjector = createContextInjector({
|
|
299
|
+
projectRoot: process.cwd(),
|
|
300
|
+
totalBudget: 4000,
|
|
301
|
+
});
|
|
302
|
+
const compactionHandler = createCompactionHandler(contextInjector);
|
|
303
|
+
const loopInjector = createLoopInjector(getLoopController());
|
|
304
|
+
|
|
305
|
+
// --- MCP lifecycle manager (lazy — servers start when skills with mcp: config activate) ---
|
|
306
|
+
const mcpManager = new McpLifecycleManager();
|
|
307
|
+
setGlobalMcpManager(mcpManager);
|
|
231
308
|
|
|
232
309
|
// --- Observability handlers ---
|
|
233
310
|
const toolStartTimes = new Map<string, number>();
|
|
@@ -312,7 +389,9 @@ const plugin: Plugin = async (input) => {
|
|
|
312
389
|
|
|
313
390
|
return {
|
|
314
391
|
tool: {
|
|
392
|
+
oc_background: ocBackground,
|
|
315
393
|
oc_configure: ocConfigure,
|
|
394
|
+
oc_delegate: ocDelegate,
|
|
316
395
|
oc_create_agent: ocCreateAgent,
|
|
317
396
|
oc_create_skill: ocCreateSkill,
|
|
318
397
|
oc_create_command: ocCreateCommand,
|
|
@@ -323,10 +402,12 @@ const plugin: Plugin = async (input) => {
|
|
|
323
402
|
oc_orchestrate: ocOrchestrate,
|
|
324
403
|
oc_doctor: ocDoctor,
|
|
325
404
|
oc_quick: ocQuick,
|
|
405
|
+
oc_recover: ocRecover,
|
|
326
406
|
oc_forensics: ocForensics,
|
|
327
407
|
oc_hashline_edit: ocHashlineEdit,
|
|
328
408
|
oc_review: ocReview,
|
|
329
409
|
oc_logs: ocLogs,
|
|
410
|
+
oc_loop: ocLoop,
|
|
330
411
|
oc_session_stats: ocSessionStats,
|
|
331
412
|
oc_pipeline_report: ocPipelineReport,
|
|
332
413
|
oc_summary: ocSummary,
|
|
@@ -337,10 +418,8 @@ const plugin: Plugin = async (input) => {
|
|
|
337
418
|
oc_memory_preferences: ocMemoryPreferences,
|
|
338
419
|
},
|
|
339
420
|
event: async ({ event }) => {
|
|
340
|
-
// 1. Observability: collect (pure observer, no side effects on session)
|
|
341
421
|
await observabilityEventHandler({ event });
|
|
342
422
|
|
|
343
|
-
// 2. Memory capture (pure observer, best-effort)
|
|
344
423
|
if (memoryCaptureHandler) {
|
|
345
424
|
try {
|
|
346
425
|
await memoryCaptureHandler({ event });
|
|
@@ -349,19 +428,45 @@ const plugin: Plugin = async (input) => {
|
|
|
349
428
|
}
|
|
350
429
|
}
|
|
351
430
|
|
|
352
|
-
// 3. First-load toast
|
|
353
431
|
if (event.type === "session.created" && isFirstLoad(config)) {
|
|
354
|
-
await
|
|
432
|
+
await notificationManager.info(
|
|
355
433
|
"Welcome to OpenCode Autopilot!",
|
|
356
434
|
"Plugin loaded. Run oc_doctor to verify your setup.",
|
|
357
|
-
"info",
|
|
358
435
|
);
|
|
359
436
|
}
|
|
360
437
|
|
|
361
|
-
|
|
438
|
+
if (event.type === "session.error") {
|
|
439
|
+
const props = event.properties;
|
|
440
|
+
const errorMsg =
|
|
441
|
+
props && typeof props === "object" && "error" in props
|
|
442
|
+
? String((props as Record<string, unknown>).error)
|
|
443
|
+
: "Unknown error";
|
|
444
|
+
const hint = getRemediationHint(errorMsg);
|
|
445
|
+
const displayMsg = hint ? `${errorMsg}\n${hint}` : errorMsg;
|
|
446
|
+
await notificationManager.error("Session Error", displayMsg);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (event.type === "message.updated") {
|
|
450
|
+
const props = (event.properties ?? {}) as Record<string, unknown>;
|
|
451
|
+
const info = props.info as Record<string, unknown> | undefined;
|
|
452
|
+
if (info) {
|
|
453
|
+
const tokens = info.tokens as { input?: number } | undefined;
|
|
454
|
+
if (tokens && typeof tokens.input === "number") {
|
|
455
|
+
contextWarningMonitor.checkUtilization(tokens.input, 200_000);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
362
460
|
if (fallbackConfig.enabled) {
|
|
363
461
|
await fallbackEventHandler({ event });
|
|
364
462
|
}
|
|
463
|
+
|
|
464
|
+
await recoveryEventHandler({ event });
|
|
465
|
+
await compactionHandler({ event });
|
|
466
|
+
|
|
467
|
+
if (event.type === "session.deleted") {
|
|
468
|
+
mcpManager.stopAll().catch(() => {});
|
|
469
|
+
}
|
|
365
470
|
},
|
|
366
471
|
config: async (cfg: Config) => {
|
|
367
472
|
openCodeConfig = cfg;
|
|
@@ -422,6 +527,8 @@ const plugin: Plugin = async (input) => {
|
|
|
422
527
|
if (memoryInjector) {
|
|
423
528
|
await memoryInjector(input, output);
|
|
424
529
|
}
|
|
530
|
+
await contextInjector(input, output);
|
|
531
|
+
await loopInjector(input, output);
|
|
425
532
|
},
|
|
426
533
|
};
|
|
427
534
|
};
|
package/src/installer.ts
CHANGED
|
@@ -9,6 +9,19 @@ import { getAssetsDir, getGlobalConfigDir } from "./utils/paths";
|
|
|
9
9
|
*/
|
|
10
10
|
const DEPRECATED_ASSETS = [
|
|
11
11
|
"agents/placeholder-agent.md",
|
|
12
|
+
"agents/auth-flow-verifier.md",
|
|
13
|
+
"agents/concurrency-checker.md",
|
|
14
|
+
"agents/dead-code-scanner.md",
|
|
15
|
+
"agents/go-idioms-auditor.md",
|
|
16
|
+
"agents/python-django-auditor.md",
|
|
17
|
+
"agents/react-patterns-auditor.md",
|
|
18
|
+
"agents/rust-safety-auditor.md",
|
|
19
|
+
"agents/scope-intent-verifier.md",
|
|
20
|
+
"agents/silent-failure-hunter.md",
|
|
21
|
+
"agents/spec-checker.md",
|
|
22
|
+
"agents/state-mgmt-auditor.md",
|
|
23
|
+
"agents/type-soundness.md",
|
|
24
|
+
"agents/wiring-inspector.md",
|
|
12
25
|
"commands/configure.md",
|
|
13
26
|
"commands/oc-configure.md",
|
|
14
27
|
"commands/brainstorm.md",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { getKernelDbPath, KERNEL_DB_FILE, kernelDbExists, openKernelDb } from "./database";
|
|
2
|
+
export { runKernelMigrations } from "./migrations";
|
|
3
|
+
export { type RetryOptions, withRetry } from "./retry";
|
|
4
|
+
export * from "./schema";
|
|
5
|
+
export * from "./transaction";
|
|
6
|
+
export * from "./types";
|
package/src/kernel/migrations.ts
CHANGED
|
@@ -9,6 +9,13 @@ function columnExists(database: Database, tableName: string, columnName: string)
|
|
|
9
9
|
return columns.some((column) => column.name === columnName);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function tableExists(database: Database, tableName: string): boolean {
|
|
13
|
+
const row = database
|
|
14
|
+
.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
15
|
+
.get(tableName) as { name?: string } | null;
|
|
16
|
+
return row?.name === tableName;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
function backfillProjectAwareColumns(database: Database): void {
|
|
13
20
|
if (!columnExists(database, "pipeline_runs", "project_id")) {
|
|
14
21
|
database.run("ALTER TABLE pipeline_runs ADD COLUMN project_id TEXT");
|
|
@@ -44,6 +51,48 @@ function backfillProjectAwareColumns(database: Database): void {
|
|
|
44
51
|
}
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
function backfillBackgroundTaskColumns(database: Database): void {
|
|
55
|
+
if (!tableExists(database, "background_tasks")) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const columnDefinitions = Object.freeze([
|
|
60
|
+
{ name: "category", ddl: "ALTER TABLE background_tasks ADD COLUMN category TEXT" },
|
|
61
|
+
{ name: "result", ddl: "ALTER TABLE background_tasks ADD COLUMN result TEXT" },
|
|
62
|
+
{ name: "error", ddl: "ALTER TABLE background_tasks ADD COLUMN error TEXT" },
|
|
63
|
+
{ name: "agent", ddl: "ALTER TABLE background_tasks ADD COLUMN agent TEXT" },
|
|
64
|
+
{ name: "model", ddl: "ALTER TABLE background_tasks ADD COLUMN model TEXT" },
|
|
65
|
+
{
|
|
66
|
+
name: "priority",
|
|
67
|
+
ddl: "ALTER TABLE background_tasks ADD COLUMN priority INTEGER NOT NULL DEFAULT 50",
|
|
68
|
+
},
|
|
69
|
+
{ name: "created_at", ddl: "ALTER TABLE background_tasks ADD COLUMN created_at TEXT" },
|
|
70
|
+
{ name: "updated_at", ddl: "ALTER TABLE background_tasks ADD COLUMN updated_at TEXT" },
|
|
71
|
+
{ name: "started_at", ddl: "ALTER TABLE background_tasks ADD COLUMN started_at TEXT" },
|
|
72
|
+
{ name: "completed_at", ddl: "ALTER TABLE background_tasks ADD COLUMN completed_at TEXT" },
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
for (const column of columnDefinitions) {
|
|
76
|
+
if (!columnExists(database, "background_tasks", column.name)) {
|
|
77
|
+
database.run(column.ddl);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const now = new Date().toISOString();
|
|
82
|
+
if (columnExists(database, "background_tasks", "created_at")) {
|
|
83
|
+
database.run(
|
|
84
|
+
"UPDATE background_tasks SET created_at = COALESCE(created_at, ?) WHERE created_at IS NULL",
|
|
85
|
+
[now],
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (columnExists(database, "background_tasks", "updated_at")) {
|
|
89
|
+
database.run(
|
|
90
|
+
"UPDATE background_tasks SET updated_at = COALESCE(updated_at, created_at, ?) WHERE updated_at IS NULL",
|
|
91
|
+
[now],
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
47
96
|
export function runKernelMigrations(database: Database): void {
|
|
48
97
|
const row = database.query("PRAGMA user_version").get() as { user_version?: number } | null;
|
|
49
98
|
const currentVersion = row?.user_version ?? 0;
|
|
@@ -55,6 +104,7 @@ export function runKernelMigrations(database: Database): void {
|
|
|
55
104
|
}
|
|
56
105
|
|
|
57
106
|
backfillProjectAwareColumns(database);
|
|
107
|
+
backfillBackgroundTaskColumns(database);
|
|
58
108
|
|
|
59
109
|
if (currentVersion < KERNEL_SCHEMA_VERSION) {
|
|
60
110
|
database.run(`PRAGMA user_version = ${KERNEL_SCHEMA_VERSION}`);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface RetryOptions {
|
|
2
|
+
readonly maxRetries?: number;
|
|
3
|
+
readonly backoffMs?: number;
|
|
4
|
+
readonly onRetry?: (attempt: number, error: Error) => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
8
|
+
const DEFAULT_BACKOFF_MS = 100;
|
|
9
|
+
|
|
10
|
+
function isBusyError(error: Error): boolean {
|
|
11
|
+
return (
|
|
12
|
+
error.message.includes("database is locked") ||
|
|
13
|
+
error.message.includes("SQLITE_BUSY") ||
|
|
14
|
+
error.message.includes("database table is locked")
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sleep(ms: number): Promise<void> {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
setTimeout(resolve, ms);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function withRetry<T>(
|
|
25
|
+
fn: () => T | Promise<T>,
|
|
26
|
+
options: RetryOptions = {},
|
|
27
|
+
): Promise<T> {
|
|
28
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
29
|
+
const backoffMs = options.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
30
|
+
|
|
31
|
+
let attempt = 0;
|
|
32
|
+
while (true) {
|
|
33
|
+
try {
|
|
34
|
+
return await fn();
|
|
35
|
+
} catch (error: unknown) {
|
|
36
|
+
if (!(error instanceof Error) || !isBusyError(error)) {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (attempt >= maxRetries) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
attempt += 1;
|
|
45
|
+
options.onRetry?.(attempt, error);
|
|
46
|
+
await sleep(backoffMs * 2 ** (attempt - 1));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/kernel/schema.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { BACKGROUND_TASKS_SCHEMA_STATEMENTS } from "../background/schema";
|
|
2
|
+
|
|
3
|
+
export const KERNEL_SCHEMA_VERSION = 3;
|
|
2
4
|
|
|
3
5
|
export const KERNEL_SCHEMA_STATEMENTS: readonly string[] = Object.freeze([
|
|
4
6
|
`CREATE TABLE IF NOT EXISTS pipeline_runs (
|
|
@@ -119,4 +121,10 @@ export const KERNEL_SCHEMA_STATEMENTS: readonly string[] = Object.freeze([
|
|
|
119
121
|
`CREATE INDEX IF NOT EXISTS idx_forensic_events_run ON forensic_events(run_id, timestamp, event_id)`,
|
|
120
122
|
`CREATE INDEX IF NOT EXISTS idx_forensic_events_dispatch ON forensic_events(dispatch_id, timestamp, event_id)`,
|
|
121
123
|
`CREATE INDEX IF NOT EXISTS idx_forensic_events_type ON forensic_events(type, timestamp, event_id)`,
|
|
124
|
+
`CREATE TABLE IF NOT EXISTS recovery_state (
|
|
125
|
+
session_id TEXT PRIMARY KEY,
|
|
126
|
+
state_json TEXT NOT NULL,
|
|
127
|
+
updated_at TEXT NOT NULL
|
|
128
|
+
)`,
|
|
129
|
+
...BACKGROUND_TASKS_SCHEMA_STATEMENTS,
|
|
122
130
|
]);
|