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
@@ -0,0 +1,276 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import { pathExists, ensureDir } from '../../core/fs.js';
4
+ import { runGit, runCommand } from '../../core/git.js';
5
+ import { normalizeRepoPath } from '../../core/path-rules.js';
6
+ import { ERROR_CODES } from '../../core/error-codes.js';
7
+ import { fail } from '../../core/response.js';
8
+ import {
9
+ applyWorktreeSymlinks,
10
+ formatWorkspaceHookWarning,
11
+ runWorktreePostCreate,
12
+ type WorkspaceHookWarning,
13
+ } from '../../core/workspace-hooks.js';
14
+ import { normalizeFromWorktree } from '../../core/utils/path-normalizers.js';
15
+ import { asArray } from '../../core/utils/field-readers.js';
16
+
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ type AnyRecord = Record<string, any>;
19
+
20
+ export interface RepoOperationsServicePort {
21
+ getRepoRoot(): string;
22
+ worktreePath(featureId: string): string;
23
+ getPolicySnapshot(): AnyRecord;
24
+ evidenceLatest(featureId: string): Promise<{ data?: { latest?: unknown } }>;
25
+ }
26
+
27
+ export class RepoOperationsService {
28
+ private readonly port: RepoOperationsServicePort;
29
+
30
+ constructor(port: RepoOperationsServicePort) {
31
+ this.port = port;
32
+ }
33
+
34
+ async repoEnsureWorktree(featureId: string | null) {
35
+ const repoRoot = this.port.getRepoRoot();
36
+ const worktree = this.port.worktreePath(featureId);
37
+ const branch = featureId;
38
+ await ensureDir(path.join(repoRoot, '.worktrees'));
39
+
40
+ if (await pathExists(worktree)) {
41
+ return {
42
+ data: {
43
+ feature_id: featureId,
44
+ branch,
45
+ worktree_path_abs: worktree,
46
+ existed: true,
47
+ },
48
+ };
49
+ }
50
+
51
+ const policy = this.port.getPolicySnapshot();
52
+ const baseBranch = (policy.worktree as { base_branch: string }).base_branch;
53
+ const baseCheck = await runGit(repoRoot, ['rev-parse', '--verify', baseBranch]);
54
+ const baseRef = baseCheck.code === 0 ? baseBranch : 'HEAD';
55
+
56
+ const branchCheck = await runGit(repoRoot, ['rev-parse', '--verify', branch]);
57
+ if (branchCheck.code !== 0) {
58
+ const branchCreate = await runGit(repoRoot, ['branch', branch, baseRef]);
59
+ if (branchCreate.code !== 0) {
60
+ throw {
61
+ normalizedResponse: fail(
62
+ ERROR_CODES.GIT_FAILURE,
63
+ 'Unable to create feature branch',
64
+ {
65
+ feature_id: featureId,
66
+ stderr: branchCreate.stderr,
67
+ retryable: false,
68
+ requires_human: true,
69
+ },
70
+ {
71
+ command: ['git', 'branch', branch, baseRef],
72
+ exit_code: branchCreate.code,
73
+ },
74
+ ),
75
+ };
76
+ }
77
+ }
78
+
79
+ const addWorktree = await runGit(repoRoot, ['worktree', 'add', worktree, branch]);
80
+ if (addWorktree.code !== 0) {
81
+ throw {
82
+ normalizedResponse: fail(
83
+ ERROR_CODES.GIT_FAILURE,
84
+ 'Unable to create git worktree',
85
+ {
86
+ feature_id: featureId,
87
+ stderr: addWorktree.stderr,
88
+ retryable: false,
89
+ requires_human: true,
90
+ },
91
+ {
92
+ command: ['git', 'worktree', 'add', worktree, branch],
93
+ exit_code: addWorktree.code,
94
+ },
95
+ ),
96
+ };
97
+ }
98
+
99
+ const worktreeConfig = policy.worktree as
100
+ | {
101
+ base_branch: string;
102
+ symlinks?: string[];
103
+ post_create?: string[];
104
+ }
105
+ | undefined;
106
+ const hookWarnings: WorkspaceHookWarning[] = [];
107
+ const collectHookWarning = (warning: WorkspaceHookWarning) => {
108
+ hookWarnings.push(warning);
109
+ };
110
+
111
+ if (worktreeConfig?.symlinks?.length) {
112
+ await applyWorktreeSymlinks(repoRoot, worktree, worktreeConfig.symlinks, collectHookWarning);
113
+ }
114
+
115
+ if (worktreeConfig?.post_create?.length) {
116
+ await runWorktreePostCreate(worktree, worktreeConfig.post_create, collectHookWarning);
117
+ }
118
+
119
+ for (const warning of hookWarnings) {
120
+ // Preserve non-fatal behavior while making hook failures observable.
121
+ console.warn(`[aop] workspace hook warning: ${formatWorkspaceHookWarning(warning)}`);
122
+ }
123
+
124
+ return {
125
+ data: {
126
+ feature_id: featureId,
127
+ branch,
128
+ worktree_path_abs: worktree,
129
+ existed: false,
130
+ },
131
+ };
132
+ }
133
+
134
+ async repoStatus(featureId: string | null) {
135
+ const repoRoot = this.port.getRepoRoot();
136
+ const worktree = this.port.worktreePath(featureId);
137
+ const status = await runGit(repoRoot, ['status', '--porcelain'], { cwd: worktree });
138
+ const branch = await runGit(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], {
139
+ cwd: worktree,
140
+ });
141
+
142
+ return {
143
+ data: {
144
+ feature_id: featureId,
145
+ branch: branch.stdout.trim(),
146
+ status_porcelain: status.stdout.trim().split('\n').filter(Boolean),
147
+ },
148
+ };
149
+ }
150
+
151
+ async repoDiff(featureId: string | null, options: string[] = []) {
152
+ const repoRoot = this.port.getRepoRoot();
153
+ const safeOptions = asArray<string>(options).filter(
154
+ (option) => typeof option === 'string' && option.startsWith('--'),
155
+ );
156
+ const diff = await runGit(repoRoot, ['diff', ...safeOptions], {
157
+ cwd: this.port.worktreePath(featureId),
158
+ });
159
+ return {
160
+ data: {
161
+ feature_id: featureId,
162
+ diff: diff.stdout,
163
+ },
164
+ };
165
+ }
166
+
167
+ async repoReadFile(featureId: string | null, filePath: string | null) {
168
+ const repoRoot = this.port.getRepoRoot();
169
+ const worktree = this.port.worktreePath(featureId);
170
+ const policy = this.port.getPolicySnapshot();
171
+ const normalized = await normalizeRepoPath(
172
+ repoRoot,
173
+ path.join(worktree, filePath),
174
+ (policy.path_rules as { allow_symlink_traversal?: boolean } | undefined)
175
+ ?.allow_symlink_traversal,
176
+ ).then((relative) => normalizeFromWorktree(worktree, repoRoot, relative));
177
+ const absolute = path.join(repoRoot, normalized);
178
+ const exists = await pathExists(absolute);
179
+ if (!exists) {
180
+ throw {
181
+ normalizedResponse: fail(ERROR_CODES.FILE_NOT_FOUND, 'File not found', {
182
+ path: normalized,
183
+ retryable: false,
184
+ requires_human: false,
185
+ }),
186
+ };
187
+ }
188
+ const content = await fs.readFile(absolute, 'utf8');
189
+ return {
190
+ data: {
191
+ feature_id: featureId,
192
+ path: normalized,
193
+ content,
194
+ },
195
+ };
196
+ }
197
+
198
+ async repoSearch(featureId: string | null, query: string | null) {
199
+ const worktree = this.port.worktreePath(featureId);
200
+ const rgResult = await runCommand('rg', ['-n', '--no-heading', query, '.'], {
201
+ cwd: worktree,
202
+ timeoutMs: 30_000,
203
+ });
204
+
205
+ if (rgResult.code === 127) {
206
+ throw {
207
+ normalizedResponse: fail(
208
+ ERROR_CODES.GIT_FAILURE,
209
+ 'ripgrep (rg) not found - required for search functionality',
210
+ {
211
+ stderr: rgResult.stderr,
212
+ retryable: false,
213
+ requires_human: true,
214
+ },
215
+ ),
216
+ };
217
+ }
218
+
219
+ if (rgResult.code !== 0 && rgResult.code !== 1) {
220
+ throw {
221
+ normalizedResponse: fail(ERROR_CODES.GIT_FAILURE, 'Search failed', {
222
+ stderr: rgResult.stderr,
223
+ retryable: true,
224
+ requires_human: false,
225
+ }),
226
+ };
227
+ }
228
+
229
+ const matches = rgResult.stdout
230
+ .trim()
231
+ .split('\n')
232
+ .filter(Boolean)
233
+ .map((line) => {
234
+ const firstColon = line.indexOf(':');
235
+ const secondColon = line.indexOf(':', firstColon + 1);
236
+ if (firstColon === -1 || secondColon === -1) {
237
+ return { raw: line };
238
+ }
239
+ return {
240
+ path: line.slice(0, firstColon),
241
+ line: Number(line.slice(firstColon + 1, secondColon)),
242
+ snippet: line.slice(secondColon + 1),
243
+ };
244
+ });
245
+
246
+ return {
247
+ data: {
248
+ feature_id: featureId,
249
+ query,
250
+ matches,
251
+ },
252
+ };
253
+ }
254
+
255
+ async repoDiffBundle(featureId: string | null) {
256
+ const repoRoot = this.port.getRepoRoot();
257
+ const worktree = this.port.worktreePath(featureId);
258
+ const stat = await runGit(repoRoot, ['diff', '--stat'], { cwd: worktree });
259
+ const full = await runGit(repoRoot, ['diff'], { cwd: worktree });
260
+ const names = await runGit(repoRoot, ['diff', '--name-only'], { cwd: worktree });
261
+ const latest = await this.port.evidenceLatest(featureId);
262
+
263
+ return {
264
+ data: {
265
+ feature_id: featureId,
266
+ diff_stat: stat.stdout,
267
+ diff: full.stdout,
268
+ touched_files: names.stdout
269
+ .split('\n')
270
+ .map((x) => x.trim())
271
+ .filter(Boolean),
272
+ last_gate_summary: latest.data?.latest ?? null,
273
+ },
274
+ };
275
+ }
276
+ }
@@ -0,0 +1,156 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { runGit } from '../../core/git.js';
3
+
4
+ interface WatchStartInput {
5
+ featureId: string;
6
+ repoRoot: string;
7
+ worktreePath: string;
8
+ pollIntervalMs: number;
9
+ maxUncommittedChanges: number;
10
+ }
11
+
12
+ interface WatcherState {
13
+ repoRoot: string;
14
+ worktreePath: string;
15
+ maxUncommittedChanges: number;
16
+ timer: NodeJS.Timeout;
17
+ changedFiles: Set<string>;
18
+ thresholdEmitted: boolean;
19
+ pollInFlight: boolean;
20
+ stopped: boolean;
21
+ }
22
+
23
+ function parseStatusPaths(statusPorcelain: string): string[] {
24
+ const files: string[] = [];
25
+ const lines = statusPorcelain
26
+ .split('\n')
27
+ .map((line) => line.trimEnd())
28
+ .filter((line) => line.length >= 4);
29
+
30
+ for (const line of lines) {
31
+ const candidate = line.slice(3).trim();
32
+ if (!candidate) {
33
+ continue;
34
+ }
35
+ const renameMarker = candidate.indexOf(' -> ');
36
+ const pathValue = renameMarker >= 0 ? candidate.slice(renameMarker + 4) : candidate;
37
+ const normalized = pathValue.replaceAll('\\\\', '/').trim();
38
+ if (normalized.length > 0) {
39
+ files.push(normalized);
40
+ }
41
+ }
42
+
43
+ return [...new Set(files)].sort((a, b) => a.localeCompare(b));
44
+ }
45
+
46
+ /**
47
+ * Polling watchdog that tracks feature-worktree changes and emits threshold events.
48
+ */
49
+ export class WorktreeWatchdogService extends EventEmitter {
50
+ private readonly watchers = new Map<string, WatcherState>();
51
+
52
+ async startWatching(input: WatchStartInput): Promise<void> {
53
+ await this.stopWatching(input.featureId);
54
+
55
+ const intervalMs =
56
+ Number.isFinite(input.pollIntervalMs) && input.pollIntervalMs > 0
57
+ ? Math.floor(input.pollIntervalMs)
58
+ : 2_000;
59
+ const maxUncommittedChanges =
60
+ Number.isFinite(input.maxUncommittedChanges) && input.maxUncommittedChanges > 0
61
+ ? Math.floor(input.maxUncommittedChanges)
62
+ : 50;
63
+
64
+ const state: WatcherState = {
65
+ repoRoot: input.repoRoot,
66
+ worktreePath: input.worktreePath,
67
+ maxUncommittedChanges,
68
+ timer: setInterval(() => {
69
+ void this.pollFeature(input.featureId);
70
+ }, intervalMs),
71
+ changedFiles: new Set<string>(),
72
+ thresholdEmitted: false,
73
+ pollInFlight: false,
74
+ stopped: false,
75
+ };
76
+
77
+ state.timer.unref?.();
78
+ this.watchers.set(input.featureId, state);
79
+ await this.pollFeature(input.featureId);
80
+ }
81
+
82
+ stopWatching(featureId: string): Promise<void> {
83
+ const existing = this.watchers.get(featureId);
84
+ if (!existing) {
85
+ return Promise.resolve();
86
+ }
87
+ existing.stopped = true;
88
+ clearInterval(existing.timer);
89
+ this.watchers.delete(featureId);
90
+ return Promise.resolve();
91
+ }
92
+
93
+ async getChangedFiles(featureId: string): Promise<string[]> {
94
+ await this.pollFeature(featureId);
95
+ const state = this.watchers.get(featureId);
96
+ if (!state) {
97
+ return [];
98
+ }
99
+ return [...state.changedFiles].sort((a, b) => a.localeCompare(b));
100
+ }
101
+
102
+ getChangeCount(featureId: string): number {
103
+ const state = this.watchers.get(featureId);
104
+ if (!state) {
105
+ return 0;
106
+ }
107
+ return state.changedFiles.size;
108
+ }
109
+
110
+ resetChangeCount(featureId: string): void {
111
+ const state = this.watchers.get(featureId);
112
+ if (!state) {
113
+ return;
114
+ }
115
+ state.changedFiles.clear();
116
+ state.thresholdEmitted = false;
117
+ }
118
+
119
+ on(event: 'changeThreshold', listener: (featureId: string) => void): this {
120
+ return super.on(event, listener);
121
+ }
122
+
123
+ off(event: 'changeThreshold', listener: (featureId: string) => void): this {
124
+ return super.off(event, listener);
125
+ }
126
+
127
+ private async pollFeature(featureId: string): Promise<void> {
128
+ const state = this.watchers.get(featureId);
129
+ if (!state || state.stopped || state.pollInFlight) {
130
+ return;
131
+ }
132
+
133
+ state.pollInFlight = true;
134
+ try {
135
+ const status = await runGit(state.repoRoot, ['status', '--porcelain'], {
136
+ cwd: state.worktreePath,
137
+ });
138
+ if (status.code !== 0) {
139
+ return;
140
+ }
141
+
142
+ const files = parseStatusPaths(status.stdout);
143
+ state.changedFiles = new Set(files);
144
+
145
+ if (state.changedFiles.size >= state.maxUncommittedChanges && !state.thresholdEmitted) {
146
+ state.thresholdEmitted = true;
147
+ this.emit('changeThreshold', featureId);
148
+ }
149
+ if (state.changedFiles.size < state.maxUncommittedChanges) {
150
+ state.thresholdEmitted = false;
151
+ }
152
+ } finally {
153
+ state.pollInFlight = false;
154
+ }
155
+ }
156
+ }
@@ -55,6 +55,11 @@ export class CliArgumentParser {
55
55
  index += 1;
56
56
  continue;
57
57
  }
