agentic-orchestrator 0.1.7 → 0.1.9
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.
- package/README.md +25 -3
- package/agentic/orchestrator/schemas/agents.schema.json +1 -1
- package/apps/control-plane/src/cli/dashboard-command-handler.ts +42 -5
- package/apps/control-plane/src/cli/env-file.ts +115 -0
- package/apps/control-plane/src/cli/help-command-handler.ts +1 -1
- package/apps/control-plane/src/cli/init-command-handler.ts +72 -2
- package/apps/control-plane/src/cli/retry-command-handler.ts +0 -1
- package/apps/control-plane/src/core/kernel.ts +1 -3
- package/apps/control-plane/src/core/tool-caller.ts +18 -3
- package/apps/control-plane/src/interfaces/cli/bootstrap.ts +10 -3
- package/apps/control-plane/src/providers/providers.ts +67 -8
- package/apps/control-plane/src/supervisor/build-wave-executor.ts +21 -4
- package/apps/control-plane/src/supervisor/qa-wave-executor.ts +21 -4
- package/apps/control-plane/src/supervisor/runtime.ts +9 -4
- package/apps/control-plane/src/supervisor/types.ts +1 -0
- package/apps/control-plane/test/cli-helpers.spec.ts +4 -0
- package/apps/control-plane/test/dashboard-command.spec.ts +36 -0
- package/apps/control-plane/test/init-wizard.spec.ts +166 -1
- package/apps/control-plane/test/providers.spec.ts +75 -2
- package/apps/control-plane/test/supervisor-collaborators.spec.ts +86 -0
- package/apps/control-plane/test/supervisor.unit.spec.ts +1 -1
- package/config/agentic/orchestrator/adapters.yaml +3 -0
- package/config/agentic/orchestrator/agents.yaml +13 -0
- package/config/agentic/orchestrator/gates.yaml +28 -0
- package/config/agentic/orchestrator/policy.yaml +22 -0
- package/config/agentic/orchestrator/prompts/builder.system.md +1 -0
- package/config/agentic/orchestrator/prompts/planner.system.md +16 -0
- package/config/agentic/orchestrator/prompts/qa.system.md +1 -0
- package/dist/apps/control-plane/cli/dashboard-command-handler.js +32 -5
- package/dist/apps/control-plane/cli/dashboard-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/env-file.d.ts +4 -0
- package/dist/apps/control-plane/cli/env-file.js +89 -0
- package/dist/apps/control-plane/cli/env-file.js.map +1 -0
- package/dist/apps/control-plane/cli/help-command-handler.js +1 -1
- package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/init-command-handler.js +53 -4
- package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/retry-command-handler.js +0 -1
- package/dist/apps/control-plane/cli/retry-command-handler.js.map +1 -1
- package/dist/apps/control-plane/core/kernel.js.map +1 -1
- package/dist/apps/control-plane/core/tool-caller.d.ts +11 -1
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js +9 -3
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
- package/dist/apps/control-plane/providers/providers.d.ts +2 -1
- package/dist/apps/control-plane/providers/providers.js +52 -7
- package/dist/apps/control-plane/providers/providers.js.map +1 -1
- package/dist/apps/control-plane/supervisor/build-wave-executor.js +20 -4
- package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js +20 -4
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/runtime.d.ts +2 -2
- package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
- package/dist/apps/control-plane/supervisor/types.d.ts +1 -1
- package/dist/apps/control-plane/supervisor/types.js.map +1 -1
- package/package.json +1 -1
- package/spec-files/completed/agentic_orchestrator_feature_gaps_closure_spec.md +2 -0
- package/spec-files/outstanding/agentic_orchestrator_provider_auth_bootstrap_spec.md +384 -0
- package/spec-files/progress.md +19 -0
|
@@ -17,6 +17,17 @@ interface BuildWaveExecutorDependencies {
|
|
|
17
17
|
reactionsService?: ReactionsService;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function isRetryableToolError(error: unknown): boolean {
|
|
21
|
+
if (!error || typeof error !== 'object') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const details = (error as { details?: unknown }).details;
|
|
25
|
+
if (!details || typeof details !== 'object') {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return (details as { retryable?: unknown }).retryable === true;
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
export class BuildWaveExecutor {
|
|
21
32
|
private readonly toolCaller: SupervisorToolCaller;
|
|
22
33
|
private readonly workerDecisionRunner: WorkerDecisionRunner;
|
|
@@ -72,6 +83,7 @@ export class BuildWaveExecutor {
|
|
|
72
83
|
let gateExitCode: number;
|
|
73
84
|
let gateEvidencePath = '';
|
|
74
85
|
let gateLogs = '';
|
|
86
|
+
let gateRetryEligible: boolean;
|
|
75
87
|
const failureHistory: GateRepairContext['failureHistory'] = [];
|
|
76
88
|
|
|
77
89
|
try {
|
|
@@ -80,18 +92,19 @@ export class BuildWaveExecutor {
|
|
|
80
92
|
TOOLS.GATES_RUN,
|
|
81
93
|
{
|
|
82
94
|
feature_id: featureId,
|
|
83
|
-
profile: null,
|
|
84
95
|
mode: 'fast',
|
|
85
96
|
},
|
|
86
97
|
);
|
|
87
98
|
gateOverall = gateResult.data.overall ?? GATE_RESULT.PASS;
|
|
88
99
|
gateExitCode = gateOverall === GATE_RESULT.FAIL ? 1 : 0;
|
|
89
100
|
gateEvidencePath = gateResult.data.evidence_path ?? '';
|
|
101
|
+
gateRetryEligible = gateOverall === GATE_RESULT.FAIL;
|
|
90
102
|
} catch (error) {
|
|
91
103
|
gateOverall = GATE_RESULT.FAIL;
|
|
92
104
|
gateExitCode = 1;
|
|
93
105
|
const typed = error as { message?: string };
|
|
94
106
|
gateLogs = typed.message ?? '';
|
|
107
|
+
gateRetryEligible = isRetryableToolError(error);
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
if (gateOverall === GATE_RESULT.FAIL) {
|
|
@@ -106,13 +119,13 @@ export class BuildWaveExecutor {
|
|
|
106
119
|
});
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
if (this.reactionsService && gateOverall === GATE_RESULT.FAIL) {
|
|
122
|
+
if (this.reactionsService && gateOverall === GATE_RESULT.FAIL && gateRetryEligible) {
|
|
110
123
|
const context = await this.toolCaller.callTool('builder', TOOLS.FEATURE_GET_CONTEXT, {
|
|
111
124
|
feature_id: featureId,
|
|
112
125
|
});
|
|
113
126
|
let retryCount = initialRetryCount;
|
|
114
127
|
const retryDelayMs = this.reactionsService.retryDelayMs();
|
|
115
|
-
while (this.reactionsService.shouldRetry(featureId, retryCount)) {
|
|
128
|
+
while (gateRetryEligible && this.reactionsService.shouldRetry(featureId, retryCount)) {
|
|
116
129
|
if (typeof this.reactionsService.waitBeforeRetry === 'function') {
|
|
117
130
|
await this.reactionsService.waitBeforeRetry();
|
|
118
131
|
}
|
|
@@ -140,7 +153,6 @@ export class BuildWaveExecutor {
|
|
|
140
153
|
TOOLS.GATES_RUN,
|
|
141
154
|
{
|
|
142
155
|
feature_id: featureId,
|
|
143
|
-
profile: null,
|
|
144
156
|
mode: 'fast',
|
|
145
157
|
},
|
|
146
158
|
);
|
|
@@ -148,11 +160,13 @@ export class BuildWaveExecutor {
|
|
|
148
160
|
gateExitCode = gateOverall === GATE_RESULT.FAIL ? 1 : 0;
|
|
149
161
|
gateEvidencePath = retryResult.data.evidence_path ?? gateEvidencePath;
|
|
150
162
|
gateLogs = '';
|
|
163
|
+
gateRetryEligible = gateOverall === GATE_RESULT.FAIL;
|
|
151
164
|
} catch (error) {
|
|
152
165
|
gateOverall = GATE_RESULT.FAIL;
|
|
153
166
|
gateExitCode = 1;
|
|
154
167
|
const typed = error as { message?: string };
|
|
155
168
|
gateLogs = typed.message ?? '';
|
|
169
|
+
gateRetryEligible = isRetryableToolError(error);
|
|
156
170
|
}
|
|
157
171
|
|
|
158
172
|
retryCount += 1;
|
|
@@ -173,6 +187,9 @@ export class BuildWaveExecutor {
|
|
|
173
187
|
if (gateOverall === GATE_RESULT.PASS) {
|
|
174
188
|
break;
|
|
175
189
|
}
|
|
190
|
+
if (!gateRetryEligible) {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
176
193
|
}
|
|
177
194
|
|
|
178
195
|
if (gateOverall === GATE_RESULT.FAIL && this.reactionsService.shouldEscalate(retryCount)) {
|
|
@@ -31,6 +31,17 @@ interface QaWaveExecutorDependencies {
|
|
|
31
31
|
reactionsService?: ReactionsService;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function isRetryableToolError(error: unknown): boolean {
|
|
35
|
+
if (!error || typeof error !== 'object') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const details = (error as { details?: unknown }).details;
|
|
39
|
+
if (!details || typeof details !== 'object') {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return (details as { retryable?: unknown }).retryable === true;
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
export class QaWaveExecutor {
|
|
35
46
|
private readonly kernel: FeatureOrchestrationPort;
|
|
36
47
|
private readonly provider: WorkerProvider;
|
|
@@ -93,22 +104,24 @@ export class QaWaveExecutor {
|
|
|
93
104
|
let gateExitCode: number;
|
|
94
105
|
let gateEvidencePath = '';
|
|
95
106
|
let gateLogs = '';
|
|
107
|
+
let gateRetryEligible: boolean;
|
|
96
108
|
const failureHistory: GateRepairContext['failureHistory'] = [];
|
|
97
109
|
|
|
98
110
|
try {
|
|
99
111
|
const gateResult = await this.toolCaller.callTool<GatesRunData>('qa', TOOLS.GATES_RUN, {
|
|
100
112
|
feature_id: featureId,
|
|
101
|
-
profile: null,
|
|
102
113
|
mode: 'full',
|
|
103
114
|
});
|
|
104
115
|
gateOverall = gateResult.data.overall ?? GATE_RESULT.PASS;
|
|
105
116
|
gateExitCode = gateOverall === GATE_RESULT.FAIL ? 1 : 0;
|
|
106
117
|
gateEvidencePath = gateResult.data.evidence_path ?? '';
|
|
118
|
+
gateRetryEligible = gateOverall === GATE_RESULT.FAIL;
|
|
107
119
|
} catch (error) {
|
|
108
120
|
gateOverall = GATE_RESULT.FAIL;
|
|
109
121
|
gateExitCode = 1;
|
|
110
122
|
const typed = error as { message?: string };
|
|
111
123
|
gateLogs = typed.message ?? '';
|
|
124
|
+
gateRetryEligible = isRetryableToolError(error);
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
if (gateOverall === GATE_RESULT.FAIL) {
|
|
@@ -123,10 +136,10 @@ export class QaWaveExecutor {
|
|
|
123
136
|
});
|
|
124
137
|
}
|
|
125
138
|
|
|
126
|
-
if (this.reactionsService && gateOverall === GATE_RESULT.FAIL) {
|
|
139
|
+
if (this.reactionsService && gateOverall === GATE_RESULT.FAIL && gateRetryEligible) {
|
|
127
140
|
let retryCount = initialRetryCount;
|
|
128
141
|
const retryDelayMs = this.reactionsService.retryDelayMs();
|
|
129
|
-
while (this.reactionsService.shouldRetry(featureId, retryCount)) {
|
|
142
|
+
while (gateRetryEligible && this.reactionsService.shouldRetry(featureId, retryCount)) {
|
|
130
143
|
if (typeof this.reactionsService.waitBeforeRetry === 'function') {
|
|
131
144
|
await this.reactionsService.waitBeforeRetry();
|
|
132
145
|
}
|
|
@@ -154,7 +167,6 @@ export class QaWaveExecutor {
|
|
|
154
167
|
TOOLS.GATES_RUN,
|
|
155
168
|
{
|
|
156
169
|
feature_id: featureId,
|
|
157
|
-
profile: null,
|
|
158
170
|
mode: 'full',
|
|
159
171
|
},
|
|
160
172
|
);
|
|
@@ -162,11 +174,13 @@ export class QaWaveExecutor {
|
|
|
162
174
|
gateExitCode = gateOverall === GATE_RESULT.FAIL ? 1 : 0;
|
|
163
175
|
gateEvidencePath = retryResult.data.evidence_path ?? gateEvidencePath;
|
|
164
176
|
gateLogs = '';
|
|
177
|
+
gateRetryEligible = gateOverall === GATE_RESULT.FAIL;
|
|
165
178
|
} catch (error) {
|
|
166
179
|
gateOverall = GATE_RESULT.FAIL;
|
|
167
180
|
gateExitCode = 1;
|
|
168
181
|
const typed = error as { message?: string };
|
|
169
182
|
gateLogs = typed.message ?? '';
|
|
183
|
+
gateRetryEligible = isRetryableToolError(error);
|
|
170
184
|
}
|
|
171
185
|
|
|
172
186
|
retryCount += 1;
|
|
@@ -187,6 +201,9 @@ export class QaWaveExecutor {
|
|
|
187
201
|
if (gateOverall === GATE_RESULT.PASS) {
|
|
188
202
|
break;
|
|
189
203
|
}
|
|
204
|
+
if (!gateRetryEligible) {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
190
207
|
}
|
|
191
208
|
|
|
192
209
|
if (gateOverall === GATE_RESULT.FAIL && this.reactionsService.shouldEscalate(retryCount)) {
|
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
InitialPlanGenerator,
|
|
33
33
|
PromptBundle,
|
|
34
34
|
RuntimeRole,
|
|
35
|
+
ToolArgs,
|
|
35
36
|
SupervisorKernelPort,
|
|
36
37
|
SupervisorOptions,
|
|
37
38
|
SupervisorRuntimeState,
|
|
@@ -472,12 +473,16 @@ export class SupervisorRuntime
|
|
|
472
473
|
}
|
|
473
474
|
}
|
|
474
475
|
|
|
475
|
-
async callTool<TData = Record<string, unknown
|
|
476
|
+
async callTool<TData = Record<string, unknown>, TToolName extends string = string>(
|
|
476
477
|
role: RuntimeRole,
|
|
477
|
-
toolName:
|
|
478
|
-
args:
|
|
478
|
+
toolName: TToolName,
|
|
479
|
+
args: ToolArgs<TToolName>,
|
|
479
480
|
): Promise<{ ok: true; data: TData }> {
|
|
480
|
-
const payload = withOperationIdIfRequired(
|
|
481
|
+
const payload = withOperationIdIfRequired(
|
|
482
|
+
toolName,
|
|
483
|
+
args as Record<string, unknown>,
|
|
484
|
+
createOperationId,
|
|
485
|
+
);
|
|
481
486
|
const roleSessionId = this.resolveRoleSessionId(role, payload);
|
|
482
487
|
|
|
483
488
|
const response = await this.toolClient.call(toolName, payload, {
|
|
@@ -221,6 +221,8 @@ describe('RetryCommandHandler', () => {
|
|
|
221
221
|
expect.objectContaining({ feature_id: 'feature_abc', mode: 'full' }),
|
|
222
222
|
expect.any(Object),
|
|
223
223
|
);
|
|
224
|
+
const gatesRunCall = callMock.mock.calls.find((call) => call[0] === TOOLS.GATES_RUN);
|
|
225
|
+
expect(gatesRunCall?.[1]).not.toHaveProperty('profile');
|
|
224
226
|
});
|
|
225
227
|
|
|
226
228
|
it('GIVEN_valid_feature_id_with_force_WHEN_execute_called_THEN_gets_current_state_and_patches_retry_count', async () => {
|
|
@@ -259,6 +261,8 @@ describe('RetryCommandHandler', () => {
|
|
|
259
261
|
expect.objectContaining({ feature_id: 'feature_abc' }),
|
|
260
262
|
expect.any(Object),
|
|
261
263
|
);
|
|
264
|
+
const gatesRunCall = callMock.mock.calls.find((call) => call[0] === TOOLS.GATES_RUN);
|
|
265
|
+
expect(gatesRunCall?.[1]).not.toHaveProperty('profile');
|
|
262
266
|
});
|
|
263
267
|
});
|
|
264
268
|
|
|
@@ -2,6 +2,12 @@ import { describe, expect, it, vi, afterEach } from 'vitest';
|
|
|
2
2
|
import { DashboardCommandHandler } from '../src/cli/dashboard-command-handler.js';
|
|
3
3
|
import type { ChildProcess } from 'node:child_process';
|
|
4
4
|
|
|
5
|
+
vi.mock('node:fs/promises', () => ({
|
|
6
|
+
default: {
|
|
7
|
+
access: vi.fn(async () => undefined),
|
|
8
|
+
},
|
|
9
|
+
}));
|
|
10
|
+
|
|
5
11
|
vi.mock('node:child_process', () => ({
|
|
6
12
|
execFile: vi.fn(),
|
|
7
13
|
spawn: vi.fn(),
|
|
@@ -104,4 +110,34 @@ describe('DashboardCommandHandler', () => {
|
|
|
104
110
|
);
|
|
105
111
|
expect(result).toMatchObject({ ok: true, data: { mode: 'dev' } });
|
|
106
112
|
});
|
|
113
|
+
|
|
114
|
+
it('GIVEN_dashboard_cli_missing_WHEN_dashboard_command_THEN_installs_workspace_dependencies_before_build', async () => {
|
|
115
|
+
const { default: fs } = await import('node:fs/promises');
|
|
116
|
+
const { execFile, spawn } = await import('node:child_process');
|
|
117
|
+
const mockChild = { unref: vi.fn(), on: vi.fn() } as unknown as ChildProcess;
|
|
118
|
+
|
|
119
|
+
vi.mocked(fs.access).mockRejectedValue(new Error('missing'));
|
|
120
|
+
vi.mocked(execFile).mockImplementation((_file, _args, _opts, cb) => {
|
|
121
|
+
cb?.(null, '', '');
|
|
122
|
+
return {} as never;
|
|
123
|
+
});
|
|
124
|
+
vi.mocked(spawn).mockReturnValue(mockChild);
|
|
125
|
+
|
|
126
|
+
const handler = new DashboardCommandHandler();
|
|
127
|
+
const result = await handler.execute({ foreground: false });
|
|
128
|
+
|
|
129
|
+
expect(execFile).toHaveBeenCalledWith(
|
|
130
|
+
'npm',
|
|
131
|
+
['install', '--workspace', '@aop/web-dashboard', '--no-audit', '--no-fund'],
|
|
132
|
+
expect.any(Object),
|
|
133
|
+
expect.any(Function),
|
|
134
|
+
);
|
|
135
|
+
expect(execFile).toHaveBeenCalledWith(
|
|
136
|
+
'npm',
|
|
137
|
+
['run', '--workspace', '@aop/web-dashboard', 'build'],
|
|
138
|
+
expect.any(Object),
|
|
139
|
+
expect.any(Function),
|
|
140
|
+
);
|
|
141
|
+
expect(result).toMatchObject({ ok: true, data: { built: true } });
|
|
142
|
+
});
|
|
107
143
|
});
|
|
@@ -27,7 +27,7 @@ import { InitCommandHandler } from '../src/cli/init-command-handler.js';
|
|
|
27
27
|
import { HelpCommandHandler } from '../src/cli/help-command-handler.js';
|
|
28
28
|
|
|
29
29
|
function makePromptFactory(responses: string[]) {
|
|
30
|
-
const question = vi.fn(async () => responses.shift() ?? '');
|
|
30
|
+
const question = vi.fn(async (_query: string) => responses.shift() ?? '');
|
|
31
31
|
const close = vi.fn();
|
|
32
32
|
const factory = vi.fn(() => ({ question, close }));
|
|
33
33
|
return { factory, question, close };
|
|
@@ -97,6 +97,7 @@ describe('InitCommandHandler', () => {
|
|
|
97
97
|
expect(agentsContent).toContain('roles:');
|
|
98
98
|
expect(agentsContent).toContain('default_provider: custom');
|
|
99
99
|
expect(agentsContent).toContain('default_model: local-default');
|
|
100
|
+
expect(agentsContent).not.toContain('provider_config_env:');
|
|
100
101
|
|
|
101
102
|
const adaptersContent = await fs.readFile(
|
|
102
103
|
path.join(cwd, 'config', 'agentic', 'orchestrator', 'adapters.yaml'),
|
|
@@ -350,6 +351,7 @@ describe('InitCommandHandler', () => {
|
|
|
350
351
|
);
|
|
351
352
|
expect(agentsContent).toContain('default_provider: claude');
|
|
352
353
|
expect(agentsContent).toContain('default_model: sonnet-4.5');
|
|
354
|
+
expect(agentsContent).not.toContain('provider_config_env:');
|
|
353
355
|
|
|
354
356
|
const adaptersContent = await fs.readFile(
|
|
355
357
|
path.join(cwd, 'config', 'agentic', 'orchestrator', 'adapters.yaml'),
|
|
@@ -358,6 +360,169 @@ describe('InitCommandHandler', () => {
|
|
|
358
360
|
expect(adaptersContent).toContain('activity-detector: claude-jsonl');
|
|
359
361
|
expect(adaptersContent).toContain('scm-provider: github');
|
|
360
362
|
});
|
|
363
|
+
|
|
364
|
+
it('GIVEN_interactive_local_cli_mode_WHEN_init_runs_THEN_provider_env_prompt_is_skipped_and_agents_yaml_omits_provider_config_env', async () => {
|
|
365
|
+
await fs.mkdir(path.join(cwd, '.git'), { recursive: true });
|
|
366
|
+
await fs.writeFile(path.join(cwd, '.git', 'config'), '[core]\n', 'utf8');
|
|
367
|
+
execFileMock.mockResolvedValue({ stdout: 'origin/main\n', stderr: '' });
|
|
368
|
+
|
|
369
|
+
const prompts = makePromptFactory([
|
|
370
|
+
'main',
|
|
371
|
+
'codex',
|
|
372
|
+
'codex-default',
|
|
373
|
+
'github',
|
|
374
|
+
'3',
|
|
375
|
+
'3000',
|
|
376
|
+
'none',
|
|
377
|
+
'vitest',
|
|
378
|
+
'yes',
|
|
379
|
+
]);
|
|
380
|
+
const handler = new InitCommandHandler(cwd, prompts.factory);
|
|
381
|
+
const result = await handler.execute({ auto: false });
|
|
382
|
+
|
|
383
|
+
expect(result.ok).toBe(true);
|
|
384
|
+
expect(
|
|
385
|
+
prompts.question.mock.calls.some((call) =>
|
|
386
|
+
String(call[0]).includes('Provider config env var name'),
|
|
387
|
+
),
|
|
388
|
+
).toBe(false);
|
|
389
|
+
|
|
390
|
+
const agentsContent = await fs.readFile(
|
|
391
|
+
path.join(cwd, 'config', 'agentic', 'orchestrator', 'agents.yaml'),
|
|
392
|
+
'utf8',
|
|
393
|
+
);
|
|
394
|
+
expect(agentsContent).not.toContain('provider_config_env:');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('GIVEN_api_backed_mode_with_existing_env_var_WHEN_init_runs_THEN_agents_yaml_uses_that_env_var', async () => {
|
|
398
|
+
await fs.mkdir(path.join(cwd, '.git'), { recursive: true });
|
|
399
|
+
await fs.writeFile(path.join(cwd, '.git', 'config'), '[core]\n', 'utf8');
|
|
400
|
+
execFileMock.mockResolvedValue({ stdout: 'origin/main\n', stderr: '' });
|
|
401
|
+
process.env.GEMINI_API_KEY = 'existing-key';
|
|
402
|
+
|
|
403
|
+
const prompts = makePromptFactory([
|
|
404
|
+
'main',
|
|
405
|
+
'gemini',
|
|
406
|
+
'gemini-default',
|
|
407
|
+
'github',
|
|
408
|
+
'3',
|
|
409
|
+
'3000',
|
|
410
|
+
'none',
|
|
411
|
+
'vitest',
|
|
412
|
+
'no',
|
|
413
|
+
'GEMINI_API_KEY',
|
|
414
|
+
]);
|
|
415
|
+
const handler = new InitCommandHandler(cwd, prompts.factory);
|
|
416
|
+
const result = await handler.execute({ auto: false }).finally(() => {
|
|
417
|
+
delete process.env.GEMINI_API_KEY;
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
expect(result.ok).toBe(true);
|
|
421
|
+
const agentsContent = await fs.readFile(
|
|
422
|
+
path.join(cwd, 'config', 'agentic', 'orchestrator', 'agents.yaml'),
|
|
423
|
+
'utf8',
|
|
424
|
+
);
|
|
425
|
+
expect(agentsContent).toContain('provider_config_env: GEMINI_API_KEY');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('GIVEN_api_backed_mode_with_env_var_in_dotenv_WHEN_init_runs_THEN_agents_yaml_uses_that_env_var_without_bootstrap_prompt', async () => {
|
|
429
|
+
await fs.mkdir(path.join(cwd, '.git'), { recursive: true });
|
|
430
|
+
await fs.writeFile(path.join(cwd, '.git', 'config'), '[core]\n', 'utf8');
|
|
431
|
+
await fs.writeFile(path.join(cwd, '.env'), 'GEMINI_API_KEY=dotenv-key\n', 'utf8');
|
|
432
|
+
execFileMock.mockResolvedValue({ stdout: 'origin/main\n', stderr: '' });
|
|
433
|
+
|
|
434
|
+
const prompts = makePromptFactory([
|
|
435
|
+
'main',
|
|
436
|
+
'gemini',
|
|
437
|
+
'gemini-default',
|
|
438
|
+
'github',
|
|
439
|
+
'3',
|
|
440
|
+
'3000',
|
|
441
|
+
'none',
|
|
442
|
+
'vitest',
|
|
443
|
+
'no',
|
|
444
|
+
'GEMINI_API_KEY',
|
|
445
|
+
]);
|
|
446
|
+
const handler = new InitCommandHandler(cwd, prompts.factory);
|
|
447
|
+
const result = await handler.execute({ auto: false });
|
|
448
|
+
|
|
449
|
+
expect(result.ok).toBe(true);
|
|
450
|
+
expect(
|
|
451
|
+
prompts.question.mock.calls.some((call) =>
|
|
452
|
+
String(call[0]).includes('Paste provider key to store in AOP_PROVIDER_CONFIG_ENV'),
|
|
453
|
+
),
|
|
454
|
+
).toBe(false);
|
|
455
|
+
const agentsContent = await fs.readFile(
|
|
456
|
+
path.join(cwd, 'config', 'agentic', 'orchestrator', 'agents.yaml'),
|
|
457
|
+
'utf8',
|
|
458
|
+
);
|
|
459
|
+
expect(agentsContent).toContain('provider_config_env: GEMINI_API_KEY');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('GIVEN_api_backed_mode_with_missing_env_var_WHEN_init_runs_THEN_bootstraps_AOP_provider_env_in_dotenv', async () => {
|
|
463
|
+
await fs.mkdir(path.join(cwd, '.git'), { recursive: true });
|
|
464
|
+
await fs.writeFile(path.join(cwd, '.git', 'config'), '[core]\n', 'utf8');
|
|
465
|
+
execFileMock.mockResolvedValue({ stdout: 'origin/main\n', stderr: '' });
|
|
466
|
+
|
|
467
|
+
const prompts = makePromptFactory([
|
|
468
|
+
'main',
|
|
469
|
+
'gemini',
|
|
470
|
+
'gemini-default',
|
|
471
|
+
'github',
|
|
472
|
+
'3',
|
|
473
|
+
'3000',
|
|
474
|
+
'none',
|
|
475
|
+
'vitest',
|
|
476
|
+
'no',
|
|
477
|
+
'GEMINI_API_KEY',
|
|
478
|
+
'bootstrap-secret',
|
|
479
|
+
]);
|
|
480
|
+
const handler = new InitCommandHandler(cwd, prompts.factory);
|
|
481
|
+
const result = await handler.execute({ auto: false });
|
|
482
|
+
|
|
483
|
+
expect(result.ok).toBe(true);
|
|
484
|
+
expect(result.data.next_steps).toContain(
|
|
485
|
+
'Stored provider credential in .env as AOP_PROVIDER_CONFIG_ENV.',
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
const agentsContent = await fs.readFile(
|
|
489
|
+
path.join(cwd, 'config', 'agentic', 'orchestrator', 'agents.yaml'),
|
|
490
|
+
'utf8',
|
|
491
|
+
);
|
|
492
|
+
expect(agentsContent).toContain('provider_config_env: AOP_PROVIDER_CONFIG_ENV');
|
|
493
|
+
|
|
494
|
+
const envContent = await fs.readFile(path.join(cwd, '.env'), 'utf8');
|
|
495
|
+
expect(envContent).toContain('AOP_PROVIDER_CONFIG_ENV=bootstrap-secret');
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('GIVEN_existing_AOP_provider_env_in_dotenv_WHEN_init_bootstraps_again_THEN_replaces_existing_value_without_duplication', async () => {
|
|
499
|
+
await fs.mkdir(path.join(cwd, '.git'), { recursive: true });
|
|
500
|
+
await fs.writeFile(path.join(cwd, '.git', 'config'), '[core]\n', 'utf8');
|
|
501
|
+
await fs.writeFile(path.join(cwd, '.env'), 'FOO=bar\nAOP_PROVIDER_CONFIG_ENV=old\n', 'utf8');
|
|
502
|
+
execFileMock.mockResolvedValue({ stdout: 'origin/main\n', stderr: '' });
|
|
503
|
+
|
|
504
|
+
const prompts = makePromptFactory([
|
|
505
|
+
'main',
|
|
506
|
+
'gemini',
|
|
507
|
+
'gemini-default',
|
|
508
|
+
'github',
|
|
509
|
+
'3',
|
|
510
|
+
'3000',
|
|
511
|
+
'none',
|
|
512
|
+
'vitest',
|
|
513
|
+
'no',
|
|
514
|
+
'MISSING_ENV_VAR',
|
|
515
|
+
'new-secret',
|
|
516
|
+
]);
|
|
517
|
+
const handler = new InitCommandHandler(cwd, prompts.factory);
|
|
518
|
+
const result = await handler.execute({ auto: false });
|
|
519
|
+
|
|
520
|
+
expect(result.ok).toBe(true);
|
|
521
|
+
const envContent = await fs.readFile(path.join(cwd, '.env'), 'utf8');
|
|
522
|
+
expect(envContent).toContain('FOO=bar');
|
|
523
|
+
expect(envContent).toContain('AOP_PROVIDER_CONFIG_ENV=new-secret');
|
|
524
|
+
expect((envContent.match(/AOP_PROVIDER_CONFIG_ENV=/g) ?? []).length).toBe(1);
|
|
525
|
+
});
|
|
361
526
|
});
|
|
362
527
|
|
|
363
528
|
describe('InitCommandHandler validation branches', () => {
|
|
@@ -13,6 +13,7 @@ describe('provider selection', () => {
|
|
|
13
13
|
env: {
|
|
14
14
|
AOP_AGENT_PROVIDER: 'codex',
|
|
15
15
|
AOP_AGENT_MODEL: 'env-model',
|
|
16
|
+
CLI_CONFIG: 'cli-token',
|
|
16
17
|
AOP_PROVIDER_CONFIG_ENV: 'ENV_CONFIG',
|
|
17
18
|
},
|
|
18
19
|
agentsConfig: {
|
|
@@ -27,6 +28,7 @@ describe('provider selection', () => {
|
|
|
27
28
|
expect(selection.provider).toBe('custom');
|
|
28
29
|
expect(selection.model).toBe('cli-model');
|
|
29
30
|
expect(selection.provider_config_env).toBe('CLI_CONFIG');
|
|
31
|
+
expect(selection.provider_config_ref).toBe('cli-token');
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
it('GIVEN_no_provider_WHEN_resolving_THEN_throws_agent_provider_not_configured', () => {
|
|
@@ -49,16 +51,36 @@ describe('provider selection', () => {
|
|
|
49
51
|
).toThrow(ERROR_CODES.UNSUPPORTED_AGENT_PROVIDER);
|
|
50
52
|
});
|
|
51
53
|
|
|
52
|
-
it('
|
|
54
|
+
it('GIVEN_credential_required_provider_without_credentials_WHEN_resolving_THEN_throws_provider_auth_missing', () => {
|
|
53
55
|
expect(() =>
|
|
54
56
|
resolveProviderSelection({
|
|
55
|
-
cli: { agent_provider: '
|
|
57
|
+
cli: { agent_provider: 'gemini', provider_config_env: 'GEMINI_API_KEY' },
|
|
56
58
|
env: {},
|
|
57
59
|
agentsConfig: {},
|
|
58
60
|
}),
|
|
59
61
|
).toThrow(ERROR_CODES.PROVIDER_AUTH_MISSING);
|
|
60
62
|
});
|
|
61
63
|
|
|
64
|
+
it('GIVEN_codex_provider_without_credentials_WHEN_resolving_THEN_does_not_throw', () => {
|
|
65
|
+
const selection = resolveProviderSelection({
|
|
66
|
+
cli: { agent_provider: 'codex' },
|
|
67
|
+
env: {},
|
|
68
|
+
agentsConfig: {},
|
|
69
|
+
});
|
|
70
|
+
expect(selection.provider).toBe('codex');
|
|
71
|
+
expect(selection.provider_config_ref).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('GIVEN_claude_provider_without_credentials_WHEN_resolving_THEN_does_not_throw', () => {
|
|
75
|
+
const selection = resolveProviderSelection({
|
|
76
|
+
cli: { agent_provider: 'claude' },
|
|
77
|
+
env: {},
|
|
78
|
+
agentsConfig: {},
|
|
79
|
+
});
|
|
80
|
+
expect(selection.provider).toBe('claude');
|
|
81
|
+
expect(selection.provider_config_ref).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
62
84
|
it('GIVEN_non_custom_provider_without_model_WHEN_resolving_THEN_uses_default_model_name', () => {
|
|
63
85
|
const selection = resolveProviderSelection({
|
|
64
86
|
cli: {
|
|
@@ -88,6 +110,57 @@ describe('provider selection', () => {
|
|
|
88
110
|
expect(selection.provider_config_ref).toBeNull();
|
|
89
111
|
});
|
|
90
112
|
|
|
113
|
+
it('GIVEN_AOP_provider_config_env_holds_direct_credential_WHEN_resolving_THEN_uses_direct_value', () => {
|
|
114
|
+
const selection = resolveProviderSelection({
|
|
115
|
+
cli: {
|
|
116
|
+
agent_provider: 'gemini',
|
|
117
|
+
},
|
|
118
|
+
env: {
|
|
119
|
+
AOP_PROVIDER_CONFIG_ENV: 'direct-bootstrap-key',
|
|
120
|
+
},
|
|
121
|
+
agentsConfig: {},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(selection.provider_config_env).toBe('AOP_PROVIDER_CONFIG_ENV');
|
|
125
|
+
expect(selection.provider_config_ref).toBe('direct-bootstrap-key');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('GIVEN_AOP_provider_config_env_points_to_existing_env_var_WHEN_resolving_THEN_uses_legacy_indirection', () => {
|
|
129
|
+
const selection = resolveProviderSelection({
|
|
130
|
+
cli: {
|
|
131
|
+
agent_provider: 'gemini',
|
|
132
|
+
},
|
|
133
|
+
env: {
|
|
134
|
+
AOP_PROVIDER_CONFIG_ENV: 'GEMINI_API_KEY',
|
|
135
|
+
GEMINI_API_KEY: 'legacy-indirect-key',
|
|
136
|
+
},
|
|
137
|
+
agentsConfig: {},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(selection.provider_config_env).toBe('GEMINI_API_KEY');
|
|
141
|
+
expect(selection.provider_config_ref).toBe('legacy-indirect-key');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('GIVEN_cli_provider_config_env_missing_but_runtime_provider_config_env_present_WHEN_resolving_THEN_runtime_env_name_is_used', () => {
|
|
145
|
+
const selection = resolveProviderSelection({
|
|
146
|
+
cli: {
|
|
147
|
+
agent_provider: 'gemini',
|
|
148
|
+
provider_config_env: 'MISSING_CLI_KEY',
|
|
149
|
+
},
|
|
150
|
+
env: {
|
|
151
|
+
CFG_KEY: 'cfg-token',
|
|
152
|
+
},
|
|
153
|
+
agentsConfig: {
|
|
154
|
+
runtime: {
|
|
155
|
+
provider_config_env: 'CFG_KEY',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(selection.provider_config_env).toBe('CFG_KEY');
|
|
161
|
+
expect(selection.provider_config_ref).toBe('cfg-token');
|
|
162
|
+
});
|
|
163
|
+
|
|
91
164
|
it('GIVEN_kiro_cli_provider_without_credentials_WHEN_resolving_THEN_allows_null_config_ref', () => {
|
|
92
165
|
const selection = resolveProviderSelection({
|
|
93
166
|
cli: {
|