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,537 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { runGit } from '../src/core/git.js';
6
+ import {
7
+ CheckpointService,
8
+ type CheckpointProviderPort,
9
+ type CheckpointRecord,
10
+ type InteractiveExecutionConfig,
11
+ } from '../src/application/services/checkpoint-service.js';
12
+ import type { WorktreeWatchdogService } from '../src/application/services/worktree-watchdog-service.js';
13
+
14
+ interface MutableState {
15
+ frontMatter: Record<string, unknown>;
16
+ body: string;
17
+ }
18
+
19
+ async function initGitRepo(prefix: string): Promise<string> {
20
+ const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
21
+ await runGit(repoRoot, ['init']);
22
+ await runGit(repoRoot, ['config', 'user.email', 'test@example.com']);
23
+ await runGit(repoRoot, ['config', 'user.name', 'AOP Test']);
24
+ await fs.writeFile(path.join(repoRoot, 'tracked.txt'), 'initial\n', 'utf8');
25
+ await runGit(repoRoot, ['add', '.']);
26
+ await runGit(repoRoot, ['commit', '-m', 'init']);
27
+ return repoRoot;
28
+ }
29
+
30
+ function makeUpdateState(state: MutableState) {
31
+ return async (
32
+ _featureId: string,
33
+ _expectedVersion: number | null,
34
+ updater: (
35
+ frontMatter: Record<string, unknown>,
36
+ body: string,
37
+ ) => Promise<{ frontMatter?: Record<string, unknown>; body?: string }>,
38
+ ): Promise<Record<string, unknown>> => {
39
+ const next = await updater(structuredClone(state.frontMatter), state.body);
40
+ state.frontMatter = {
41
+ ...state.frontMatter,
42
+ ...(next.frontMatter ?? {}),
43
+ version:
44
+ typeof state.frontMatter['version'] === 'number' ? state.frontMatter['version'] + 1 : 1,
45
+ };
46
+ state.body = next.body ?? state.body;
47
+ return state.frontMatter;
48
+ };
49
+ }
50
+
51
+ function defaultConfig(
52
+ overrides: Partial<InteractiveExecutionConfig> = {},
53
+ ): InteractiveExecutionConfig {
54
+ return {
55
+ checkpointIntervalMs: 30_000,
56
+ watchdogPollIntervalMs: 2_000,
57
+ maxUncommittedChanges: 50,
58
+ validationOnCheckpoint: true,
59
+ revertOnViolation: false,
60
+ violationSeverity: 'warning',
61
+ ...overrides,
62
+ };
63
+ }
64
+
65
+ describe('CheckpointService', () => {
66
+ it('creates a valid checkpoint and persists diff snapshot metadata', async () => {
67
+ const repoRoot = await initGitRepo('aop-checkpoint-valid-');
68
+ const state: MutableState = {
69
+ frontMatter: { version: 1, status: 'building' },
70
+ body: 'state body',
71
+ };
72
+
73
+ const watchdog = {
74
+ getChangedFiles: vi.fn(async () => ['tracked.txt']),
75
+ resetChangeCount: vi.fn(),
76
+ } as unknown as WorktreeWatchdogService;
77
+
78
+ const provider = {
79
+ selection: {
80
+ provider: 'custom',
81
+ model: 'local-default',
82
+ provider_config_env: null,
83
+ provider_config_ref: null,
84
+ },
85
+ runWorker: vi.fn(),
86
+ sendMessage: vi.fn(async () => undefined),
87
+ } as unknown as CheckpointProviderPort;
88
+
89
+ const service = new CheckpointService({
90
+ repoRoot,
91
+ featurePathResolver: (featureId: string) =>
92
+ path.join(repoRoot, '.aop', 'features', featureId),
93
+ worktreePathResolver: () => repoRoot,
94
+ validateDiff: vi.fn(async () => ({
95
+ valid: true,
96
+ violations: [],
97
+ changed_files: ['tracked.txt'],
98
+ })),
99
+ watchdog,
100
+ provider,
101
+ readState: async () => ({ frontMatter: state.frontMatter }),
102
+ updateState: makeUpdateState(state),
103
+ config: defaultConfig(),
104
+ });
105
+
106
+ try {
107
+ await fs.writeFile(path.join(repoRoot, 'tracked.txt'), 'updated\n', 'utf8');
108
+
109
+ const result = await service.createCheckpoint({
110
+ featureId: 'feature_valid',
111
+ trigger: 'final',
112
+ });
113
+
114
+ expect(result.valid).toBe(true);
115
+ expect(result.blockMerge).toBe(false);
116
+ expect(result.checkpoint.validation_status).toBe('valid');
117
+ expect(result.checkpoint.files_changed).toEqual(['tracked.txt']);
118
+ expect(provider.sendMessage as unknown as ReturnType<typeof vi.fn>).not.toHaveBeenCalled();
119
+ expect(watchdog.resetChangeCount as unknown as ReturnType<typeof vi.fn>).toHaveBeenCalledWith(
120
+ 'feature_valid',
121
+ );
122
+
123
+ const snapshotPath = path.join(repoRoot, result.checkpoint.diff_snapshot);
124
+ await expect(fs.stat(snapshotPath)).resolves.toBeTruthy();
125
+
126
+ const checkpoints = (state.frontMatter['checkpoints'] as CheckpointRecord[]) ?? [];
127
+ expect(checkpoints).toHaveLength(1);
128
+ expect(checkpoints[0].checkpoint_id).toBe(result.checkpoint.checkpoint_id);
129
+ expect(state.frontMatter['execution_mode']).toBe('interactive');
130
+
131
+ const metricsPath = path.join(repoRoot, '.aop', 'analytics', 'interactive-mode.json');
132
+ const metricsRaw = await fs.readFile(metricsPath, 'utf8');
133
+ const metrics = JSON.parse(metricsRaw) as {
134
+ totals: {
135
+ checkpoint_count: number;
136
+ valid_count: number;
137
+ invalid_count: number;
138
+ };
139
+ latest: {
140
+ feature_id: string;
141
+ checkpoint_id: string;
142
+ };
143
+ };
144
+ expect(metrics.totals.checkpoint_count).toBe(1);
145
+ expect(metrics.totals.valid_count).toBe(1);
146
+ expect(metrics.totals.invalid_count).toBe(0);
147
+ expect(metrics.latest.feature_id).toBe('feature_valid');
148
+ expect(metrics.latest.checkpoint_id).toBe(result.checkpoint.checkpoint_id);
149
+ } finally {
150
+ await fs.rm(repoRoot, { recursive: true, force: true });
151
+ }
152
+ });
153
+
154
+ it('marks invalid checkpoints, notifies agent, and reverts files when configured', async () => {
155
+ const repoRoot = await initGitRepo('aop-checkpoint-invalid-');
156
+ const state: MutableState = {
157
+ frontMatter: { version: 1, status: 'building' },
158
+ body: 'state body',
159
+ };
160
+
161
+ const watchdog = {
162
+ getChangedFiles: vi.fn(async () => ['tracked.txt']),
163
+ resetChangeCount: vi.fn(),
164
+ } as unknown as WorktreeWatchdogService;
165
+
166
+ const provider = {
167
+ selection: {
168
+ provider: 'custom',
169
+ model: 'local-default',
170
+ provider_config_env: null,
171
+ provider_config_ref: null,
172
+ },
173
+ runWorker: vi.fn(),
174
+ sendMessage: vi.fn(async () => undefined),
175
+ } as unknown as CheckpointProviderPort;
176
+
177
+ const service = new CheckpointService({
178
+ repoRoot,
179
+ featurePathResolver: (featureId: string) =>
180
+ path.join(repoRoot, '.aop', 'features', featureId),
181
+ worktreePathResolver: () => repoRoot,
182
+ validateDiff: vi.fn(async () => {
183
+ throw {
184
+ normalizedResponse: {
185
+ error: {
186
+ details: {
187
+ violations: [{ path: 'tracked.txt', reason: 'outside_allowed_areas' }],
188
+ },
189
+ },
190
+ },
191
+ };
192
+ }),
193
+ watchdog,
194
+ provider,
195
+ readState: async () => ({ frontMatter: state.frontMatter }),
196
+ updateState: makeUpdateState(state),
197
+ config: defaultConfig({ revertOnViolation: true, violationSeverity: 'error' }),
198
+ });
199
+
200
+ try {
201
+ await fs.writeFile(path.join(repoRoot, 'tracked.txt'), 'violating change\n', 'utf8');
202
+
203
+ const result = await service.createCheckpoint({
204
+ featureId: 'feature_invalid',
205
+ sessionId: 'builder-session-1',
206
+ trigger: 'final',
207
+ });
208
+
209
+ expect(result.valid).toBe(false);
210
+ expect(result.blockMerge).toBe(true);
211
+ expect(result.checkpoint.validation_status).toBe('invalid');
212
+ expect(result.checkpoint.violations).toContain('tracked.txt: outside_allowed_areas');
213
+ expect(result.checkpoint.severity).toBe('error');
214
+ expect(provider.sendMessage as unknown as ReturnType<typeof vi.fn>).toHaveBeenCalledTimes(1);
215
+
216
+ const fileContent = await fs.readFile(path.join(repoRoot, 'tracked.txt'), 'utf8');
217
+ expect(fileContent).toBe('initial\n');
218
+ } finally {
219
+ await fs.rm(repoRoot, { recursive: true, force: true });
220
+ }
221
+ });
222
+
223
+ it('returns filtered checkpoints from state frontmatter', async () => {
224
+ const repoRoot = await initGitRepo('aop-checkpoint-read-');
225
+ const state: MutableState = {
226
+ frontMatter: {
227
+ version: 3,
228
+ checkpoints: [
229
+ {
230
+ checkpoint_id: 'checkpoint-valid',
231
+ timestamp: new Date().toISOString(),
232
+ files_changed: ['tracked.txt'],
233
+ validation_status: 'valid',
234
+ violations: [],
235
+ severity: 'warning',
236
+ diff_snapshot: '.aop/features/feature_read/checkpoints/checkpoint-valid.diff',
237
+ },
238
+ { checkpoint_id: 'checkpoint-invalid-shape' },
239
+ 'not-an-object',
240
+ ],
241
+ },
242
+ body: 'state body',
243
+ };
244
+
245
+ const service = new CheckpointService({
246
+ repoRoot,
247
+ featurePathResolver: (featureId: string) =>
248
+ path.join(repoRoot, '.aop', 'features', featureId),
249
+ worktreePathResolver: () => repoRoot,
250
+ validateDiff: vi.fn(async () => ({ valid: true, violations: [], changed_files: [] })),
251
+ watchdog: {
252
+ getChangedFiles: vi.fn(async () => []),
253
+ resetChangeCount: vi.fn(),
254
+ } as unknown as WorktreeWatchdogService,
255
+ provider: {
256
+ selection: {
257
+ provider: 'custom',
258
+ model: 'local-default',
259
+ provider_config_env: null,
260
+ provider_config_ref: null,
261
+ },
262
+ runWorker: vi.fn(),
263
+ } as unknown as CheckpointProviderPort,
264
+ readState: async () => ({ frontMatter: state.frontMatter }),
265
+ updateState: makeUpdateState(state),
266
+ config: defaultConfig(),
267
+ });
268
+
269
+ try {
270
+ const checkpoints = await service.getCheckpoints('feature_read');
271
+ expect(checkpoints).toHaveLength(1);
272
+ expect(checkpoints[0]).toMatchObject({
273
+ checkpoint_id: 'checkpoint-valid',
274
+ validation_status: 'valid',
275
+ });
276
+ } finally {
277
+ await fs.rm(repoRoot, { recursive: true, force: true });
278
+ }
279
+ });
280
+
281
+ it('supports skipped checkpoints and message-only validation error extraction', async () => {
282
+ const repoRoot = await initGitRepo('aop-checkpoint-skipped-');
283
+ const state: MutableState = {
284
+ frontMatter: { version: 1, status: 'building' },
285
+ body: 'state body',
286
+ };
287
+ const sendMessage = vi.fn(async () => undefined);
288
+
289
+ const service = new CheckpointService({
290
+ repoRoot,
291
+ featurePathResolver: (featureId: string) =>
292
+ path.join(repoRoot, '.aop', 'features', featureId),
293
+ worktreePathResolver: (_featureId: string) => path.join(repoRoot, '.missing-worktree'),
294
+ validateDiff: vi.fn(async () => {
295
+ throw {
296
+ normalizedResponse: {
297
+ error: {
298
+ details: {
299
+ message: 'checkpoint message violation',
300
+ },
301
+ },
302
+ },
303
+ };
304
+ }),
305
+ watchdog: {
306
+ getChangedFiles: vi.fn(async () => ['tracked.txt']),
307
+ resetChangeCount: vi.fn(),
308
+ } as unknown as WorktreeWatchdogService,
309
+ provider: {
310
+ selection: {
311
+ provider: 'custom',
312
+ model: 'local-default',
313
+ provider_config_env: null,
314
+ provider_config_ref: null,
315
+ },
316
+ runWorker: vi.fn(),
317
+ sendMessage,
318
+ } as unknown as CheckpointProviderPort,
319
+ readState: async () => ({ frontMatter: state.frontMatter }),
320
+ updateState: makeUpdateState(state),
321
+ config: defaultConfig({ validationOnCheckpoint: true }),
322
+ });
323
+
324
+ try {
325
+ // Missing worktree path forces git diff failure => skipped checkpoint branch.
326
+ const skipped = await service.createCheckpoint({
327
+ featureId: 'feature_skipped',
328
+ sessionId: 'builder-session-skipped',
329
+ trigger: 'final',
330
+ });
331
+ expect(skipped.checkpoint.validation_status).toBe('skipped');
332
+
333
+ // Valid worktree + message-only violation details exercises extractViolationMessages fallback.
334
+ const validatingService = new CheckpointService({
335
+ repoRoot,
336
+ featurePathResolver: (featureId: string) =>
337
+ path.join(repoRoot, '.aop', 'features', featureId),
338
+ worktreePathResolver: () => repoRoot,
339
+ validateDiff: vi.fn(async () => {
340
+ throw {
341
+ normalizedResponse: {
342
+ error: {
343
+ details: {
344
+ message: 'checkpoint message violation',
345
+ },
346
+ },
347
+ },
348
+ };
349
+ }),
350
+ watchdog: {
351
+ getChangedFiles: vi.fn(async () => ['tracked.txt']),
352
+ resetChangeCount: vi.fn(),
353
+ } as unknown as WorktreeWatchdogService,
354
+ provider: {
355
+ selection: {
356
+ provider: 'custom',
357
+ model: 'local-default',
358
+ provider_config_env: null,
359
+ provider_config_ref: null,
360
+ },
361
+ runWorker: vi.fn(),
362
+ sendMessage: vi.fn(async () => {
363
+ throw new Error('send failed');
364
+ }),
365
+ } as unknown as CheckpointProviderPort,
366
+ readState: async () => ({ frontMatter: state.frontMatter }),
367
+ updateState: makeUpdateState(state),
368
+ config: defaultConfig({ revertOnViolation: false, violationSeverity: 'warning' }),
369
+ });
370
+
371
+ await fs.writeFile(path.join(repoRoot, 'tracked.txt'), 'changed\\n', 'utf8');
372
+ const invalid = await validatingService.createCheckpoint({
373
+ featureId: 'feature_message',
374
+ sessionId: 'builder-session-message',
375
+ trigger: 'final',
376
+ });
377
+ expect(invalid.checkpoint.validation_status).toBe('invalid');
378
+ expect(invalid.checkpoint.violations).toEqual(['checkpoint message violation']);
379
+ expect(invalid.blockMerge).toBe(false);
380
+ } finally {
381
+ await fs.rm(repoRoot, { recursive: true, force: true });
382
+ }
383
+ });
384
+
385
+ it('merges existing interactive metrics snapshot and updates rolling histories', async () => {
386
+ const repoRoot = await initGitRepo('aop-checkpoint-metrics-');
387
+ const state: MutableState = {
388
+ frontMatter: { version: 1, status: 'building' },
389
+ body: 'state body',
390
+ };
391
+ const metricsPath = path.join(repoRoot, '.aop', 'analytics', 'interactive-mode.json');
392
+ await fs.mkdir(path.dirname(metricsPath), { recursive: true });
393
+ await fs.writeFile(
394
+ metricsPath,
395
+ JSON.stringify(
396
+ {
397
+ schema_version: 1,
398
+ updated_at: '2026-03-06T00:00:00.000Z',
399
+ totals: {
400
+ checkpoint_count: 2,
401
+ valid_count: 1,
402
+ invalid_count: 1,
403
+ skipped_count: 0,
404
+ files_changed_total: 3,
405
+ },
406
+ histories: {
407
+ checkpoint_latency_ms: [120, 240],
408
+ validation_latency_ms: [90, 180],
409
+ diff_capture_latency_ms: [30, 60],
410
+ },
411
+ latest: null,
412
+ },
413
+ null,
414
+ 2,
415
+ ),
416
+ 'utf8',
417
+ );
418
+
419
+ const service = new CheckpointService({
420
+ repoRoot,
421
+ featurePathResolver: (featureId: string) =>
422
+ path.join(repoRoot, '.aop', 'features', featureId),
423
+ worktreePathResolver: () => repoRoot,
424
+ validateDiff: vi.fn(async () => ({
425
+ valid: true,
426
+ violations: [],
427
+ changed_files: ['tracked.txt'],
428
+ })),
429
+ watchdog: {
430
+ getChangedFiles: vi.fn(async () => ['tracked.txt']),
431
+ resetChangeCount: vi.fn(),
432
+ } as unknown as WorktreeWatchdogService,
433
+ provider: {
434
+ selection: {
435
+ provider: 'custom',
436
+ model: 'local-default',
437
+ provider_config_env: null,
438
+ provider_config_ref: null,
439
+ },
440
+ runWorker: vi.fn(),
441
+ } as unknown as CheckpointProviderPort,
442
+ readState: async () => ({ frontMatter: state.frontMatter }),
443
+ updateState: makeUpdateState(state),
444
+ config: defaultConfig(),
445
+ });
446
+
447
+ try {
448
+ await fs.writeFile(path.join(repoRoot, 'tracked.txt'), 'metrics-update\\n', 'utf8');
449
+ const result = await service.createCheckpoint({
450
+ featureId: 'feature_metrics',
451
+ trigger: 'interval',
452
+ });
453
+
454
+ const metricsRaw = await fs.readFile(metricsPath, 'utf8');
455
+ const metrics = JSON.parse(metricsRaw) as {
456
+ totals: {
457
+ checkpoint_count: number;
458
+ valid_count: number;
459
+ invalid_count: number;
460
+ files_changed_total: number;
461
+ };
462
+ histories: {
463
+ checkpoint_latency_ms: number[];
464
+ validation_latency_ms: number[];
465
+ diff_capture_latency_ms: number[];
466
+ };
467
+ latest: {
468
+ feature_id: string;
469
+ checkpoint_id: string;
470
+ trigger: string;
471
+ };
472
+ };
473
+
474
+ expect(metrics.totals.checkpoint_count).toBe(3);
475
+ expect(metrics.totals.valid_count).toBe(2);
476
+ expect(metrics.totals.invalid_count).toBe(1);
477
+ expect(metrics.totals.files_changed_total).toBeGreaterThanOrEqual(4);
478
+ expect(metrics.histories.checkpoint_latency_ms.length).toBe(3);
479
+ expect(metrics.histories.validation_latency_ms.length).toBe(3);
480
+ expect(metrics.histories.diff_capture_latency_ms.length).toBe(3);
481
+ expect(metrics.latest.feature_id).toBe('feature_metrics');
482
+ expect(metrics.latest.checkpoint_id).toBe(result.checkpoint.checkpoint_id);
483
+ expect(metrics.latest.trigger).toBe('interval');
484
+ } finally {
485
+ await fs.rm(repoRoot, { recursive: true, force: true });
486
+ }
487
+ });
488
+
489
+ it('serializes concurrent checkpoints for the same feature', async () => {
490
+ const repoRoot = await initGitRepo('aop-checkpoint-lock-');
491
+ const state: MutableState = {
492
+ frontMatter: { version: 1, status: 'building' },
493
+ body: 'state body',
494
+ };
495
+ const validateDiff = vi.fn(async () => {
496
+ await new Promise((resolve) => setTimeout(resolve, 40));
497
+ return { valid: true, violations: [], changed_files: ['tracked.txt'] };
498
+ });
499
+
500
+ const service = new CheckpointService({
501
+ repoRoot,
502
+ featurePathResolver: (featureId: string) =>
503
+ path.join(repoRoot, '.aop', 'features', featureId),
504
+ worktreePathResolver: () => repoRoot,
505
+ validateDiff,
506
+ watchdog: {
507
+ getChangedFiles: vi.fn(async () => ['tracked.txt']),
508
+ resetChangeCount: vi.fn(),
509
+ } as unknown as WorktreeWatchdogService,
510
+ provider: {
511
+ selection: {
512
+ provider: 'custom',
513
+ model: 'local-default',
514
+ provider_config_env: null,
515
+ provider_config_ref: null,
516
+ },
517
+ runWorker: vi.fn(),
518
+ } as unknown as CheckpointProviderPort,
519
+ readState: async () => ({ frontMatter: state.frontMatter }),
520
+ updateState: makeUpdateState(state),
521
+ config: defaultConfig(),
522
+ });
523
+
524
+ try {
525
+ await fs.writeFile(path.join(repoRoot, 'tracked.txt'), 'concurrent\\n', 'utf8');
526
+ const [first, second] = await Promise.all([
527
+ service.createCheckpoint({ featureId: 'feature_lock', trigger: 'final' }),
528
+ service.createCheckpoint({ featureId: 'feature_lock', trigger: 'final' }),
529
+ ]);
530
+
531
+ expect(first.checkpoint.checkpoint_id).not.toBe(second.checkpoint.checkpoint_id);
532
+ expect(validateDiff).toHaveBeenCalledTimes(2);
533
+ } finally {
534
+ await fs.rm(repoRoot, { recursive: true, force: true });
535
+ }
536
+ });
537
+ });
@@ -34,6 +34,8 @@ describe('cli helper modules', () => {
34
34
  '--takeover-stale-run',
35
35
  '--message',
36
36
  'hello world',
37
+ '--checkpoint',
38
+ 'checkpoint-001',
37
39
  ]);
