agentic-orchestrator 0.1.4 → 0.1.6

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 (108) hide show
  1. package/.claude/settings.local.json +6 -1
  2. package/README.md +81 -54
  3. package/agentic/orchestrator/defaults/policy.defaults.yaml +212 -0
  4. package/agentic/orchestrator/schemas/adapters.schema.json +17 -4
  5. package/agentic/orchestrator/schemas/agents.schema.json +58 -13
  6. package/agentic/orchestrator/schemas/gates.schema.json +88 -17
  7. package/agentic/orchestrator/schemas/index.schema.json +172 -37
  8. package/agentic/orchestrator/schemas/multi-project.schema.json +85 -17
  9. package/agentic/orchestrator/schemas/plan.schema.json +135 -30
  10. package/agentic/orchestrator/schemas/policy.schema.json +198 -69
  11. package/agentic/orchestrator/schemas/policy.user.schema.json +165 -0
  12. package/agentic/orchestrator/schemas/qa_test_index.schema.json +71 -14
  13. package/agentic/orchestrator/schemas/state.schema.json +196 -39
  14. package/apps/control-plane/scripts/validate-architecture-rules.mjs +5 -3
  15. package/apps/control-plane/scripts/validate-docker-mcp-contract.mjs +0 -3
  16. package/apps/control-plane/scripts/validate-mcp-contracts.ts +2 -6
  17. package/apps/control-plane/src/application/services/policy-loader-service.ts +115 -0
  18. package/apps/control-plane/src/cli/aop.ts +35 -1
  19. package/apps/control-plane/src/cli/cli-argument-parser.ts +4 -0
  20. package/apps/control-plane/src/cli/help-command-handler.ts +2 -1
  21. package/apps/control-plane/src/cli/init-command-handler.ts +136 -27
  22. package/apps/control-plane/src/cli/retry-command-handler.ts +3 -6
  23. package/apps/control-plane/src/cli/types.ts +1 -0
  24. package/apps/control-plane/src/core/git.ts +1 -3
  25. package/apps/control-plane/src/core/kernel.ts +3 -6
  26. package/apps/control-plane/src/core/schemas.ts +36 -1
  27. package/apps/control-plane/src/interfaces/cli/bootstrap.ts +1 -1
  28. package/apps/control-plane/src/mcp/tool-registry-loader.ts +37 -1
  29. package/apps/control-plane/src/supervisor/qa-wave-executor.ts +1 -1
  30. package/apps/control-plane/test/aop.spec.ts +37 -0
  31. package/apps/control-plane/test/batch-operations.spec.ts +5 -3
  32. package/apps/control-plane/test/bootstrap-attach.spec.ts +28 -18
  33. package/apps/control-plane/test/bootstrap-edge-cases.spec.ts +25 -15
  34. package/apps/control-plane/test/bootstrap.spec.ts +30 -22
  35. package/apps/control-plane/test/cli.unit.spec.ts +55 -10
  36. package/apps/control-plane/test/core-utils.spec.ts +12 -3
  37. package/apps/control-plane/test/feature-deletion-service.spec.ts +2 -1
  38. package/apps/control-plane/test/feature-lifecycle.spec.ts +1 -1
  39. package/apps/control-plane/test/helpers.ts +22 -2
  40. package/apps/control-plane/test/init-wizard.spec.ts +160 -7
  41. package/apps/control-plane/test/issue-tracker.spec.ts +3 -3
  42. package/apps/control-plane/test/kernel.spec.ts +62 -0
  43. package/apps/control-plane/test/lock-service.spec.ts +4 -4
  44. package/apps/control-plane/test/policy-loader-service.spec.ts +346 -0
  45. package/apps/control-plane/test/reactions.spec.ts +8 -6
  46. package/apps/control-plane/test/resume-command.spec.ts +31 -15
  47. package/apps/control-plane/test/supervisor.unit.spec.ts +2 -2
  48. package/apps/control-plane/test/tool-registry-loader.spec.ts +12 -0
  49. package/apps/control-plane/vitest.config.ts +1 -1
  50. package/dist/apps/control-plane/application/services/policy-loader-service.d.ts +24 -0
  51. package/dist/apps/control-plane/application/services/policy-loader-service.js +90 -0
  52. package/dist/apps/control-plane/application/services/policy-loader-service.js.map +1 -0
  53. package/dist/apps/control-plane/application/tools/tool-metadata.js +1 -0
  54. package/dist/apps/control-plane/application/tools/tool-metadata.js.map +1 -1
  55. package/dist/apps/control-plane/cli/aop.js +33 -1
  56. package/dist/apps/control-plane/cli/aop.js.map +1 -1
  57. package/dist/apps/control-plane/cli/cli-argument-parser.js +4 -0
  58. package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
  59. package/dist/apps/control-plane/cli/dashboard-command-handler.js +11 -5
  60. package/dist/apps/control-plane/cli/dashboard-command-handler.js.map +1 -1
  61. package/dist/apps/control-plane/cli/help-command-handler.js +5 -4
  62. package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
  63. package/dist/apps/control-plane/cli/init-command-handler.d.ts +1 -0
  64. package/dist/apps/control-plane/cli/init-command-handler.js +110 -26
  65. package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
  66. package/dist/apps/control-plane/cli/retry-command-handler.js +3 -6
  67. package/dist/apps/control-plane/cli/retry-command-handler.js.map +1 -1
  68. package/dist/apps/control-plane/cli/types.d.ts +1 -0
  69. package/dist/apps/control-plane/core/git.js +1 -3
  70. package/dist/apps/control-plane/core/git.js.map +1 -1
  71. package/dist/apps/control-plane/core/kernel.js +3 -5
  72. package/dist/apps/control-plane/core/kernel.js.map +1 -1
  73. package/dist/apps/control-plane/core/schemas.d.ts +2 -0
  74. package/dist/apps/control-plane/core/schemas.js +31 -1
  75. package/dist/apps/control-plane/core/schemas.js.map +1 -1
  76. package/dist/apps/control-plane/interfaces/cli/bootstrap.js +1 -1
  77. package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
  78. package/dist/apps/control-plane/mcp/tool-registry-loader.d.ts +2 -0
  79. package/dist/apps/control-plane/mcp/tool-registry-loader.js +31 -1
  80. package/dist/apps/control-plane/mcp/tool-registry-loader.js.map +1 -1
  81. package/dist/apps/control-plane/supervisor/build-wave-executor.js +1 -1
  82. package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
  83. package/dist/apps/control-plane/supervisor/qa-wave-executor.js +1 -1
  84. package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
  85. package/package.json +18 -17
  86. package/packages/web-dashboard/package.json +1 -1
  87. package/packages/web-dashboard/src/app/api/actions/route.ts +2 -2
  88. package/packages/web-dashboard/src/app/api/events/route.ts +1 -1
  89. package/packages/web-dashboard/src/app/api/features/[id]/checkout/route.ts +1 -1
  90. package/packages/web-dashboard/src/app/api/features/[id]/diff/route.ts +1 -1
  91. package/packages/web-dashboard/src/app/api/features/[id]/evidence/[artifact]/route.ts +1 -1
  92. package/packages/web-dashboard/src/app/api/features/[id]/review/route.ts +2 -2
  93. package/packages/web-dashboard/src/app/api/features/[id]/route.ts +1 -1
  94. package/packages/web-dashboard/src/app/api/projects/route.ts +2 -2
  95. package/packages/web-dashboard/src/app/api/status/route.ts +1 -1
  96. package/packages/web-dashboard/src/app/page.tsx +1 -1
  97. package/packages/web-dashboard/src/lib/aop-client.ts +1 -1
  98. package/packages/web-dashboard/src/lib/orchestrator-tools.ts +1 -1
  99. package/packages/web-dashboard/tsconfig.json +1 -0
  100. package/spec-files/completed/agentic_orchestrator_init_policy_ux_simplification_spec.md +481 -0
  101. package/spec-files/outstanding/agentic_orchestrator_artifact_database_publishing_spec.md +500 -0
  102. package/spec-files/outstanding/agentic_orchestrator_observability_integrity_diagnostics_spec.md +500 -195
  103. package/spec-files/outstanding/agentic_orchestrator_performance_improvements_spec.md +471 -162
  104. package/spec-files/outstanding/agentic_orchestrator_planning_review_quality_spec.md +532 -268
  105. package/spec-files/outstanding/agentic_orchestrator_productization_commercial_spec.md +645 -0
  106. package/spec-files/progress.md +99 -2
  107. package/tsconfig.base.json +4 -0
  108. /package/spec-files/{outstanding → completed}/agentic_orchestrator_validator_hardening_spec.md +0 -0