58
+ if (token === '--checkpoint') {
59
+ options.checkpoint = next;
60
+ index += 1;
61
+ continue;
62
+ }
58
63
  if (token === '--dry-run') {
59
64
  const parsed = parseOptionalBooleanToken(next);
60
65
  if (parsed === null) {
@@ -106,6 +111,13 @@ export class CliArgumentParser {
106
111
  }
107
112
  continue;
108
113
  }
114
+ if (token === '--execution-mode') {
115
+ if (next === 'deterministic' || next === 'interactive') {
116
+ options.execution_mode = next;
117
+ index += 1;
118
+ }
119
+ continue;
120
+ }
109
121
  if (token === '--transport') {
110
122
  options.transport = next;
111
123
  index += 1;
@@ -31,6 +31,10 @@ const COMMAND_HELP: Record<CliCommand, CommandHelp> = {
31
31
  flag: '--worker-provider-mode <live|stub>',
32
32
  description: 'Worker execution mode override (default: live for run/resume)',
33
33
  },
34
+ {
35
+ flag: '--execution-mode <deterministic|interactive>',
36
+ description: 'Execution mode override for worker decision flow (default: deterministic)',
37
+ },
34
38
  { flag: '--transport <inprocess|mcp>', description: 'Tool transport layer (default: mcp)' },
35
39
  { flag: '--takeover-stale-run', description: 'Take over a stale run lease' },
36
40
  { flag: '--project <name>', description: 'Select project from multi-project.yaml' },
@@ -55,6 +59,10 @@ const COMMAND_HELP: Record<CliCommand, CommandHelp> = {
55
59
  flag: '--worker-provider-mode <live|stub>',
56
60
  description: 'Worker execution mode override (default: live)',
57
61
  },
62
+ {
63
+ flag: '--execution-mode <deterministic|interactive>',
64
+ description: 'Execution mode override for worker decision flow (default: deterministic)',
65
+ },
58
66
  { flag: '--transport <inprocess|mcp>', description: 'Tool transport layer (default: mcp)' },
59
67
  ],
60
68
  },
