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.
Files changed (58) hide show
  1. package/README.md +25 -3
  2. package/agentic/orchestrator/schemas/agents.schema.json +1 -1
  3. package/apps/control-plane/src/cli/dashboard-command-handler.ts +42 -5
  4. package/apps/control-plane/src/cli/env-file.ts +115 -0
  5. package/apps/control-plane/src/cli/help-command-handler.ts +1 -1
  6. package/apps/control-plane/src/cli/init-command-handler.ts +72 -2
  7. package/apps/control-plane/src/cli/retry-command-handler.ts +0 -1
  8. package/apps/control-plane/src/core/kernel.ts +1 -3
  9. package/apps/control-plane/src/core/tool-caller.ts +18 -3
  10. package/apps/control-plane/src/interfaces/cli/bootstrap.ts +10 -3
  11. package/apps/control-plane/src/providers/providers.ts +67 -8
  12. package/apps/control-plane/src/supervisor/build-wave-executor.ts +21 -4
  13. package/apps/control-plane/src/supervisor/qa-wave-executor.ts +21 -4
  14. package/apps/control-plane/src/supervisor/runtime.ts +9 -4
  15. package/apps/control-plane/src/supervisor/types.ts +1 -0
  16. package/apps/control-plane/test/cli-helpers.spec.ts +4 -0
  17. package/apps/control-plane/test/dashboard-command.spec.ts +36 -0
  18. package/apps/control-plane/test/init-wizard.spec.ts +166 -1
  19. package/apps/control-plane/test/providers.spec.ts +75 -2
  20. package/apps/control-plane/test/supervisor-collaborators.spec.ts +86 -0
  21. package/apps/control-plane/test/supervisor.unit.spec.ts +1 -1
  22. package/config/agentic/orchestrator/adapters.yaml +3 -0
  23. package/config/agentic/orchestrator/agents.yaml +13 -0
  24. package/config/agentic/orchestrator/gates.yaml +28 -0
  25. package/config/agentic/orchestrator/policy.yaml +22 -0
  26. package/config/agentic/orchestrator/prompts/builder.system.md +1 -0
  27. package/config/agentic/orchestrator/prompts/planner.system.md +16 -0
  28. package/config/agentic/orchestrator/prompts/qa.system.md +1 -0
  29. package/dist/apps/control-plane/cli/dashboard-command-handler.js +32 -5
  30. package/dist/apps/control-plane/cli/dashboard-command-handler.js.map +1 -1
  31. package/dist/apps/control-plane/cli/env-file.d.ts +4 -0
  32. package/dist/apps/control-plane/cli/env-file.js +89 -0
  33. package/dist/apps/control-plane/cli/env-file.js.map +1 -0
  34. package/dist/apps/control-plane/cli/help-command-handler.js +1 -1
  35. package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
  36. package/dist/apps/control-plane/cli/init-command-handler.js +53 -4
  37. package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
  38. package/dist/apps/control-plane/cli/retry-command-handler.js +0 -1
  39. package/dist/apps/control-plane/cli/retry-command-handler.js.map +1 -1
  40. package/dist/apps/control-plane/core/kernel.js.map +1 -1
  41. package/dist/apps/control-plane/core/tool-caller.d.ts +11 -1
  42. package/dist/apps/control-plane/interfaces/cli/bootstrap.js +9 -3
  43. package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
  44. package/dist/apps/control-plane/providers/providers.d.ts +2 -1
  45. package/dist/apps/control-plane/providers/providers.js +52 -7
  46. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  47. package/dist/apps/control-plane/supervisor/build-wave-executor.js +20 -4
  48. package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
  49. package/dist/apps/control-plane/supervisor/qa-wave-executor.js +20 -4
  50. package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
  51. package/dist/apps/control-plane/supervisor/runtime.d.ts +2 -2
  52. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  53. package/dist/apps/control-plane/supervisor/types.d.ts +1 -1
  54. package/dist/apps/control-plane/supervisor/types.js.map +1 -1
  55. package/package.json +1 -1
  56. package/spec-files/completed/agentic_orchestrator_feature_gaps_closure_spec.md +2 -0
  57. package/spec-files/outstanding/agentic_orchestrator_provider_auth_bootstrap_spec.md +384 -0
  58. package/spec-files/progress.md +19 -0
package/README.md CHANGED
@@ -247,6 +247,12 @@ Behavior:
247
247
  1. CLI flags (`--agent-provider`, `--agent-model`, `--agent-config`, `--provider-config-env`)