@@ -0,0 +1,115 @@
1
+ import fsSync from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import YAML from 'yaml';
6
+ import type { SchemaValidator } from '../../core/schemas.js';
7
+
8
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ export interface LoadPolicyResult {
11
+ userPolicy: Record<string, unknown>;
12
+ mergedPolicy: Record<string, unknown>;
13
+ }
14
+
15
+ function resolveBundledDefaultsPath(startDir: string): string | null {
16
+ let current = path.resolve(startDir);
17
+ while (true) {
18
+ const candidate = path.join(current, 'agentic', 'orchestrator', 'defaults', 'policy.defaults.yaml');
19
+ if (fsSync.existsSync(candidate)) {
20
+ return candidate;
21
+ }
22
+ const parent = path.dirname(current);
23
+ if (parent === current) {
24
+ return null;
25
+ }
26
+ current = parent;
27
+ }
28
+ }
29
+
30
+ const BUNDLED_DEFAULTS_PATH = resolveBundledDefaultsPath(MODULE_DIR);
31
+
32
+ function isObject(value: unknown): value is Record<string, unknown> {
33
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
34
+ }
35
+
36
+ function clone(value: unknown): unknown {
37
+ if (Array.isArray(value)) {
38
+ return value.map(clone);
39
+ }
40
+ if (isObject(value)) {
41
+ const out: Record<string, unknown> = {};
42
+ for (const key of Object.keys(value)) {
43
+ out[key] = clone(value[key]);
44
+ }
45
+ return out;
46
+ }
47
+ return value;
48
+ }
49
+
50
+ /**
51
+ * Deep-merges `user` over `defaults` according to ADP-3 semantics:
52
+ * - scalar in user → replaces default scalar
53
+ * - object in both → recursive merge
54
+ * - array in user → replaces default array wholesale
55
+ * - undefined user fields → inherit from defaults
56
+ */
57
+ export function mergePolicy(defaults: unknown, user: unknown): unknown {
58
+ if (Array.isArray(user)) {
59
+ return user.map(clone);
60
+ }
61
+ if (isObject(defaults) && isObject(user)) {
62
+ const out: Record<string, unknown> = clone(defaults) as Record<string, unknown>;
63
+ for (const key of Object.keys(user)) {
64
+ const userVal = user[key];
65
+ if (userVal === undefined) {
66
+ continue;
67
+ }
68
+ out[key] = key in out ? mergePolicy(out[key], userVal) : clone(userVal);
69
+ }
70
+ return out;
71
+ }
72
+ return clone(user);
73
+ }
74
+
75
+ /**
76
+ * Loads and composes a canonical policy by:
77
+ * 1. Reading bundled `policy.defaults.yaml`
78
+ * 2. Reading the user's `policy.yaml` at `policyPath`
79
+ * 3. Deep-merging user overrides over defaults (ADP-3 semantics)
80
+ * 4. Validating the merged result against `policy.schema.json`
81
+ *
82
+ * Throws with a descriptive error if defaults cannot be located, if either
83
+ * file cannot be read, or if the merged result fails schema validation.
84
+ */
85
+ export async function loadComposedPolicy(
86
+ _repoRoot: string,
87
+ policyPath: string,
88
+ schemaRegistry: SchemaValidator
89
+ ): Promise<LoadPolicyResult> {
90
+ if (!BUNDLED_DEFAULTS_PATH) {
91
+ throw new Error(
92
+ 'policy_defaults_not_found: bundled defaults artifact could not be located; ' +
93
+ 'ensure agentic/orchestrator/defaults/policy.defaults.yaml exists in the AOP installation'
94
+ );
95
+ }
96
+
97
+ const defaultsText = await fs.readFile(BUNDLED_DEFAULTS_PATH, 'utf8');
98
+ const defaults = YAML.parse(defaultsText) as Record<string, unknown>;
99
+
100
+ const userText = await fs.readFile(policyPath, 'utf8');
101
+ const userPolicy = (YAML.parse(userText) as Record<string, unknown> | null) ?? {};
102
+
103
+ const mergedPolicy = mergePolicy(defaults, userPolicy) as Record<string, unknown>;
104
+
105
+ const validation = await schemaRegistry.validate('policy.schema.json', mergedPolicy);
106
+ if (!validation.valid) {
107
+ const messages = validation.errors.map((e) => e.message ?? String(e)).join('; ');
108
+ throw new Error(
109
+ `invalid_policy_yaml: ${messages} ` +
110
+ `(merged from ${policyPath} over bundled defaults; check policy.yaml for invalid overrides)`
111
+ );
112
+ }
113
+
114
+ return { userPolicy, mergedPolicy };
115
+ }
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
2
5
  import { runCli } from '../interfaces/cli/bootstrap.js';
