agentic-orchestrator 0.1.26 → 0.1.28

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 (172) hide show
  1. package/AGENTS.md +2 -2
  2. package/CLAUDE.md +2 -2
  3. package/README.md +47 -14
  4. package/agentic/orchestrator/agents.yaml +13 -0
  5. package/agentic/orchestrator/policy.yaml +3 -0
  6. package/agentic/orchestrator/schemas/agents.schema.json +76 -0
  7. package/agentic/orchestrator/schemas/policy.schema.json +16 -0
  8. package/agentic/orchestrator/schemas/policy.user.schema.json +16 -0
  9. package/agentic/orchestrator/schemas/state.schema.json +53 -0
  10. package/apps/control-plane/src/application/configuration-service.ts +181 -0
  11. package/apps/control-plane/src/application/kernel-tool-wiring.ts +292 -0
  12. package/apps/control-plane/src/application/services/checkpoint-service.ts +523 -0
  13. package/apps/control-plane/src/application/services/feature-send-message-service.ts +132 -0
  14. package/apps/control-plane/src/application/services/patch-service.ts +29 -5
  15. package/apps/control-plane/src/application/services/repo-operations-service.ts +276 -0
  16. package/apps/control-plane/src/application/services/worktree-watchdog-service.ts +156 -0
  17. package/apps/control-plane/src/cli/cli-argument-parser.ts +12 -0
  18. package/apps/control-plane/src/cli/help-command-handler.ts +17 -0
  19. package/apps/control-plane/src/cli/init-command-handler.ts +31 -0
  20. package/apps/control-plane/src/cli/resume-command-handler.ts +31 -4
  21. package/apps/control-plane/src/cli/rollback-command-handler.ts +217 -0
  22. package/apps/control-plane/src/cli/run-command-handler.ts +8 -0
  23. package/apps/control-plane/src/cli/types.ts +3 -0
  24. package/apps/control-plane/src/core/kernel-types.ts +55 -0
  25. package/apps/control-plane/src/core/kernel.ts +61 -878
  26. package/apps/control-plane/src/core/tool-caller.ts +10 -0
  27. package/apps/control-plane/src/core/utils/field-readers.ts +38 -0
  28. package/apps/control-plane/src/core/utils/index-normalizer.ts +119 -0
  29. package/apps/control-plane/src/core/utils/path-normalizers.ts +22 -0
  30. package/apps/control-plane/src/interfaces/cli/bootstrap.ts +15 -0
  31. package/apps/control-plane/src/providers/api-worker-provider.ts +14 -12
  32. package/apps/control-plane/src/providers/cli-worker-provider.ts +82 -12
  33. package/apps/control-plane/src/providers/providers.ts +45 -24
  34. package/apps/control-plane/src/providers/worker-provider-factory.ts +36 -1
  35. package/apps/control-plane/src/supervisor/run-coordinator.ts +91 -36
  36. package/apps/control-plane/src/supervisor/runtime.ts +107 -1
  37. package/apps/control-plane/src/supervisor/types.ts +9 -0
  38. package/apps/control-plane/src/supervisor/worker-decision-loop.ts +253 -14
  39. package/apps/control-plane/test/checkpoint-service.spec.ts +537 -0
  40. package/apps/control-plane/test/cli-helpers.spec.ts +28 -0
  41. package/apps/control-plane/test/cli.unit.spec.ts +52 -0
  42. package/apps/control-plane/test/configuration-service.spec.ts +466 -0
  43. package/apps/control-plane/test/dashboard-api.integration.spec.ts +537 -0
  44. package/apps/control-plane/test/dashboard-client.spec.ts +233 -0
  45. package/apps/control-plane/test/feature-send-message-service.spec.ts +314 -0
  46. package/apps/control-plane/test/init-wizard.spec.ts +35 -0
  47. package/apps/control-plane/test/path-normalizers.spec.ts +41 -0
  48. package/apps/control-plane/test/repo-operations-service.spec.ts +339 -0
  49. package/apps/control-plane/test/resume-command.spec.ts +33 -0
  50. package/apps/control-plane/test/review-workspace-logic.spec.ts +130 -0
  51. package/apps/control-plane/test/rollback-command.spec.ts +208 -0
  52. package/apps/control-plane/test/run-coordinator.spec.ts +119 -0
  53. package/apps/control-plane/test/worker-decision-loop.spec.ts +209 -0
  54. package/apps/control-plane/test/worker-provider-adapters.spec.ts +102 -0
  55. package/apps/control-plane/test/worker-provider-factory.spec.ts +14 -0
  56. package/apps/control-plane/test/worktree-watchdog-service.spec.ts +147 -0
  57. package/config/agentic/orchestrator/agents.yaml +13 -0
  58. package/dist/apps/control-plane/application/configuration-service.d.ts +19 -0
  59. package/dist/apps/control-plane/application/configuration-service.js +123 -0
  60. package/dist/apps/control-plane/application/configuration-service.js.map +1 -0
  61. package/dist/apps/control-plane/application/kernel-tool-wiring.d.ts +39 -0
  62. package/dist/apps/control-plane/application/kernel-tool-wiring.js +38 -0
  63. package/dist/apps/control-plane/application/kernel-tool-wiring.js.map +1 -0
  64. package/dist/apps/control-plane/application/services/checkpoint-service.d.ts +84 -0
  65. package/dist/apps/control-plane/application/services/checkpoint-service.js +367 -0
  66. package/dist/apps/control-plane/application/services/checkpoint-service.js.map +1 -0
  67. package/dist/apps/control-plane/application/services/feature-send-message-service.d.ts +25 -0
  68. package/dist/apps/control-plane/application/services/feature-send-message-service.js +105 -0
  69. package/dist/apps/control-plane/application/services/feature-send-message-service.js.map +1 -0
  70. package/dist/apps/control-plane/application/services/patch-service.d.ts +6 -0
  71. package/dist/apps/control-plane/application/services/patch-service.js +11 -2
  72. package/dist/apps/control-plane/application/services/patch-service.js.map +1 -1
  73. package/dist/apps/control-plane/application/services/repo-operations-service.d.ts +70 -0
  74. package/dist/apps/control-plane/application/services/repo-operations-service.js +213 -0
  75. package/dist/apps/control-plane/application/services/repo-operations-service.js.map +1 -0
  76. package/dist/apps/control-plane/application/services/worktree-watchdog-service.d.ts +23 -0
  77. package/dist/apps/control-plane/application/services/worktree-watchdog-service.js +119 -0
  78. package/dist/apps/control-plane/application/services/worktree-watchdog-service.js.map +1 -0
  79. package/dist/apps/control-plane/cli/cli-argument-parser.js +12 -0
  80. package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
  81. package/dist/apps/control-plane/cli/help-command-handler.js +17 -0
  82. package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
  83. package/dist/apps/control-plane/cli/init-command-handler.js +23 -0
  84. package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
  85. package/dist/apps/control-plane/cli/resume-command-handler.js +25 -5
  86. package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
  87. package/dist/apps/control-plane/cli/rollback-command-handler.d.ts +6 -0
  88. package/dist/apps/control-plane/cli/rollback-command-handler.js +177 -0
  89. package/dist/apps/control-plane/cli/rollback-command-handler.js.map +1 -0
  90. package/dist/apps/control-plane/cli/run-command-handler.js +7 -1
  91. package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
  92. package/dist/apps/control-plane/cli/types.d.ts +3 -0
  93. package/dist/apps/control-plane/cli/types.js +1 -0
  94. package/dist/apps/control-plane/cli/types.js.map +1 -1
  95. package/dist/apps/control-plane/core/configuration-service.d.ts +25 -0
  96. package/dist/apps/control-plane/core/configuration-service.js +130 -0
  97. package/dist/apps/control-plane/core/configuration-service.js.map +1 -0
  98. package/dist/apps/control-plane/core/kernel-tool-wiring.d.ts +50 -0
  99. package/dist/apps/control-plane/core/kernel-tool-wiring.js +44 -0
  100. package/dist/apps/control-plane/core/kernel-tool-wiring.js.map +1 -0
  101. package/dist/apps/control-plane/core/kernel-types.d.ts +48 -0
  102. package/dist/apps/control-plane/core/kernel-types.js +2 -0
  103. package/dist/apps/control-plane/core/kernel-types.js.map +1 -0
  104. package/dist/apps/control-plane/core/kernel.d.ts +17 -48
  105. package/dist/apps/control-plane/core/kernel.js +44 -539
  106. package/dist/apps/control-plane/core/kernel.js.map +1 -1
  107. package/dist/apps/control-plane/core/tool-caller.d.ts +10 -0
  108. package/dist/apps/control-plane/core/utils/error-normalizer.d.ts +2 -0
  109. package/dist/apps/control-plane/core/utils/error-normalizer.js +51 -0
  110. package/dist/apps/control-plane/core/utils/error-normalizer.js.map +1 -0
  111. package/dist/apps/control-plane/core/utils/field-readers.d.ts +9 -0
  112. package/dist/apps/control-plane/core/utils/field-readers.js +30 -0
  113. package/dist/apps/control-plane/core/utils/field-readers.js.map +1 -0
  114. package/dist/apps/control-plane/core/utils/index-normalizer.d.ts +7 -0
  115. package/dist/apps/control-plane/core/utils/index-normalizer.js +92 -0
  116. package/dist/apps/control-plane/core/utils/index-normalizer.js.map +1 -0
  117. package/dist/apps/control-plane/core/utils/path-normalizers.d.ts +2 -0
  118. package/dist/apps/control-plane/core/utils/path-normalizers.js +17 -0
  119. package/dist/apps/control-plane/core/utils/path-normalizers.js.map +1 -0
  120. package/dist/apps/control-plane/interfaces/cli/bootstrap.js +13 -1
  121. package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
  122. package/dist/apps/control-plane/providers/api-worker-provider.d.ts +4 -13
  123. package/dist/apps/control-plane/providers/api-worker-provider.js +10 -0
  124. package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -1
  125. package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +11 -13
  126. package/dist/apps/control-plane/providers/cli-worker-provider.js +64 -0
  127. package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -1
  128. package/dist/apps/control-plane/providers/providers.d.ts +31 -24
  129. package/dist/apps/control-plane/providers/providers.js +10 -0
  130. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  131. package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +11 -0
  132. package/dist/apps/control-plane/providers/worker-provider-factory.js +20 -1
  133. package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
  134. package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +3 -0
  135. package/dist/apps/control-plane/supervisor/run-coordinator.js +81 -33
  136. package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
  137. package/dist/apps/control-plane/supervisor/runtime.d.ts +8 -1
  138. package/dist/apps/control-plane/supervisor/runtime.js +90 -0
  139. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  140. package/dist/apps/control-plane/supervisor/types.d.ts +11 -0
  141. package/dist/apps/control-plane/supervisor/types.js.map +1 -1
  142. package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +21 -1
  143. package/dist/apps/control-plane/supervisor/worker-decision-loop.js +207 -13
  144. package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
  145. package/package.json +1 -1
  146. package/packages/web-dashboard/package.json +2 -0
  147. package/packages/web-dashboard/src/app/analytics/page.tsx +83 -2
  148. package/packages/web-dashboard/src/app/api/actions/route.ts +92 -1
  149. package/packages/web-dashboard/src/app/api/analytics/route.ts +5 -2
  150. package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/[checkpointId]/diff/route.ts +43 -0
  151. package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/compare/route.ts +45 -0
  152. package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/stream/route.ts +170 -0
  153. package/packages/web-dashboard/src/app/api/features/[id]/file-diff/route.ts +144 -0
  154. package/packages/web-dashboard/src/app/api/features/[id]/log-stream/route.ts +167 -0
  155. package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/[filename]/route.ts +65 -0
  156. package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/route.ts +63 -0
  157. package/packages/web-dashboard/src/app/api/features/[id]/timeline/route.ts +60 -0
  158. package/packages/web-dashboard/src/app/feature/[id]/page.tsx +32 -11
  159. package/packages/web-dashboard/src/app/globals.css +2 -0
  160. package/packages/web-dashboard/src/components/detail-panel.tsx +483 -0
  161. package/packages/web-dashboard/src/components/review-workspace.tsx +1162 -0
  162. package/packages/web-dashboard/src/lib/aop-client.ts +725 -0
  163. package/packages/web-dashboard/src/lib/review-contracts.ts +182 -0
  164. package/packages/web-dashboard/src/lib/review-workspace-logic.ts +64 -0
  165. package/packages/web-dashboard/src/lib/types.ts +131 -0
  166. package/packages/web-dashboard/src/styles/dashboard.module.css +333 -0
  167. package/spec-files/completed/agentic_orchestrator_execution_mode_spec.md +1905 -0
  168. package/spec-files/outstanding/agentic_orchestrator_runtime_inspection_spec.md +940 -0
  169. package/spec-files/outstanding/execution_mode_critical_review.md +355 -0
  170. package/spec-files/outstanding/shadow_workspace_implementation_spec.md +1271 -0
  171. package/spec-files/outstanding/shadow_workspace_spec_summary.md +222 -0
  172. package/spec-files/progress.md +269 -1