248
248
  2. env vars (`AOP_AGENT_PROVIDER`, `AOP_AGENT_MODEL`, `AOP_AGENT_CONFIG`/`AOP_AGENT_CONFIG_JSON`, `AOP_PROVIDER_CONFIG_ENV`)
249
249
  3. `config/agentic/orchestrator/agents.yaml` runtime defaults
250
+ - Provider credential resolution (`provider_config_ref`):
251
+ 1. `--provider-config-env <NAME>` if `NAME` exists in env
252
+ 2. `agents.yaml runtime.provider_config_env` if that env var exists
253
+ 3. `AOP_PROVIDER_CONFIG_ENV` fallback:
254
+ - if it looks like an env-var name and that env var exists, use that value (legacy indirection)
255
+ - otherwise use `AOP_PROVIDER_CONFIG_ENV` as the direct credential value
250
256
  - Runtime start:
251
257
  - starts `SupervisorRuntime` with `max_active_features=5`, `max_parallel_gate_runs=3`.
252
258
  - `max_iterations_per_phase` resolves from `policy.yaml` (`supervisor.max_iterations_per_phase`, default `6`).
@@ -376,7 +382,7 @@ When `cleanup.auto_after_merge` is enabled in `policy.yaml`, the runtime automat
376
382
 
377
383
  ### `init`
378
384
 
379
- Initialises agentic orchestrator configuration in the current directory. Generates `policy.yaml`, `gates.yaml`, `agents.yaml`, `adapters.yaml`, and system prompt templates. The wizard also captures default agent `provider`/`model` and `scm-provider`.
385
+ Initialises agentic orchestrator configuration in the current directory. Generates `policy.yaml`, `gates.yaml`, `agents.yaml`, `adapters.yaml`, and system prompt templates. The wizard also captures default agent `provider`/`model`, `scm-provider`, and provider-auth mode.
380
386
 
381
387
  Schema files are bundled with AOP and validated from the installation path; `aop init` does not copy schemas into the target repository.
382
388
 
@@ -414,6 +420,14 @@ At runtime the kernel always composes a canonical full policy by:
414
420
 
415
421
  Existing repositories with full `policy.yaml` files continue to work without changes — user values override defaults entirely.
416
422
 
423
+ #### Provider Auth Flow In `aop init`
424
+
425
+ - wizard prompt: `Will you use a local agent CLI ... (yes/no)` (default `yes`)
426
+ - if `yes`: init skips `runtime.provider_config_env` in `agents.yaml`
427
+ - if `no`: init asks for provider env var name and checks both process env and repo `.env`
428
+ - if missing: init prompts for a key, stores it in `.env` as `AOP_PROVIDER_CONFIG_ENV`, and writes `runtime.provider_config_env: AOP_PROVIDER_CONFIG_ENV`
429
+ - `aop init --auto` defaults to local-CLI mode and does not emit `provider_config_env`
430
+
417
431
  ### `dashboard`
418
432
 
419
433
  Starts the web dashboard server (`packages/web-dashboard/`). Provides a real-time Kanban view of all features, review approval/denial, checkout, and SSE-based live updates.
@@ -484,7 +498,7 @@ Supported options:
484
498
  | `--agent-provider <codex\\ | claude\\ | gemini\\ | custom\\ | kiro-cli\\ | copilot>` | Provider selection |
485
499
  | `--agent-model <model-id>` | Model selection |
486
500
  | `--agent-config <json-object>` | Additional provider-specific agent config (for example command/args payload) |
487
- | `--provider-config-env <ENV_VAR>` | Provider auth/config environment variable name |
501
+ | `--provider-config-env <ENV_VAR>` | Provider auth/config env var name for API-backed providers |
488
502
  | `--transport <mcp\\ | inprocess>` | Tool transport selection (default `mcp`) |
489
503
  | `--takeover-stale-run` | Allow stale run-lease takeover during `run` |
490
504
  | `--project <name>` | Select project from `multi-project.yaml` (run, status, resume, retry) |
@@ -525,6 +539,14 @@ Provider resolution precedence:
525
539
  2. env vars (`AOP_AGENT_PROVIDER`, `AOP_AGENT_MODEL`, `AOP_AGENT_CONFIG`/`AOP_AGENT_CONFIG_JSON`, `AOP_PROVIDER_CONFIG_ENV`)
526
540
  3. `config/agentic/orchestrator/agents.yaml` runtime defaults
527
541
 