38
40
  expect(miscParsed).toMatchObject({
39
41
  command: 'status',
@@ -48,6 +50,7 @@ describe('cli helper modules', () => {
48
50
  batch: true,
49
51
  takeover_stale_run: true,
50
52
  message: 'hello world',
53
+ checkpoint: 'checkpoint-001',
51
54
  });
52
55
  // --port with non-numeric next token (no value consumed)
53
56
  const portNoNum = parser.parse(['run', '--port', '--foreground']);
@@ -68,6 +71,8 @@ describe('cli helper modules', () => {
68
71
  'AOP_PROVIDER_CFG',
69
72
  '--transport',
70
73
  'mcp',
74
+ '--execution-mode',
75
+ 'interactive',
71
76
  ]);
72
77
 
73
78
  expect(parsed).toMatchObject({
@@ -76,6 +81,7 @@ describe('cli helper modules', () => {
76
81
  agent_config: '{"command":"kiro-cli","args":["chat","--agent","dev"]}',
77
82
  provider_config_env: 'AOP_PROVIDER_CFG',
78
83
  transport: 'mcp',
84
+ execution_mode: 'interactive',
79
85
  });
80
86
  const deleteParsed = parser.parse([
81
87
  'delete',
@@ -467,6 +473,7 @@ describe('HelpCommandHandler', () => {
467
473
  expect(result.data.help).toContain('Commands:');
468
474
  expect(result.data.help).toContain('run');
469
475
  expect(result.data.help).toContain('status');
476
+ expect(result.data.help).toContain('rollback');
470
477
  });
471
478
 
472
479
  it('GIVEN_valid_subcommand_WHEN_execute_called_THEN_returns_detailed_help_for_command', async () => {
@@ -476,6 +483,16 @@ describe('HelpCommandHandler', () => {
476
483
  expect(result.ok).toBe(true);
477
484
  expect(result.data.help).toContain('Usage: aop run');
478
485
  expect(result.data.help).toContain('Flags:');
486
+ expect(result.data.help).toContain('--execution-mode <deterministic|interactive>');
487
+ });
488
+
489
+ it('GIVEN_resume_help_WHEN_execute_called_THEN_documents_execution_mode_override', async () => {
490
+ const { HelpCommandHandler } = await import('../src/cli/help-command-handler.js');
491
+ const handler = new HelpCommandHandler();
492
+ const result = handler.execute('resume');
493
+ expect(result.ok).toBe(true);
494
+ expect(result.data.help).toContain('Usage: aop resume [flags]');
495
+ expect(result.data.help).toContain('--execution-mode <deterministic|interactive>');
479
496
  });
480
497
 
481
498
  it('GIVEN_unknown_subcommand_WHEN_execute_called_THEN_returns_full_help_text', async () => {
@@ -497,6 +514,17 @@ describe('HelpCommandHandler', () => {
497
514
  expect(result.data.help).toContain('--project <name>');
498
515
  });
499
516
 
517
+ it('GIVEN_rollback_help_WHEN_execute_called_THEN_renders_required_flags', async () => {
518
+ const { HelpCommandHandler } = await import('../src/cli/help-command-handler.js');
519
+ const handler = new HelpCommandHandler();
520
+ const result = handler.execute('rollback');
521
+ expect(result.ok).toBe(true);
522
+ expect(result.data.help).toContain('Usage: aop rollback [flags]');
523
+ expect(result.data.help).toContain('--feature-id <id>');
524
+ expect(result.data.help).toContain('--checkpoint <id>');
525
+ expect(result.data.help).toContain('--dry-run');
526
+ });
527
+
500
528
  it('GIVEN_delete_help_WHEN_execute_called_THEN_documents_yes_for_destructive_delete', async () => {
501
529
  const { HelpCommandHandler } = await import('../src/cli/help-command-handler.js');
502
530
  const handler = new HelpCommandHandler();
@@ -161,6 +161,10 @@ vi.mock('../src/providers/worker-provider-factory.js', () => ({
161
161
  },
162
162
  resolveWorkerProviderMode: vi.fn((_cliMode, _configuredMode, fallback) => fallback ?? 'live'),
163
163
  resolveWorkerProviderPolicy: vi.fn(() => ({ require_live_provider_for_run: true })),
164
+ resolveWorkerProviderObservability: vi.fn(() => ({
165
+ raw_agent_logs_enabled: false,
166
+ raw_agent_logs_retention_days: 60,
167
+ })),
164
168
  resolveWorkerProviderRuntime: vi.fn(() => ({
165
169
  worker_response_timeout_ms: 120000,
166
170
  max_consecutive_no_progress_iterations: 2,
@@ -364,6 +368,54 @@ describe('aop CLI unit', () => {
364
368
  });
365
369
  });
366
370
 
371
+ it('GIVEN_rollback_command_with_dry_run_WHEN_main_runs_THEN_returns_preview_payload', async () => {
372
+ const featureRoot = path.join(cwd, '.aop', 'features', 'feature_resume');
373
+ const checkpointsDir = path.join(featureRoot, 'checkpoints');
374
+ await fs.mkdir(checkpointsDir, { recursive: true });
375
+ await fs.writeFile(
376
+ path.join(checkpointsDir, 'checkpoint-001.diff'),
377
+ 'diff --git a/a b/a\n',
378
+ 'utf8',
379
+ );
380
+ await fs.writeFile(
381
+ path.join(featureRoot, 'state.md'),
382
+ [
383
+ '---',
384
+ 'feature_id: feature_resume',
385
+ 'version: 1',
386
+ `worktree_path: ${cwd}`,
387
+ 'checkpoints:',
388
+ ' - checkpoint_id: checkpoint-001',
389
+ ' timestamp: 2026-03-06T00:00:00.000Z',
390
+ ' files_changed: [tracked.txt]',
391
+ ' validation_status: valid',
392
+ ' violations: []',
393
+ ' diff_snapshot: .aop/features/feature_resume/checkpoints/checkpoint-001.diff',
394
+ '---',
395
+ '',
396
+ ].join('\n'),
397
+ 'utf8',
398
+ );
399
+
400
+ const code = await main(
401
+ ['rollback', '--feature-id', 'feature_resume', '--checkpoint', 'checkpoint-001', '--dry-run'],
402
+ { cwd, env: {} as NodeJS.ProcessEnv },
403
+ );
404
+ const writes = asJsonWrites(stdoutSpy);
405
+
406
+ expect(code).toBe(0);
407
+ expect(writes[0]).toMatchObject({
408
+ ok: true,
409
+ data: {
410
+ command: 'rollback',
411
+ feature_id: 'feature_resume',
412
+ checkpoint_id: 'checkpoint-001',
413
+ dry_run: true,
414
+ applied: false,
415
+ },
416
+ });
417
+ });
418
+
367
419
  it('GIVEN_delete_without_feature_id_WHEN_main_runs_THEN_returns_invalid_cli_args', async () => {
368
420
  const code = await main(['delete'], { cwd, env: {} as NodeJS.ProcessEnv });
369
421
  const writes = asJsonWrites(stdoutSpy);