3
6
  import type { RuntimeContext } from './types.js';
4
7
 
@@ -9,8 +12,39 @@ export async function main(
9
12
  return await runCli(argv, runtime);
10
13
  }
11
14
 
15
+ function normalizePathCandidates(value: string): Set<string> {
16
+ const candidates = new Set<string>();
17
+ const resolved = path.resolve(value);
18
+ candidates.add(resolved);
19
+ try {
20
+ candidates.add(fs.realpathSync.native(resolved));
21
+ } catch {
22
+ // Keep resolved path fallback when realpath is unavailable.
23
+ }
24
+ return candidates;
25
+ }
26
+
12
27
  export function isDirectExecution(importMetaUrl: string, argv1: string | undefined): boolean {
13
- return importMetaUrl === `file://${argv1 ?? ''}`;
28
+ if (!argv1) {
29
+ return false;
30
+ }
31
+
32
+ let importMetaPath: string;
33
+ try {
34
+ importMetaPath = fileURLToPath(importMetaUrl);
35
+ } catch {
36
+ return false;
37
+ }
38
+
39
+ const modulePathCandidates = normalizePathCandidates(importMetaPath);
40
+ const argvPathCandidates = normalizePathCandidates(argv1);
41
+
42
+ for (const candidate of modulePathCandidates) {
43
+ if (argvPathCandidates.has(candidate)) {
44
+ return true;
45
+ }
46
+ }
47
+ return false;
14
48
  }