@@ -8,12 +8,9 @@ import {
8
8
  atomicWriteFile,
9
9
  withFileLock,
10
10
  nowIso,
11
- stableHash,
12
11
  } from './fs.js';
13
- import { SchemaRegistry, loadAndValidateYaml } from './schemas.js';
14
- import { normalizeRepoPath } from './path-rules.js';
12
+ import { SchemaRegistry } from './schemas.js';
15
13
  import { parseFrontMatter, buildFrontMatter } from './frontmatter.js';
16
- import { runGit, runCommand } from './git.js';
17
14
  import { ERROR_CODES } from './error-codes.js';
18
15
  import { ok, fail, withSuggestedActions, type ToolResponse } from './response.js';
19
16
  import {
@@ -22,17 +19,9 @@ import {
22
19
  DEFAULT_ROLE_STATUS,
23
20
  GATE_RESULT,
24
21
  STATUS,
25
- TOOLS,
26
22
  } from './constants.js';
27
- import { AopPathLayout, ensureAopRuntimeLayout } from './path-layout.js';
28
- import {
29
- applyWorktreeSymlinks,
30
- formatWorkspaceHookWarning,
31
- runWorktreePostCreate,
32
- type WorkspaceHookWarning,
33
- } from './workspace-hooks.js';
23
+ import { AopPathLayout } from './path-layout.js';
34
24
  import type { RuntimeSessionsSnapshot } from './runtime-sessions.js';
35
- import { ToolRegistryLoader } from '../mcp/tool-registry-loader.js';
36
25
  import {
37
26
  ToolHandlerRegistry,
38
27
  ToolRouter,
@@ -47,7 +36,7 @@ import { ReportingService } from '../application/services/reporting-service.js';
47
36
  import { FeatureStateService } from '../application/services/feature-state-service.js';
48
37
  import { FeatureLifecycleService } from '../application/services/feature-lifecycle-service.js';
49
38
  import { PlanService } from '../application/services/plan-service.js';
50
- import { PatchService } from '../application/services/patch-service.js';
39
+ import { PatchService, type PatchValidationResult } from '../application/services/patch-service.js';
51
40
  import { GateService } from '../application/services/gate-service.js';
52
41
  import { QaIndexService } from '../application/services/qa-index-service.js';
53
42
  import { MergeService } from '../application/services/merge-service.js';
@@ -62,112 +51,27 @@ import {
62
51
  type FeatureOutcome,
63
52
  } from '../application/services/performance-analytics-service.js';
64
53
  import { GateSelectionService } from '../application/services/gate-selection-service.js';
65
- import { loadComposedPolicy } from '../application/services/policy-loader-service.js';
66
- import {
67
- ACTIVITY_DETECTOR_SLOT,
68
- NOTIFICATION_CHANNEL_SLOT,
69
- SCM_PROVIDER_SLOT,
70
- globalAdapterRegistry,
71
- } from '../application/adapters/adapter-registry.js';
72
54
  import type { WorkerProvider } from '../providers/providers.js';
73
-
74
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
- type AnyRecord = Record<string, any>;
76
-
77
- interface KernelContext {
78
- actor_type?: string;
79
- actor_id?: string;
80
- }
81
-
82
- interface KernelConfigOverrides {
83
- policyPath?: string;
84
- gatesPath?: string;
85
- agentsPath?: string;
86
- adaptersPath?: string;
87
- }
88
-
89
- export interface AgentsRoleConfig {
90
- system_prompt_path?: string;
91
- }
92
-
93
- export interface AgentsRuntimeConfig {
94
- default_provider?: string;
95
- default_model?: string;
96
- provider_config_env?: string;
97
- default_agent_config?: Record<string, unknown>;
98
- provider_configs?: Record<string, Record<string, unknown>>;
99
- worker_provider_mode?: 'live' | 'stub' | string;
100
- worker_response_timeout_ms?: number;
101
- max_consecutive_no_progress_iterations?: number;
102
- }
103
-
104
- export interface AgentsConfigSnapshot {
105
- version?: number;
106
- roles?: Record<string, AgentsRoleConfig>;
107
- missing_prompt_behavior?: 'ignore' | 'error' | string;
108
- runtime?: AgentsRuntimeConfig;
109
- }
110
-
111
- function asArray<T = unknown>(value: unknown): T[] {
112
- return Array.isArray(value) ? (value as T[]) : [];
113
- }
114
-
115
- function readStringField(record: AnyRecord, key: string): string | null {
116
- const value = record[key];
117
- return typeof value === 'string' ? value : null;
118
- }
119
-
120
- function readNumberField(record: AnyRecord, key: string): number | null {
121
- const value = record[key];
122
- return typeof value === 'number' && Number.isFinite(value) ? value : null;
123
- }
124
-
125
- function readPositiveIntegerField(record: AnyRecord, key: string): number | null {
126
- const value = record[key];
127
- if (typeof value !== 'number' || !Number.isFinite(value) || value < 1) {
128
- return null;
129
- }
130
- return Math.floor(value);
131
- }
132
-
133
- function readBooleanField(record: AnyRecord, key: string): boolean | null {
134
- const value = record[key];
135
- return typeof value === 'boolean' ? value : null;
136
- }
137
-
138
- function readObjectField(record: AnyRecord, key: string): AnyRecord {
139
- const value = record[key];
140
- return value && typeof value === 'object' ? (value as AnyRecord) : {};
141
- }
142
-
143
- interface PolicyConfigSnapshot extends AnyRecord {
144
- rbac?: Record<string, string[]>;
145
- }
146
-
147
- function normalizeSet(array: string[]): string[] {
148
- return [...new Set(array)].sort((a, b) => a.localeCompare(b));
149
- }
150
-
151
- function validateAgentRuntimeTimeoutRelationships(agentsConfig: AnyRecord): void {
152
- const runtime = readObjectField(agentsConfig, 'runtime');
153
- if (Object.keys(runtime).length === 0) {
154
- return;
155
- }
156
- const responseTimeoutMs = readPositiveIntegerField(runtime, 'worker_response_timeout_ms');
157
- const spawnTimeoutMs = readPositiveIntegerField(runtime, 'worker_spawn_timeout_ms');
158
- const idleTimeoutMs = readPositiveIntegerField(runtime, 'worker_idle_timeout_ms');
159
-
160
- if (responseTimeoutMs != null && spawnTimeoutMs != null && spawnTimeoutMs >= responseTimeoutMs) {
161
- throw new Error(
162
- 'invalid_agents_yaml:runtime.worker_spawn_timeout_ms must be less than runtime.worker_response_timeout_ms',
163
- );
164
- }
165
- if (responseTimeoutMs != null && idleTimeoutMs != null && idleTimeoutMs > responseTimeoutMs) {
166
- throw new Error(
167
- 'invalid_agents_yaml:runtime.worker_idle_timeout_ms must be less than or equal to runtime.worker_response_timeout_ms',
168
- );
169
- }
170
- }
55
+ import type {
56
+ AnyRecord,
57
+ KernelContext,
58
+ KernelConfigOverrides,
59
+ AgentsConfigSnapshot,
60
+ PolicyConfigSnapshot,
61
+ } from './kernel-types.js';
62
+ export type { AgentsConfigSnapshot } from './kernel-types.js';
63
+ import { readStringField } from './utils/field-readers.js';
64
+ import {
65
+ emptyRuntimeSessions as utilEmptyRuntimeSessions,
66
+ normalizeRuntimeSessions as utilNormalizeRuntimeSessions,
67
+ normalizeIndexShape as utilNormalizeIndexShape,
68
+ isRunLeaseFresh as utilIsRunLeaseFresh,
69
+ } from './utils/index-normalizer.js';
70
+ import { normalizeRepoPathForState } from './utils/path-normalizers.js';
71
+ import { ConfigurationService } from '../application/configuration-service.js';
72
+ import { RepoOperationsService } from '../application/services/repo-operations-service.js';
73
+ import { FeatureSendMessageService } from '../application/services/feature-send-message-service.js';
74
+ import { registerKernelTools } from '../application/kernel-tool-wiring.js';
171
75
 
172
76
  /**
173
77
  * Deterministic orchestration kernel for multi-agent feature development.
@@ -230,6 +134,8 @@ export class AopKernel {
230
134
  private readonly costTrackingService: CostTrackingService;
231
135
  private readonly performanceAnalyticsService: PerformanceAnalyticsService;
232
136
  private readonly gateSelectionService: GateSelectionService;
137
+ private readonly repoOperationsService: RepoOperationsService;
138
+ private readonly sendMessageService: FeatureSendMessageService;
233
139
  private readonly pathLayout: AopPathLayout;
234
140
  readonly instanceId: string;
235
141
  private provider: WorkerProvider | null = null;
@@ -259,7 +165,7 @@ export class AopKernel {
259
165
  this.adaptersConfig = {};
260
166
  this.toolRegistry = null;
261
167
  this.toolHandlers = new ToolHandlerRegistry();
262
- this.registerToolHandlers();
168
+ registerKernelTools(this.toolHandlers, this);
263
169
  this.runLeaseService = new RunLeaseService(this);
264
170
  this.collisionQueueService = new CollisionQueueService(this);
265
171
  this.lockService = new LockService(this);
@@ -275,6 +181,12 @@ export class AopKernel {
275
181
  this.costTrackingService = new CostTrackingService(this);
276
182
  this.performanceAnalyticsService = new PerformanceAnalyticsService(this);
277
183
  this.gateSelectionService = new GateSelectionService(this);
184
+ this.repoOperationsService = new RepoOperationsService(this);
185
+ this.sendMessageService = new FeatureSendMessageService({
186
+ readState: (id: string) => this.readState(id),
187
+ getRuntimeSessions: () => this.getRuntimeSessions(),
188
+ getProvider: () => this.provider,
189
+ });
278
190
  this.toolRouter = new ToolRouter(this.toolHandlers, (toolName) =>
279
191
  Promise.resolve(
280
192
  fail(ERROR_CODES.INVALID_ARGUMENT, `Unknown tool ${toolName}`, {
@@ -342,229 +254,34 @@ export class AopKernel {
342
254
  }
343
255
 
344
256
  emptyRuntimeSessions(at = nowIso()): RuntimeSessionsSnapshot {
345
- return {
346
- run_id: 'none',
347
- orchestrator_session_id: 'unknown',
348
- provider: 'unknown',
349
- model: 'unknown',
350
- provider_config_ref_hash: stableHash('none'),
351
- owner_instance_id: 'none',
352
- lease_id: 'none',
353
- started_at: at,
354
- last_heartbeat_at: at,
355
- lease_expires_at: at,
356
- orchestrator_epoch: 0,
357
- feature_sessions: {},
358
- };
257
+ return utilEmptyRuntimeSessions(at);
359
258
  }
360
259
 
361
260
  normalizeRuntimeSessions(value: unknown, at = nowIso()): RuntimeSessionsSnapshot {
362
- const fallback = this.emptyRuntimeSessions(at);
363
- const source = value && typeof value === 'object' ? (value as Record<string, unknown>) : {};
364
- const featureSessionsInput =
365
- source.feature_sessions && typeof source.feature_sessions === 'object'
366
- ? (source.feature_sessions as Record<string, unknown>)
367
- : {};
368
- const featureSessions: RuntimeSessionsSnapshot['feature_sessions'] = {};
369
-
370
- for (const [featureId, raw] of Object.entries(featureSessionsInput)) {
371
- if (!featureId || typeof raw !== 'object' || !raw) {
372
- continue;
373
- }
374
- const typed = raw as Record<string, unknown>;
375
- featureSessions[featureId] = {
376
- planner_session_id:
377
- typeof typed.planner_session_id === 'string' ? typed.planner_session_id : 'unassigned',
378
- builder_session_id:
379
- typeof typed.builder_session_id === 'string' ? typed.builder_session_id : 'unassigned',
380
- qa_session_id: typeof typed.qa_session_id === 'string' ? typed.qa_session_id : 'unassigned',
381
- };
382
- }
383
-
384
- const epoch =
385
- typeof source.orchestrator_epoch === 'number' && Number.isFinite(source.orchestrator_epoch)
386
- ? Math.max(0, Math.floor(source.orchestrator_epoch))
387
- : 0;
388
-
389
- return {
390
- run_id: typeof source.run_id === 'string' && source.run_id ? source.run_id : fallback.run_id,
391
- orchestrator_session_id:
392
- typeof source.orchestrator_session_id === 'string' && source.orchestrator_session_id
393
- ? source.orchestrator_session_id
394
- : fallback.orchestrator_session_id,
395
- provider:
396
- typeof source.provider === 'string' && source.provider
397
- ? source.provider
398
- : fallback.provider,
399
- model: typeof source.model === 'string' && source.model ? source.model : fallback.model,
400
- provider_config_ref_hash:
401
- typeof source.provider_config_ref_hash === 'string' && source.provider_config_ref_hash
402
- ? source.provider_config_ref_hash
403
- : fallback.provider_config_ref_hash,
404
- owner_instance_id:
405
- typeof source.owner_instance_id === 'string' && source.owner_instance_id
406
- ? source.owner_instance_id
407
- : fallback.owner_instance_id,
408
- lease_id:
409
- typeof source.lease_id === 'string' && source.lease_id
410
- ? source.lease_id
411
- : fallback.lease_id,
412
- started_at:
413
- typeof source.started_at === 'string' && source.started_at
414
- ? source.started_at
415
- : fallback.started_at,
416
- last_heartbeat_at:
417
- typeof source.last_heartbeat_at === 'string' && source.last_heartbeat_at
418
- ? source.last_heartbeat_at
419
- : fallback.last_heartbeat_at,
420
- lease_expires_at:
421
- typeof source.lease_expires_at === 'string' && source.lease_expires_at
422
- ? source.lease_expires_at
423
- : fallback.lease_expires_at,
424
- orchestrator_epoch: epoch,
425
- feature_sessions: featureSessions,
426
- };
261
+ return utilNormalizeRuntimeSessions(value, at);
427
262
  }
428
263
 
429
264
  normalizeIndexShape(value: unknown): AnyRecord {
430
- const now = nowIso();
431
- const source = value && typeof value === 'object' ? (value as Record<string, unknown>) : {};
432
- return {
433
- version:
434
- typeof source.version === 'number' && Number.isFinite(source.version)
435
- ? Math.max(1, Math.floor(source.version))
436
- : 1,
437
- active: normalizeSet(
438
- asArray<string>(source.active).filter((item) => typeof item === 'string'),
439
- ),
440
- blocked: normalizeSet(
441
- asArray<string>(source.blocked).filter((item) => typeof item === 'string'),
442
- ),
443
- merged: normalizeSet(
444
- asArray<string>(source.merged).filter((item) => typeof item === 'string'),
445
- ),
446
- locks: source.locks && typeof source.locks === 'object' ? source.locks : {},
447
- lock_leases:
448
- source.lock_leases && typeof source.lock_leases === 'object' ? source.lock_leases : {},
449
- blocked_queue: asArray(source.blocked_queue).filter(
450
- (item) => item && typeof item === 'object',
451
- ),
452
- dep_blocked: asArray(source.dep_blocked).filter((item) => item && typeof item === 'object'),
453
- updated_at:
454
- typeof source.updated_at === 'string' && source.updated_at ? source.updated_at : now,
455
- runtime_sessions: this.normalizeRuntimeSessions(source.runtime_sessions, now),
456
- };
265
+ return utilNormalizeIndexShape(value);
457
266
  }
458
267
 
459
268
  isRunLeaseFresh(runtimeSessions: RuntimeSessionsSnapshot): boolean {
460
- const expiry = new Date(runtimeSessions.lease_expires_at).getTime();
461
- if (!Number.isFinite(expiry)) {
462
- return false;
463
- }
464
- return expiry > Date.now();
465
- }
466
-
467
- private async resolveDefaultConfigPath(fileName: string): Promise<string> {
468
- const primary = path.join(this.pathLayout.orchestratorRoot, fileName);
469
- if (await pathExists(primary)) {
470
- return primary;
471
- }
472
- const legacy = path.join(this.pathLayout.legacyOrchestratorRoot, fileName);
473
- if (await pathExists(legacy)) {
474
- return legacy;
475
- }
476
- return primary;
269
+ return utilIsRunLeaseFresh(runtimeSessions);
477
270
  }
478
271
 
479
272
  async load() {
480
- await ensureAopRuntimeLayout(this.pathLayout);
481
-
482
- const gatesPath =
483
- this.configOverrides.gatesPath ?? (await this.resolveDefaultConfigPath('gates.yaml'));
484
- const policyPath =
485
- this.configOverrides.policyPath ?? (await this.resolveDefaultConfigPath('policy.yaml'));
486
- const agentsPath =
487
- this.configOverrides.agentsPath ?? (await this.resolveDefaultConfigPath('agents.yaml'));
488
- const adaptersPath =
489
- this.configOverrides.adaptersPath ?? (await this.resolveDefaultConfigPath('adapters.yaml'));
490
-
491
- const gates = await loadAndValidateYaml(this.schemaRegistry, 'gates.schema.json', gatesPath);
492
- if (!gates.validation.valid) {
493
- throw new Error(`invalid_gates_yaml:${JSON.stringify(gates.validation.errors)}`);
494
- }
495
-
496
- const { mergedPolicy } = await loadComposedPolicy(
273
+ const configService = new ConfigurationService(
497
274
  this.repoRoot,
498
- policyPath,
275
+ this.pathLayout,
499
276
  this.schemaRegistry,
500
- );
501
- const parsedPolicy = mergedPolicy as PolicyConfigSnapshot;
502
- const implementation = readObjectField(parsedPolicy, 'implementation');
503
- const testing = readObjectField(parsedPolicy, 'testing');
504
-
505
- if (readStringField(implementation, 'workspace') !== 'nx') {
506
- throw new Error(ERROR_CODES.INVALID_WORKSPACE_IMPLEMENTATION);
507
- }
508
- if (readStringField(testing, 'framework') !== 'vitest') {
509
- throw new Error(ERROR_CODES.INVALID_WORKSPACE_IMPLEMENTATION);
510
- }
511
-
512
- const agentsExists = await pathExists(agentsPath);
513
- let agents = { parsed: { version: 1, roles: {} }, validation: { valid: true, errors: [] } };
514
- if (agentsExists) {
515
- agents = await loadAndValidateYaml(this.schemaRegistry, 'agents.schema.json', agentsPath);
516
- if (!agents.validation.valid) {
517
- throw new Error(`invalid_agents_yaml:${JSON.stringify(agents.validation.errors)}`);
518
- }
519
- validateAgentRuntimeTimeoutRelationships(agents.parsed as AnyRecord);
520
- }
521
-
522
- const adaptersExists = await pathExists(adaptersPath);
523
- let adapters = { parsed: {}, validation: { valid: true, errors: [] as unknown[] } };
524
- if (adaptersExists) {
525
- adapters = await loadAndValidateYaml(
526
- this.schemaRegistry,
527
- 'adapters.schema.json',
528
- adaptersPath,
529
- );
530
- if (!adapters.validation.valid) {
531
- throw new Error(`invalid_adapters_yaml:${JSON.stringify(adapters.validation.errors)}`);
532
- }
533
- const parsedAdapters = readObjectField(adapters, 'parsed');
534
- const notificationChannel = readStringField(parsedAdapters, NOTIFICATION_CHANNEL_SLOT.name);
535
- if (notificationChannel) {
536
- try {
537
- globalAdapterRegistry.resolve(NOTIFICATION_CHANNEL_SLOT, notificationChannel, {});
538
- } catch {
539
- throw new Error(
540
- `adapter_not_found:${NOTIFICATION_CHANNEL_SLOT.name}:${notificationChannel}`,
541
- );
542
- }
543
- }
544
- const activityDetector = readStringField(parsedAdapters, ACTIVITY_DETECTOR_SLOT.name);
545
- if (activityDetector) {
546
- try {
547
- globalAdapterRegistry.resolve(ACTIVITY_DETECTOR_SLOT, activityDetector, {});
548
- } catch {
549
- throw new Error(`adapter_not_found:${ACTIVITY_DETECTOR_SLOT.name}:${activityDetector}`);
550
- }
551
- }
552
- const scmProvider = readStringField(parsedAdapters, SCM_PROVIDER_SLOT.name);
553
- if (scmProvider) {
554
- try {
555
- globalAdapterRegistry.resolve(SCM_PROVIDER_SLOT, scmProvider, {});
556
- } catch {
557
- throw new Error(`adapter_not_found:${SCM_PROVIDER_SLOT.name}:${scmProvider}`);
558
- }
559
- }
560
- }
561
-
562
- this.gatesConfig = gates.parsed;
563
- this.policy = parsedPolicy;
564
- this.agentsConfig = agents.parsed as AgentsConfigSnapshot;
565
- this.adaptersConfig = readObjectField(adapters, 'parsed');
566
- const registryLoader = new ToolRegistryLoader(this.repoRoot);
567
- this.toolRegistry = await registryLoader.load();
277
+ this.configOverrides,
278
+ );
279
+ const config = await configService.loadAll();
280
+ this.gatesConfig = config.gatesConfig;
281
+ this.policy = config.policy;
282
+ this.agentsConfig = config.agentsConfig;
283
+ this.adaptersConfig = config.adaptersConfig;
284
+ this.toolRegistry = config.toolRegistry;
568
285
  this.loaded = true;
569
286
  }
570
287
 
@@ -684,208 +401,6 @@ export class AopKernel {
684
401
  return await this.toolRouter.route(toolName, args, context);
685
402
  }
686
403
 
687
- private registerToolHandlers(): void {
688
- this.toolHandlers.register(
689
- TOOLS.FEATURE_DISCOVER_SPECS,
690
- async () => await this.featureDiscoverSpecs(),
691
- );
692
- this.toolHandlers.register(
693
- TOOLS.FEATURE_INIT,
694
- async (args) => await this.featureInit(readStringField(args, 'feature_id')),
695
- );
696
- this.toolHandlers.register(
697
- TOOLS.FEATURE_GET_CONTEXT,
698
- async (args) => await this.featureGetContext(readStringField(args, 'feature_id')),
699
- );
700
- this.toolHandlers.register(
701
- TOOLS.FEATURE_STATE_GET,
702
- async (args) => await this.featureStateGet(readStringField(args, 'feature_id')),
703
- );
704
- this.toolHandlers.register(
705
- TOOLS.FEATURE_STATE_PATCH,
706
- async (args) =>
707
- await this.featureStatePatch(
708
- readStringField(args, 'feature_id'),
709
- readNumberField(args, 'expected_version'),
710
- args.patch,
711
- ),
712
- );
713
- this.toolHandlers.register(
714
- TOOLS.FEATURE_LOG_APPEND,
715
- async (args, context) =>
716
- await this.featureLogAppend(
717
- readStringField(args, 'feature_id'),
718
- readStringField(args, 'note'),
719
- context,
720
- ),
721
- );
722
- this.toolHandlers.register(
723
- TOOLS.PLAN_SUBMIT,
724
- async (args) =>
725
- await this.planSubmit(
726
- readStringField(args, 'feature_id'),
727
- args.plan_json,
728
- readNumberField(args, 'expected_version'),
729
- ),
730
- );
731
- this.toolHandlers.register(
732
- TOOLS.PLAN_GET,
733
- async (args) => await this.planGet(readStringField(args, 'feature_id')),
734
- );
735
- this.toolHandlers.register(
736
- TOOLS.PLAN_UPDATE,
737
- async (args) =>
738
- await this.planUpdate(
739
- readStringField(args, 'feature_id'),
740
- readNumberField(args, 'expected_plan_version'),
741
- args.plan_json,
742
- ),
743
- );
744
- this.toolHandlers.register(
745
- TOOLS.REPO_ENSURE_WORKTREE,
746
- async (args) => await this.repoEnsureWorktree(readStringField(args, 'feature_id')),
747
- );
748
- this.toolHandlers.register(
749
- TOOLS.REPO_APPLY_PATCH,
750
- async (args) =>
751
- await this.repoApplyPatch(
752
- readStringField(args, 'feature_id'),
753
- readStringField(args, 'unified_diff'),
754
- ),
755
- );
756
- this.toolHandlers.register(
757
- TOOLS.REPO_STATUS,
758
- async (args) => await this.repoStatus(readStringField(args, 'feature_id')),
759
- );
760
- this.toolHandlers.register(
761
- TOOLS.REPO_DIFF,
762
- async (args) =>
763
- await this.repoDiff(readStringField(args, 'feature_id'), asArray<string>(args.options)),
764
- );
765
- this.toolHandlers.register(
766
- TOOLS.REPO_READ_FILE,
767
- async (args) =>
768
- await this.repoReadFile(readStringField(args, 'feature_id'), readStringField(args, 'path')),
769
- );
770
- this.toolHandlers.register(
771
- TOOLS.REPO_SEARCH,
772
- async (args) =>
773
- await this.repoSearch(readStringField(args, 'feature_id'), readStringField(args, 'query')),
774
- );
775
- this.toolHandlers.register(
776
- TOOLS.REPO_DIFF_BUNDLE,
777
- async (args) => await this.repoDiffBundle(readStringField(args, 'feature_id')),
778
- );
779
- this.toolHandlers.register(
780
- TOOLS.FEATURE_READY_TO_MERGE,
781
- async (args) =>
782
- await this.featureReadyToMerge(
783
- readStringField(args, 'feature_id'),
784
- readStringField(args, 'commit_message'),
785
- readStringField(args, 'merge_strategy'),
786
- readStringField(args, 'user_approval_token'),
787
- ),
788
- );
789
- this.toolHandlers.register(
790
- TOOLS.FEATURE_DELETE,
791
- async (args) =>
792
- await this.featureDelete(
793
- readStringField(args, 'feature_id'),
794
- readBooleanField(args, 'dry_run'),
795
- readBooleanField(args, 'confirm'),
796
- readBooleanField(args, 'remove_worktree'),
797
- readStringField(args, 'remove_branch'),
798
- ),
799
- );
800
- this.toolHandlers.register(
801
- TOOLS.GATES_LIST,
802
- async (args) => await this.gatesList(readStringField(args, 'profile')),
803
- );
804
- this.toolHandlers.register(
805
- TOOLS.GATES_RUN,
806
- async (args) =>
807
- await this.gatesRun(
808
- readStringField(args, 'feature_id'),
809
- readStringField(args, 'profile'),
810
- readStringField(args, 'mode'),
811
- ),
812
- );
813
- this.toolHandlers.register(
814
- TOOLS.EVIDENCE_LATEST,
815
- async (args) => await this.evidenceLatest(readStringField(args, 'feature_id')),
816
- );
817
- this.toolHandlers.register(
818
- TOOLS.QA_TEST_INDEX_GET,
819
- async (args) => await this.qaTestIndexGet(readStringField(args, 'feature_id')),
820
- );
821
- this.toolHandlers.register(
822
- TOOLS.QA_TEST_INDEX_UPDATE,
823
- async (args) =>
824
- await this.qaTestIndexUpdate(
825
- readStringField(args, 'feature_id'),
826
- readNumberField(args, 'expected_version'),
827
- args.updates,
828
- asArray(args.evidence_refs),
829
- ),
830
- );
831
- this.toolHandlers.register(
832
- TOOLS.LOCKS_ACQUIRE,
833
- async (args) =>
834
- await this.locksAcquire(
835
- readStringField(args, 'resource'),
836
- readStringField(args, 'feature_id'),
837
- readNumberField(args, 'wait_timeout_seconds'),
838
- ),
839
- );
840
- this.toolHandlers.register(
841
- TOOLS.LOCKS_RELEASE,
842
- async (args) =>
843
- await this.locksRelease(
844
- readStringField(args, 'resource'),
845
- readStringField(args, 'feature_id'),
846
- ),
847
- );
848
- this.toolHandlers.register(TOOLS.COLLISIONS_SCAN, async () => await this.collisionsScan());
849
- this.toolHandlers.register(TOOLS.REPORT_DASHBOARD, async () => await this.reportDashboard());
850
- this.toolHandlers.register(
851
- TOOLS.REPORT_FEATURE_SUMMARY,
852
- async (args) => await this.reportFeatureSummary(readStringField(args, 'feature_id')),
853
- );
854
- this.toolHandlers.register(
855
- TOOLS.FEATURE_SEND_MESSAGE,
856
- async (args) =>
857
- await this.featureSendMessage(
858
- readStringField(args, 'feature_id'),
859
- readStringField(args, 'message'),
860
- ),
861
- );
862
- this.toolHandlers.register(
863
- TOOLS.COST_RECORD,
864
- async (args) =>
865
- await this.costRecord(
866
- readStringField(args, 'feature_id'),
867
- typeof args.tokens_used_delta === 'number' ? args.tokens_used_delta : 0,
868
- typeof args.estimated_cost_usd_delta === 'number' ? args.estimated_cost_usd_delta : 0,
869
- ),
870
- );
871
- this.toolHandlers.register(
872
- TOOLS.COST_GET,
873
- async (args) => await this.costGet(readStringField(args, 'feature_id')),
874
- );
875
- this.toolHandlers.register(
876
- TOOLS.PERFORMANCE_RECORD_OUTCOME,
877
- async (args) => await this.performanceRecordOutcome(args),
878
- );
879
- this.toolHandlers.register(
880
- TOOLS.PERFORMANCE_GET_ANALYTICS,
881
- async (args) =>
882
- await this.performanceGetAnalytics(
883
- readStringField(args, 'provider'),
884
- readStringField(args, 'model'),
885
- ),
886
- );
887
- }
888
-
889
404
  featurePath(featureId) {
890
405
  return this.pathLayout.featureRoot(featureId);
891
406
  }
@@ -946,12 +461,15 @@ export class AopKernel {
946
461
  }
947
462
 
948
463
  makeDefaultState(featureId, branch, worktreePath) {
464
+ const configuredExecutionMode =
465
+ this.agentsConfig.runtime?.execution_mode === 'interactive' ? 'interactive' : 'deterministic';
949
466
  return {
950
467
  feature_id: featureId,
951
468
  version: 1,
952
469
  branch,
953
470
  worktree_path: normalizeRepoPathForState(this.repoRoot, worktreePath),
954
471
  status: STATUS.PLANNING,
472
+ execution_mode: configuredExecutionMode,
955
473
  gate_profile: 'default',
956
474
  gates: {
957
475
  plan: GATE_RESULT.NA,
@@ -969,6 +487,7 @@ export class AopKernel {
969
487
  },
970
488
  cluster: { ...DEFAULT_CLUSTER },
971
489
  role_status: { ...DEFAULT_ROLE_STATUS },
490
+ checkpoints: [],
972
491
  last_updated: nowIso(),
973
492
  };
974
493
  }
@@ -1256,106 +775,7 @@ export class AopKernel {
1256
775
  }
1257
776
 
1258
777
  async repoEnsureWorktree(featureId) {
1259
- const worktree = this.worktreePath(featureId);
1260
- const branch = featureId;
1261
- await ensureDir(path.join(this.repoRoot, '.worktrees'));
1262
-
1263
- if (await pathExists(worktree)) {
1264
- return {
1265
- data: {
1266
- feature_id: featureId,
1267
- branch,
1268
- worktree_path_abs: worktree,
1269
- existed: true,
1270
- },
1271
- };
1272
- }
1273
-
1274
- const baseBranch = this.policy.worktree.base_branch;
1275
- const baseCheck = await runGit(this.repoRoot, ['rev-parse', '--verify', baseBranch]);
1276
- const baseRef = baseCheck.code === 0 ? baseBranch : 'HEAD';
1277
-
1278
- const branchCheck = await runGit(this.repoRoot, ['rev-parse', '--verify', branch]);
1279
- if (branchCheck.code !== 0) {
1280
- const branchCreate = await runGit(this.repoRoot, ['branch', branch, baseRef]);
1281
- if (branchCreate.code !== 0) {
1282
- throw {
1283
- normalizedResponse: fail(
1284
- ERROR_CODES.GIT_FAILURE,
1285
- 'Unable to create feature branch',
1286
- {
1287
- feature_id: featureId,
1288
- stderr: branchCreate.stderr,
1289
- retryable: false,
1290
- requires_human: true,
1291
- },
1292
- {
1293
- command: ['git', 'branch', branch, baseRef],
1294
- exit_code: branchCreate.code,
1295
- },
1296
- ),
1297
- };
1298
- }
1299
- }
1300
-
1301
- const addWorktree = await runGit(this.repoRoot, ['worktree', 'add', worktree, branch]);
1302
- if (addWorktree.code !== 0) {
1303
- throw {
1304
- normalizedResponse: fail(
1305
- ERROR_CODES.GIT_FAILURE,
1306
- 'Unable to create git worktree',
1307
- {
1308
- feature_id: featureId,
1309
- stderr: addWorktree.stderr,
1310
- retryable: false,
1311
- requires_human: true,
1312
- },
1313
- {
1314
- command: ['git', 'worktree', 'add', worktree, branch],
1315
- exit_code: addWorktree.code,
1316
- },
1317
- ),
1318
- };
1319
- }
1320
-
1321
- const worktreeConfig = this.policy.worktree as
1322
- | {
1323
- base_branch: string;
1324
- symlinks?: string[];
1325
- post_create?: string[];
1326
- }
1327
- | undefined;
1328
- const hookWarnings: WorkspaceHookWarning[] = [];
1329
- const collectHookWarning = (warning: WorkspaceHookWarning) => {
1330
- hookWarnings.push(warning);
1331
- };
1332
-
1333
- if (worktreeConfig?.symlinks?.length) {
1334
- await applyWorktreeSymlinks(
1335
- this.repoRoot,
1336
- worktree,
1337
- worktreeConfig.symlinks,
1338
- collectHookWarning,
1339
- );
1340
- }
1341
-
1342
- if (worktreeConfig?.post_create?.length) {
1343
- await runWorktreePostCreate(worktree, worktreeConfig.post_create, collectHookWarning);
1344
- }
1345
-
1346
- for (const warning of hookWarnings) {
1347
- // Preserve non-fatal behavior while making hook failures observable.
1348
- console.warn(`[aop] workspace hook warning: ${formatWorkspaceHookWarning(warning)}`);
1349
- }
1350
-
1351
- return {
1352
- data: {
1353
- feature_id: featureId,
1354
- branch,
1355
- worktree_path_abs: worktree,
1356
- existed: false,
1357
- },
1358
- };
778
+ return this.repoOperationsService.repoEnsureWorktree(featureId);
1359
779
  }
1360
780
 
1361
781
  async loadAcceptedPlan(featureId) {
@@ -1370,145 +790,28 @@ export class AopKernel {
1370
790
  return await this.patchService.repoApplyPatch(featureId, unifiedDiff);
1371
791
  }
1372
792
 
1373
- async repoStatus(featureId) {
1374
- const worktree = this.worktreePath(featureId);
1375
- const status = await runGit(this.repoRoot, ['status', '--porcelain'], { cwd: worktree });
1376
- const branch = await runGit(this.repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], {
1377
- cwd: worktree,
1378
- });
793
+ async validatePatchDiff(featureId: string, parsedDiff: unknown): Promise<PatchValidationResult> {
794
+ return await this.patchService.validateDiff(featureId, parsedDiff);
795
+ }
1379
796
 
1380
- return {
1381
- data: {
1382
- feature_id: featureId,
1383
- branch: branch.stdout.trim(),
1384
- status_porcelain: status.stdout.trim().split('\n').filter(Boolean),
1385
- },
1386
- };
797
+ async repoStatus(featureId) {
798
+ return this.repoOperationsService.repoStatus(featureId);
1387
799
  }
1388
800
 
1389
801
  async repoDiff(featureId, options = []) {
1390
- const safeOptions = asArray<string>(options).filter(
1391
- (option) => typeof option === 'string' && option.startsWith('--'),
1392
- );
1393
- const diff = await runGit(this.repoRoot, ['diff', ...safeOptions], {
1394
- cwd: this.worktreePath(featureId),
1395
- });
1396
- return {
1397
- data: {
1398
- feature_id: featureId,
1399
- diff: diff.stdout,
1400
- },
1401
- };
802
+ return this.repoOperationsService.repoDiff(featureId, options);
1402
803
  }
1403
804
 
1404
805
  async repoReadFile(featureId, filePath) {
1405
- const normalized = await normalizeRepoPath(
1406
- this.repoRoot,
1407
- path.join(this.worktreePath(featureId), filePath),
1408
- this.policy.path_rules.allow_symlink_traversal,
1409
- ).then((relative) =>
1410
- normalizeFromWorktree(this.worktreePath(featureId), this.repoRoot, relative),
1411
- );
1412
- const absolute = path.join(this.repoRoot, normalized);
1413
- const exists = await pathExists(absolute);
1414
- if (!exists) {
1415
- throw {
1416
- normalizedResponse: fail(ERROR_CODES.FILE_NOT_FOUND, 'File not found', {
1417
- path: normalized,
1418
- retryable: false,
1419
- requires_human: false,
1420
- }),
1421
- };
1422
- }
1423
- const content = await fs.readFile(absolute, 'utf8');
1424
- return {
1425
- data: {
1426
- feature_id: featureId,
1427
- path: normalized,
1428
- content,
1429
- },
1430
- };
806
+ return this.repoOperationsService.repoReadFile(featureId, filePath);
1431
807
  }
1432
808
 
1433
809
  async repoSearch(featureId, query) {
1434
- const worktree = this.worktreePath(featureId);
1435
- const rgResult = await runCommand('rg', ['-n', '--no-heading', query, '.'], {
1436
- cwd: worktree,
1437
- timeoutMs: 30_000,
1438
- });
1439
-
1440
- if (rgResult.code === 127) {
1441
- throw {
1442
- normalizedResponse: fail(
1443
- ERROR_CODES.GIT_FAILURE,
1444
- 'ripgrep (rg) not found - required for search functionality',
1445
- {
1446
- stderr: rgResult.stderr,
1447
- retryable: false,
1448
- requires_human: true,
1449
- },
1450
- ),
1451
- };
1452
- }
1453
-
1454
- if (rgResult.code !== 0 && rgResult.code !== 1) {
1455
- throw {
1456
- normalizedResponse: fail(ERROR_CODES.GIT_FAILURE, 'Search failed', {
1457
- stderr: rgResult.stderr,
1458
- retryable: true,
1459
- requires_human: false,
1460
- }),
1461
- };
1462
- }
1463
-
1464
- const matches = rgResult.stdout
1465
- .trim()
1466
- .split('\n')
1467
- .filter(Boolean)
1468
- .map((line) => {
1469
- const firstColon = line.indexOf(':');
1470
- const secondColon = line.indexOf(':', firstColon + 1);
1471
- if (firstColon === -1 || secondColon === -1) {
1472
- return { raw: line };
1473
- }
1474
- return {
1475
- path: line.slice(0, firstColon),
1476
- line: Number(line.slice(firstColon + 1, secondColon)),
1477
- snippet: line.slice(secondColon + 1),
1478
- };
1479
- });
1480
-
1481
- return {
1482
- data: {
1483
- feature_id: featureId,
1484
- query,
1485
- matches,
1486
- },
1487
- };
810
+ return this.repoOperationsService.repoSearch(featureId, query);
1488
811
  }
1489
812
 
1490
813
  async repoDiffBundle(featureId) {
1491
- const stat = await runGit(this.repoRoot, ['diff', '--stat'], {
1492
- cwd: this.worktreePath(featureId),
1493
- });
1494
- const full = await runGit(this.repoRoot, ['diff'], { cwd: this.worktreePath(featureId) });
1495
- const names = await runGit(this.repoRoot, ['diff', '--name-only'], {
1496
- cwd: this.worktreePath(featureId),
1497
- });
1498
- const latest = await this.evidenceLatest(featureId);
1499
-
1500
- return {
1501
- data: {
1502
- feature_id: featureId,
1503
- diff_stat: stat.stdout,
1504
- diff: full.stdout,
1505
- touched_files: names.stdout
1506
- .split('\n')
1507
- .map((x) => x.trim())
1508
- .filter(Boolean),
1509
- last_gate_summary: latest.data?.latest ?? null,
1510
- },
1511
- };
814
+ return this.repoOperationsService.repoDiffBundle(featureId);
1512
815
  }
1513
816
 
1514
817
  async gatesList(profileName = null) {
@@ -1592,107 +895,8 @@ export class AopKernel {
1592
895
  return await this.lockService.recoverFromState();
1593
896
  }
1594
897
 
1595
- private async waitForSessionToBecomeActive(sessionId: string): Promise<void> {
1596
- if (!this.provider?.getSessionInfo) {
1597
- return;
1598
- }
1599
-
1600
- const timeoutMs = 5000;
1601
- const pollIntervalMs = 250;
1602
- const deadline = Date.now() + timeoutMs;
1603
-
1604
- while (Date.now() <= deadline) {
1605
- const sessionInfo = await this.provider.getSessionInfo(sessionId).catch(() => null);
1606
- if (sessionInfo?.active) {
1607
- return;
1608
- }
1609
- await new Promise<void>((resolve) => {
1610
- setTimeout(resolve, pollIntervalMs);
1611
- });
1612
- }
1613
- }
1614
-
1615
898
  async featureSendMessage(featureId: string | null, message: string | null): Promise<unknown> {
1616
- if (!featureId) {
1617
- throw {
1618
- normalizedResponse: fail(ERROR_CODES.INVALID_ARGUMENT, 'feature_id is required', {
1619
- retryable: false,
1620
- requires_human: false,
1621
- }),
1622
- };
1623
- }
1624
- if (!message) {
1625
- throw {
1626
- normalizedResponse: fail(
1627
- ERROR_CODES.INVALID_ARGUMENT,
1628
- 'message is required and must not be empty',
1629
- { retryable: false, requires_human: false },
1630
- ),
1631
- };
1632
- }
1633
-
1634
- const runtimeSessions = await this.getRuntimeSessions();
1635
- const featureSession = runtimeSessions.feature_sessions?.[featureId];
1636
- if (!featureSession) {
1637
- throw {
1638
- normalizedResponse: {
1639
- ok: false,
1640
- error: { code: 'session_not_found', message: 'No active session cluster for feature' },
1641
- },
1642
- };
1643
- }
1644
-
1645
- const state = await this.readState(featureId);
1646
- const status = typeof state.frontMatter.status === 'string' ? state.frontMatter.status : '';
1647
- const gates = readObjectField(state.frontMatter, 'gates');
1648
-
1649
- let targetRole = 'orchestrator';
1650
- let targetSessionId = runtimeSessions.orchestrator_session_id;
1651
-
1652
- if (status === STATUS.PLANNING) {
1653
- targetRole = 'planner';
1654
- targetSessionId = featureSession.planner_session_id;
1655
- } else if (status === STATUS.BUILDING) {
1656
- targetRole = 'builder';
1657
- targetSessionId = featureSession.builder_session_id;
1658
- } else if (status === STATUS.QA || status === STATUS.READY_TO_MERGE) {
1659
- targetRole = 'qa';
1660
- targetSessionId = featureSession.qa_session_id;
1661
- } else if (status === STATUS.BLOCKED) {
1662
- const fastGate = readStringField(gates, 'fast');
1663
- const fullGate = readStringField(gates, 'full');
1664
- if (fastGate === GATE_RESULT.FAIL && fullGate !== GATE_RESULT.FAIL) {
1665
- targetRole = 'builder';
1666
- targetSessionId = featureSession.builder_session_id;
1667
- } else {
1668
- targetRole = 'qa';
1669
- targetSessionId = featureSession.qa_session_id;
1670
- }
1671
- }
1672
-
1673
- if (!targetSessionId || targetSessionId === 'unassigned' || targetSessionId === 'unknown') {
1674
- targetRole = 'orchestrator';
1675
- targetSessionId = runtimeSessions.orchestrator_session_id;
1676
- }
1677
-
1678
- if (!this.provider?.sendMessage) {
1679
- throw {
1680
- normalizedResponse: {
1681
- ok: false,
1682
- error: { code: 'provider_unsupported', message: 'Provider does not support sendMessage' },
1683
- },
1684
- };
1685
- }
1686
-
1687
- await this.waitForSessionToBecomeActive(targetSessionId);
1688
- await this.provider.sendMessage(targetSessionId, message);
1689
- return {
1690
- feature_id: featureId,
1691
- session_id: targetSessionId,
1692
- target_role: targetRole,
1693
- status,
1694
- delivered: true,
1695
- };
899
+ return this.sendMessageService.featureSendMessage(featureId, message);
1696
900
  }
1697
901
 
1698
902
  async costRecord(featureId: string, tokensDelta: number, costUsdDelta: number) {
@@ -1745,24 +949,3 @@ export class AopKernel {
1745
949
  return { ok: true as const, data: snapshot };
1746
950
  }
1747
951
  }
1748
-
1749
- function normalizeRepoPathForState(repoRoot: string, absolutePath: string) {
1750
- const relative = path.relative(repoRoot, absolutePath).replaceAll('\\\\', '/');
1751
- if (!relative || relative === '.') {
1752
- return '.';
1753
- }
1754
- return relative;
1755
- }
1756
-
1757
- function normalizeFromWorktree(
1758
- worktreePath: string,
1759
- repoRoot: string,
1760
- repoRelativeFromWorktree: string,
1761
- ) {
1762
- const absolute = path.resolve(repoRoot, repoRelativeFromWorktree);
1763
- const maybeRelativeToWorktree = path.relative(worktreePath, absolute).replaceAll('\\\\', '/');
1764
- if (!maybeRelativeToWorktree.startsWith('../')) {
1765
- return maybeRelativeToWorktree;
1766
- }
1767
- return path.relative(repoRoot, absolute).replaceAll('\\\\', '/');
1768
- }