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.
Files changed (207) hide show
  1. package/.cursor/commands/oxe-ask.md +1 -1
  2. package/.cursor/commands/oxe-capabilities.md +1 -1
  3. package/.cursor/commands/oxe-checkpoint.md +1 -1
  4. package/.cursor/commands/oxe-compact.md +1 -1
  5. package/.cursor/commands/oxe-dashboard.md +1 -1
  6. package/.cursor/commands/oxe-debug.md +1 -1
  7. package/.cursor/commands/oxe-discuss.md +1 -1
  8. package/.cursor/commands/oxe-execute.md +2 -2
  9. package/.cursor/commands/oxe-forensics.md +1 -1
  10. package/.cursor/commands/oxe-help.md +1 -1
  11. package/.cursor/commands/oxe-loop.md +1 -1
  12. package/.cursor/commands/oxe-milestone.md +1 -1
  13. package/.cursor/commands/oxe-next.md +1 -1
  14. package/.cursor/commands/oxe-obs.md +1 -1
  15. package/.cursor/commands/oxe-plan-agent.md +1 -1
  16. package/.cursor/commands/oxe-plan.md +1 -1
  17. package/.cursor/commands/oxe-project.md +1 -1
  18. package/.cursor/commands/oxe-quick.md +1 -1
  19. package/.cursor/commands/oxe-research.md +1 -1
  20. package/.cursor/commands/oxe-retro.md +1 -1
  21. package/.cursor/commands/oxe-review-pr.md +1 -1
  22. package/.cursor/commands/oxe-route.md +1 -1
  23. package/.cursor/commands/oxe-scan.md +1 -1
  24. package/.cursor/commands/oxe-security.md +1 -1
  25. package/.cursor/commands/oxe-session.md +2 -2
  26. package/.cursor/commands/oxe-ship.md +45 -0
  27. package/.cursor/commands/oxe-skill.md +1 -1
  28. package/.cursor/commands/oxe-spec.md +1 -1
  29. package/.cursor/commands/oxe-ui-review.md +1 -1
  30. package/.cursor/commands/oxe-ui-spec.md +1 -1
  31. package/.cursor/commands/oxe-update.md +1 -1
  32. package/.cursor/commands/oxe-validate-gaps.md +1 -1
  33. package/.cursor/commands/oxe-verify.md +1 -1
  34. package/.cursor/commands/oxe-workstream.md +1 -1
  35. package/.cursor/commands/oxe.md +4 -4
  36. package/.github/copilot-instructions.md +91 -1
  37. package/.github/prompts/oxe-ask.prompt.md +1 -1
  38. package/.github/prompts/oxe-capabilities.prompt.md +1 -1
  39. package/.github/prompts/oxe-checkpoint.prompt.md +1 -1
  40. package/.github/prompts/oxe-compact.prompt.md +1 -1
  41. package/.github/prompts/oxe-dashboard.prompt.md +1 -1
  42. package/.github/prompts/oxe-debug.prompt.md +1 -1
  43. package/.github/prompts/oxe-discuss.prompt.md +1 -1
  44. package/.github/prompts/oxe-execute.prompt.md +2 -2
  45. package/.github/prompts/oxe-forensics.prompt.md +1 -1
  46. package/.github/prompts/oxe-help.prompt.md +1 -1
  47. package/.github/prompts/oxe-loop.prompt.md +1 -1
  48. package/.github/prompts/oxe-milestone.prompt.md +1 -1
  49. package/.github/prompts/oxe-next.prompt.md +1 -1
  50. package/.github/prompts/oxe-obs.prompt.md +1 -1
  51. package/.github/prompts/oxe-plan-agent.prompt.md +1 -1
  52. package/.github/prompts/oxe-plan.prompt.md +1 -1
  53. package/.github/prompts/oxe-project.prompt.md +1 -1
  54. package/.github/prompts/oxe-quick.prompt.md +1 -1
  55. package/.github/prompts/oxe-research.prompt.md +1 -1
  56. package/.github/prompts/oxe-retro.prompt.md +1 -1
  57. package/.github/prompts/oxe-review-pr.prompt.md +1 -1
  58. package/.github/prompts/oxe-route.prompt.md +1 -1
  59. package/.github/prompts/oxe-scan.prompt.md +1 -1
  60. package/.github/prompts/oxe-security.prompt.md +1 -1
  61. package/.github/prompts/oxe-session.prompt.md +2 -2
  62. package/.github/prompts/oxe-ship.prompt.md +45 -0
  63. package/.github/prompts/oxe-skill.prompt.md +1 -1
  64. package/.github/prompts/oxe-spec.prompt.md +1 -1
  65. package/.github/prompts/oxe-ui-review.prompt.md +1 -1
  66. package/.github/prompts/oxe-ui-spec.prompt.md +1 -1
  67. package/.github/prompts/oxe-update.prompt.md +1 -1
  68. package/.github/prompts/oxe-validate-gaps.prompt.md +1 -1
  69. package/.github/prompts/oxe-verify.prompt.md +1 -1
  70. package/.github/prompts/oxe-workstream.prompt.md +1 -1
  71. package/.github/prompts/oxe.prompt.md +3 -3
  72. package/AGENTS.md +43 -28
  73. package/CHANGELOG.md +158 -0
  74. package/README.md +72 -50
  75. package/bin/banner.txt +1 -1
  76. package/bin/lib/oxe-project-health.cjs +1 -1
  77. package/commands/oxe/ask.md +5 -1
  78. package/commands/oxe/checkpoint.md +1 -1
  79. package/commands/oxe/compact.md +1 -1
  80. package/commands/oxe/debug.md +1 -1
  81. package/commands/oxe/execute.md +2 -2
  82. package/commands/oxe/forensics.md +1 -1
  83. package/commands/oxe/loop.md +1 -1
  84. package/commands/oxe/milestone.md +1 -1
  85. package/commands/oxe/next.md +1 -1
  86. package/commands/oxe/obs.md +1 -1
  87. package/commands/oxe/oxe.md +3 -3
  88. package/commands/oxe/project.md +1 -1
  89. package/commands/oxe/research.md +1 -1
  90. package/commands/oxe/retro.md +1 -1
  91. package/commands/oxe/review-pr.md +1 -1
  92. package/commands/oxe/route.md +1 -1
  93. package/commands/oxe/scan.md +1 -1
  94. package/commands/oxe/security.md +1 -1
  95. package/commands/oxe/session.md +2 -2
  96. package/commands/oxe/ship.md +49 -0
  97. package/commands/oxe/spec.md +2 -2
  98. package/commands/oxe/ui-review.md +1 -1
  99. package/commands/oxe/ui-spec.md +1 -1
  100. package/commands/oxe/validate-gaps.md +1 -1
  101. package/commands/oxe/verify.md +2 -2
  102. package/commands/oxe/workstream.md +1 -1
  103. package/lib/runtime/audit/audit-trail.d.ts +71 -0
  104. package/lib/runtime/audit/audit-trail.js +154 -0
  105. package/lib/runtime/audit/index.d.ts +2 -0
  106. package/lib/runtime/audit/index.js +18 -0
  107. package/lib/runtime/audit/policy-pack.d.ts +15 -0
  108. package/lib/runtime/audit/policy-pack.js +57 -0
  109. package/lib/runtime/context/context-pack-builder.d.ts +15 -0
  110. package/lib/runtime/context/context-pack-builder.js +42 -0
  111. package/lib/runtime/context/context-pack-store.d.ts +38 -0
  112. package/lib/runtime/context/context-pack-store.js +142 -0
  113. package/lib/runtime/context/context-profiles.d.ts +11 -0
  114. package/lib/runtime/context/context-profiles.js +51 -0
  115. package/lib/runtime/context/index.d.ts +2 -0
  116. package/lib/runtime/context/index.js +2 -0
  117. package/lib/runtime/decision/decision-engine.d.ts +43 -0
  118. package/lib/runtime/decision/decision-engine.js +127 -0
  119. package/lib/runtime/decision/decision-memo.d.ts +53 -0
  120. package/lib/runtime/decision/decision-memo.js +173 -0
  121. package/lib/runtime/decision/index.d.ts +2 -0
  122. package/lib/runtime/decision/index.js +18 -0
  123. package/lib/runtime/delivery/index.d.ts +1 -0
  124. package/lib/runtime/delivery/index.js +1 -0
  125. package/lib/runtime/delivery/promotion-pipeline.d.ts +39 -0
  126. package/lib/runtime/delivery/promotion-pipeline.js +127 -0
  127. package/lib/runtime/index.d.ts +3 -0
  128. package/lib/runtime/index.js +4 -0
  129. package/lib/runtime/plugins/capability-matrix.d.ts +20 -0
  130. package/lib/runtime/plugins/capability-matrix.js +59 -0
  131. package/lib/runtime/plugins/index.d.ts +2 -0
  132. package/lib/runtime/plugins/index.js +2 -0
  133. package/lib/runtime/plugins/plugin-manifest.d.ts +22 -0
  134. package/lib/runtime/plugins/plugin-manifest.js +91 -0
  135. package/lib/runtime/plugins/plugin-registry.js +5 -0
  136. package/lib/runtime/policy/policy-engine.d.ts +28 -1
  137. package/lib/runtime/policy/policy-engine.js +96 -5
  138. package/lib/runtime/reducers/run-state-reducer.d.ts +26 -0
  139. package/lib/runtime/reducers/run-state-reducer.js +117 -1
  140. package/lib/runtime/scheduler/agent-registry.d.ts +44 -0
  141. package/lib/runtime/scheduler/agent-registry.js +96 -0
  142. package/lib/runtime/scheduler/agent-roles.d.ts +54 -0
  143. package/lib/runtime/scheduler/agent-roles.js +62 -0
  144. package/lib/runtime/scheduler/index.d.ts +3 -0
  145. package/lib/runtime/scheduler/index.js +3 -0
  146. package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +2 -0
  147. package/lib/runtime/scheduler/multi-agent-coordinator.js +91 -4
  148. package/lib/runtime/scheduler/run-journal.d.ts +18 -0
  149. package/lib/runtime/scheduler/run-journal.js +54 -0
  150. package/lib/runtime/scheduler/scheduler.d.ts +11 -1
  151. package/lib/runtime/scheduler/scheduler.js +135 -7
  152. package/lib/runtime/verification/index.d.ts +1 -0
  153. package/lib/runtime/verification/index.js +1 -0
  154. package/lib/runtime/verification/verification-manifest.d.ts +58 -0
  155. package/lib/runtime/verification/verification-manifest.js +129 -0
  156. package/oxe/workflows/ask.md +4 -0
  157. package/oxe/workflows/checkpoint.md +14 -10
  158. package/oxe/workflows/debug.md +19 -15
  159. package/oxe/workflows/execute.md +30 -2
  160. package/oxe/workflows/forensics.md +13 -9
  161. package/oxe/workflows/help.md +97 -49
  162. package/oxe/workflows/loop.md +17 -13
  163. package/oxe/workflows/obs.md +4 -0
  164. package/oxe/workflows/oxe.md +64 -31
  165. package/oxe/workflows/project.md +6 -1
  166. package/oxe/workflows/references/workflow-runtime-contracts.json +23 -0
  167. package/oxe/workflows/research.md +32 -28
  168. package/oxe/workflows/retro.md +4 -0
  169. package/oxe/workflows/review-pr.md +15 -11
  170. package/oxe/workflows/scan.md +4 -0
  171. package/oxe/workflows/security.md +14 -10
  172. package/oxe/workflows/session.md +17 -1
  173. package/oxe/workflows/ship.md +142 -0
  174. package/oxe/workflows/spec.md +15 -0
  175. package/oxe/workflows/ui-review.md +20 -16
  176. package/oxe/workflows/ui-spec.md +7 -3
  177. package/oxe/workflows/validate-gaps.md +13 -9
  178. package/oxe/workflows/verify.md +42 -3
  179. package/package.json +1 -1
  180. package/packages/runtime/src/audit/audit-trail.ts +243 -0
  181. package/packages/runtime/src/audit/index.ts +2 -0
  182. package/packages/runtime/src/audit/policy-pack.ts +62 -0
  183. package/packages/runtime/src/context/context-pack-builder.ts +66 -0
  184. package/packages/runtime/src/context/context-pack-store.ts +197 -0
  185. package/packages/runtime/src/context/context-profiles.ts +60 -0
  186. package/packages/runtime/src/context/index.ts +2 -0
  187. package/packages/runtime/src/decision/decision-engine.ts +174 -0
  188. package/packages/runtime/src/decision/decision-memo.ts +211 -0
  189. package/packages/runtime/src/decision/index.ts +2 -0
  190. package/packages/runtime/src/delivery/index.ts +1 -0
  191. package/packages/runtime/src/delivery/promotion-pipeline.ts +180 -0
  192. package/packages/runtime/src/index.ts +5 -0
  193. package/packages/runtime/src/plugins/capability-matrix.ts +83 -0
  194. package/packages/runtime/src/plugins/index.ts +2 -0
  195. package/packages/runtime/src/plugins/plugin-manifest.ts +113 -0
  196. package/packages/runtime/src/plugins/plugin-registry.ts +5 -0
  197. package/packages/runtime/src/policy/policy-engine.ts +138 -7
  198. package/packages/runtime/src/reducers/run-state-reducer.ts +143 -1
  199. package/packages/runtime/src/scheduler/agent-registry.ts +132 -0
  200. package/packages/runtime/src/scheduler/agent-roles.ts +109 -0
  201. package/packages/runtime/src/scheduler/index.ts +3 -0
  202. package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +106 -4
  203. package/packages/runtime/src/scheduler/run-journal.ts +62 -0
  204. package/packages/runtime/src/scheduler/scheduler.ts +168 -8
  205. package/packages/runtime/src/verification/index.ts +1 -0
  206. package/packages/runtime/src/verification/verification-manifest.ts +192 -0
  207. 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(private readonly rules: PolicyRule[] = []) {}
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; // no scope declared — pass
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
- static fromConfig(config: { policies?: PolicyRule[] }): PolicyEngine {
100
- return new PolicyEngine(config.policies ?? []);
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
+ }