542
+ Provider credential fallback:
543
+
544
+ 1. `--provider-config-env <NAME>` if `NAME` exists
545
+ 2. `runtime.provider_config_env` if that env var exists
546
+ 3. `AOP_PROVIDER_CONFIG_ENV`:
547
+ - `AOP_PROVIDER_CONFIG_ENV=OTHER_ENV` and `OTHER_ENV` exists -> use `OTHER_ENV`
548
+ - otherwise treat `AOP_PROVIDER_CONFIG_ENV` as direct credential value
549
+
528
550
  Transport behavior:
529
551
 
530
552
  - default transport is `mcp`
@@ -571,7 +593,7 @@ Coverage parser:
571
593
  ### Agents (`config/agentic/orchestrator/agents.yaml`)
572
594
 
573
595
  - role-specific system prompt paths
574
- - default provider/model/config-env fallback values
596
+ - default provider/model values with optional `runtime.provider_config_env` for API-backed providers
575
597
  - optional `runtime.provider_configs.<provider>` objects for provider-specific payloads (for example `kiro-cli chat --agent dev`)
576
598
  - `worktree.post_create` commands and `worktree.symlinks` for workspace hook automation
577
599
  - stack-specific examples: [`example-configurations/node/`](example-configurations/node) and [`example-configurations/java/`](example-configurations/java)
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "provider_config_env": {
56
56
  "type": "string",
57
- "description": "Name of an environment variable whose value is a JSON provider-config blob. Takes lower priority than the CLI flag but higher than this file."
57
+ "description": "Optional env var name for API-backed provider credentials/config. Local CLI providers typically omit this. Runtime also supports AOP_PROVIDER_CONFIG_ENV fallback (legacy indirection or direct credential value)."
58
58
  },