@@ -96,6 +104,15 @@ const COMMAND_HELP: Record<CliCommand, CommandHelp> = {
96
104
  },
97
105
  ],
98
106
  },
107
+ [CliCommand.Rollback]: {
108
+ usage: 'aop rollback [flags]',
109
+ description: 'Rollback a feature worktree by applying the inverse of a checkpoint snapshot.',
110
+ flags: [
111
+ { flag: '--feature-id <id>', description: 'Feature to roll back' },
112
+ { flag: '--checkpoint <id>', description: 'Checkpoint id to reverse-apply' },
113
+ { flag: '--dry-run', description: 'Preview rollback inputs without applying git changes' },
114
+ ],
115
+ },
99
116
  [CliCommand.Init]: {
100
117
  usage: 'aop init [flags]',
101
118
  description: 'Initialise agentic orchestrator configuration in the current directory.',
@@ -30,6 +30,7 @@ export interface InitOptions {
30
30
  }
31
31
 
32
32
  type TestFramework = 'vitest' | 'jest' | 'pytest' | 'maven' | 'gradle';
33
+ type ExecutionMode = 'deterministic' | 'interactive';
33
34
 
34
35
  interface PromptSession {
35
36
  question(query: string): Promise<string>;
@@ -46,6 +47,7 @@ interface WizardConfig {
46
47
  maxParallelGateRuns: number;
47
48
  dashboardPort: number;
48
49
  framework: TestFramework;
50
+ executionMode: ExecutionMode;
49
51
  defaultProvider: string;
50
52
  defaultModel: string;
51
53
  providerConfigEnv: string | null;
@@ -423,6 +425,19 @@ missing_prompt_behavior: ignore
423
425
  runtime:
424
426
  default_provider: ${wizard.defaultProvider}
425
427
  default_model: ${wizard.defaultModel}
428
+ execution_mode: ${wizard.executionMode}
429
+ interactive:
430
+ watchdog_poll_interval_ms: 2000
431
+ checkpoint_interval_ms: 30000
432
+ max_uncommitted_changes: 50
433
+ validation_on_checkpoint: true
434
+ revert_on_violation: false
435
+ violation_severity: warning
436
+ shadow_workspace:
437
+ enabled: false
438
+ promotion_strategy: atomic
439
+ cleanup_on_failure: true
440
+ max_shadow_size_mb: 2048
426
441
  worker_provider_mode: live
427
442
  worker_response_timeout_ms: 600000
428
443
  worker_spawn_timeout_ms: 15000
@@ -478,6 +493,14 @@ function parseFramework(raw: string, fallback: TestFramework): TestFramework {
478
493
  return fallback;
479
494
  }
480
495
 
496
+ function parseExecutionMode(raw: string, fallback: ExecutionMode): ExecutionMode {
497
+ const value = raw.trim().toLowerCase();
498
+ if (value === 'deterministic' || value === 'interactive') {
499
+ return value;
500
+ }
501
+ return fallback;
502
+ }
503
+
481
504
  function parseNotificationChannels(raw: string): {
482
505
  desktop: boolean;
483
506
  slack: boolean;
@@ -643,6 +666,12 @@ async function collectWizardConfig(
643
666
  }
644
667
  }
645
668
 
669
+ const executionModeRaw = await askWithDefault(
670
+ prompt,
671
+ 'Default execution mode (deterministic|interactive)',
672
+ 'deterministic',
673
+ );
674
+
646
675
  return {
647
676
  baseBranch,
648
677
  defaultProvider: parseAdapterName(
@@ -651,6 +680,7 @@ async function collectWizardConfig(
651
680
  defaults.defaultProvider,
652
681
  ),
653
682
  defaultModel,
683
+ executionMode: parseExecutionMode(executionModeRaw, 'deterministic'),
654
684
  providerConfigEnv,
655
685
  providerCredentialBootstrapped,
656
686
  scmProvider: parseAdapterName(
@@ -711,6 +741,7 @@ export class InitCommandHandler {
711
741
  baseBranch: gitContext.defaultBranch,
712
742
  defaultProvider: DEFAULT_AGENT_PROVIDER,
713
743
  defaultModel: DEFAULT_AGENT_MODEL,
744
+ executionMode: 'deterministic' as const,
714
745
  providerConfigEnv: null,
715
746
  providerCredentialBootstrapped: false,
716
747
  scmProvider: DEFAULT_SCM_PROVIDER,
@@ -3,6 +3,7 @@ import { resolveProviderSelection } from '../providers/providers.js';
3
3
  import {
4
4
  DefaultWorkerProviderFactory,
5
5
  resolveWorkerProviderMode,
6
+ resolveWorkerProviderObservability,
6
7
  resolveWorkerProviderPolicy,
7
8
  resolveWorkerProviderRuntime,
8
9
  type WorkerProviderFactory,
@@ -52,6 +53,14 @@ function asStringArray(value: unknown): string[] {
52
53
  return normalized;
53
54
  }
54
55
 
56
+ function asFeatureId(value: unknown): string | null {
57
+ if (typeof value !== 'string') {
58
+ return null;
59
+ }
60
+ const trimmed = value.trim();
61
+ return trimmed.length > 0 ? trimmed : null;
62
+ }
63
+
55
64
  function toResumePhase(status: string): string {
56
65
  if (status === STATUS.BUILDING) {
57
66
  return STATUS.BUILDING;
@@ -98,9 +107,10 @@ export class ResumeCommandHandler {
98
107
  }
99
108
 
100
109
  async execute(context: ResumeCommandContext): Promise<unknown> {
101
- const { env, runId, transport, options, kernel, toolClient } = context;
110
+ const { repoRoot, env, runId, transport, options, kernel, toolClient } = context;
102
111
  const recovery = await kernel.recoverFromState();
103
- const resumePlan = await this.buildResumePlan(kernel, runId);
112
+ const requestedFeatureId = asFeatureId(options.feature_id);
113
+ const resumePlan = await this.buildResumePlan(kernel, runId, requestedFeatureId);
104
114
 
105
115
  if (resumePlan.features.length === 0) {
106
116
  return {
@@ -127,6 +137,10 @@ export class ResumeCommandHandler {
127
137
  policySnapshot.execution && typeof policySnapshot.execution === 'object'
128
138
  ? (policySnapshot.execution as Record<string, unknown>)
129
139
  : null;
140
+ const observabilityPolicy =
141
+ policySnapshot.observability && typeof policySnapshot.observability === 'object'
142
+ ? (policySnapshot.observability as Record<string, unknown>)
143
+ : null;
130
144
  const provider = this.workerProviderFactory.create({
131
145
  selection,
132
146
  mode: resolveWorkerProviderMode(
@@ -136,7 +150,9 @@ export class ResumeCommandHandler {
136
150
  ),
137
151
  context: 'resume',
138
152
  policy: resolveWorkerProviderPolicy(executionPolicy),
153
+ observability: resolveWorkerProviderObservability(observabilityPolicy),
139
154
  runtime: runtimeConfig,
155
+ repoRoot,
140
156
  });
141
157
  const supervisor = new SupervisorRuntime(kernel, provider, toolClient, {
142
158
  max_active_features: 5,
@@ -144,6 +160,7 @@ export class ResumeCommandHandler {
144
160
  run_id: resumePlan.run_id,
145
161
  owner_instance_id: resumePlan.owner_instance_id,
146
162
  takeover_stale_run: options.takeover_stale_run,
163
+ execution_mode: options.execution_mode,
147
164
  });
148
165
 
149
166
  const startResult = await supervisor.start(
@@ -168,7 +185,11 @@ export class ResumeCommandHandler {
168
185
  };
169
186
  }
170
187
 
171
- private async buildResumePlan(kernel: AopKernel, fallbackRunId: string): Promise<ResumePlan> {
188
+ private async buildResumePlan(
189
+ kernel: AopKernel,
190
+ fallbackRunId: string,
191
+ requestedFeatureId?: string | null,
192
+ ): Promise<ResumePlan> {
172
193
  const index = await kernel.readIndex();
173
194
  const runtimeSessions = await kernel.getRuntimeSessions();
174
195
  const discovered = await kernel.featureDiscoverSpecs();
@@ -201,7 +222,13 @@ export class ResumeCommandHandler {
201
222
  }
202
223
 
203
224
  const resumable: ResumeFeature[] = [];
204
- const sortedFeatureIds = [...sourcesByFeature.keys()].sort((a, b) => a.localeCompare(b));
225
+ const normalizedRequestedFeatureId = asFeatureId(requestedFeatureId);
226
+ if (normalizedRequestedFeatureId) {
227
+ this.addSource(sourcesByFeature, normalizedRequestedFeatureId, 'cli.feature_id');
228
+ }
229
+ const sortedFeatureIds = normalizedRequestedFeatureId
230
+ ? [normalizedRequestedFeatureId]
231
+ : [...sourcesByFeature.keys()].sort((a, b) => a.localeCompare(b));
205
232
  for (const featureId of sortedFeatureIds) {
206
233
  try {
207
234
  const state = await kernel.featureStateGet(featureId);