15
49
 
16
50
  export function runAsEntrypoint(
@@ -120,6 +120,10 @@ export class CliArgumentParser {
120
120
  options.auto = true;
121
121
  continue;
122
122
  }
123
+ if (token === '--advanced-policy') {
124
+ options.advanced_policy = true;
125
+ continue;
126
+ }
123
127
  if (token === '--force') {
124
128
  options.force = true;
125
129
  continue;
@@ -71,7 +71,8 @@ const COMMAND_HELP: Record<CliCommand, CommandHelp> = {
71
71
  description: 'Initialise agentic orchestrator configuration in the current directory.',
72
72
  flags: [
73
73
  { flag: '--auto', description: 'Run non-interactively with defaults' },
74
- { flag: '--force', description: 'Overwrite existing configuration' }
74
+ { flag: '--force', description: 'Overwrite existing configuration' },
75
+ { flag: '--advanced-policy', description: 'Generate full explicit policy.yaml with all advanced controls (default: lean policy)' }
75
76
  ]
76
77
  },
77
78
  [CliCommand.Dashboard]: {
@@ -7,14 +7,20 @@ import { stdin as input, stdout as output } from 'node:process';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import YAML from 'yaml';
9
9
  import { SchemaRegistry } from '../core/schemas.js';
10
+ import { loadComposedPolicy } from '../application/services/policy-loader-service.js';
11
+ import { AGENT_PROVIDER_SLOT, SCM_PROVIDER_SLOT, globalAdapterRegistry } from '../application/adapters/adapter-registry.js';
10
12
 
11
13
  const execFileAsync = promisify(execFile);
12
14
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
13
15
  const TEMPLATE_ROOT = path.resolve(MODULE_DIR, '../../../../agentic/orchestrator');
16
+ const DEFAULT_AGENT_PROVIDER = 'custom';
17
+ const DEFAULT_AGENT_MODEL = 'local-default';
18
+ const DEFAULT_SCM_PROVIDER = 'github';
14
19
 
15
20
  export interface InitOptions {
16
21
  auto?: boolean;
17
22
  force?: boolean;
23
+ advanced_policy?: boolean;
18
24
  }
19
25
 
20
26
  type TestFramework = 'vitest' | 'jest' | 'pytest' | 'maven' | 'gradle';
@@ -34,6 +40,9 @@ interface WizardConfig {
34
40
  maxParallelGateRuns: number;
35
41
  dashboardPort: number;
36
42
  framework: TestFramework;
43
+ defaultProvider: string;
44
+ defaultModel: string;
45
+ scmProvider: string;
37
46
  notifications: {
38
47
  desktop: boolean;
39
48
  slack: boolean;
@@ -159,7 +168,34 @@ function escapeYamlString(value: string): string {
159
168
  return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
160
169
  }
161
170
 
162
- function generatePolicyYaml(context: GitContext, wizard: WizardConfig): string {
171
+ function generateLeanPolicyYaml(wizard: WizardConfig): string {
172
+ const notificationsEnabled = wizard.notifications.desktop || wizard.notifications.slack || wizard.notifications.webhook;
173
+ return `version: 1
174
+ worktree:
175
+ base_branch: ${wizard.baseBranch}
176
+ supervisor:
177
+ max_parallel_gate_runs: ${wizard.maxParallelGateRuns}
178
+ dashboard:
179
+ enabled: true
180
+ port: ${wizard.dashboardPort}
181
+ merge_policy:
182
+ require_user_approval: true
183
+ notifications:
184
+ enabled: ${notificationsEnabled}
185
+ channels:
186
+ desktop:
187
+ enabled: ${wizard.notifications.desktop}
188
+ slack:
189
+ enabled: ${wizard.notifications.slack}
190
+ webhook: "${escapeYamlString(wizard.notifications.slackWebhook)}"
191
+ channel: "#aop-alerts"
192
+ webhook:
193
+ enabled: ${wizard.notifications.webhook}
194
+ url: "${escapeYamlString(wizard.notifications.webhookUrl)}"
195
+ `;
196
+ }
197
+
198
+ function generateFullPolicyYaml(context: GitContext, wizard: WizardConfig): string {
163
199
  const notificationsEnabled = wizard.notifications.desktop || wizard.notifications.slack || wizard.notifications.webhook;
164
200
  return `version: 1
165
201
  commit_policy:
@@ -186,7 +222,6 @@ locks:
186
222
  multiplier: 2
187
223
  jitter_ms: 150
188
224
  protected_areas:
189
- - agentic/orchestrator/schemas
190
225
  - agentic/orchestrator/policy.yaml
191
226
  required_modes:
192
227
  - fast
@@ -353,7 +388,7 @@ capabilities:
353
388
  `;
354
389
  }
355
390
 
356
- function generateAgentsYaml(): string {
391
+ function generateAgentsYaml(wizard: WizardConfig): string {
357
392
  return `version: 1
358
393
  roles:
359
394
  planner:
@@ -364,17 +399,25 @@ roles:
364
399
  system_prompt_path: agentic/orchestrator/prompts/qa.system.md
365
400
  missing_prompt_behavior: ignore
366
401
  runtime:
367
- default_provider: custom
368
- default_model: local-default
402
+ default_provider: ${wizard.defaultProvider}
403
+ default_model: ${wizard.defaultModel}
369
404
  provider_config_env: AOP_PROVIDER_CONFIG_ENV
370
405
  role_provider_overrides: {}
371
406
  `;
372
407
  }
373
408
 
374
- function generateAdaptersYaml(): string {
375
- return `notification-channel: desktop
376
- activity-detector: claude-jsonl
377
- scm-provider: github
409
+ function generateAdaptersYaml(wizard: WizardConfig): string {
410
+ const notificationChannel = wizard.notifications.desktop
411
+ ? 'desktop'
412
+ : wizard.notifications.slack
413
+ ? 'slack'
414
+ : wizard.notifications.webhook
415
+ ? 'webhook'
416
+ : 'desktop';
417
+ const activityDetector = resolveActivityDetector(wizard.defaultProvider);
418
+ return `notification-channel: ${notificationChannel}
419
+ activity-detector: ${activityDetector}
420
+ scm-provider: ${wizard.scmProvider}
378
421
  `;
379
422
  }
380
423
 
@@ -410,18 +453,56 @@ function parseNotificationChannels(raw: string): { desktop: boolean; slack: bool
410
453
  };
411
454
  }
412
455
 
456
+ function parseAdapterName(raw: string, supportedNames: ReadonlySet<string>, fallback: string): string {
457
+ const value = raw.trim();
458
+ if (supportedNames.has(value)) {
459
+ return value;
460
+ }
461
+ return fallback;
462
+ }
463
+
464
+ function resolveActivityDetector(defaultProvider: string): string {
465
+ if (defaultProvider === 'claude') {
466
+ return 'claude-jsonl';
467
+ }
468
+ if (defaultProvider === 'codex') {
469
+ return 'codex-rpc';
470
+ }
471
+ return 'process-heuristic';
472
+ }
473
+
413
474
  async function askWithDefault(prompt: PromptSession, label: string, defaultValue: string): Promise<string> {
414
475
  const raw = (await prompt.question(`${label} [${defaultValue}]: `)).trim();
415
476
  return raw.length > 0 ? raw : defaultValue;
416
477
  }
417
478
 
418
479
  async function collectWizardConfig(
419
- defaults: { branch: string; framework: TestFramework },
480
+ defaults: {
481
+ branch: string;
482
+ framework: TestFramework;
483
+ defaultProvider: string;
484
+ defaultModel: string;
485
+ defaultScmProvider: string;
486
+ },
420
487
  promptFactory: () => PromptSession
421
488
  ): Promise<WizardConfig> {
422
489
  const prompt = promptFactory();
423
490
  try {
491
+ const supportedProviders = new Set(globalAdapterRegistry.list(AGENT_PROVIDER_SLOT.name).map((adapter) => adapter.name));
492
+ const supportedScmProviders = new Set(globalAdapterRegistry.list(SCM_PROVIDER_SLOT.name).map((adapter) => adapter.name));
493
+
424
494
  const baseBranch = await askWithDefault(prompt, 'Worktree base branch', defaults.branch);
495
+ const defaultProviderRaw = await askWithDefault(
496
+ prompt,
497
+ `Default agent provider (${Array.from(supportedProviders).join('|')})`,
498
+ defaults.defaultProvider
499
+ );
500
+ const defaultModel = await askWithDefault(prompt, 'Default model', defaults.defaultModel);
501
+ const scmProviderRaw = await askWithDefault(
502
+ prompt,
503
+ `SCM provider (${Array.from(supportedScmProviders).join('|')})`,
504
+ defaults.defaultScmProvider
505
+ );
425
506
  const maxParallelGateRunsRaw = await askWithDefault(prompt, 'Max parallel gate runs', '3');
426
507
  const dashboardPortRaw = await askWithDefault(prompt, 'Dashboard port', '3000');
427
508
  const channelsRaw = await askWithDefault(
@@ -448,6 +529,9 @@ async function collectWizardConfig(
448
529
 
449
530
  return {
450
531
  baseBranch,
532
+ defaultProvider: parseAdapterName(defaultProviderRaw, supportedProviders, defaults.defaultProvider),
533
+ defaultModel,
534
+ scmProvider: parseAdapterName(scmProviderRaw, supportedScmProviders, defaults.defaultScmProvider),
451
535
  maxParallelGateRuns: parseInteger(maxParallelGateRunsRaw, 3, 1, 64),
452
536
  dashboardPort: parseInteger(dashboardPortRaw, 3000, 1, 65535),
453
537
  framework: parseFramework(frameworkRaw, defaults.framework),
@@ -465,7 +549,7 @@ async function collectWizardConfig(
465
549
  }
466
550
 
467
551
  async function loadTemplateFiles(
468
- subdir: 'prompts' | 'schemas',
552
+ subdir: 'prompts',
469
553
  matcher: (filename: string) => boolean
470
554
  ): Promise<Array<{ rel: string; content: string }>> {
471
555
  const sourceDir = path.join(TEMPLATE_ROOT, subdir);
@@ -499,6 +583,9 @@ export class InitCommandHandler {
499
583
  const wizard = options.auto
500
584
  ? {
501
585
  baseBranch: gitContext.defaultBranch,
586
+ defaultProvider: DEFAULT_AGENT_PROVIDER,
587
+ defaultModel: DEFAULT_AGENT_MODEL,
588
+ scmProvider: DEFAULT_SCM_PROVIDER,
502
589
  maxParallelGateRuns: 3,
503
590
  dashboardPort: 3000,
504
591
  framework,
@@ -513,7 +600,10 @@ export class InitCommandHandler {
513
600
  : await collectWizardConfig(
514
601
  {
515
602
  branch: gitContext.defaultBranch,
516
- framework
603
+ framework,
604
+ defaultProvider: DEFAULT_AGENT_PROVIDER,
605
+ defaultModel: DEFAULT_AGENT_MODEL,
606
+ defaultScmProvider: DEFAULT_SCM_PROVIDER
517
607
  },
518
608
  this.promptFactory
519
609
  );
@@ -521,19 +611,20 @@ export class InitCommandHandler {
521
611
  const orchestratorDir = path.join(this.repoRoot, 'agentic', 'orchestrator');
522
612
  await fs.mkdir(orchestratorDir, { recursive: true });
523
613
 
614
+ const policyContent = options.advanced_policy
615
+ ? generateFullPolicyYaml({ ...gitContext, defaultBranch: wizard.baseBranch }, wizard)
616
+ : generateLeanPolicyYaml(wizard);
617
+
524
618
  const configFiles: Array<{ rel: string; content: string }> = [
525
619
  {
526
620
  rel: 'agentic/orchestrator/policy.yaml',
527
- content: generatePolicyYaml({ ...gitContext, defaultBranch: wizard.baseBranch }, wizard)
621
+ content: policyContent
528
622
  },
529
623
  { rel: 'agentic/orchestrator/gates.yaml', content: generateGatesYaml(wizard.framework) },
530
- { rel: 'agentic/orchestrator/agents.yaml', content: generateAgentsYaml() },
531
- { rel: 'agentic/orchestrator/adapters.yaml', content: generateAdaptersYaml() }
532
- ];
533
- const templateFiles = [
534
- ...(await loadTemplateFiles('prompts', (filename) => filename.endsWith('.md'))),
535
- ...(await loadTemplateFiles('schemas', (filename) => filename.endsWith('.json')))
624
+ { rel: 'agentic/orchestrator/agents.yaml', content: generateAgentsYaml(wizard) },
625
+ { rel: 'agentic/orchestrator/adapters.yaml', content: generateAdaptersYaml(wizard) }
536
626
  ];
627
+ const templateFiles = await loadTemplateFiles('prompts', (filename) => filename.endsWith('.md'));
537
628
  const writableFiles = [...configFiles, ...templateFiles];
538
629
 
539
630
  const created: string[] = [];
@@ -566,14 +657,25 @@ export class InitCommandHandler {
566
657
 
567
658
  const validationWarnings: string[] = [];
568
659
  const schemaRegistry = new SchemaRegistry(this.repoRoot);
569
- const validationTargets: Array<{ schema: string; rel: string }> = [
570
- { schema: 'policy.schema.json', rel: 'agentic/orchestrator/policy.yaml' },
660
+
661
+ // Validate policy.yaml via composition loader (merges user policy over bundled defaults
662
+ // then validates the merged result against the full schema).
663
+ const policyRel = 'agentic/orchestrator/policy.yaml';
664
+ if (!skipped.includes(policyRel)) {
665
+ try {
666
+ await loadComposedPolicy(this.repoRoot, path.join(this.repoRoot, policyRel), schemaRegistry);
667
+ } catch (err) {
668
+ validationWarnings.push(`Failed to validate ${policyRel}: ${String(err)}`);
669
+ }
670
+ }
671
+
672
+ const nonPolicyTargets: Array<{ schema: string; rel: string }> = [
571
673
  { schema: 'gates.schema.json', rel: 'agentic/orchestrator/gates.yaml' },
572
674
  { schema: 'agents.schema.json', rel: 'agentic/orchestrator/agents.yaml' },
573
675
  { schema: 'adapters.schema.json', rel: 'agentic/orchestrator/adapters.yaml' }
574
676
  ];
575
677
 
576
- for (const { schema, rel } of validationTargets) {
678
+ for (const { schema, rel } of nonPolicyTargets) {
577
679
  if (skipped.includes(rel)) {
578
680
  continue;
579
681
  }
@@ -590,6 +692,17 @@ export class InitCommandHandler {
590
692
  }
591
693
  }
592
694
 
695
+ const policyMode = options.advanced_policy ? 'full (advanced)' : 'lean (common controls only)';
696
+ const nextSteps = [
697
+ `Generated policy.yaml in ${policyMode} mode`,
698
+ 'Review generated files in agentic/orchestrator/{policy,gates,agents,adapters}.yaml',
699
+ 'Verify prompts under agentic/orchestrator/prompts/',
700
+ 'Run: aop run -fi <spec.md> to start orchestrating features'
701
+ ];
702
+ if (!options.advanced_policy) {
703
+ nextSteps.push('To generate a full explicit policy with all advanced controls, re-run: aop init --advanced-policy --force');
704
+ }
705
+
593
706
  return {
594
707
  ok: true,
595
708
  data: {
@@ -598,11 +711,7 @@ export class InitCommandHandler {
598
711
  updated,
599
712
  skipped,
600
713
  validation_warnings: validationWarnings,
601
- next_steps: [
602
- 'Review generated files in agentic/orchestrator/{policy,gates,agents,adapters}.yaml',
603
- 'Verify prompts under agentic/orchestrator/prompts/ and schemas under agentic/orchestrator/schemas/',
604
- 'Run: aop run -fi <spec.md> to start orchestrating features'
605
- ]
714
+ next_steps: nextSteps
606
715
  }
607
716
  };
608
717
  }
@@ -54,10 +54,7 @@ export class RetryCommandHandler {
54
54
 
55
55
  const force = options.force === true;
56
56
  let retryCountReset = false;
57
- let retryExecuted: boolean;
58
57
  let forcedRetryExecuted = false;
59
- let retryMode: string | null;
60
- let retryOutcome: string | null;
61
58
 
62
59
  const current = await callCliTool(this.toolClient, this.runId, TOOLS.FEATURE_STATE_GET, {
63
60
  feature_id: featureId
@@ -118,10 +115,10 @@ export class RetryCommandHandler {
118
115
  profile: null,
119
116
  mode: inferredMode
120
117
  });
121
- retryMode = inferredMode;
118
+ const retryExecuted = true;
119
+ const retryMode = inferredMode;
122
120
  const gateData = gate.data as { overall?: string };
123
- retryOutcome = typeof gateData.overall === 'string' ? gateData.overall : null;
124
- retryExecuted = true;
121
+ const retryOutcome = typeof gateData.overall === 'string' ? gateData.overall : null;
125
122
 
126
123
  return {
127
124
  ok: true,
@@ -33,6 +33,7 @@ export interface CliOptions {
33
33
  summary?: boolean;
34
34
  auto?: boolean;
35
35
  force?: boolean;
36
+ advanced_policy?: boolean;
36
37
  project?: string;
37
38
  all?: boolean;
38
39
  port?: number;
@@ -27,7 +27,6 @@ export class NodeCommandRunner implements CommandRunner {
27
27
  async run(cmd: string, args: string[], options: RunCommandOptions = {}): Promise<CommandResult> {
28
28
  return await new Promise((resolve) => {
29
29
  let child;
30
- let spawnError: Error | null = null;
31
30
 
32
31
  try {
33
32
  child = spawn(cmd, args, {
@@ -36,12 +35,11 @@ export class NodeCommandRunner implements CommandRunner {
36
35
  stdio: ['pipe', 'pipe', 'pipe']
37
36
  });
38
37
  } catch (err) {
39
- spawnError = err as Error;
40
38
  return resolve({
41
39
  code: 127,
42
40
  signal: null,
43
41
  stdout: '',
44
- stderr: spawnError.message,
42
+ stderr: (err as Error).message,
45
43
  timeout: false
46
44
  });
47
45
  }
@@ -32,6 +32,7 @@ import { CollisionQueueService } from '../application/services/collision-queue-s
32
32
  import { FeatureDeletionService, type FeatureDeleteResult } from '../application/services/feature-deletion-service.js';
33
33
  import { CostTrackingService } from '../application/services/cost-tracking-service.js';
34
34
  import { PerformanceAnalyticsService, type FeatureOutcome } from '../application/services/performance-analytics-service.js';
35
+ import { loadComposedPolicy } from '../application/services/policy-loader-service.js';
35
36
  import {
36
37
  ACTIVITY_DETECTOR_SLOT,
37
38
  NOTIFICATION_CHANNEL_SLOT,
@@ -344,12 +345,8 @@ export class AopKernel {
344
345
  throw new Error(`invalid_gates_yaml:${JSON.stringify(gates.validation.errors)}`);
345
346
  }
346
347
 
347
- const policy = await loadAndValidateYaml(this.schemaRegistry, 'policy.schema.json', policyPath);
348
- if (!policy.validation.valid) {
349
- throw new Error(`invalid_policy_yaml:${JSON.stringify(policy.validation.errors)}`);
350
- }
351
-
352
- const parsedPolicy = policy.parsed as PolicyConfigSnapshot;
348
+ const { mergedPolicy } = await loadComposedPolicy(this.repoRoot, policyPath, this.schemaRegistry);
349
+ const parsedPolicy = mergedPolicy as PolicyConfigSnapshot;
353
350
  const implementation = readObjectField(parsedPolicy, 'implementation');
354
351
  const testing = readObjectField(parsedPolicy, 'testing');
355
352
 
@@ -1,5 +1,7 @@
1
+ import fsSync from 'node:fs';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
3
5
  import YAML from 'yaml';
4
6
  import Ajv2020Import from 'ajv/dist/2020.js';
5
7
  import type { AnySchema, ErrorObject, ValidateFunction } from 'ajv';
@@ -30,6 +32,26 @@ type AjvLike = {
30
32
 
31
33
  const RFC3339_DATE_TIME =
32
34
  /^(\d{4})-(\d{2})-(\d{2})T([0-2]\d):([0-5]\d):([0-5]\d)(\.\d{1,9})?(Z|([+-])([0-2]\d):([0-5]\d))$/;
35
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
36
+
37
+ function resolveBundledSchemaRoot(startDir: string): string | null {
38
+ let current = path.resolve(startDir);
39
+
40
+ while (true) {
41
+ const candidate = path.join(current, 'agentic', 'orchestrator', 'schemas', 'policy.schema.json');
42
+ if (fsSync.existsSync(candidate)) {
43
+ return path.dirname(candidate);
44
+ }
45
+
46
+ const parent = path.dirname(current);
47
+ if (parent === current) {
48
+ return null;
49
+ }
50
+ current = parent;
51
+ }
52
+ }
53
+
54
+ const BUNDLED_SCHEMA_ROOT = resolveBundledSchemaRoot(MODULE_DIR);
33
55
 
34
56
  function isStrictRfc3339DateTime(value: string): boolean {
35
57
  if (typeof value !== 'string') {
@@ -72,11 +94,13 @@ function isStrictRfc3339DateTime(value: string): boolean {
72
94
 
73
95
  export class SchemaRegistry implements SchemaValidator {
74
96
  readonly repoRoot: string;
97
+ readonly schemaRoot: string;
75
98
  private readonly ajv: AjvLike;
76
99
  private readonly validators: Map<SchemaId, ValidateFunction>;
77
100
 
78
101
  constructor(repoRoot: string) {
79
102
  this.repoRoot = repoRoot;
103
+ this.schemaRoot = this.resolveSchemaRoot();
80
104
  const Ajv2020Ctor = Ajv2020Import as unknown as new (options?: Record<string, unknown>) => AjvLike;
81
105
  this.ajv = new Ajv2020Ctor({ allErrors: true, strict: false });
82
106
  this.ajv.addFormat?.('date-time', {
@@ -86,8 +110,19 @@ export class SchemaRegistry implements SchemaValidator {
86
110
  this.validators = new Map();
87
111
  }
88
112
 
113
+ private resolveSchemaRoot(): string {
114
+ const repoSchemaRoot = path.join(this.repoRoot, 'agentic', 'orchestrator', 'schemas');
115
+ if (fsSync.existsSync(path.join(repoSchemaRoot, 'policy.schema.json'))) {
116
+ return repoSchemaRoot;
117
+ }
118
+ if (BUNDLED_SCHEMA_ROOT) {
119
+ return BUNDLED_SCHEMA_ROOT;
120
+ }
121
+ return repoSchemaRoot;
122
+ }
123
+
89
124
  schemaPath(name: SchemaId): string {
90
- return path.join(this.repoRoot, 'agentic', 'orchestrator', 'schemas', name);
125
+ return path.join(this.schemaRoot, name);
91
126
  }
92
127
 
93
128
  async loadSchema(name: SchemaId): Promise<ValidateFunction> {
@@ -157,7 +157,7 @@ export async function runCli(
157
157
 
158
158
  if (options.command === CliCommand.Init) {
159
159
  const handler = new InitCommandHandler(repoRoot);
160
- const payload = await handler.execute({ auto: options.auto, force: options.force });
160
+ const payload = await handler.execute({ auto: options.auto, force: options.force, advanced_policy: options.advanced_policy });
161
161
  printPayload(payload);
162
162
  return 0;
163
163
  }