59
59
  "provider_configs": {
60
60
  "type": "object",
@@ -1,10 +1,46 @@
1
1
  import { execFile, spawn } from 'node:child_process';
2
+ import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { promisify } from 'node:util';
5
6
 
6
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
8
  const execFileAsync = promisify(execFile);
9
+ const DASHBOARD_WORKSPACE = '@aop/web-dashboard';
10
+
11
+ async function hasDashboardCli(repoRoot: string): Promise<boolean> {
12
+ const candidates = [
13
+ path.join(repoRoot, 'node_modules', '.bin', 'next'),
14
+ path.join(repoRoot, 'packages', 'web-dashboard', 'node_modules', '.bin', 'next'),
15
+ ];
16
+ for (const candidate of candidates) {
17
+ try {
18
+ await fs.access(candidate);
19
+ return true;
20
+ } catch {
21
+ // continue
22
+ }
23
+ }
24
+ return false;
25
+ }
26
+
27
+ async function ensureDashboardDependencies(
28
+ repoRoot: string,
29
+ env: NodeJS.ProcessEnv,
30
+ ): Promise<void> {
31
+ if (await hasDashboardCli(repoRoot)) {
32
+ return;
33
+ }
34
+
35
+ await execFileAsync(
36
+ 'npm',
37
+ ['install', '--workspace', DASHBOARD_WORKSPACE, '--no-audit', '--no-fund'],
38
+ {
39
+ cwd: repoRoot,
40
+ env,
41
+ },
42
+ );
43
+ }
8
44
 
9
45
  export class DashboardCommandHandler {
10
46
  async execute(options: {
@@ -14,7 +50,6 @@ export class DashboardCommandHandler {
14
50
  }): Promise<Record<string, unknown>> {
15
51
  const port = options.port ?? 3000;
16
52
  const repoRoot = path.resolve(__dirname, '../../../../');
17
- const dashboardWorkspace = '@aop/web-dashboard';
18
53
  const devMode = options.dev === true;
19
54
  const foreground = options.foreground === true || devMode;
20
55
 
@@ -25,8 +60,10 @@ export class DashboardCommandHandler {
25
60
  AOP_ROOT: process.cwd(),
26
61
  };
27
62
 
63
+ await ensureDashboardDependencies(repoRoot, env);
64
+
28
65
  if (devMode) {
29
- const child = spawn('npm', ['run', '--workspace', dashboardWorkspace, 'dev'], {
66
+ const child = spawn('npm', ['run', '--workspace', DASHBOARD_WORKSPACE, 'dev'], {
30
67
  cwd: repoRoot,
31
68
  env,
32
69
  stdio: 'inherit',
@@ -38,13 +75,13 @@ export class DashboardCommandHandler {
38
75
  return { ok: true, data: { message: 'Dashboard stopped', port, mode: 'dev' } };
39
76
  }
40
77
 
41
- await execFileAsync('npm', ['run', '--workspace', dashboardWorkspace, 'build'], {
78
+ await execFileAsync('npm', ['run', '--workspace', DASHBOARD_WORKSPACE, 'build'], {
42
79
  cwd: repoRoot,
43
80
  env,
44
81
  });
45
82
 
46
83
  if (foreground) {
47
- const child = spawn('npm', ['run', '--workspace', dashboardWorkspace, 'start'], {
84
+ const child = spawn('npm', ['run', '--workspace', DASHBOARD_WORKSPACE, 'start'], {
48
85
  cwd: repoRoot,
49
86
  env,
50
87
  stdio: 'inherit',
@@ -56,7 +93,7 @@ export class DashboardCommandHandler {
56
93
  return { ok: true, data: { message: 'Dashboard stopped', port, mode: 'production' } };
57
94
  }
58
95
 
59
- const child = spawn('npm', ['run', '--workspace', dashboardWorkspace, 'start'], {
96
+ const child = spawn('npm', ['run', '--workspace', DASHBOARD_WORKSPACE, 'start'], {
60
97
  cwd: repoRoot,
61
98
  env,
62
99
  detached: true,
@@ -0,0 +1,115 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ export type EnvFileValues = Record<string, string>;
4
+
5
+ const SAFE_UNQUOTED_ENV_VALUE = /^[A-Za-z0-9_./:@+=-]+$/;
6
+
7
+ function unquoteValue(raw: string): string {
8
+ const trimmed = raw.trim();
9
+ if (
10
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
11
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
12
+ ) {
13
+ return trimmed.slice(1, -1);
14
+ }
15
+ return trimmed;
16
+ }
17
+
18
+ function parseEnvLine(line: string): { key: string; value: string } | null {
19
+ const trimmed = line.trim();
20
+ if (trimmed.length === 0 || trimmed.startsWith('#')) {
21
+ return null;
22
+ }
23
+
24
+ const normalized = trimmed.startsWith('export ') ? trimmed.slice('export '.length) : trimmed;
25
+ const separatorIndex = normalized.indexOf('=');
26
+ if (separatorIndex <= 0) {
27
+ return null;
28
+ }
29
+
30
+ const key = normalized.slice(0, separatorIndex).trim();
31
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
32
+ return null;
33
+ }
34
+
35
+ const value = normalized.slice(separatorIndex + 1);
36
+ return { key, value: unquoteValue(value) };
37
+ }
38
+
39
+ function formatEnvValue(value: string): string {
40
+ if (SAFE_UNQUOTED_ENV_VALUE.test(value)) {
41
+ return value;
42
+ }
43
+ return JSON.stringify(value);
44
+ }
45
+
46
+ export async function readEnvFileValues(envPath: string): Promise<EnvFileValues> {
47
+ let content: string;
48
+ try {
49
+ content = await fs.readFile(envPath, 'utf8');
50
+ } catch {
51
+ return {};
52
+ }
53
+
54
+ const values: EnvFileValues = {};
55
+ for (const line of content.split(/\r?\n/)) {
56
+ const parsed = parseEnvLine(line);
57
+ if (!parsed) {
58
+ continue;
59
+ }
60
+ values[parsed.key] = parsed.value;
61
+ }
62
+ return values;
63
+ }
64
+
65
+ export function readNonEmptyEnvValue(
66
+ key: string,
67
+ runtimeEnv: NodeJS.ProcessEnv,
68
+ envFileValues: EnvFileValues,
69
+ ): string | null {
70
+ const runtimeValue = runtimeEnv[key];
71
+ if (typeof runtimeValue === 'string' && runtimeValue.trim().length > 0) {
72
+ return runtimeValue;
73
+ }
74
+
75
+ const fileValue = envFileValues[key];
76
+ if (typeof fileValue === 'string' && fileValue.trim().length > 0) {
77
+ return fileValue;
78
+ }
79
+
80
+ return null;
81
+ }
82
+
83
+ export async function upsertEnvFileValue(
84
+ envPath: string,
85
+ key: string,
86
+ value: string,
87
+ ): Promise<void> {
88
+ let content = '';
89
+ try {
90
+ content = await fs.readFile(envPath, 'utf8');
91
+ } catch {
92
+ // create file from scratch
93
+ }
94
+
95
+ const lines = content.length > 0 ? content.split(/\r?\n/) : [];
96
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
97
+ const matcher = new RegExp(`^\\s*(?:export\\s+)?${escapedKey}\\s*=`);
98
+ const nextLine = `${key}=${formatEnvValue(value)}`;
99
+
100
+ let replaced = false;
101
+ for (let index = 0; index < lines.length; index += 1) {
102
+ if (matcher.test(lines[index])) {
103
+ lines[index] = nextLine;
104
+ replaced = true;
105
+ break;
106
+ }
107
+ }
108
+
109
+ if (!replaced) {
110
+ lines.push(nextLine);
111
+ }
112
+
113
+ const normalized = `${lines.join('\n').replace(/\n*$/, '')}\n`;
114
+ await fs.writeFile(envPath, normalized, 'utf8');
115
+ }
@@ -25,7 +25,7 @@ const COMMAND_HELP: Record<CliCommand, CommandHelp> = {
25
25
  { flag: '--agent-config <PATH>', description: 'Path to agent config file' },
26
26
  {
27
27
  flag: '--provider-config-env <var>',
28
- description: 'Env var name that holds provider API key',
28
+ description: 'Env var name used for API-backed provider auth/config',
29
29
  },
30
30
  { flag: '--transport <inprocess|mcp>', description: 'Tool transport layer (default: mcp)' },
31
31
  { flag: '--takeover-stale-run', description: 'Take over a stale run lease' },
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
8
8
  import YAML from 'yaml';
9
9
  import { SchemaRegistry } from '../core/schemas.js';
10
10
  import { loadComposedPolicy } from '../application/services/policy-loader-service.js';
11
+ import { readEnvFileValues, readNonEmptyEnvValue, upsertEnvFileValue } from './env-file.js';
11
12
  import {
12
13
  AGENT_PROVIDER_SLOT,
13
14
  SCM_PROVIDER_SLOT,
@@ -47,6 +48,8 @@ interface WizardConfig {
47
48
  framework: TestFramework;
48
49
  defaultProvider: string;
49
50
  defaultModel: string;
51
+ providerConfigEnv: string | null;
52
+ providerCredentialBootstrapped: boolean;
50
53
  scmProvider: string;
51
54
  notifications: {
52
55
  desktop: boolean;
@@ -402,6 +405,9 @@ capabilities:
402
405
  }
403
406
 
404
407
  function generateAgentsYaml(wizard: WizardConfig): string {
408
+ const providerConfigEnvLine = wizard.providerConfigEnv
409
+ ? ` provider_config_env: ${wizard.providerConfigEnv}\n`
410
+ : '';
405
411
  return `version: 1
406
412
  roles:
407
413
  planner:
@@ -414,8 +420,7 @@ missing_prompt_behavior: ignore
414
420
  runtime:
415
421
  default_provider: ${wizard.defaultProvider}
416
422
  default_model: ${wizard.defaultModel}
417
- provider_config_env: AOP_PROVIDER_CONFIG_ENV
418
- role_provider_overrides: {}
423
+ ${providerConfigEnvLine} role_provider_overrides: {}
419
424
  `;
420
425
  }
421
426
 
@@ -512,7 +517,26 @@ async function askWithDefault(
512
517
  return raw.length > 0 ? raw : defaultValue;
513
518
  }
514
519
 
520
+ function parseYesNo(raw: string, fallback: boolean): boolean {
521
+ const normalized = raw.trim().toLowerCase();
522
+ if (normalized === 'yes' || normalized === 'y' || normalized === 'true') {
523
+ return true;
524
+ }
525
+ if (normalized === 'no' || normalized === 'n' || normalized === 'false') {
526
+ return false;
527
+ }
528
+ return fallback;
529
+ }
530
+
531
+ function defaultProviderConfigEnvName(provider: string): string {
532
+ if (provider === 'gemini') {
533
+ return 'GEMINI_API_KEY';
534
+ }
535
+ return 'AOP_PROVIDER_CONFIG_ENV';
536
+ }
537
+
515
538
  async function collectWizardConfig(
539
+ repoRoot: string,
516
540
  defaults: {
517
541
  branch: string;
518
542
  framework: TestFramework;
@@ -524,6 +548,8 @@ async function collectWizardConfig(
524
548
  ): Promise<WizardConfig> {
525
549
  const prompt = promptFactory();
526
550
  try {
551
+ const envFilePath = path.join(repoRoot, '.env');
552
+ const envFileValues = await readEnvFileValues(envFilePath);
527
553
  const supportedProviders = new Set(
528
554
  globalAdapterRegistry.list(AGENT_PROVIDER_SLOT.name).map((adapter) => adapter.name),
529
555
  );
@@ -567,6 +593,42 @@ async function collectWizardConfig(
567
593
  webhookUrl = (await prompt.question('Webhook URL []: ')).trim();
568
594
  }
569
595
 
596
+ const localCliAnswer = await askWithDefault(
597
+ prompt,
598
+ 'Will you use a local agent CLI (codex/claude-code/kiro-cli/copilot)? (yes/no)',
599
+ 'yes',
600
+ );
601
+ const usesLocalCli = parseYesNo(localCliAnswer, true);
602
+
603
+ let providerConfigEnv: string | null = null;
604
+ let providerCredentialBootstrapped = false;
605
+
606
+ if (!usesLocalCli) {
607
+ const envVarName = await askWithDefault(
608
+ prompt,
609
+ 'Provider config env var name',
610
+ defaultProviderConfigEnvName(defaultProviderRaw),
611
+ );
612
+ const envValue = readNonEmptyEnvValue(envVarName, process.env, envFileValues);
613
+
614
+ if (envValue) {
615
+ providerConfigEnv = envVarName;
616
+ } else {
617
+ output.write(
618
+ `Environment variable "${envVarName}" was not found in process env or ${envFilePath}.\n`,
619
+ );
620
+ let pastedKey = '';
621
+ while (pastedKey.length === 0) {
622
+ pastedKey = (
623
+ await prompt.question('Paste provider key to store in AOP_PROVIDER_CONFIG_ENV: ')
624
+ ).trim();
625
+ }
626
+ await upsertEnvFileValue(envFilePath, 'AOP_PROVIDER_CONFIG_ENV', pastedKey);
627
+ providerConfigEnv = 'AOP_PROVIDER_CONFIG_ENV';
628
+ providerCredentialBootstrapped = true;
629
+ }
630
+ }
631
+
570
632
  return {
571
633
  baseBranch,
572
634
  defaultProvider: parseAdapterName(
@@ -575,6 +637,8 @@ async function collectWizardConfig(
575
637
  defaults.defaultProvider,
576
638
  ),
577
639
  defaultModel,
640
+ providerConfigEnv,
641
+ providerCredentialBootstrapped,
578
642
  scmProvider: parseAdapterName(
579
643
  scmProviderRaw,
580
644
  supportedScmProviders,
@@ -633,6 +697,8 @@ export class InitCommandHandler {
633
697
  baseBranch: gitContext.defaultBranch,
634
698
  defaultProvider: DEFAULT_AGENT_PROVIDER,
635
699
  defaultModel: DEFAULT_AGENT_MODEL,
700
+ providerConfigEnv: null,
701
+ providerCredentialBootstrapped: false,
636
702
  scmProvider: DEFAULT_SCM_PROVIDER,
637
703
  maxParallelGateRuns: 3,
638
704
  dashboardPort: 3000,
@@ -646,6 +712,7 @@ export class InitCommandHandler {
646
712
  },
647
713
  }
648
714
  : await collectWizardConfig(
715
+ this.repoRoot,
649
716
  {
650
717
  branch: gitContext.defaultBranch,
651
718
  framework,
@@ -758,6 +825,9 @@ export class InitCommandHandler {
758
825
  'To generate a full explicit policy with all advanced controls, re-run: aop init --advanced-policy --force',
759
826
  );
760
827
  }
828
+ if (wizard.providerCredentialBootstrapped) {
829
+ nextSteps.push('Stored provider credential in .env as AOP_PROVIDER_CONFIG_ENV.');
830
+ }
761
831
 
762
832
  return {
763
833
  ok: true,
@@ -113,7 +113,6 @@ export class RetryCommandHandler {
113
113
 
114
114
  const gate = await callCliTool(this.toolClient, this.runId, TOOLS.GATES_RUN, {
115
115
  feature_id: featureId,
116
- profile: null,
117
116
  mode: inferredMode,
118
117
  });
119
118
  const retryExecuted = true;
@@ -1001,9 +1001,7 @@ export class AopKernel {
1001
1001
  await atomicWriteJson(runLeasePath, data);
1002
1002
  }
1003
1003
 
1004
- async acquireRunLease(
1005
- input: AcquireRunLeaseInput,
1006
- ): Promise<{
1004
+ async acquireRunLease(input: AcquireRunLeaseInput): Promise<{
1007
1005
  data: {
1008
1006
  runtime_sessions: RuntimeSessionsSnapshot;
1009
1007
  took_over_stale: boolean;
@@ -1,10 +1,25 @@
1
1
  export type RuntimeRole = 'orchestrator' | 'planner' | 'builder' | 'qa';
2
2
 
3
+ export interface GatesRunToolArgs {
4
+ feature_id: string;
5
+ mode: string;
6
+ profile?: string;
7
+ operation_id?: string;
8
+ }
9
+
10
+ export interface ToolArgsByName {
11
+ 'gates.run': GatesRunToolArgs;
12
+ }
13
+
14
+ export type ToolArgs<TToolName extends string> = TToolName extends keyof ToolArgsByName
15
+ ? ToolArgsByName[TToolName]
16
+ : Record<string, unknown>;
17
+
3
18
  export interface ToolCaller {
4
- callTool<TData = Record<string, unknown>>(
19
+ callTool<TData = Record<string, unknown>, TToolName extends string = string>(
5
20
  role: RuntimeRole,
6
- toolName: string,
7
- args: Record<string, unknown>,
21
+ toolName: TToolName,
22
+ args: ToolArgs<TToolName>,
8
23
  ): Promise<{ ok: true; data: TData }>;
9
24
  }
10
25
 
@@ -24,6 +24,7 @@ import { RetryCommandHandler } from '../../cli/retry-command-handler.js';
24
24
  import { SendCommandHandler } from '../../cli/send-command-handler.js';
25
25
  import { AttachCommandHandler } from '../../cli/attach-command-handler.js';
26
26
  import { HelpCommandHandler } from '../../cli/help-command-handler.js';
27
+ import { readEnvFileValues } from '../../cli/env-file.js';
27
28
  import { MultiProjectLoader } from '../../application/multi-project-loader.js';
28
29
  import { NullWorkerProvider, resolveProviderSelection } from '../../providers/providers.js';
29
30
  import type { RuntimeContext } from '../../cli/types.js';
@@ -154,6 +155,12 @@ export async function runCli(
154
155
  }
155
156
 
156
157
  try {
158
+ const envFileValues = await readEnvFileValues(path.join(repoRoot, '.env'));
159
+ const effectiveEnv: NodeJS.ProcessEnv = {
160
+ ...envFileValues,
161
+ ...runtime.env,
162
+ };
163
+
157
164
  if (!SUPPORTED_COMMANDS.has(options.command)) {
158
165
  printError(ERROR_CODES.INVALID_CLI_ARGS, `Unknown command: ${options.command}`, {
159
166
  command: options.command,
@@ -264,7 +271,7 @@ export async function runCli(
264
271
  try {
265
272
  selection = resolveProviderSelection({
266
273
  cli: options as unknown as Record<string, string | undefined>,
267
- env: runtime.env,
274
+ env: effectiveEnv,
268
275
  agentsConfig: kernel.getAgentsConfig(),
269
276
  });
270
277
  } catch {
@@ -290,7 +297,7 @@ export async function runCli(
290
297
  const handler = new ResumeCommandHandler();
291
298
  const payload = await handler.execute({
292
299
  repoRoot,
293
- env: runtime.env,
300
+ env: effectiveEnv,
294
301
  runId,
295
302
  transport,
296
303
  options,
@@ -362,7 +369,7 @@ export async function runCli(
362
369
  const handler = new RunCommandHandler();
363
370
  const payload = await handler.execute({
364
371
  repoRoot,
365
- env: runtime.env,
372
+ env: effectiveEnv,
366
373
  runId,
367
374
  transport,
368
375
  options,
@@ -31,6 +31,7 @@ interface ResolveSelectionInput {
31
31
  }
32
32
 
33
33
  type ResolveSelectionRuntimeConfig = NonNullable<ResolveSelectionInput['agentsConfig']>['runtime'];
34
+ const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
34
35
 
35
36
  function isPlainObject(value: unknown): value is Record<string, unknown> {
36
37
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -87,8 +88,66 @@ function resolveConfiguredAgentConfig(
87
88
  return null;
88
89
  }
89
90
 
91
+ function readNonEmptyEnvValue(env: NodeJS.ProcessEnv, key: string | null): string | null {
92
+ if (!key) {
93
+ return null;
94
+ }
95
+ const value = env[key];
96
+ if (typeof value !== 'string') {
97
+ return null;
98
+ }
99
+ return value.trim().length > 0 ? value : null;
100
+ }
101
+
102
+ function resolveProviderCredentialReference(
103
+ cliProviderConfigEnv: string | undefined,
104
+ configProviderConfigEnv: string | undefined,
105
+ env: NodeJS.ProcessEnv,
106
+ ): { providerConfigEnv: string | null; providerConfigRef: string | null } {
107
+ const cliConfiguredName = cliProviderConfigEnv?.trim() || null;
108
+ const configConfiguredName = configProviderConfigEnv?.trim() || null;
109
+ const unresolvedConfiguredName = cliConfiguredName ?? configConfiguredName ?? null;
110
+
111
+ const cliValue = readNonEmptyEnvValue(env, cliConfiguredName);
112
+ if (cliValue) {
113
+ return { providerConfigEnv: cliConfiguredName, providerConfigRef: cliValue };
114
+ }
115
+
116
+ const configValue = readNonEmptyEnvValue(env, configConfiguredName);
117
+ if (configValue) {
118
+ return { providerConfigEnv: configConfiguredName, providerConfigRef: configValue };
119
+ }
120
+
121
+ const aopFallback = readNonEmptyEnvValue(env, 'AOP_PROVIDER_CONFIG_ENV');
122
+ if (aopFallback) {
123
+ if (ENV_VAR_NAME_PATTERN.test(aopFallback)) {
124
+ const indirectValue = readNonEmptyEnvValue(env, aopFallback);
125
+ if (indirectValue) {
126
+ return { providerConfigEnv: aopFallback, providerConfigRef: indirectValue };
127
+ }
128
+ }
129
+
130
+ return {
131
+ providerConfigEnv: 'AOP_PROVIDER_CONFIG_ENV',
132
+ providerConfigRef: aopFallback,
133
+ };
134
+ }
135
+
136
+ return {
137
+ providerConfigEnv: unresolvedConfiguredName,
138
+ providerConfigRef: null,
139
+ };
140
+ }
141
+
90
142
  export const SUPPORTED_PROVIDERS: Set<string> = new Set(REGISTERED_PROVIDER_NAMES);
91
- export const AUTH_REQUIRED_PROVIDERS: Set<string> = new Set(['codex', 'claude', 'gemini']);
143
+ export const LOCAL_CLI_PROVIDERS: Set<string> = new Set([
144
+ 'codex',
145
+ 'claude',
146
+ 'kiro-cli',
147
+ 'copilot',
148
+ 'custom',
149
+ ]);
150
+ export const CREDENTIAL_REQUIRED_PROVIDERS: Set<string> = new Set(['gemini']);
92
151
  export type ProviderSelectionResolver = (input: ResolveSelectionInput) => ProviderSelection;
93
152
 
94
153
  export const resolveProviderSelection: ProviderSelectionResolver = ({ cli, env, agentsConfig }) => {
@@ -98,11 +157,11 @@ export const resolveProviderSelection: ProviderSelectionResolver = ({ cli, env,
98
157
  const model =
99
158
  cli.agent_model || env.AOP_AGENT_MODEL || agentsConfig?.runtime?.default_model || null;
100
159
 
101
- const providerConfigEnv =
102
- cli.provider_config_env ||
103
- env.AOP_PROVIDER_CONFIG_ENV ||
104
- agentsConfig?.runtime?.provider_config_env ||
105
- null;
160
+ const { providerConfigEnv, providerConfigRef } = resolveProviderCredentialReference(
161
+ cli.provider_config_env,
162
+ agentsConfig?.runtime?.provider_config_env,
163
+ env,
164
+ );
106
165
 
107
166
  if (!provider) {
108
167
  const error = new Error(ERROR_CODES.AGENT_PROVIDER_NOT_CONFIGURED) as AppError;
@@ -117,7 +176,7 @@ export const resolveProviderSelection: ProviderSelectionResolver = ({ cli, env,
117
176
  throw error;
118
177
  }
119
178
 
120
- if (AUTH_REQUIRED_PROVIDERS.has(provider) && (!providerConfigEnv || !env[providerConfigEnv])) {
179
+ if (CREDENTIAL_REQUIRED_PROVIDERS.has(provider) && !providerConfigRef) {
121
180
  const error = new Error(ERROR_CODES.PROVIDER_AUTH_MISSING) as AppError;
122
181
  error.code = ERROR_CODES.PROVIDER_AUTH_MISSING;
123
182
  error.details = {
@@ -140,7 +199,7 @@ export const resolveProviderSelection: ProviderSelectionResolver = ({ cli, env,
140
199
  provider,
141
200
  model: model ?? `${provider}-default`,
142
201
  provider_config_env: providerConfigEnv,
143
- provider_config_ref: providerConfigEnv ? (env[providerConfigEnv] ?? null) : null,
202
+ provider_config_ref: providerConfigRef,
144
203
  agent_config: agentConfig,
145
204
  };
146
205
  };