@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
|
@@ -6,6 +6,29 @@ export interface TransactionOptions {
|
|
|
6
6
|
useImmediate?: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
const transactionDepthByDatabase = new WeakMap<Database, number>();
|
|
10
|
+
|
|
11
|
+
function getTransactionDepth(db: Database): number {
|
|
12
|
+
return transactionDepthByDatabase.get(db) ?? 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function enterTransaction(db: Database): void {
|
|
16
|
+
const currentDepth = getTransactionDepth(db);
|
|
17
|
+
if (currentDepth > 0) {
|
|
18
|
+
throw new Error("Nested transactions are not supported for this database instance");
|
|
19
|
+
}
|
|
20
|
+
transactionDepthByDatabase.set(db, currentDepth + 1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function exitTransaction(db: Database): void {
|
|
24
|
+
const currentDepth = getTransactionDepth(db);
|
|
25
|
+
if (currentDepth <= 1) {
|
|
26
|
+
transactionDepthByDatabase.delete(db);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
transactionDepthByDatabase.set(db, currentDepth - 1);
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
export function withTransaction<T>(db: Database, fn: () => T, options: TransactionOptions = {}): T {
|
|
10
33
|
const maxRetries = options.maxRetries ?? 5;
|
|
11
34
|
const backoffMs = options.backoffMs ?? 100;
|
|
@@ -14,20 +37,25 @@ export function withTransaction<T>(db: Database, fn: () => T, options: Transacti
|
|
|
14
37
|
let attempts = 0;
|
|
15
38
|
while (true) {
|
|
16
39
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
enterTransaction(db);
|
|
41
|
+
try {
|
|
42
|
+
if (useImmediate) {
|
|
43
|
+
db.run("BEGIN IMMEDIATE");
|
|
44
|
+
try {
|
|
45
|
+
const result = fn();
|
|
46
|
+
db.run("COMMIT");
|
|
47
|
+
return result;
|
|
48
|
+
} catch (innerError) {
|
|
49
|
+
db.run("ROLLBACK");
|
|
50
|
+
throw innerError;
|
|
51
|
+
}
|
|
26
52
|
}
|
|
27
|
-
}
|
|
28
53
|
|
|
29
|
-
|
|
30
|
-
|
|
54
|
+
const transaction = db.transaction(fn);
|
|
55
|
+
return transaction();
|
|
56
|
+
} finally {
|
|
57
|
+
exitTransaction(db);
|
|
58
|
+
}
|
|
31
59
|
} catch (error: unknown) {
|
|
32
60
|
const e = error as Error;
|
|
33
61
|
const isBusyError =
|
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
import type { ForensicEventDomain, ForensicEventType } from "../observability/forensic-types";
|
|
6
6
|
import type { LogEntry, LogSink } from "./types";
|
|
7
7
|
|
|
8
|
+
function normalizeTaskId(taskId: unknown): number | null {
|
|
9
|
+
return typeof taskId === "number" && Number.isInteger(taskId) && taskId > 0 ? taskId : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
export function createForensicSinkForArtifactDir(artifactDir: string): LogSink {
|
|
9
13
|
return {
|
|
10
14
|
write(entry: LogEntry): void {
|
|
@@ -63,7 +67,7 @@ export function createForensicSinkForArtifactDir(artifactDir: string): LogSink {
|
|
|
63
67
|
parentSessionId: (parentSessionId as string) ?? null,
|
|
64
68
|
phase: (phase as string) ?? null,
|
|
65
69
|
dispatchId: (dispatchId as string) ?? null,
|
|
66
|
-
taskId: (taskId
|
|
70
|
+
taskId: normalizeTaskId(taskId),
|
|
67
71
|
agent: (agent as string) ?? null,
|
|
68
72
|
type: forensicType,
|
|
69
73
|
code: (code as string) ?? null,
|
|
@@ -136,7 +140,7 @@ export function createForensicSink(projectRoot: string): LogSink {
|
|
|
136
140
|
parentSessionId: (parentSessionId as string) ?? null,
|
|
137
141
|
phase: (phase as string) ?? null,
|
|
138
142
|
dispatchId: (dispatchId as string) ?? null,
|
|
139
|
-
taskId: (taskId
|
|
143
|
+
taskId: normalizeTaskId(taskId),
|
|
140
144
|
agent: (agent as string) ?? null,
|
|
141
145
|
type: forensicType,
|
|
142
146
|
code: (code as string) ?? null,
|
package/src/logging/index.ts
CHANGED
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { McpLifecycleManager } from "./manager";
|
|
2
|
+
export type { McpToolAction, ScopeViolation } from "./scope-filter";
|
|
3
|
+
export { filterByScope, isActionAllowed } from "./scope-filter";
|
|
4
|
+
export type {
|
|
5
|
+
ManagedMcpServer,
|
|
6
|
+
McpHealthResult,
|
|
7
|
+
McpScope,
|
|
8
|
+
McpServerState,
|
|
9
|
+
McpTransport,
|
|
10
|
+
SkillMcpConfig,
|
|
11
|
+
} from "./types";
|
|
12
|
+
export {
|
|
13
|
+
mcpScopeSchema,
|
|
14
|
+
mcpTransportSchema,
|
|
15
|
+
skillMcpConfigSchema,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
let globalMcpManager: InstanceType<typeof import("./manager").McpLifecycleManager> | null = null;
|
|
19
|
+
|
|
20
|
+
export function setGlobalMcpManager(
|
|
21
|
+
manager: InstanceType<typeof import("./manager").McpLifecycleManager>,
|
|
22
|
+
): void {
|
|
23
|
+
globalMcpManager = manager;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getGlobalMcpManager(): InstanceType<
|
|
27
|
+
typeof import("./manager").McpLifecycleManager
|
|
28
|
+
> | null {
|
|
29
|
+
return globalMcpManager;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resetGlobalMcpManager(): void {
|
|
33
|
+
globalMcpManager = null;
|
|
34
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { ManagedMcpServer, McpHealthResult, McpServerState, SkillMcpConfig } from "./types";
|
|
2
|
+
import { skillMcpConfigSchema } from "./types";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_HEALTH_CHECK_TIMEOUT_MS = 5000;
|
|
5
|
+
|
|
6
|
+
const VALID_STATE_TRANSITIONS = Object.freeze({
|
|
7
|
+
stopped: Object.freeze(["starting"] as const),
|
|
8
|
+
starting: Object.freeze(["healthy", "unhealthy", "stopping"] as const),
|
|
9
|
+
healthy: Object.freeze(["healthy", "unhealthy", "stopping"] as const),
|
|
10
|
+
unhealthy: Object.freeze(["starting", "healthy", "unhealthy", "stopping"] as const),
|
|
11
|
+
stopping: Object.freeze(["stopped"] as const),
|
|
12
|
+
}) satisfies Readonly<Record<McpServerState, readonly McpServerState[]>>;
|
|
13
|
+
|
|
14
|
+
interface InternalManagedMcpServer {
|
|
15
|
+
config: SkillMcpConfig;
|
|
16
|
+
skillName: string;
|
|
17
|
+
state: McpServerState;
|
|
18
|
+
startedAt: string | null;
|
|
19
|
+
lastHealthCheck: string | null;
|
|
20
|
+
connectionCount: number;
|
|
21
|
+
referencingSkills: Set<string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cloneServerSnapshot(server: InternalManagedMcpServer): ManagedMcpServer {
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
config: {
|
|
27
|
+
...server.config,
|
|
28
|
+
args: [...server.config.args],
|
|
29
|
+
env: { ...server.config.env },
|
|
30
|
+
scope: [...server.config.scope],
|
|
31
|
+
},
|
|
32
|
+
skillName: server.skillName,
|
|
33
|
+
state: server.state,
|
|
34
|
+
startedAt: server.startedAt,
|
|
35
|
+
lastHealthCheck: server.lastHealthCheck,
|
|
36
|
+
connectionCount: server.connectionCount,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function cloneHealthResult(result: McpHealthResult): McpHealthResult {
|
|
41
|
+
return Object.freeze({ ...result });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getValidationErrorMessage(config: SkillMcpConfig): string | null {
|
|
45
|
+
if (config.transport === "stdio" && !config.command) {
|
|
46
|
+
return "stdio transport requires a command";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if ((config.transport === "sse" || config.transport === "http") && !config.url) {
|
|
50
|
+
return `${config.transport} transport requires a url`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createInternalServer(skillName: string, config: SkillMcpConfig): InternalManagedMcpServer {
|
|
57
|
+
return {
|
|
58
|
+
config,
|
|
59
|
+
skillName,
|
|
60
|
+
state: "stopped",
|
|
61
|
+
startedAt: null,
|
|
62
|
+
lastHealthCheck: null,
|
|
63
|
+
connectionCount: 0,
|
|
64
|
+
referencingSkills: new Set<string>(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class McpLifecycleManager {
|
|
69
|
+
private readonly healthCheckTimeoutMs: number;
|
|
70
|
+
|
|
71
|
+
private readonly servers = new Map<string, InternalManagedMcpServer>();
|
|
72
|
+
|
|
73
|
+
public constructor(options?: { healthCheckTimeoutMs?: number }) {
|
|
74
|
+
this.healthCheckTimeoutMs = options?.healthCheckTimeoutMs ?? DEFAULT_HEALTH_CHECK_TIMEOUT_MS;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async startServer(skillName: string, config: SkillMcpConfig): Promise<ManagedMcpServer> {
|
|
78
|
+
const parsedConfig = skillMcpConfigSchema.parse({
|
|
79
|
+
...config,
|
|
80
|
+
healthCheckTimeoutMs: config.healthCheckTimeoutMs ?? this.healthCheckTimeoutMs,
|
|
81
|
+
});
|
|
82
|
+
const existingServer = this.servers.get(parsedConfig.serverName);
|
|
83
|
+
|
|
84
|
+
if (existingServer) {
|
|
85
|
+
this.ensureCompatibleConfig(existingServer.config, parsedConfig);
|
|
86
|
+
existingServer.referencingSkills.add(skillName);
|
|
87
|
+
existingServer.connectionCount = existingServer.referencingSkills.size;
|
|
88
|
+
|
|
89
|
+
if (existingServer.state === "unhealthy") {
|
|
90
|
+
this.transitionState(existingServer, "starting");
|
|
91
|
+
await this.healthCheck(parsedConfig.serverName);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return cloneServerSnapshot(existingServer);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const server = createInternalServer(skillName, parsedConfig);
|
|
98
|
+
server.referencingSkills.add(skillName);
|
|
99
|
+
server.connectionCount = 1;
|
|
100
|
+
server.startedAt = new Date().toISOString();
|
|
101
|
+
this.transitionState(server, "starting");
|
|
102
|
+
this.servers.set(parsedConfig.serverName, server);
|
|
103
|
+
|
|
104
|
+
await this.healthCheck(parsedConfig.serverName);
|
|
105
|
+
return cloneServerSnapshot(server);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public async stopServer(serverName: string): Promise<void> {
|
|
109
|
+
const server = this.servers.get(serverName);
|
|
110
|
+
if (!server) return;
|
|
111
|
+
|
|
112
|
+
if (server.connectionCount > 1) {
|
|
113
|
+
const [firstSkillName] = server.referencingSkills;
|
|
114
|
+
if (firstSkillName) {
|
|
115
|
+
server.referencingSkills.delete(firstSkillName);
|
|
116
|
+
}
|
|
117
|
+
server.connectionCount = server.referencingSkills.size;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.transitionState(server, "stopping");
|
|
122
|
+
server.connectionCount = 0;
|
|
123
|
+
server.referencingSkills.clear();
|
|
124
|
+
server.startedAt = null;
|
|
125
|
+
this.transitionState(server, "stopped");
|
|
126
|
+
this.servers.delete(serverName);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public async stopAll(): Promise<void> {
|
|
130
|
+
await Promise.all([...this.servers.keys()].map((serverName) => this.stopServer(serverName)));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public getServer(serverName: string): ManagedMcpServer | undefined {
|
|
134
|
+
const server = this.servers.get(serverName);
|
|
135
|
+
return server ? cloneServerSnapshot(server) : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public listServers(): readonly ManagedMcpServer[] {
|
|
139
|
+
return Object.freeze(
|
|
140
|
+
[...this.servers.values()]
|
|
141
|
+
.sort((left, right) => left.config.serverName.localeCompare(right.config.serverName))
|
|
142
|
+
.map((server) => cloneServerSnapshot(server)),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public async healthCheck(serverName: string): Promise<McpHealthResult> {
|
|
147
|
+
const server = this.servers.get(serverName);
|
|
148
|
+
if (!server) {
|
|
149
|
+
return cloneHealthResult({
|
|
150
|
+
serverName,
|
|
151
|
+
skillName: "",
|
|
152
|
+
state: "stopped",
|
|
153
|
+
latencyMs: null,
|
|
154
|
+
error: "Server not found",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const startedAt = Date.now();
|
|
159
|
+
const validationMessage = getValidationErrorMessage(server.config);
|
|
160
|
+
const latencyMs = Math.min(Date.now() - startedAt, server.config.healthCheckTimeoutMs);
|
|
161
|
+
server.lastHealthCheck = new Date().toISOString();
|
|
162
|
+
|
|
163
|
+
if (validationMessage !== null) {
|
|
164
|
+
this.transitionState(server, "unhealthy");
|
|
165
|
+
return cloneHealthResult({
|
|
166
|
+
serverName: server.config.serverName,
|
|
167
|
+
skillName: server.skillName,
|
|
168
|
+
state: server.state,
|
|
169
|
+
latencyMs,
|
|
170
|
+
error: validationMessage,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.transitionState(server, "healthy");
|
|
175
|
+
return cloneHealthResult({
|
|
176
|
+
serverName: server.config.serverName,
|
|
177
|
+
skillName: server.skillName,
|
|
178
|
+
state: server.state,
|
|
179
|
+
latencyMs,
|
|
180
|
+
error: null,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public async healthCheckAll(): Promise<readonly McpHealthResult[]> {
|
|
185
|
+
const healthResults = await Promise.all(
|
|
186
|
+
[...this.servers.keys()].map((serverName) => this.healthCheck(serverName)),
|
|
187
|
+
);
|
|
188
|
+
return Object.freeze(healthResults);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private ensureCompatibleConfig(current: SkillMcpConfig, next: SkillMcpConfig): void {
|
|
192
|
+
const currentConfig = JSON.stringify(current);
|
|
193
|
+
const nextConfig = JSON.stringify(next);
|
|
194
|
+
if (currentConfig !== nextConfig) {
|
|
195
|
+
throw new Error(`Conflicting MCP configuration for server '${current.serverName}'`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private transitionState(server: InternalManagedMcpServer, nextState: McpServerState): void {
|
|
200
|
+
const allowedTransitions = VALID_STATE_TRANSITIONS[server.state] as readonly McpServerState[];
|
|
201
|
+
if (!allowedTransitions.includes(nextState)) {
|
|
202
|
+
throw new Error(`Invalid MCP state transition: ${server.state} -> ${nextState}`);
|
|
203
|
+
}
|
|
204
|
+
server.state = nextState;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { McpScope } from "./types";
|
|
2
|
+
|
|
3
|
+
export type McpToolAction = "read" | "write" | "execute";
|
|
4
|
+
|
|
5
|
+
export interface ScopeViolation {
|
|
6
|
+
readonly serverName: string;
|
|
7
|
+
readonly skillName: string;
|
|
8
|
+
readonly requestedAction: McpToolAction;
|
|
9
|
+
readonly allowedScopes: readonly McpScope[];
|
|
10
|
+
readonly toolName: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isActionAllowed(
|
|
14
|
+
action: McpToolAction,
|
|
15
|
+
allowedScopes: readonly McpScope[],
|
|
16
|
+
): boolean {
|
|
17
|
+
return allowedScopes.includes(action);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function filterByScope(
|
|
21
|
+
toolName: string,
|
|
22
|
+
action: McpToolAction,
|
|
23
|
+
serverName: string,
|
|
24
|
+
skillName: string,
|
|
25
|
+
allowedScopes: readonly McpScope[],
|
|
26
|
+
): { allowed: boolean; violation: ScopeViolation | null } {
|
|
27
|
+
if (isActionAllowed(action, allowedScopes)) {
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
allowed: true,
|
|
30
|
+
violation: null,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
allowed: false,
|
|
36
|
+
violation: Object.freeze({
|
|
37
|
+
serverName,
|
|
38
|
+
skillName,
|
|
39
|
+
requestedAction: action,
|
|
40
|
+
allowedScopes: Object.freeze([...allowedScopes]),
|
|
41
|
+
toolName,
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
}
|
package/src/mcp/types.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const mcpTransportSchema = z.enum(["stdio", "sse", "http"]);
|
|
4
|
+
export type McpTransport = z.infer<typeof mcpTransportSchema>;
|
|
5
|
+
|
|
6
|
+
export const mcpScopeSchema = z.enum(["read", "write", "execute"]);
|
|
7
|
+
export type McpScope = z.infer<typeof mcpScopeSchema>;
|
|
8
|
+
|
|
9
|
+
export const skillMcpConfigSchema = z.object({
|
|
10
|
+
serverName: z.string().min(1),
|
|
11
|
+
transport: mcpTransportSchema.default("stdio"),
|
|
12
|
+
command: z.string().min(1).optional(),
|
|
13
|
+
args: z.array(z.string()).default([]),
|
|
14
|
+
url: z.string().url().optional(),
|
|
15
|
+
env: z.record(z.string(), z.string()).default({}),
|
|
16
|
+
scope: z.array(mcpScopeSchema).default(["read"]),
|
|
17
|
+
healthCheckTimeoutMs: z.number().int().positive().default(5000),
|
|
18
|
+
});
|
|
19
|
+
export type SkillMcpConfig = z.infer<typeof skillMcpConfigSchema>;
|
|
20
|
+
|
|
21
|
+
export type McpServerState = "stopped" | "starting" | "healthy" | "unhealthy" | "stopping";
|
|
22
|
+
|
|
23
|
+
export interface ManagedMcpServer {
|
|
24
|
+
readonly config: SkillMcpConfig;
|
|
25
|
+
readonly skillName: string;
|
|
26
|
+
readonly state: McpServerState;
|
|
27
|
+
readonly startedAt: string | null;
|
|
28
|
+
readonly lastHealthCheck: string | null;
|
|
29
|
+
readonly connectionCount: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface McpHealthResult {
|
|
33
|
+
readonly serverName: string;
|
|
34
|
+
readonly skillName: string;
|
|
35
|
+
readonly state: McpServerState;
|
|
36
|
+
readonly latencyMs: number | null;
|
|
37
|
+
readonly error: string | null;
|
|
38
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { getLogger } from "../logging/domains";
|
|
2
3
|
import { getObservationsByProject, getProjectByPath } from "../memory/repository";
|
|
3
4
|
import { summarizeConfidence } from "./confidence";
|
|
4
5
|
import type { ConfidenceEntry } from "./types";
|
|
@@ -16,6 +17,7 @@ const LEVEL_ORDER: Readonly<Record<ConfidenceLevel, number>> = Object.freeze({
|
|
|
16
17
|
MEDIUM: 2,
|
|
17
18
|
LOW: 1,
|
|
18
19
|
});
|
|
20
|
+
const logger = getLogger("orchestrator", "arena");
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Determines arena debate depth based on aggregate confidence.
|
|
@@ -66,7 +68,11 @@ export function getMemoryTunedDepth(
|
|
|
66
68
|
return Math.min(baseDepth + 1, 3);
|
|
67
69
|
}
|
|
68
70
|
} catch (err) {
|
|
69
|
-
|
|
71
|
+
logger.warn("memory-tuned depth failed, using base depth", {
|
|
72
|
+
operation: "memory_tuned_depth",
|
|
73
|
+
projectPath,
|
|
74
|
+
error: err instanceof Error ? err.message : String(err),
|
|
75
|
+
});
|
|
70
76
|
}
|
|
71
77
|
return baseDepth;
|
|
72
78
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { getLogger } from "../../logging/domains";
|
|
1
2
|
import type { FallbackConfig } from "./fallback-config";
|
|
2
3
|
import type { FallbackManager } from "./fallback-manager";
|
|
3
4
|
import { replayWithDegradation } from "./message-replay";
|
|
4
5
|
import type { MessagePart } from "./types";
|
|
5
6
|
|
|
7
|
+
const logger = getLogger("orchestrator", "fallback-event-handler");
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* SDK operations interface for dependency injection.
|
|
8
11
|
* Enables testing without the OpenCode runtime.
|
|
@@ -272,8 +275,16 @@ async function handleFallbackError(
|
|
|
272
275
|
|
|
273
276
|
// Release lock after dispatch (or skip if model unparseable)
|
|
274
277
|
manager.releaseRetryLock(sessionID);
|
|
275
|
-
} catch {
|
|
278
|
+
} catch (error: unknown) {
|
|
276
279
|
// On failure, release the lock to allow future retries
|
|
280
|
+
logger.warn("fallback replay failed", {
|
|
281
|
+
operation: "fallback",
|
|
282
|
+
sessionId: sessionID,
|
|
283
|
+
failedModel: plan.failedModel,
|
|
284
|
+
nextModel: plan.newModel,
|
|
285
|
+
reason: plan.reason,
|
|
286
|
+
error: error instanceof Error ? error.message : String(error),
|
|
287
|
+
});
|
|
277
288
|
manager.releaseRetryLock(sessionID);
|
|
278
289
|
}
|
|
279
290
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { getLogger } from "../../logging/domains";
|
|
1
2
|
import { sanitizeTemplateContent } from "../../review/sanitize";
|
|
2
3
|
import { fileExists } from "../../utils/fs-helpers";
|
|
3
4
|
import { ensurePhaseDir, getArtifactRef } from "../artifacts";
|
|
4
5
|
import type { PipelineState } from "../types";
|
|
5
6
|
import { AGENT_NAMES, type DispatchResult, type PhaseHandlerContext } from "./types";
|
|
6
7
|
|
|
8
|
+
const logger = getLogger("orchestrator", "challenge");
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* CHALLENGE phase handler — dispatches oc-challenger with RECON artifact references.
|
|
9
12
|
* References files by path (not content injection) per D-11.
|
|
@@ -18,7 +21,11 @@ export async function handleChallenge(
|
|
|
18
21
|
// Warn if artifact wasn't written (best-effort — still complete the phase)
|
|
19
22
|
const artifactPath = getArtifactRef(artifactDir, "CHALLENGE", "brief.md");
|
|
20
23
|
if (!(await fileExists(artifactPath))) {
|
|
21
|
-
|
|
24
|
+
logger.warn("CHALLENGE completed but artifact not found", {
|
|
25
|
+
operation: "phase_transition",
|
|
26
|
+
phase: "CHALLENGE",
|
|
27
|
+
artifactPath,
|
|
28
|
+
});
|
|
22
29
|
}
|
|
23
30
|
return Object.freeze({
|
|
24
31
|
action: "complete" as const,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { getLogger } from "../../logging/domains";
|
|
2
3
|
import { isEnoentError } from "../../utils/fs-helpers";
|
|
3
4
|
import { getArtifactRef } from "../artifacts";
|
|
4
5
|
import { normalizePlanTasks, planTasksArtifactSchema } from "../contracts/phase-artifacts";
|
|
@@ -12,6 +13,7 @@ import { AGENT_NAMES } from "./types";
|
|
|
12
13
|
const EXPECTED_COLUMN_COUNT = 6;
|
|
13
14
|
const taskIdPattern = /^W(\d+)-T(\d+)$/i;
|
|
14
15
|
const separatorCellPattern = /^:?-{3,}:?$/;
|
|
16
|
+
const logger = getLogger("orchestrator", "plan");
|
|
15
17
|
|
|
16
18
|
function parseTableColumns(line: string): readonly string[] | null {
|
|
17
19
|
const trimmed = line.trim();
|
|
@@ -146,7 +148,12 @@ export const handlePlan: PhaseHandler = async (_state, artifactDir, result?) =>
|
|
|
146
148
|
if (usedLegacyMarkdown) {
|
|
147
149
|
const msg =
|
|
148
150
|
"PLAN fallback: parsed legacy tasks.md (tasks.json missing). Migrate planner output to tasks.json.";
|
|
149
|
-
|
|
151
|
+
logger.warn(msg, {
|
|
152
|
+
operation: "phase_transition",
|
|
153
|
+
phase: "PLAN",
|
|
154
|
+
tasksJsonPath,
|
|
155
|
+
tasksPath,
|
|
156
|
+
});
|
|
150
157
|
logOrchestrationEvent(artifactDir, {
|
|
151
158
|
timestamp: new Date().toISOString(),
|
|
152
159
|
phase: "PLAN",
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { getLogger } from "../../logging/domains";
|
|
1
2
|
import { sanitizeTemplateContent } from "../../review/sanitize";
|
|
2
3
|
import { fileExists } from "../../utils/fs-helpers";
|
|
3
4
|
import { ensurePhaseDir, getArtifactRef } from "../artifacts";
|
|
4
5
|
import type { PipelineState } from "../types";
|
|
5
6
|
import { AGENT_NAMES, type DispatchResult, type PhaseHandlerContext } from "./types";
|
|
6
7
|
|
|
8
|
+
const logger = getLogger("orchestrator", "recon");
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* RECON phase handler — dispatches oc-researcher with idea and artifact path.
|
|
9
12
|
* Uses file references (not content injection) per D-11.
|
|
@@ -18,7 +21,11 @@ export async function handleRecon(
|
|
|
18
21
|
// Warn if artifact wasn't written (best-effort — still complete the phase)
|
|
19
22
|
const artifactPath = getArtifactRef(artifactDir, "RECON", "report.md");
|
|
20
23
|
if (!(await fileExists(artifactPath))) {
|
|
21
|
-
|
|
24
|
+
logger.warn("RECON completed but artifact not found", {
|
|
25
|
+
operation: "phase_transition",
|
|
26
|
+
phase: "RECON",
|
|
27
|
+
artifactPath,
|
|
28
|
+
});
|
|
22
29
|
}
|
|
23
30
|
return Object.freeze({
|
|
24
31
|
action: "complete" as const,
|
|
@@ -6,12 +6,12 @@ export const AGENT_NAMES = Object.freeze({
|
|
|
6
6
|
CHALLENGE: "oc-challenger",
|
|
7
7
|
ARCHITECT: "oc-architect",
|
|
8
8
|
CRITIC: "oc-critic",
|
|
9
|
-
EXPLORE: "oc-
|
|
9
|
+
EXPLORE: "oc-researcher",
|
|
10
10
|
PLAN: "oc-planner",
|
|
11
11
|
BUILD: "oc-implementer",
|
|
12
12
|
REVIEW: "oc-reviewer",
|
|
13
13
|
SHIP: "oc-shipper",
|
|
14
|
-
RETROSPECTIVE: "oc-
|
|
14
|
+
RETROSPECTIVE: "oc-shipper",
|
|
15
15
|
} as const);
|
|
16
16
|
|
|
17
17
|
export interface DispatchResult {
|
|
@@ -14,6 +14,7 @@ import { randomBytes } from "node:crypto";
|
|
|
14
14
|
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { loadLessonMemoryFromKernel, saveLessonMemoryToKernel } from "../kernel/repository";
|
|
17
|
+
import { getLogger } from "../logging/domains";
|
|
17
18
|
import { ensureDir, isEnoentError } from "../utils/fs-helpers";
|
|
18
19
|
import { getProjectArtifactDir } from "../utils/paths";
|
|
19
20
|
import { lessonMemorySchema } from "./lesson-schemas";
|
|
@@ -25,6 +26,7 @@ const LESSON_FILE = "lesson-memory.json";
|
|
|
25
26
|
const MAX_LESSONS = 50;
|
|
26
27
|
const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
27
28
|
let legacyLessonMemoryMirrorWarned = false;
|
|
29
|
+
const logger = getLogger("orchestrator", "lesson-memory");
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Create a valid empty lesson memory object.
|
|
@@ -97,7 +99,10 @@ export async function saveLessonMemory(memory: LessonMemory, projectRoot: string
|
|
|
97
99
|
} catch (error: unknown) {
|
|
98
100
|
if (!legacyLessonMemoryMirrorWarned) {
|
|
99
101
|
legacyLessonMemoryMirrorWarned = true;
|
|
100
|
-
|
|
102
|
+
logger.warn("lesson-memory.json mirror write failed", {
|
|
103
|
+
operation: "legacy_lesson_memory_mirror",
|
|
104
|
+
error: error instanceof Error ? error.message : String(error),
|
|
105
|
+
});
|
|
101
106
|
}
|
|
102
107
|
}
|
|
103
108
|
}
|
|
@@ -2,6 +2,8 @@ import { getLogger } from "../logging/domains";
|
|
|
2
2
|
import { createForensicSinkForArtifactDir } from "../logging/forensic-writer";
|
|
3
3
|
import type { LogLevel } from "../logging/types";
|
|
4
4
|
|
|
5
|
+
const logger = getLogger("orchestrator", "orchestration-logger");
|
|
6
|
+
|
|
5
7
|
export interface OrchestrationEvent {
|
|
6
8
|
readonly timestamp: string;
|
|
7
9
|
readonly phase: string;
|
|
@@ -36,10 +38,12 @@ export function logOrchestrationEvent(artifactDir: string, event: OrchestrationE
|
|
|
36
38
|
|
|
37
39
|
const metadata = {
|
|
38
40
|
domain,
|
|
41
|
+
subsystem: event.phase.toLowerCase(),
|
|
39
42
|
operation,
|
|
40
43
|
runId: event.runId ?? null,
|
|
41
44
|
sessionId: event.sessionId ?? null,
|
|
42
45
|
phase: event.phase,
|
|
46
|
+
timestamp: event.timestamp,
|
|
43
47
|
dispatchId: event.dispatchId ?? null,
|
|
44
48
|
taskId: event.taskId ?? null,
|
|
45
49
|
agent: event.agent ?? null,
|
|
@@ -61,9 +65,17 @@ export function logOrchestrationEvent(artifactDir: string, event: OrchestrationE
|
|
|
61
65
|
|
|
62
66
|
const globalLogger = getLogger(domain);
|
|
63
67
|
if (event.action === "error") {
|
|
64
|
-
globalLogger.error(event.message ?? event.action,
|
|
68
|
+
globalLogger.error(event.message ?? event.action, metadata);
|
|
65
69
|
} else {
|
|
66
|
-
globalLogger.info(event.message ?? event.action,
|
|
70
|
+
globalLogger.info(event.message ?? event.action, metadata);
|
|
67
71
|
}
|
|
68
|
-
} catch {
|
|
72
|
+
} catch (error: unknown) {
|
|
73
|
+
logger.warn("failed to log orchestration event", {
|
|
74
|
+
operation: "error",
|
|
75
|
+
error: error instanceof Error ? error.message : String(error),
|
|
76
|
+
phase: event.phase,
|
|
77
|
+
action: event.action,
|
|
78
|
+
timestamp: event.timestamp,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
69
81
|
}
|