oxe-cc 1.0.0 → 1.2.1
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/.cursor/commands/oxe-ask.md +1 -1
- package/.cursor/commands/oxe-capabilities.md +1 -1
- package/.cursor/commands/oxe-checkpoint.md +1 -1
- package/.cursor/commands/oxe-compact.md +1 -1
- package/.cursor/commands/oxe-dashboard.md +1 -1
- package/.cursor/commands/oxe-debug.md +1 -1
- package/.cursor/commands/oxe-discuss.md +1 -1
- package/.cursor/commands/oxe-execute.md +2 -2
- package/.cursor/commands/oxe-forensics.md +1 -1
- package/.cursor/commands/oxe-help.md +1 -1
- package/.cursor/commands/oxe-loop.md +1 -1
- package/.cursor/commands/oxe-milestone.md +1 -1
- package/.cursor/commands/oxe-next.md +1 -1
- package/.cursor/commands/oxe-obs.md +1 -1
- package/.cursor/commands/oxe-plan-agent.md +1 -1
- package/.cursor/commands/oxe-plan.md +1 -1
- package/.cursor/commands/oxe-project.md +1 -1
- package/.cursor/commands/oxe-quick.md +1 -1
- package/.cursor/commands/oxe-research.md +1 -1
- package/.cursor/commands/oxe-retro.md +1 -1
- package/.cursor/commands/oxe-review-pr.md +1 -1
- package/.cursor/commands/oxe-route.md +1 -1
- package/.cursor/commands/oxe-scan.md +1 -1
- package/.cursor/commands/oxe-security.md +1 -1
- package/.cursor/commands/oxe-session.md +2 -2
- package/.cursor/commands/oxe-ship.md +45 -0
- package/.cursor/commands/oxe-skill.md +1 -1
- package/.cursor/commands/oxe-spec.md +1 -1
- package/.cursor/commands/oxe-ui-review.md +1 -1
- package/.cursor/commands/oxe-ui-spec.md +1 -1
- package/.cursor/commands/oxe-update.md +1 -1
- package/.cursor/commands/oxe-validate-gaps.md +1 -1
- package/.cursor/commands/oxe-verify.md +1 -1
- package/.cursor/commands/oxe-workstream.md +1 -1
- package/.cursor/commands/oxe.md +4 -4
- package/.github/copilot-instructions.md +91 -1
- package/.github/prompts/oxe-ask.prompt.md +1 -1
- package/.github/prompts/oxe-capabilities.prompt.md +1 -1
- package/.github/prompts/oxe-checkpoint.prompt.md +1 -1
- package/.github/prompts/oxe-compact.prompt.md +1 -1
- package/.github/prompts/oxe-dashboard.prompt.md +1 -1
- package/.github/prompts/oxe-debug.prompt.md +1 -1
- package/.github/prompts/oxe-discuss.prompt.md +1 -1
- package/.github/prompts/oxe-execute.prompt.md +2 -2
- package/.github/prompts/oxe-forensics.prompt.md +1 -1
- package/.github/prompts/oxe-help.prompt.md +1 -1
- package/.github/prompts/oxe-loop.prompt.md +1 -1
- package/.github/prompts/oxe-milestone.prompt.md +1 -1
- package/.github/prompts/oxe-next.prompt.md +1 -1
- package/.github/prompts/oxe-obs.prompt.md +1 -1
- package/.github/prompts/oxe-plan-agent.prompt.md +1 -1
- package/.github/prompts/oxe-plan.prompt.md +1 -1
- package/.github/prompts/oxe-project.prompt.md +1 -1
- package/.github/prompts/oxe-quick.prompt.md +1 -1
- package/.github/prompts/oxe-research.prompt.md +1 -1
- package/.github/prompts/oxe-retro.prompt.md +1 -1
- package/.github/prompts/oxe-review-pr.prompt.md +1 -1
- package/.github/prompts/oxe-route.prompt.md +1 -1
- package/.github/prompts/oxe-scan.prompt.md +1 -1
- package/.github/prompts/oxe-security.prompt.md +1 -1
- package/.github/prompts/oxe-session.prompt.md +2 -2
- package/.github/prompts/oxe-ship.prompt.md +45 -0
- package/.github/prompts/oxe-skill.prompt.md +1 -1
- package/.github/prompts/oxe-spec.prompt.md +1 -1
- package/.github/prompts/oxe-ui-review.prompt.md +1 -1
- package/.github/prompts/oxe-ui-spec.prompt.md +1 -1
- package/.github/prompts/oxe-update.prompt.md +1 -1
- package/.github/prompts/oxe-validate-gaps.prompt.md +1 -1
- package/.github/prompts/oxe-verify.prompt.md +1 -1
- package/.github/prompts/oxe-workstream.prompt.md +1 -1
- package/.github/prompts/oxe.prompt.md +3 -3
- package/AGENTS.md +43 -28
- package/CHANGELOG.md +158 -0
- package/README.md +72 -50
- package/bin/banner.txt +1 -1
- package/bin/lib/oxe-project-health.cjs +1 -1
- package/commands/oxe/ask.md +5 -1
- package/commands/oxe/checkpoint.md +1 -1
- package/commands/oxe/compact.md +1 -1
- package/commands/oxe/debug.md +1 -1
- package/commands/oxe/execute.md +2 -2
- package/commands/oxe/forensics.md +1 -1
- package/commands/oxe/loop.md +1 -1
- package/commands/oxe/milestone.md +1 -1
- package/commands/oxe/next.md +1 -1
- package/commands/oxe/obs.md +1 -1
- package/commands/oxe/oxe.md +3 -3
- package/commands/oxe/project.md +1 -1
- package/commands/oxe/research.md +1 -1
- package/commands/oxe/retro.md +1 -1
- package/commands/oxe/review-pr.md +1 -1
- package/commands/oxe/route.md +1 -1
- package/commands/oxe/scan.md +1 -1
- package/commands/oxe/security.md +1 -1
- package/commands/oxe/session.md +2 -2
- package/commands/oxe/ship.md +49 -0
- package/commands/oxe/spec.md +2 -2
- package/commands/oxe/ui-review.md +1 -1
- package/commands/oxe/ui-spec.md +1 -1
- package/commands/oxe/validate-gaps.md +1 -1
- package/commands/oxe/verify.md +2 -2
- package/commands/oxe/workstream.md +1 -1
- package/lib/runtime/audit/audit-trail.d.ts +71 -0
- package/lib/runtime/audit/audit-trail.js +154 -0
- package/lib/runtime/audit/index.d.ts +2 -0
- package/lib/runtime/audit/index.js +18 -0
- package/lib/runtime/audit/policy-pack.d.ts +15 -0
- package/lib/runtime/audit/policy-pack.js +57 -0
- package/lib/runtime/context/context-pack-builder.d.ts +15 -0
- package/lib/runtime/context/context-pack-builder.js +42 -0
- package/lib/runtime/context/context-pack-store.d.ts +38 -0
- package/lib/runtime/context/context-pack-store.js +142 -0
- package/lib/runtime/context/context-profiles.d.ts +11 -0
- package/lib/runtime/context/context-profiles.js +51 -0
- package/lib/runtime/context/index.d.ts +2 -0
- package/lib/runtime/context/index.js +2 -0
- package/lib/runtime/decision/decision-engine.d.ts +43 -0
- package/lib/runtime/decision/decision-engine.js +127 -0
- package/lib/runtime/decision/decision-memo.d.ts +53 -0
- package/lib/runtime/decision/decision-memo.js +173 -0
- package/lib/runtime/decision/index.d.ts +2 -0
- package/lib/runtime/decision/index.js +18 -0
- package/lib/runtime/delivery/index.d.ts +1 -0
- package/lib/runtime/delivery/index.js +1 -0
- package/lib/runtime/delivery/promotion-pipeline.d.ts +39 -0
- package/lib/runtime/delivery/promotion-pipeline.js +127 -0
- package/lib/runtime/index.d.ts +3 -0
- package/lib/runtime/index.js +4 -0
- package/lib/runtime/plugins/capability-matrix.d.ts +20 -0
- package/lib/runtime/plugins/capability-matrix.js +59 -0
- package/lib/runtime/plugins/index.d.ts +2 -0
- package/lib/runtime/plugins/index.js +2 -0
- package/lib/runtime/plugins/plugin-manifest.d.ts +22 -0
- package/lib/runtime/plugins/plugin-manifest.js +91 -0
- package/lib/runtime/plugins/plugin-registry.js +5 -0
- package/lib/runtime/policy/policy-engine.d.ts +28 -1
- package/lib/runtime/policy/policy-engine.js +96 -5
- package/lib/runtime/reducers/run-state-reducer.d.ts +26 -0
- package/lib/runtime/reducers/run-state-reducer.js +117 -1
- package/lib/runtime/scheduler/agent-registry.d.ts +44 -0
- package/lib/runtime/scheduler/agent-registry.js +96 -0
- package/lib/runtime/scheduler/agent-roles.d.ts +54 -0
- package/lib/runtime/scheduler/agent-roles.js +62 -0
- package/lib/runtime/scheduler/index.d.ts +3 -0
- package/lib/runtime/scheduler/index.js +3 -0
- package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +2 -0
- package/lib/runtime/scheduler/multi-agent-coordinator.js +91 -4
- package/lib/runtime/scheduler/run-journal.d.ts +18 -0
- package/lib/runtime/scheduler/run-journal.js +54 -0
- package/lib/runtime/scheduler/scheduler.d.ts +11 -1
- package/lib/runtime/scheduler/scheduler.js +135 -7
- package/lib/runtime/verification/index.d.ts +1 -0
- package/lib/runtime/verification/index.js +1 -0
- package/lib/runtime/verification/verification-manifest.d.ts +58 -0
- package/lib/runtime/verification/verification-manifest.js +129 -0
- package/oxe/workflows/ask.md +4 -0
- package/oxe/workflows/checkpoint.md +14 -10
- package/oxe/workflows/debug.md +19 -15
- package/oxe/workflows/execute.md +30 -2
- package/oxe/workflows/forensics.md +13 -9
- package/oxe/workflows/help.md +97 -49
- package/oxe/workflows/loop.md +17 -13
- package/oxe/workflows/obs.md +4 -0
- package/oxe/workflows/oxe.md +64 -31
- package/oxe/workflows/project.md +6 -1
- package/oxe/workflows/references/workflow-runtime-contracts.json +23 -0
- package/oxe/workflows/research.md +32 -28
- package/oxe/workflows/retro.md +4 -0
- package/oxe/workflows/review-pr.md +15 -11
- package/oxe/workflows/scan.md +4 -0
- package/oxe/workflows/security.md +14 -10
- package/oxe/workflows/session.md +17 -1
- package/oxe/workflows/ship.md +142 -0
- package/oxe/workflows/spec.md +15 -0
- package/oxe/workflows/ui-review.md +20 -16
- package/oxe/workflows/ui-spec.md +7 -3
- package/oxe/workflows/validate-gaps.md +13 -9
- package/oxe/workflows/verify.md +42 -3
- package/package.json +1 -1
- package/packages/runtime/src/audit/audit-trail.ts +243 -0
- package/packages/runtime/src/audit/index.ts +2 -0
- package/packages/runtime/src/audit/policy-pack.ts +62 -0
- package/packages/runtime/src/context/context-pack-builder.ts +66 -0
- package/packages/runtime/src/context/context-pack-store.ts +197 -0
- package/packages/runtime/src/context/context-profiles.ts +60 -0
- package/packages/runtime/src/context/index.ts +2 -0
- package/packages/runtime/src/decision/decision-engine.ts +174 -0
- package/packages/runtime/src/decision/decision-memo.ts +211 -0
- package/packages/runtime/src/decision/index.ts +2 -0
- package/packages/runtime/src/delivery/index.ts +1 -0
- package/packages/runtime/src/delivery/promotion-pipeline.ts +180 -0
- package/packages/runtime/src/index.ts +5 -0
- package/packages/runtime/src/plugins/capability-matrix.ts +83 -0
- package/packages/runtime/src/plugins/index.ts +2 -0
- package/packages/runtime/src/plugins/plugin-manifest.ts +113 -0
- package/packages/runtime/src/plugins/plugin-registry.ts +5 -0
- package/packages/runtime/src/policy/policy-engine.ts +138 -7
- package/packages/runtime/src/reducers/run-state-reducer.ts +143 -1
- package/packages/runtime/src/scheduler/agent-registry.ts +132 -0
- package/packages/runtime/src/scheduler/agent-roles.ts +109 -0
- package/packages/runtime/src/scheduler/index.ts +3 -0
- package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +106 -4
- package/packages/runtime/src/scheduler/run-journal.ts +62 -0
- package/packages/runtime/src/scheduler/scheduler.ts +168 -8
- package/packages/runtime/src/verification/index.ts +1 -0
- package/packages/runtime/src/verification/verification-manifest.ts +192 -0
- package/vscode-extension/oxe-agents-1.0.0.vsix +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { OxePlugin } from './plugin-abi';
|
|
2
|
+
|
|
3
|
+
export const CURRENT_ABI_VERSION = '1.0.0';
|
|
4
|
+
|
|
5
|
+
export interface PluginManifest {
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
abi_version: string;
|
|
9
|
+
capabilities: Array<'tool' | 'workspace' | 'verifier' | 'context' | 'hooks'>;
|
|
10
|
+
tool_action_types?: string[];
|
|
11
|
+
workspace_strategies?: string[];
|
|
12
|
+
verifier_check_types?: string[];
|
|
13
|
+
context_provider_names?: string[];
|
|
14
|
+
hook_names?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PluginValidationResult {
|
|
18
|
+
valid: boolean;
|
|
19
|
+
errors: string[];
|
|
20
|
+
warnings: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function extractManifest(plugin: OxePlugin): PluginManifest {
|
|
24
|
+
const capabilities: PluginManifest['capabilities'] = [];
|
|
25
|
+
if (plugin.toolProviders?.length) capabilities.push('tool');
|
|
26
|
+
if (plugin.workspaceProviders?.length) capabilities.push('workspace');
|
|
27
|
+
if (plugin.verifierProviders?.length) capabilities.push('verifier');
|
|
28
|
+
if (plugin.contextProviders?.length) capabilities.push('context');
|
|
29
|
+
if (plugin.hooks && Object.keys(plugin.hooks).length > 0) capabilities.push('hooks');
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
name: plugin.name,
|
|
33
|
+
version: plugin.version ?? '0.0.0',
|
|
34
|
+
abi_version: CURRENT_ABI_VERSION,
|
|
35
|
+
capabilities,
|
|
36
|
+
tool_action_types: plugin.toolProviders?.flatMap((p) =>
|
|
37
|
+
['read_code', 'generate_patch', 'run_tests', 'collect_evidence', 'custom'].filter((t) => p.supports(t))
|
|
38
|
+
) ?? [],
|
|
39
|
+
workspace_strategies: plugin.workspaceProviders?.map((p) => p.name) ?? [],
|
|
40
|
+
verifier_check_types: plugin.verifierProviders?.flatMap((p) =>
|
|
41
|
+
['unit', 'integration', 'smoke', 'policy', 'security', 'custom'].filter((t) => p.supports(t))
|
|
42
|
+
) ?? [],
|
|
43
|
+
context_provider_names: plugin.contextProviders?.map((p) => p.name) ?? [],
|
|
44
|
+
hook_names: plugin.hooks ? Object.keys(plugin.hooks) : [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validatePlugin(plugin: OxePlugin): PluginValidationResult {
|
|
49
|
+
const errors: string[] = [];
|
|
50
|
+
const warnings: string[] = [];
|
|
51
|
+
|
|
52
|
+
if (!plugin.name || typeof plugin.name !== 'string') {
|
|
53
|
+
errors.push('Plugin must have a non-empty string name');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (plugin.version && !/^\d+\.\d+\.\d+/.test(plugin.version)) {
|
|
57
|
+
warnings.push(`Plugin version "${plugin.version}" does not follow semver`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!plugin.toolProviders?.length &&
|
|
61
|
+
!plugin.workspaceProviders?.length &&
|
|
62
|
+
!plugin.verifierProviders?.length &&
|
|
63
|
+
!plugin.contextProviders?.length &&
|
|
64
|
+
!plugin.hooks) {
|
|
65
|
+
warnings.push('Plugin declares no providers or hooks — it has no effect');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Validate each tool provider
|
|
69
|
+
for (const tp of plugin.toolProviders ?? []) {
|
|
70
|
+
if (!tp.name) errors.push('ToolProvider missing name');
|
|
71
|
+
if (typeof tp.supports !== 'function') errors.push(`ToolProvider "${tp.name}" missing supports() method`);
|
|
72
|
+
if (typeof tp.invoke !== 'function') errors.push(`ToolProvider "${tp.name}" missing invoke() method`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate each workspace provider
|
|
76
|
+
for (const wp of plugin.workspaceProviders ?? []) {
|
|
77
|
+
if (!wp.name) errors.push('WorkspaceProvider missing name');
|
|
78
|
+
if (typeof wp.supportsStrategy !== 'function') errors.push(`WorkspaceProvider "${wp.name}" missing supportsStrategy()`);
|
|
79
|
+
if (typeof wp.allocate !== 'function') errors.push(`WorkspaceProvider "${wp.name}" missing allocate()`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate each verifier provider
|
|
83
|
+
for (const vp of plugin.verifierProviders ?? []) {
|
|
84
|
+
if (!vp.name) errors.push('VerifierProvider missing name');
|
|
85
|
+
if (typeof vp.supports !== 'function') errors.push(`VerifierProvider "${vp.name}" missing supports()`);
|
|
86
|
+
if (typeof vp.execute !== 'function') errors.push(`VerifierProvider "${vp.name}" missing execute()`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isAbiCompatible(pluginAbiVersion: string): boolean {
|
|
93
|
+
// Major version must match; minor/patch are backwards-compatible
|
|
94
|
+
const [currMajor] = CURRENT_ABI_VERSION.split('.').map(Number);
|
|
95
|
+
const [plugMajor] = pluginAbiVersion.split('.').map(Number);
|
|
96
|
+
return currMajor === plugMajor;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function sandboxInvoke<T>(
|
|
100
|
+
fn: () => Promise<T>,
|
|
101
|
+
timeoutMs = 10_000
|
|
102
|
+
): Promise<T> {
|
|
103
|
+
return new Promise<T>((resolve, reject) => {
|
|
104
|
+
const timer = setTimeout(() => {
|
|
105
|
+
reject(new Error(`Plugin invocation timed out after ${timeoutMs}ms`));
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
|
|
108
|
+
fn().then(
|
|
109
|
+
(result) => { clearTimeout(timer); resolve(result); },
|
|
110
|
+
(err) => { clearTimeout(timer); reject(err instanceof Error ? err : new Error(String(err))); }
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
VerifierProvider,
|
|
8
8
|
ContextProvider,
|
|
9
9
|
} from './plugin-abi';
|
|
10
|
+
import { validatePlugin } from './plugin-manifest';
|
|
10
11
|
|
|
11
12
|
export class PluginRegistry {
|
|
12
13
|
private plugins: OxePlugin[] = [];
|
|
@@ -15,6 +16,10 @@ export class PluginRegistry {
|
|
|
15
16
|
if (this.plugins.some((p) => p.name === plugin.name)) {
|
|
16
17
|
throw new Error(`Plugin "${plugin.name}" is already registered`);
|
|
17
18
|
}
|
|
19
|
+
const validation = validatePlugin(plugin);
|
|
20
|
+
if (!validation.valid && validation.errors.length > 0) {
|
|
21
|
+
throw new Error(`Plugin "${plugin.name}" failed validation: ${validation.errors.join('; ')}`);
|
|
22
|
+
}
|
|
18
23
|
this.plugins.push(plugin);
|
|
19
24
|
}
|
|
20
25
|
|
|
@@ -1,15 +1,42 @@
|
|
|
1
1
|
export type PolicyAction = 'allow' | 'deny' | 'require_human_gate';
|
|
2
2
|
|
|
3
|
+
export type SideEffectClass =
|
|
4
|
+
| 'read_fs'
|
|
5
|
+
| 'write_fs'
|
|
6
|
+
| 'spawn_process'
|
|
7
|
+
| 'network_call'
|
|
8
|
+
| 'git_mutation'
|
|
9
|
+
| 'db_change'
|
|
10
|
+
| 'secret_access'
|
|
11
|
+
| 'infra_operation';
|
|
12
|
+
|
|
13
|
+
export type AutonomyTier = 'L0' | 'L1' | 'L2' | 'L3';
|
|
14
|
+
|
|
3
15
|
export interface PolicyWhenClause {
|
|
4
16
|
tool?: string;
|
|
5
17
|
env?: string;
|
|
6
18
|
kind?: string;
|
|
19
|
+
side_effect_class?: SideEffectClass;
|
|
20
|
+
autonomy_tier?: AutonomyTier;
|
|
7
21
|
}
|
|
8
22
|
|
|
9
23
|
export interface PolicyAssertClause {
|
|
10
24
|
diff_within_scope?: boolean;
|
|
11
25
|
}
|
|
12
26
|
|
|
27
|
+
export interface NodePolicyConfig {
|
|
28
|
+
max_retries: number;
|
|
29
|
+
mutation_budget?: number;
|
|
30
|
+
autonomy_tier?: AutonomyTier;
|
|
31
|
+
allowed_side_effects?: SideEffectClass[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface EnvironmentGuardrail {
|
|
35
|
+
protected_paths: string[];
|
|
36
|
+
protected_branches: string[];
|
|
37
|
+
require_human_gate_on: SideEffectClass[];
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
export interface PolicyRule {
|
|
14
41
|
id: string;
|
|
15
42
|
when: PolicyWhenClause;
|
|
@@ -23,6 +50,10 @@ export interface PolicyContext {
|
|
|
23
50
|
kind?: string;
|
|
24
51
|
mutation_scope?: string[];
|
|
25
52
|
affected_paths?: string[];
|
|
53
|
+
side_effect_class?: SideEffectClass;
|
|
54
|
+
autonomy_tier?: AutonomyTier;
|
|
55
|
+
mutation_count?: number;
|
|
56
|
+
node_policy?: NodePolicyConfig;
|
|
26
57
|
}
|
|
27
58
|
|
|
28
59
|
export interface PolicyDecision {
|
|
@@ -39,10 +70,40 @@ const ALLOW_ALL: PolicyDecision = {
|
|
|
39
70
|
rule_id: null,
|
|
40
71
|
};
|
|
41
72
|
|
|
73
|
+
const DEFAULT_GUARDRAIL: EnvironmentGuardrail = {
|
|
74
|
+
protected_paths: ['.oxe/config.json', '.env', 'package.json'],
|
|
75
|
+
protected_branches: ['main', 'master', 'production', 'release'],
|
|
76
|
+
require_human_gate_on: ['infra_operation', 'db_change', 'secret_access'],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Autonomy tier → max side effect class allowed without a gate
|
|
80
|
+
const TIER_SIDE_EFFECT_MAP: Record<AutonomyTier, SideEffectClass[]> = {
|
|
81
|
+
L0: ['read_fs'],
|
|
82
|
+
L1: ['read_fs', 'write_fs', 'spawn_process'],
|
|
83
|
+
L2: ['read_fs', 'write_fs', 'spawn_process', 'network_call', 'git_mutation'],
|
|
84
|
+
L3: ['read_fs', 'write_fs', 'spawn_process', 'network_call', 'git_mutation', 'db_change', 'secret_access', 'infra_operation'],
|
|
85
|
+
};
|
|
86
|
+
|
|
42
87
|
export class PolicyEngine {
|
|
43
|
-
constructor(
|
|
88
|
+
constructor(
|
|
89
|
+
private readonly rules: PolicyRule[] = [],
|
|
90
|
+
private readonly guardrail: EnvironmentGuardrail = DEFAULT_GUARDRAIL
|
|
91
|
+
) {}
|
|
44
92
|
|
|
45
93
|
evaluate(ctx: PolicyContext): PolicyDecision {
|
|
94
|
+
// Check autonomy tier first — a denial takes priority over guardrail gates
|
|
95
|
+
const tierDecision = this.checkAutonomyTier(ctx);
|
|
96
|
+
if (tierDecision) return tierDecision;
|
|
97
|
+
|
|
98
|
+
// Check environment guardrails (may require gate even when tier permits)
|
|
99
|
+
const guardrailDecision = this.checkGuardrails(ctx);
|
|
100
|
+
if (guardrailDecision) return guardrailDecision;
|
|
101
|
+
|
|
102
|
+
// Check mutation budget
|
|
103
|
+
const budgetDecision = this.checkMutationBudget(ctx);
|
|
104
|
+
if (budgetDecision) return budgetDecision;
|
|
105
|
+
|
|
106
|
+
// Evaluate rules (first match wins)
|
|
46
107
|
for (const rule of this.rules) {
|
|
47
108
|
if (!this.matches(rule.when, ctx)) continue;
|
|
48
109
|
|
|
@@ -67,13 +128,72 @@ export class PolicyEngine {
|
|
|
67
128
|
return { allowed: true, gate_required: true, reason: `Gate required by rule ${rule.id}`, rule_id: rule.id };
|
|
68
129
|
}
|
|
69
130
|
}
|
|
131
|
+
|
|
70
132
|
return ALLOW_ALL;
|
|
71
133
|
}
|
|
72
134
|
|
|
135
|
+
private checkGuardrails(ctx: PolicyContext): PolicyDecision | null {
|
|
136
|
+
// Protected path check
|
|
137
|
+
const affected = ctx.affected_paths ?? [];
|
|
138
|
+
for (const p of affected) {
|
|
139
|
+
if (this.guardrail.protected_paths.some((pp) => p === pp || p.startsWith(pp + '/'))) {
|
|
140
|
+
return {
|
|
141
|
+
allowed: true,
|
|
142
|
+
gate_required: true,
|
|
143
|
+
reason: `Protected path affected: ${p}`,
|
|
144
|
+
rule_id: '__guardrail_path',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Side effect class requiring gate
|
|
150
|
+
if (ctx.side_effect_class && this.guardrail.require_human_gate_on.includes(ctx.side_effect_class)) {
|
|
151
|
+
return {
|
|
152
|
+
allowed: true,
|
|
153
|
+
gate_required: true,
|
|
154
|
+
reason: `Side effect class '${ctx.side_effect_class}' requires human gate`,
|
|
155
|
+
rule_id: '__guardrail_side_effect',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private checkAutonomyTier(ctx: PolicyContext): PolicyDecision | null {
|
|
163
|
+
if (!ctx.autonomy_tier || !ctx.side_effect_class) return null;
|
|
164
|
+
const allowed = TIER_SIDE_EFFECT_MAP[ctx.autonomy_tier] ?? [];
|
|
165
|
+
if (!allowed.includes(ctx.side_effect_class)) {
|
|
166
|
+
return {
|
|
167
|
+
allowed: false,
|
|
168
|
+
gate_required: false,
|
|
169
|
+
reason: `Autonomy tier ${ctx.autonomy_tier} does not permit side effect '${ctx.side_effect_class}'`,
|
|
170
|
+
rule_id: '__autonomy_tier',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private checkMutationBudget(ctx: PolicyContext): PolicyDecision | null {
|
|
177
|
+
const budget = ctx.node_policy?.mutation_budget;
|
|
178
|
+
if (budget === undefined || budget === null) return null;
|
|
179
|
+
const count = ctx.mutation_count ?? 0;
|
|
180
|
+
if (count >= budget) {
|
|
181
|
+
return {
|
|
182
|
+
allowed: false,
|
|
183
|
+
gate_required: false,
|
|
184
|
+
reason: `Mutation budget exhausted: ${count}/${budget}`,
|
|
185
|
+
rule_id: '__mutation_budget',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
73
191
|
private matches(when: PolicyWhenClause, ctx: PolicyContext): boolean {
|
|
74
192
|
if (when.tool && when.tool !== ctx.tool) return false;
|
|
75
193
|
if (when.env && when.env !== ctx.env) return false;
|
|
76
194
|
if (when.kind && when.kind !== ctx.kind) return false;
|
|
195
|
+
if (when.side_effect_class && when.side_effect_class !== ctx.side_effect_class) return false;
|
|
196
|
+
if (when.autonomy_tier && when.autonomy_tier !== ctx.autonomy_tier) return false;
|
|
77
197
|
return true;
|
|
78
198
|
}
|
|
79
199
|
|
|
@@ -81,7 +201,7 @@ export class PolicyEngine {
|
|
|
81
201
|
if (assert.diff_within_scope === true) {
|
|
82
202
|
const scope = ctx.mutation_scope ?? [];
|
|
83
203
|
const affected = ctx.affected_paths ?? [];
|
|
84
|
-
if (scope.length === 0) return null;
|
|
204
|
+
if (scope.length === 0) return null;
|
|
85
205
|
const outsideScope = affected.filter(
|
|
86
206
|
(p) => !scope.some((s) => p.startsWith(s) || s.startsWith(p))
|
|
87
207
|
);
|
|
@@ -93,21 +213,32 @@ export class PolicyEngine {
|
|
|
93
213
|
}
|
|
94
214
|
|
|
95
215
|
withRule(rule: PolicyRule): PolicyEngine {
|
|
96
|
-
return new PolicyEngine([...this.rules, rule]);
|
|
216
|
+
return new PolicyEngine([...this.rules, rule], this.guardrail);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
withGuardrail(guardrail: EnvironmentGuardrail): PolicyEngine {
|
|
220
|
+
return new PolicyEngine(this.rules, guardrail);
|
|
97
221
|
}
|
|
98
222
|
|
|
99
|
-
|
|
100
|
-
return
|
|
223
|
+
getGuardrail(): EnvironmentGuardrail {
|
|
224
|
+
return this.guardrail;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
static fromConfig(config: { policies?: PolicyRule[]; guardrail?: EnvironmentGuardrail }): PolicyEngine {
|
|
228
|
+
return new PolicyEngine(config.policies ?? [], config.guardrail ?? DEFAULT_GUARDRAIL);
|
|
101
229
|
}
|
|
102
230
|
|
|
103
231
|
static fromConfigFile(configPath: string): PolicyEngine {
|
|
104
232
|
try {
|
|
105
|
-
// Dynamic require to avoid bundling issues
|
|
106
233
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
107
|
-
const cfg = require(configPath) as { policies?: PolicyRule[] };
|
|
234
|
+
const cfg = require(configPath) as { policies?: PolicyRule[]; guardrail?: EnvironmentGuardrail };
|
|
108
235
|
return PolicyEngine.fromConfig(cfg);
|
|
109
236
|
} catch {
|
|
110
237
|
return new PolicyEngine();
|
|
111
238
|
}
|
|
112
239
|
}
|
|
240
|
+
|
|
241
|
+
static defaultGuardrail(): EnvironmentGuardrail {
|
|
242
|
+
return { ...DEFAULT_GUARDRAIL };
|
|
243
|
+
}
|
|
113
244
|
}
|
|
@@ -4,6 +4,19 @@ import type { WorkItem } from '../models/work-item';
|
|
|
4
4
|
import type { Attempt } from '../models/attempt';
|
|
5
5
|
import type { Workspace } from '../models/workspace';
|
|
6
6
|
|
|
7
|
+
export interface PolicyDecisionRecord {
|
|
8
|
+
allowed: boolean;
|
|
9
|
+
gate_required: boolean;
|
|
10
|
+
reason: string;
|
|
11
|
+
rule_id: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ToolFailureRecord {
|
|
15
|
+
tool: string;
|
|
16
|
+
error: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
export interface RunState {
|
|
8
21
|
run: Run | null;
|
|
9
22
|
workItems: Map<string, WorkItem>;
|
|
@@ -12,6 +25,14 @@ export interface RunState {
|
|
|
12
25
|
completedWorkItems: Set<string>;
|
|
13
26
|
failedWorkItems: Set<string>;
|
|
14
27
|
blockedWorkItems: Set<string>;
|
|
28
|
+
// Phase 1 extensions
|
|
29
|
+
retryCounts: Map<string, number>;
|
|
30
|
+
policyDecisions: Map<string, PolicyDecisionRecord>;
|
|
31
|
+
pendingGates: Set<string>;
|
|
32
|
+
resolvedGates: Map<string, { decision: string; actor?: string }>;
|
|
33
|
+
verificationStatus: Map<string, 'started' | 'completed' | 'failed'>;
|
|
34
|
+
evidenceRefs: Map<string, string[]>;
|
|
35
|
+
toolFailures: Map<string, ToolFailureRecord[]>;
|
|
15
36
|
}
|
|
16
37
|
|
|
17
38
|
export function createEmptyRunState(): RunState {
|
|
@@ -23,6 +44,13 @@ export function createEmptyRunState(): RunState {
|
|
|
23
44
|
completedWorkItems: new Set(),
|
|
24
45
|
failedWorkItems: new Set(),
|
|
25
46
|
blockedWorkItems: new Set(),
|
|
47
|
+
retryCounts: new Map(),
|
|
48
|
+
policyDecisions: new Map(),
|
|
49
|
+
pendingGates: new Set(),
|
|
50
|
+
resolvedGates: new Map(),
|
|
51
|
+
verificationStatus: new Map(),
|
|
52
|
+
evidenceRefs: new Map(),
|
|
53
|
+
toolFailures: new Map(),
|
|
26
54
|
};
|
|
27
55
|
}
|
|
28
56
|
|
|
@@ -56,7 +84,6 @@ function applyEvent(state: RunState, event: OxeEvent): RunState {
|
|
|
56
84
|
if (existing) {
|
|
57
85
|
workItems.set(event.work_item_id, { ...existing, status: 'ready' });
|
|
58
86
|
} else {
|
|
59
|
-
// First time we see this work item — create from payload
|
|
60
87
|
const item = event.payload as unknown as WorkItem;
|
|
61
88
|
workItems.set(event.work_item_id, { ...item, work_item_id: event.work_item_id, status: 'ready' });
|
|
62
89
|
}
|
|
@@ -97,6 +124,14 @@ function applyEvent(state: RunState, event: OxeEvent): RunState {
|
|
|
97
124
|
if (item) workItems.set(event.work_item_id, { ...item, status: 'completed' });
|
|
98
125
|
const completedWorkItems = new Set(state.completedWorkItems);
|
|
99
126
|
completedWorkItems.add(event.work_item_id);
|
|
127
|
+
// Collect evidence refs from payload
|
|
128
|
+
const evidence = (event.payload as { evidence?: string[] }).evidence ?? [];
|
|
129
|
+
if (evidence.length > 0) {
|
|
130
|
+
const evidenceRefs = new Map(state.evidenceRefs);
|
|
131
|
+
const existing = evidenceRefs.get(event.work_item_id) ?? [];
|
|
132
|
+
evidenceRefs.set(event.work_item_id, [...existing, ...evidence]);
|
|
133
|
+
return { ...state, workItems, completedWorkItems, evidenceRefs };
|
|
134
|
+
}
|
|
100
135
|
return { ...state, workItems, completedWorkItems };
|
|
101
136
|
}
|
|
102
137
|
|
|
@@ -110,6 +145,93 @@ function applyEvent(state: RunState, event: OxeEvent): RunState {
|
|
|
110
145
|
return { ...state, workItems, blockedWorkItems };
|
|
111
146
|
}
|
|
112
147
|
|
|
148
|
+
case 'RetryScheduled': {
|
|
149
|
+
if (!event.work_item_id) return state;
|
|
150
|
+
const retryCounts = new Map(state.retryCounts);
|
|
151
|
+
const current = retryCounts.get(event.work_item_id) ?? 0;
|
|
152
|
+
retryCounts.set(event.work_item_id, current + 1);
|
|
153
|
+
return { ...state, retryCounts };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case 'PolicyEvaluated': {
|
|
157
|
+
const p = event.payload as {
|
|
158
|
+
work_item_id?: string;
|
|
159
|
+
allowed?: boolean;
|
|
160
|
+
gate_required?: boolean;
|
|
161
|
+
reason?: string;
|
|
162
|
+
rule_id?: string | null;
|
|
163
|
+
};
|
|
164
|
+
const key = p.work_item_id ?? event.work_item_id;
|
|
165
|
+
if (!key) return state;
|
|
166
|
+
const policyDecisions = new Map(state.policyDecisions);
|
|
167
|
+
policyDecisions.set(key, {
|
|
168
|
+
allowed: p.allowed ?? true,
|
|
169
|
+
gate_required: p.gate_required ?? false,
|
|
170
|
+
reason: p.reason ?? '',
|
|
171
|
+
rule_id: p.rule_id ?? null,
|
|
172
|
+
});
|
|
173
|
+
return { ...state, policyDecisions };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
case 'GateRequested': {
|
|
177
|
+
const gateId = (event.payload as { gate_id?: string }).gate_id;
|
|
178
|
+
if (!gateId) return state;
|
|
179
|
+
const pendingGates = new Set(state.pendingGates);
|
|
180
|
+
pendingGates.add(gateId);
|
|
181
|
+
return { ...state, pendingGates };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case 'GateResolved': {
|
|
185
|
+
const p = event.payload as { gate_id?: string; decision?: string; actor?: string };
|
|
186
|
+
if (!p.gate_id) return state;
|
|
187
|
+
const pendingGates = new Set(state.pendingGates);
|
|
188
|
+
pendingGates.delete(p.gate_id);
|
|
189
|
+
const resolvedGates = new Map(state.resolvedGates);
|
|
190
|
+
resolvedGates.set(p.gate_id, { decision: p.decision ?? 'approved', actor: p.actor });
|
|
191
|
+
return { ...state, pendingGates, resolvedGates };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case 'VerificationStarted': {
|
|
195
|
+
const key = event.work_item_id ?? (event.payload as { work_item_id?: string }).work_item_id;
|
|
196
|
+
if (!key) return state;
|
|
197
|
+
const verificationStatus = new Map(state.verificationStatus);
|
|
198
|
+
verificationStatus.set(key, 'started');
|
|
199
|
+
return { ...state, verificationStatus };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case 'VerificationCompleted': {
|
|
203
|
+
const p = event.payload as { work_item_id?: string; status?: 'completed' | 'failed' };
|
|
204
|
+
const key = event.work_item_id ?? p.work_item_id;
|
|
205
|
+
if (!key) return state;
|
|
206
|
+
const verificationStatus = new Map(state.verificationStatus);
|
|
207
|
+
verificationStatus.set(key, p.status ?? 'completed');
|
|
208
|
+
return { ...state, verificationStatus };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case 'ToolFailed': {
|
|
212
|
+
if (!event.work_item_id) return state;
|
|
213
|
+
const p = event.payload as { tool?: string; error?: string };
|
|
214
|
+
const toolFailures = new Map(state.toolFailures);
|
|
215
|
+
const existing = toolFailures.get(event.work_item_id) ?? [];
|
|
216
|
+
toolFailures.set(event.work_item_id, [
|
|
217
|
+
...existing,
|
|
218
|
+
{ tool: p.tool ?? 'unknown', error: p.error ?? '', timestamp: event.timestamp },
|
|
219
|
+
]);
|
|
220
|
+
return { ...state, toolFailures };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case 'EvidenceCollected': {
|
|
224
|
+
const p = event.payload as { work_item_id?: string; refs?: string[]; ref?: string };
|
|
225
|
+
const key = event.work_item_id ?? p.work_item_id;
|
|
226
|
+
if (!key) return state;
|
|
227
|
+
const refs = p.refs ?? (p.ref ? [p.ref] : []);
|
|
228
|
+
if (refs.length === 0) return state;
|
|
229
|
+
const evidenceRefs = new Map(state.evidenceRefs);
|
|
230
|
+
const existing = evidenceRefs.get(key) ?? [];
|
|
231
|
+
evidenceRefs.set(key, [...existing, ...refs]);
|
|
232
|
+
return { ...state, evidenceRefs };
|
|
233
|
+
}
|
|
234
|
+
|
|
113
235
|
default:
|
|
114
236
|
return state;
|
|
115
237
|
}
|
|
@@ -125,3 +247,23 @@ export function getWorkItemStatus(
|
|
|
125
247
|
export function getAttemptCount(state: RunState, workItemId: string): number {
|
|
126
248
|
return state.attempts.get(workItemId)?.length ?? 0;
|
|
127
249
|
}
|
|
250
|
+
|
|
251
|
+
export function getRetryCount(state: RunState, workItemId: string): number {
|
|
252
|
+
return state.retryCounts.get(workItemId) ?? 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function getPolicyDecision(state: RunState, workItemId: string): PolicyDecisionRecord | null {
|
|
256
|
+
return state.policyDecisions.get(workItemId) ?? null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function getVerificationStatus(state: RunState, workItemId: string): 'started' | 'completed' | 'failed' | null {
|
|
260
|
+
return state.verificationStatus.get(workItemId) ?? null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function getEvidenceRefs(state: RunState, workItemId: string): string[] {
|
|
264
|
+
return state.evidenceRefs.get(workItemId) ?? [];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function getToolFailures(state: RunState, workItemId: string): ToolFailureRecord[] {
|
|
268
|
+
return state.toolFailures.get(workItemId) ?? [];
|
|
269
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { TaskExecutor } from './scheduler';
|
|
2
|
+
import type { WorkspaceManager } from '../workspace/workspace-manager';
|
|
3
|
+
import type { AgentRole, AgentActionLog } from './agent-roles';
|
|
4
|
+
|
|
5
|
+
export type AgentStatus = 'idle' | 'running' | 'paused' | 'failed' | 'timeout';
|
|
6
|
+
|
|
7
|
+
export interface AgentHeartbeat {
|
|
8
|
+
agent_id: string;
|
|
9
|
+
last_seen: string;
|
|
10
|
+
current_task: string | null;
|
|
11
|
+
status: AgentStatus;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RegisteredAgent {
|
|
15
|
+
id: string;
|
|
16
|
+
executor: TaskExecutor;
|
|
17
|
+
workspaceManager: WorkspaceManager;
|
|
18
|
+
assignedTaskIds: string[];
|
|
19
|
+
heartbeat: AgentHeartbeat;
|
|
20
|
+
role?: AgentRole;
|
|
21
|
+
actionLog: AgentActionLog[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class AgentRegistry {
|
|
25
|
+
private agents = new Map<string, RegisteredAgent>();
|
|
26
|
+
private readonly heartbeatTimeoutMs: number;
|
|
27
|
+
|
|
28
|
+
constructor(heartbeatTimeoutMs = 30_000) {
|
|
29
|
+
this.heartbeatTimeoutMs = heartbeatTimeoutMs;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
register(
|
|
33
|
+
id: string,
|
|
34
|
+
executor: TaskExecutor,
|
|
35
|
+
workspaceManager: WorkspaceManager,
|
|
36
|
+
assignedTaskIds: string[] = [],
|
|
37
|
+
role?: AgentRole
|
|
38
|
+
): RegisteredAgent {
|
|
39
|
+
if (this.agents.has(id)) throw new Error(`Agent "${id}" is already registered`);
|
|
40
|
+
const agent: RegisteredAgent = {
|
|
41
|
+
id,
|
|
42
|
+
executor,
|
|
43
|
+
workspaceManager,
|
|
44
|
+
assignedTaskIds,
|
|
45
|
+
heartbeat: {
|
|
46
|
+
agent_id: id,
|
|
47
|
+
last_seen: new Date().toISOString(),
|
|
48
|
+
current_task: null,
|
|
49
|
+
status: 'idle',
|
|
50
|
+
},
|
|
51
|
+
role,
|
|
52
|
+
actionLog: [],
|
|
53
|
+
};
|
|
54
|
+
this.agents.set(id, agent);
|
|
55
|
+
return agent;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
unregister(id: string): void {
|
|
59
|
+
this.agents.delete(id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
beat(id: string, currentTask: string | null = null): void {
|
|
63
|
+
const agent = this.agents.get(id);
|
|
64
|
+
if (!agent) return;
|
|
65
|
+
agent.heartbeat.last_seen = new Date().toISOString();
|
|
66
|
+
agent.heartbeat.current_task = currentTask;
|
|
67
|
+
agent.heartbeat.status = currentTask ? 'running' : 'idle';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setStatus(id: string, status: AgentStatus): void {
|
|
71
|
+
const agent = this.agents.get(id);
|
|
72
|
+
if (agent) agent.heartbeat.status = status;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
isAlive(id: string): boolean {
|
|
76
|
+
const agent = this.agents.get(id);
|
|
77
|
+
if (!agent) return false;
|
|
78
|
+
const elapsed = Date.now() - new Date(agent.heartbeat.last_seen).getTime();
|
|
79
|
+
return elapsed < this.heartbeatTimeoutMs;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Returns agents that haven't sent a heartbeat within the timeout window */
|
|
83
|
+
timedOut(): RegisteredAgent[] {
|
|
84
|
+
return [...this.agents.values()].filter((a) => !this.isAlive(a.id));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
liveAgents(): RegisteredAgent[] {
|
|
88
|
+
return [...this.agents.values()].filter((a) => this.isAlive(a.id));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get(id: string): RegisteredAgent | null {
|
|
92
|
+
return this.agents.get(id) ?? null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
list(): RegisteredAgent[] {
|
|
96
|
+
return [...this.agents.values()];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reassign orphaned tasks from timed-out agents to a fallback agent.
|
|
101
|
+
* Returns the list of task IDs that were reassigned.
|
|
102
|
+
*/
|
|
103
|
+
failover(fallbackAgentId: string): string[] {
|
|
104
|
+
const fallback = this.agents.get(fallbackAgentId);
|
|
105
|
+
if (!fallback) throw new Error(`Fallback agent "${fallbackAgentId}" not found`);
|
|
106
|
+
|
|
107
|
+
const orphaned: string[] = [];
|
|
108
|
+
for (const agent of this.timedOut()) {
|
|
109
|
+
orphaned.push(...agent.assignedTaskIds);
|
|
110
|
+
agent.assignedTaskIds = [];
|
|
111
|
+
agent.heartbeat.status = 'failed';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fallback.assignedTaskIds = [...fallback.assignedTaskIds, ...orphaned];
|
|
115
|
+
return orphaned;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Return all agents assigned to a given role */
|
|
119
|
+
getByRole(role: AgentRole): RegisteredAgent[] {
|
|
120
|
+
return [...this.agents.values()].filter((a) => a.role === role);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Append an action log entry for a registered agent (no-op if unknown) */
|
|
124
|
+
logAction(agentId: string, log: AgentActionLog): void {
|
|
125
|
+
const agent = this.agents.get(agentId);
|
|
126
|
+
if (agent) agent.actionLog.push(log);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
clear(): void {
|
|
130
|
+
this.agents.clear();
|
|
131
|
+
}
|
|
132
|
+
}
|