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,339 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ const pathExistsMock = vi.hoisted(() => vi.fn(async () => false));
4
+ const ensureDirMock = vi.hoisted(() => vi.fn(async () => {}));
5
+ const runGitMock = vi.hoisted(() => vi.fn());
6
+ const runCommandMock = vi.hoisted(() => vi.fn());
7
+ const normalizeRepoPathMock = vi.hoisted(() => vi.fn(async (_root: string, p: string) => p));
8
+ const normalizeFromWorktreeMock = vi.hoisted(() =>
9
+ vi.fn((_worktree: string, _root: string, p: string) => p),
10
+ );
11
+ const applyWorktreeSymlinksMock = vi.hoisted(() => vi.fn(async () => {}));
12
+ const runWorktreePostCreateMock = vi.hoisted(() => vi.fn(async () => {}));
13
+ const formatWorkspaceHookWarningMock = vi.hoisted(() => vi.fn(() => 'hook warning'));
14
+ const fsReadFileMock = vi.hoisted(() => vi.fn(async () => 'file content'));
15
+
16
+ vi.mock('../src/core/fs.js', () => ({
17
+ pathExists: pathExistsMock,
18
+ ensureDir: ensureDirMock,
19
+ }));
20
+
21
+ vi.mock('../src/core/git.js', () => ({
22
+ runGit: runGitMock,
23
+ runCommand: runCommandMock,
24
+ }));
25
+
26
+ vi.mock('../src/core/path-rules.js', () => ({
27
+ normalizeRepoPath: normalizeRepoPathMock,
28
+ }));
29
+
30
+ vi.mock('../src/core/utils/path-normalizers.js', () => ({
31
+ normalizeFromWorktree: normalizeFromWorktreeMock,
32
+ }));
33
+
34
+ vi.mock('../src/core/workspace-hooks.js', () => ({
35
+ applyWorktreeSymlinks: applyWorktreeSymlinksMock,
36
+ runWorktreePostCreate: runWorktreePostCreateMock,
37
+ formatWorkspaceHookWarning: formatWorkspaceHookWarningMock,
38
+ }));
39
+
40
+ vi.mock('node:fs/promises', () => ({
41
+ default: {
42
+ readFile: fsReadFileMock,
43
+ },
44
+ }));
45
+
46
+ import {
47
+ RepoOperationsService,
48
+ type RepoOperationsServicePort,
49
+ } from '../src/application/services/repo-operations-service.js';
50
+
51
+ function commandResult(
52
+ overrides: Partial<{
53
+ code: number;
54
+ signal: NodeJS.Signals | null;
55
+ stdout: string;
56
+ stderr: string;
57
+ timeout: boolean;
58
+ }> = {},
59
+ ) {
60
+ return { code: 0, signal: null, stdout: '', stderr: '', timeout: false, ...overrides };
61
+ }
62
+
63
+ function makePort(overrides: Partial<RepoOperationsServicePort> = {}): RepoOperationsServicePort {
64
+ return {
65
+ getRepoRoot: vi.fn(() => '/repo'),
66
+ worktreePath: vi.fn((_featureId: string) => '/repo/.worktrees/feat1'),
67
+ getPolicySnapshot: vi.fn(() => ({
68
+ worktree: {
69
+ base_branch: 'main',
70
+ symlinks: [],
71
+ post_create: [],
72
+ },
73
+ path_rules: { allow_symlink_traversal: false },
74
+ })),
75
+ evidenceLatest: vi.fn(async () => ({ data: { latest: null } })),
76
+ ...overrides,
77
+ };
78
+ }
79
+
80
+ describe('RepoOperationsService.repoEnsureWorktree', () => {
81
+ beforeEach(() => {
82
+ vi.clearAllMocks();
83
+ ensureDirMock.mockResolvedValue(undefined);
84
+ });
85
+
86
+ it('GIVEN_worktree_already_exists_WHEN_repoEnsureWorktree_THEN_returns_existed_true', async () => {
87
+ pathExistsMock.mockResolvedValue(true);
88
+ const service = new RepoOperationsService(makePort());
89
+ const result = await service.repoEnsureWorktree('feat1');
90
+ expect(result.data.existed).toBe(true);
91
+ expect(result.data.feature_id).toBe('feat1');
92
+ expect(runGitMock).not.toHaveBeenCalled();
93
+ });
94
+
95
+ it('GIVEN_worktree_not_exists_base_branch_valid_branch_new_WHEN_repoEnsureWorktree_THEN_creates_worktree', async () => {
96
+ pathExistsMock.mockResolvedValue(false);
97
+ runGitMock
98
+ .mockResolvedValueOnce(commandResult({ code: 0 })) // rev-parse base_branch → valid
99
+ .mockResolvedValueOnce(commandResult({ code: 1 })) // rev-parse branch → not found
100
+ .mockResolvedValueOnce(commandResult({ code: 0 })) // branch create → success
101
+ .mockResolvedValueOnce(commandResult({ code: 0 })); // worktree add → success
102
+
103
+ const service = new RepoOperationsService(makePort());
104
+ const result = await service.repoEnsureWorktree('feat1');
105
+ expect(result.data.existed).toBe(false);
106
+ expect(result.data.feature_id).toBe('feat1');
107
+ });
108
+
109
+ it('GIVEN_worktree_not_exists_branch_already_exists_WHEN_repoEnsureWorktree_THEN_skips_branch_create', async () => {
110
+ pathExistsMock.mockResolvedValue(false);
111
+ runGitMock
112
+ .mockResolvedValueOnce(commandResult({ code: 0 })) // rev-parse base_branch → valid
113
+ .mockResolvedValueOnce(commandResult({ code: 0 })) // rev-parse branch → exists
114
+ .mockResolvedValueOnce(commandResult({ code: 0 })); // worktree add → success
115
+
116
+ const service = new RepoOperationsService(makePort());
117
+ const result = await service.repoEnsureWorktree('feat1');
118
+ expect(result.data.existed).toBe(false);
119
+ });
120
+
121
+ it('GIVEN_base_branch_invalid_WHEN_repoEnsureWorktree_THEN_uses_HEAD_as_base', async () => {
122
+ pathExistsMock.mockResolvedValue(false);
123
+ runGitMock
124
+ .mockResolvedValueOnce(commandResult({ code: 1 })) // rev-parse base_branch → invalid → use HEAD
125
+ .mockResolvedValueOnce(commandResult({ code: 1 })) // rev-parse branch → not found
126
+ .mockResolvedValueOnce(commandResult({ code: 0 })) // branch create → success
127
+ .mockResolvedValueOnce(commandResult({ code: 0 })); // worktree add → success
128
+
129
+ const service = new RepoOperationsService(makePort());
130
+ const result = await service.repoEnsureWorktree('feat1');
131
+ expect(result.data.existed).toBe(false);
132
+ // verify HEAD was used as base
133
+ expect(runGitMock).toHaveBeenCalledWith('/repo', ['branch', 'feat1', 'HEAD']);
134
+ });
135
+
136
+ it('GIVEN_branch_create_fails_WHEN_repoEnsureWorktree_THEN_throws_GIT_FAILURE', async () => {
137
+ pathExistsMock.mockResolvedValue(false);
138
+ runGitMock
139
+ .mockResolvedValueOnce(commandResult({ code: 0 })) // rev-parse base_branch → valid
140
+ .mockResolvedValueOnce(commandResult({ code: 1 })) // rev-parse branch → not found
141
+ .mockResolvedValueOnce(commandResult({ code: 1, stderr: 'branch error' })); // branch create fails
142
+
143
+ const service = new RepoOperationsService(makePort());
144
+ await expect(service.repoEnsureWorktree('feat1')).rejects.toMatchObject({
145
+ normalizedResponse: expect.objectContaining({ ok: false }),
146
+ });
147
+ });
148
+
149
+ it('GIVEN_worktree_add_fails_WHEN_repoEnsureWorktree_THEN_throws_GIT_FAILURE', async () => {
150
+ pathExistsMock.mockResolvedValue(false);
151
+ runGitMock
152
+ .mockResolvedValueOnce(commandResult({ code: 0 })) // rev-parse base_branch → valid
153
+ .mockResolvedValueOnce(commandResult({ code: 0 })) // rev-parse branch → exists
154
+ .mockResolvedValueOnce(commandResult({ code: 1, stderr: 'worktree error' })); // worktree add fails
155
+
156
+ const service = new RepoOperationsService(makePort());
157
+ await expect(service.repoEnsureWorktree('feat1')).rejects.toMatchObject({
158
+ normalizedResponse: expect.objectContaining({ ok: false }),
159
+ });
160
+ });
161
+
162
+ it('GIVEN_policy_has_symlinks_WHEN_repoEnsureWorktree_THEN_applies_symlinks', async () => {
163
+ pathExistsMock.mockResolvedValue(false);
164
+ runGitMock
165
+ .mockResolvedValueOnce(commandResult({ code: 0 }))
166
+ .mockResolvedValueOnce(commandResult({ code: 0 }))
167
+ .mockResolvedValueOnce(commandResult({ code: 0 }));
168
+
169
+ const port = makePort({
170
+ getPolicySnapshot: vi.fn(() => ({
171
+ worktree: {
172
+ base_branch: 'main',
173
+ symlinks: ['.env'],
174
+ post_create: [],
175
+ },
176
+ path_rules: { allow_symlink_traversal: false },
177
+ })),
178
+ });
179
+
180
+ const service = new RepoOperationsService(port);
181
+ await service.repoEnsureWorktree('feat1');
182
+ expect(applyWorktreeSymlinksMock).toHaveBeenCalled();
183
+ });
184
+
185
+ it('GIVEN_policy_has_post_create_WHEN_repoEnsureWorktree_THEN_runs_post_create', async () => {
186
+ pathExistsMock.mockResolvedValue(false);
187
+ runGitMock
188
+ .mockResolvedValueOnce(commandResult({ code: 0 }))
189
+ .mockResolvedValueOnce(commandResult({ code: 0 }))
190
+ .mockResolvedValueOnce(commandResult({ code: 0 }));
191
+
192
+ const port = makePort({
193
+ getPolicySnapshot: vi.fn(() => ({
194
+ worktree: {
195
+ base_branch: 'main',
196
+ symlinks: [],
197
+ post_create: ['npm ci'],
198
+ },
199
+ path_rules: { allow_symlink_traversal: false },
200
+ })),
201
+ });
202
+
203
+ const service = new RepoOperationsService(port);
204
+ await service.repoEnsureWorktree('feat1');
205
+ expect(runWorktreePostCreateMock).toHaveBeenCalled();
206
+ });
207
+ });
208
+
209
+ describe('RepoOperationsService.repoReadFile', () => {
210
+ beforeEach(() => {
211
+ vi.clearAllMocks();
212
+ normalizeRepoPathMock.mockImplementation(async (_root: string, p: string) => p);
213
+ normalizeFromWorktreeMock.mockImplementation(
214
+ (_worktree: string, _root: string, p: string) => p,
215
+ );
216
+ });
217
+
218
+ it('GIVEN_file_not_found_WHEN_repoReadFile_THEN_throws_FILE_NOT_FOUND', async () => {
219
+ pathExistsMock.mockResolvedValue(false);
220
+ const service = new RepoOperationsService(makePort());
221
+ await expect(service.repoReadFile('feat1', 'src/missing.ts')).rejects.toMatchObject({
222
+ normalizedResponse: expect.objectContaining({ ok: false }),
223
+ });
224
+ });
225
+
226
+ it('GIVEN_file_exists_WHEN_repoReadFile_THEN_returns_content', async () => {
227
+ pathExistsMock.mockResolvedValue(true);
228
+ fsReadFileMock.mockResolvedValue('file contents here');
229
+ const service = new RepoOperationsService(makePort());
230
+ const result = await service.repoReadFile('feat1', 'src/file.ts');
231
+ expect(result.data.content).toBe('file contents here');
232
+ expect(result.data.feature_id).toBe('feat1');
233
+ });
234
+ });
235
+
236
+ describe('RepoOperationsService.repoSearch', () => {
237
+ beforeEach(() => {
238
+ vi.clearAllMocks();
239
+ });
240
+
241
+ it('GIVEN_ripgrep_not_found_WHEN_repoSearch_THEN_throws_rg_not_found', async () => {
242
+ runCommandMock.mockResolvedValue(commandResult({ code: 127, stderr: 'rg: not found' }));
243
+ const service = new RepoOperationsService(makePort());
244
+ await expect(service.repoSearch('feat1', 'TODO')).rejects.toMatchObject({
245
+ normalizedResponse: expect.objectContaining({ ok: false }),
246
+ });
247
+ });
248
+
249
+ it('GIVEN_search_fails_with_non_zero_non_one_WHEN_repoSearch_THEN_throws_search_failed', async () => {
250
+ runCommandMock.mockResolvedValue(commandResult({ code: 2, stderr: 'error' }));
251
+ const service = new RepoOperationsService(makePort());
252
+ await expect(service.repoSearch('feat1', 'TODO')).rejects.toMatchObject({
253
+ normalizedResponse: expect.objectContaining({ ok: false }),
254
+ });
255
+ });
256
+
257
+ it('GIVEN_search_with_code_1_no_matches_WHEN_repoSearch_THEN_returns_empty_matches', async () => {
258
+ runCommandMock.mockResolvedValue(commandResult({ code: 1, stdout: '' }));
259
+ const service = new RepoOperationsService(makePort());
260
+ const result = await service.repoSearch('feat1', 'TODO');
261
+ expect(result.data.matches).toEqual([]);
262
+ });
263
+
264
+ it('GIVEN_search_succeeds_WHEN_repoSearch_THEN_returns_parsed_matches', async () => {
265
+ const stdout = 'src/foo.ts:10:const x = 1;\nsrc/bar.ts:20:const y = 2;';
266
+ runCommandMock.mockResolvedValue(commandResult({ code: 0, stdout }));
267
+ const service = new RepoOperationsService(makePort());
268
+ const result = await service.repoSearch('feat1', 'const');
269
+ expect(result.data.matches).toHaveLength(2);
270
+ expect(result.data.matches[0]).toMatchObject({
271
+ path: 'src/foo.ts',
272
+ line: 10,
273
+ snippet: 'const x = 1;',
274
+ });
275
+ });
276
+
277
+ it('GIVEN_match_line_without_colons_WHEN_repoSearch_THEN_returns_raw_entry', async () => {
278
+ runCommandMock.mockResolvedValue(commandResult({ code: 0, stdout: 'linewithoutseparator' }));
279
+ const service = new RepoOperationsService(makePort());
280
+ const result = await service.repoSearch('feat1', 'test');
281
+ expect(result.data.matches[0]).toMatchObject({ raw: 'linewithoutseparator' });
282
+ });
283
+ });
284
+
285
+ describe('RepoOperationsService.repoStatus', () => {
286
+ beforeEach(() => {
287
+ vi.clearAllMocks();
288
+ });
289
+
290
+ it('GIVEN_feature_id_WHEN_repoStatus_THEN_returns_status_and_branch', async () => {
291
+ runGitMock
292
+ .mockResolvedValueOnce(commandResult({ code: 0, stdout: ' M src/file.ts\n' }))
293
+ .mockResolvedValueOnce(commandResult({ code: 0, stdout: 'feat1\n' }));
294
+ const service = new RepoOperationsService(makePort());
295
+ const result = await service.repoStatus('feat1');
296
+ expect(result.data.branch).toBe('feat1');
297
+ expect(result.data.feature_id).toBe('feat1');
298
+ });
299
+ });
300
+
301
+ describe('RepoOperationsService.repoDiff', () => {
302
+ beforeEach(() => {
303
+ vi.clearAllMocks();
304
+ });
305
+
306
+ it('GIVEN_feature_id_WHEN_repoDiff_THEN_returns_diff', async () => {
307
+ runGitMock.mockResolvedValue(commandResult({ code: 0, stdout: 'diff output' }));
308
+ const service = new RepoOperationsService(makePort());
309
+ const result = await service.repoDiff('feat1', ['--stat']);
310
+ expect(result.data.diff).toBe('diff output');
311
+ });
312
+
313
+ it('GIVEN_options_without_dashes_WHEN_repoDiff_THEN_filters_invalid_options', async () => {
314
+ runGitMock.mockResolvedValue(commandResult({ code: 0, stdout: '' }));
315
+ const service = new RepoOperationsService(makePort());
316
+ await service.repoDiff('feat1', ['--stat', 'badoption']);
317
+ // badoption should be filtered out
318
+ expect(runGitMock).toHaveBeenCalledWith('/repo', ['diff', '--stat'], expect.any(Object));
319
+ });
320
+ });
321
+
322
+ describe('RepoOperationsService.repoDiffBundle', () => {
323
+ beforeEach(() => {
324
+ vi.clearAllMocks();
325
+ });
326
+
327
+ it('GIVEN_feature_id_WHEN_repoDiffBundle_THEN_returns_bundle', async () => {
328
+ runGitMock
329
+ .mockResolvedValueOnce(commandResult({ code: 0, stdout: 'stat output' }))
330
+ .mockResolvedValueOnce(commandResult({ code: 0, stdout: 'full diff' }))
331
+ .mockResolvedValueOnce(commandResult({ code: 0, stdout: 'src/a.ts\nsrc/b.ts\n' }));
332
+
333
+ const service = new RepoOperationsService(makePort());
334
+ const result = await service.repoDiffBundle('feat1');
335
+ expect(result.data.diff_stat).toBe('stat output');
336
+ expect(result.data.diff).toBe('full diff');
337
+ expect(result.data.touched_files).toEqual(['src/a.ts', 'src/b.ts']);
338
+ });
339
+ });
@@ -51,6 +51,10 @@ vi.mock('../src/providers/worker-provider-factory.js', () => ({
51
51
  },
52
52
  resolveWorkerProviderMode: vi.fn(() => 'live'),
53
53
  resolveWorkerProviderPolicy: vi.fn(() => ({ require_live_provider_for_run: true })),
54
+ resolveWorkerProviderObservability: vi.fn(() => ({
55
+ raw_agent_logs_enabled: false,
56
+ raw_agent_logs_retention_days: 60,
57
+ })),
54
58
  resolveWorkerProviderRuntime: vi.fn(() => ({
55
59
  worker_response_timeout_ms: 120000,
56
60
  max_consecutive_no_progress_iterations: 2,
@@ -271,6 +275,35 @@ describe('ResumeCommandHandler', () => {
271
275
 
272
276
  expect(result.ok).toBe(true);
273
277
  });
278
+
279
+ it('GIVEN_resume_feature_id_filter_WHEN_execute_THEN_targets_only_requested_feature', async () => {
280
+ const kernel = makeKernel({
281
+ readIndex: vi.fn(async () => ({
282
+ active: ['feature_a', 'feature_b'],
283
+ blocked: [],
284
+ blocked_queue: [],
285
+ })),
286
+ featureStateGet: vi.fn(async (featureId: string) => ({
287
+ data: {
288
+ front_matter: {
289
+ status: featureId === 'feature_b' ? STATUS.BUILDING : STATUS.PLANNING,
290
+ },
291
+ },
292
+ })),
293
+ });
294
+ const handler = new ResumeCommandHandler();
295
+ const context = makeContext(kernel);
296
+ context.options = { feature_id: 'feature_b' } as any;
297
+ const result = (await handler.execute(context)) as any;
298
+
299
+ expect(result.ok).toBe(true);
300
+ expect(result.data.resumed).toBe(true);
301
+ expect(result.data.resumed_features).toEqual([
302
+ expect.objectContaining({ feature_id: 'feature_b' }),
303
+ ]);
304
+ expect(kernel.featureStateGet).toHaveBeenCalledTimes(1);
305
+ expect(kernel.featureStateGet).toHaveBeenCalledWith('feature_b');
306
+ });
274
307
  });
275
308
 
276
309
  describe('ResumeCommandHandler additional branches', () => {
@@ -0,0 +1,130 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ appendCappedLogEntries,
4
+ isConsoleSendAllowed,
5
+ stepHunkIndex,
6
+ } from '../../../packages/web-dashboard/src/lib/review-workspace-logic.js';
7
+ import { isEditableTarget } from '../../../packages/web-dashboard/src/lib/review-contracts.js';
8
+ import type {
9
+ AgentLogEntry,
10
+ AgentSessionCluster,
11
+ } from '../../../packages/web-dashboard/src/lib/types.js';
12
+
13
+ function makeEntry(index: number): AgentLogEntry {
14
+ return {
15
+ id: `entry-${index}`,
16
+ timestamp: new Date(1_700_000_000_000 + index).toISOString(),
17
+ actor: 'builder:feature_checkout',
18
+ role: 'builder',
19
+ text: `message ${index}`,
20
+ };
21
+ }
22
+
23
+ function activeCluster(): AgentSessionCluster {
24
+ return {
25
+ orchestrator_session_id: 'orch-1',
26
+ planner_session_id: 'planner-1',
27
+ builder_session_id: 'builder-1',
28
+ qa_session_id: 'qa-1',
29
+ };
30
+ }
31
+
32
+ describe('review workspace logic', () => {
33
+ it('caps streamed log entries to a fixed buffer size', () => {
34
+ const current = [makeEntry(1), makeEntry(2)];
35
+ const incoming = [makeEntry(3), makeEntry(4), makeEntry(5)];
36
+
37
+ const result = appendCappedLogEntries(current, incoming, 3);
38
+
39
+ expect(result.map((entry) => entry.id)).toEqual(['entry-3', 'entry-4', 'entry-5']);
40
+ });
41
+
42
+ it('allows console send only for active non-terminal sessions and valid interval', () => {
43
+ expect(
44
+ isConsoleSendAllowed({
45
+ status: 'qa',
46
+ cluster: activeCluster(),
47
+ hasActiveRuntimeSession: true,
48
+ submitting: false,
49
+ message: 'Please re-run gate',
50
+ nowMs: 2_000,
51
+ lastMessageSentAtMs: 500,
52
+ }),
53
+ ).toBe(true);
54
+
55
+ expect(
56
+ isConsoleSendAllowed({
57
+ status: 'merged',
58
+ cluster: activeCluster(),
59
+ hasActiveRuntimeSession: true,
60
+ submitting: false,
61
+ message: 'Should not send',
62
+ nowMs: 2_000,
63
+ lastMessageSentAtMs: 0,
64
+ }),
65
+ ).toBe(false);
66
+
67
+ expect(
68
+ isConsoleSendAllowed({
69
+ status: 'qa',
70
+ cluster: {
71
+ orchestrator_session_id: 'unknown',
72
+ planner_session_id: 'unknown',
73
+ builder_session_id: 'unknown',
74
+ qa_session_id: 'unknown',
75
+ },
76
+ hasActiveRuntimeSession: true,
77
+ submitting: false,
78
+ message: 'Should not send',
79
+ nowMs: 2_000,
80
+ lastMessageSentAtMs: 0,
81
+ }),
82
+ ).toBe(false);
83
+
84
+ expect(
85
+ isConsoleSendAllowed({
86
+ status: 'qa',
87
+ cluster: activeCluster(),
88
+ hasActiveRuntimeSession: false,
89
+ submitting: false,
90
+ message: 'No runtime',
91
+ nowMs: 2_000,
92
+ lastMessageSentAtMs: 0,
93
+ }),
94
+ ).toBe(false);
95
+
96
+ expect(
97
+ isConsoleSendAllowed({
98
+ status: 'qa',
99
+ cluster: activeCluster(),
100
+ hasActiveRuntimeSession: true,
101
+ submitting: false,
102
+ message: 'Rate limited',
103
+ nowMs: 1_200,
104
+ lastMessageSentAtMs: 500,
105
+ }),
106
+ ).toBe(false);
107
+ });
108
+
109
+ it('clamps hunk index movement', () => {
110
+ expect(stepHunkIndex(0, -1, 5)).toBe(0);
111
+ expect(stepHunkIndex(3, -1, 5)).toBe(2);
112
+ expect(stepHunkIndex(0, 1, 1)).toBe(0);
113
+ expect(stepHunkIndex(3, 1, 5)).toBe(4);
114
+ expect(stepHunkIndex(4, 1, 5)).toBe(4);
115
+ });
116
+
117
+ it('marks editable event targets for shortcut suppression', () => {
118
+ expect(isEditableTarget({ tagName: 'INPUT' } as unknown as EventTarget)).toBe(true);
119
+ expect(isEditableTarget({ tagName: 'textarea' } as unknown as EventTarget)).toBe(true);
120
+ expect(isEditableTarget({ isContentEditable: true } as unknown as EventTarget)).toBe(true);
121
+ expect(
122
+ isEditableTarget({
123
+ closest: (selector: string) => (selector === '.monaco-editor' ? {} : null),
124
+ } as unknown as EventTarget),
125
+ ).toBe(true);
126
+ expect(
127
+ isEditableTarget({ tagName: 'div', isContentEditable: false } as unknown as EventTarget),
128
+ ).toBe(false);
129
+ });
130
+ });