@renseiai/agentfactory 0.8.2 → 0.8.4

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 (33) hide show
  1. package/dist/src/config/index.d.ts +2 -2
  2. package/dist/src/config/index.d.ts.map +1 -1
  3. package/dist/src/config/index.js +1 -1
  4. package/dist/src/config/repository-config.d.ts +92 -1
  5. package/dist/src/config/repository-config.d.ts.map +1 -1
  6. package/dist/src/config/repository-config.js +80 -2
  7. package/dist/src/config/repository-config.test.js +303 -2
  8. package/dist/src/orchestrator/detect-work-type.test.d.ts +2 -0
  9. package/dist/src/orchestrator/detect-work-type.test.d.ts.map +1 -0
  10. package/dist/src/orchestrator/detect-work-type.test.js +62 -0
  11. package/dist/src/orchestrator/heartbeat-writer.test.d.ts +2 -0
  12. package/dist/src/orchestrator/heartbeat-writer.test.d.ts.map +1 -0
  13. package/dist/src/orchestrator/heartbeat-writer.test.js +139 -0
  14. package/dist/src/orchestrator/orchestrator-utils.test.d.ts +2 -0
  15. package/dist/src/orchestrator/orchestrator-utils.test.d.ts.map +1 -0
  16. package/dist/src/orchestrator/orchestrator-utils.test.js +41 -0
  17. package/dist/src/orchestrator/orchestrator.d.ts +26 -0
  18. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  19. package/dist/src/orchestrator/orchestrator.js +111 -51
  20. package/dist/src/orchestrator/state-recovery.test.d.ts +2 -0
  21. package/dist/src/orchestrator/state-recovery.test.d.ts.map +1 -0
  22. package/dist/src/orchestrator/state-recovery.test.js +425 -0
  23. package/dist/src/orchestrator/types.d.ts +11 -1
  24. package/dist/src/orchestrator/types.d.ts.map +1 -1
  25. package/dist/src/providers/claude-provider.d.ts.map +1 -1
  26. package/dist/src/providers/claude-provider.js +6 -0
  27. package/dist/src/providers/index.d.ts +71 -15
  28. package/dist/src/providers/index.d.ts.map +1 -1
  29. package/dist/src/providers/index.js +156 -28
  30. package/dist/src/providers/index.test.d.ts +2 -0
  31. package/dist/src/providers/index.test.d.ts.map +1 -0
  32. package/dist/src/providers/index.test.js +225 -0
  33. package/package.json +3 -3
@@ -1,3 +1,3 @@
1
- export { loadRepositoryConfig, RepositoryConfigSchema, getEffectiveAllowedProjects } from './repository-config.js';
2
- export type { RepositoryConfig } from './repository-config.js';
1
+ export { loadRepositoryConfig, RepositoryConfigSchema, ProjectConfigSchema, ProvidersConfigSchema, getEffectiveAllowedProjects, getProjectConfig, getProjectPath, getProvidersConfig } from './repository-config.js';
2
+ export type { RepositoryConfig, ProjectConfig } from './repository-config.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAA;AAClH,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AACpN,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA"}
@@ -1 +1 @@
1
- export { loadRepositoryConfig, RepositoryConfigSchema, getEffectiveAllowedProjects } from './repository-config.js';
1
+ export { loadRepositoryConfig, RepositoryConfigSchema, ProjectConfigSchema, ProvidersConfigSchema, getEffectiveAllowedProjects, getProjectConfig, getProjectPath, getProvidersConfig } from './repository-config.js';
@@ -7,12 +7,64 @@
7
7
  * - Project allowlisting for the orchestrator (allowedProjects field)
8
8
  */
9
9
  import { z } from 'zod';
10
+ import type { ProvidersConfig } from '../providers/index.js';
11
+ /** Per-project configuration (object form of projectPaths values) */
12
+ export declare const ProjectConfigSchema: z.ZodObject<{
13
+ path: z.ZodString;
14
+ packageManager: z.ZodOptional<z.ZodEnum<{
15
+ pnpm: "pnpm";
16
+ npm: "npm";
17
+ yarn: "yarn";
18
+ bun: "bun";
19
+ none: "none";
20
+ }>>;
21
+ buildCommand: z.ZodOptional<z.ZodString>;
22
+ testCommand: z.ZodOptional<z.ZodString>;
23
+ validateCommand: z.ZodOptional<z.ZodString>;
24
+ }, z.core.$strip>;
25
+ export type ProjectConfig = z.infer<typeof ProjectConfigSchema>;
26
+ /** Provider selection configuration */
27
+ export declare const ProvidersConfigSchema: z.ZodObject<{
28
+ default: z.ZodOptional<z.ZodEnum<{
29
+ claude: "claude";
30
+ codex: "codex";
31
+ amp: "amp";
32
+ "spring-ai": "spring-ai";
33
+ a2a: "a2a";
34
+ }>>;
35
+ byWorkType: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
36
+ claude: "claude";
37
+ codex: "codex";
38
+ amp: "amp";
39
+ "spring-ai": "spring-ai";
40
+ a2a: "a2a";
41
+ }>>>;
42
+ byProject: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
43
+ claude: "claude";
44
+ codex: "codex";
45
+ amp: "amp";
46
+ "spring-ai": "spring-ai";
47
+ a2a: "a2a";
48
+ }>>>;
49
+ }, z.core.$strip>;
10
50
  export declare const RepositoryConfigSchema: z.ZodObject<{
11
51
  apiVersion: z.ZodString;
12
52
  kind: z.ZodLiteral<"RepositoryConfig">;
13
53
  repository: z.ZodOptional<z.ZodString>;
14
54
  allowedProjects: z.ZodOptional<z.ZodArray<z.ZodString>>;
15
- projectPaths: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
55
+ projectPaths: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
56
+ path: z.ZodString;
57
+ packageManager: z.ZodOptional<z.ZodEnum<{
58
+ pnpm: "pnpm";
59
+ npm: "npm";
60
+ yarn: "yarn";
61
+ bun: "bun";
62
+ none: "none";
63
+ }>>;
64
+ buildCommand: z.ZodOptional<z.ZodString>;
65
+ testCommand: z.ZodOptional<z.ZodString>;
66
+ validateCommand: z.ZodOptional<z.ZodString>;
67
+ }, z.core.$strip>]>>>;
16
68
  sharedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
17
69
  linearCli: z.ZodOptional<z.ZodString>;
18
70
  packageManager: z.ZodOptional<z.ZodEnum<{
@@ -25,6 +77,29 @@ export declare const RepositoryConfigSchema: z.ZodObject<{
25
77
  buildCommand: z.ZodOptional<z.ZodString>;
26
78
  testCommand: z.ZodOptional<z.ZodString>;
27
79
  validateCommand: z.ZodOptional<z.ZodString>;
80
+ providers: z.ZodOptional<z.ZodObject<{
81
+ default: z.ZodOptional<z.ZodEnum<{
82
+ claude: "claude";
83
+ codex: "codex";
84
+ amp: "amp";
85
+ "spring-ai": "spring-ai";
86
+ a2a: "a2a";
87
+ }>>;
88
+ byWorkType: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
89
+ claude: "claude";
90
+ codex: "codex";
91
+ amp: "amp";
92
+ "spring-ai": "spring-ai";
93
+ a2a: "a2a";
94
+ }>>>;
95
+ byProject: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
96
+ claude: "claude";
97
+ codex: "codex";
98
+ amp: "amp";
99
+ "spring-ai": "spring-ai";
100
+ a2a: "a2a";
101
+ }>>>;
102
+ }, z.core.$strip>>;
28
103
  }, z.core.$strip>;
29
104
  export type RepositoryConfig = z.infer<typeof RepositoryConfigSchema>;
30
105
  /**
@@ -33,6 +108,22 @@ export type RepositoryConfig = z.infer<typeof RepositoryConfigSchema>;
33
108
  * Otherwise falls back to `allowedProjects`.
34
109
  */
35
110
  export declare function getEffectiveAllowedProjects(config: RepositoryConfig): string[] | undefined;
111
+ /**
112
+ * Returns the normalized ProjectConfig for a given project name.
113
+ * Handles both string shorthand and object form of projectPaths values.
114
+ * Per-project overrides take precedence over repo-wide defaults.
115
+ */
116
+ export declare function getProjectConfig(config: RepositoryConfig, projectName: string): ProjectConfig | null;
117
+ /**
118
+ * Returns just the path string for a given project name.
119
+ * Handles both string shorthand and object form.
120
+ */
121
+ export declare function getProjectPath(config: RepositoryConfig, projectName: string): string | undefined;
122
+ /**
123
+ * Returns the providers config from a RepositoryConfig, if present.
124
+ * Convenience helper for passing to ProviderResolutionContext.configProviders.
125
+ */
126
+ export declare function getProvidersConfig(config: RepositoryConfig): ProvidersConfig | undefined;
36
127
  /**
37
128
  * Load and validate .agentfactory/config.yaml from the given git root.
38
129
  *
@@ -1 +1 @@
1
- {"version":3,"file":"repository-config.d.ts","sourceRoot":"","sources":["../../../src/config/repository-config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AASvB,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;iBA0ClC,CAAA;AAMD,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AAMrE;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE,GAAG,SAAS,CAK1F;AAMD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAQ7E"}
1
+ {"version":3,"file":"repository-config.d.ts","sourceRoot":"","sources":["../../../src/config/repository-config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAM5D,qEAAqE;AACrE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAW9B,CAAA;AAEF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAW/D,uCAAuC;AACvC,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;iBAOhC,CAAA;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmDlC,CAAA;AAMD,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AAMrE;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE,GAAG,SAAS,CAK1F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAepG;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKhG;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,GAAG,eAAe,GAAG,SAAS,CAExF;AAMD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAQ7E"}
@@ -13,13 +13,46 @@ import YAML from 'yaml';
13
13
  // ---------------------------------------------------------------------------
14
14
  // Zod Schema
15
15
  // ---------------------------------------------------------------------------
16
+ /** Per-project configuration (object form of projectPaths values) */
17
+ export const ProjectConfigSchema = z.object({
18
+ /** Root directory for this project within the repo */
19
+ path: z.string(),
20
+ /** Package manager override for this project */
21
+ packageManager: z.enum(['pnpm', 'npm', 'yarn', 'bun', 'none']).optional(),
22
+ /** Build command override for this project */
23
+ buildCommand: z.string().optional(),
24
+ /** Test command override for this project */
25
+ testCommand: z.string().optional(),
26
+ /** Validation command override for this project */
27
+ validateCommand: z.string().optional(),
28
+ });
29
+ /**
30
+ * projectPaths values can be a string (path shorthand) or a full ProjectConfig object.
31
+ * String values are normalized to { path: value } by getProjectConfig().
32
+ */
33
+ const ProjectPathValueSchema = z.union([z.string(), ProjectConfigSchema]);
34
+ /** Valid agent provider names */
35
+ const AgentProviderNameSchema = z.enum(['claude', 'codex', 'amp', 'spring-ai', 'a2a']);
36
+ /** Provider selection configuration */
37
+ export const ProvidersConfigSchema = z.object({
38
+ /** Default provider for all agents */
39
+ default: AgentProviderNameSchema.optional(),
40
+ /** Provider overrides by work type (e.g., { qa: 'codex' }) */
41
+ byWorkType: z.record(z.string(), AgentProviderNameSchema).optional(),
42
+ /** Provider overrides by project name (e.g., { Social: 'codex' }) */
43
+ byProject: z.record(z.string(), AgentProviderNameSchema).optional(),
44
+ });
16
45
  export const RepositoryConfigSchema = z.object({
17
46
  apiVersion: z.string(),
18
47
  kind: z.literal('RepositoryConfig'),
19
48
  repository: z.string().optional(),
20
49
  allowedProjects: z.array(z.string()).optional(),
21
- /** Maps Linear project names to their root directory within the repo (e.g., { Family: 'apps/family' }) */
22
- projectPaths: z.record(z.string(), z.string()).optional(),
50
+ /**
51
+ * Maps Linear project names to their root directory or full config.
52
+ * String shorthand: { Family: 'apps/family' }
53
+ * Object form: { 'Family iOS': { path: 'apps/family-ios', packageManager: 'none', buildCommand: 'make build' } }
54
+ */
55
+ projectPaths: z.record(z.string(), ProjectPathValueSchema).optional(),
23
56
  /** Shared directories that any project's agent may modify (e.g., ['packages/ui']) */
24
57
  sharedPaths: z.array(z.string()).optional(),
25
58
  /**
@@ -52,6 +85,11 @@ export const RepositoryConfigSchema = z.object({
52
85
  * Injected into workflow templates as {{validateCommand}}.
53
86
  */
54
87
  validateCommand: z.string().optional(),
88
+ /**
89
+ * Provider selection configuration.
90
+ * Allows routing agents to different providers by work type or project.
91
+ */
92
+ providers: ProvidersConfigSchema.optional(),
55
93
  }).refine((data) => !(data.allowedProjects && data.projectPaths), { message: 'allowedProjects and projectPaths are mutually exclusive — use one or the other' });
56
94
  // ---------------------------------------------------------------------------
57
95
  // Helpers
@@ -67,6 +105,46 @@ export function getEffectiveAllowedProjects(config) {
67
105
  }
68
106
  return config.allowedProjects;
69
107
  }
108
+ /**
109
+ * Returns the normalized ProjectConfig for a given project name.
110
+ * Handles both string shorthand and object form of projectPaths values.
111
+ * Per-project overrides take precedence over repo-wide defaults.
112
+ */
113
+ export function getProjectConfig(config, projectName) {
114
+ if (!config.projectPaths)
115
+ return null;
116
+ const value = config.projectPaths[projectName];
117
+ if (value === undefined)
118
+ return null;
119
+ // Normalize string shorthand to object form
120
+ const projectConfig = typeof value === 'string' ? { path: value } : value;
121
+ return {
122
+ path: projectConfig.path,
123
+ packageManager: projectConfig.packageManager ?? config.packageManager,
124
+ buildCommand: projectConfig.buildCommand ?? config.buildCommand,
125
+ testCommand: projectConfig.testCommand ?? config.testCommand,
126
+ validateCommand: projectConfig.validateCommand ?? config.validateCommand,
127
+ };
128
+ }
129
+ /**
130
+ * Returns just the path string for a given project name.
131
+ * Handles both string shorthand and object form.
132
+ */
133
+ export function getProjectPath(config, projectName) {
134
+ if (!config.projectPaths)
135
+ return undefined;
136
+ const value = config.projectPaths[projectName];
137
+ if (value === undefined)
138
+ return undefined;
139
+ return typeof value === 'string' ? value : value.path;
140
+ }
141
+ /**
142
+ * Returns the providers config from a RepositoryConfig, if present.
143
+ * Convenience helper for passing to ProviderResolutionContext.configProviders.
144
+ */
145
+ export function getProvidersConfig(config) {
146
+ return config.providers;
147
+ }
70
148
  // ---------------------------------------------------------------------------
71
149
  // Loader
72
150
  // ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { existsSync, readFileSync } from 'fs';
3
- import { loadRepositoryConfig, RepositoryConfigSchema, getEffectiveAllowedProjects } from './repository-config.js';
3
+ import { loadRepositoryConfig, RepositoryConfigSchema, getEffectiveAllowedProjects, getProjectConfig, getProjectPath, getProvidersConfig, ProvidersConfigSchema } from './repository-config.js';
4
4
  vi.mock('fs', () => ({
5
5
  existsSync: vi.fn(),
6
6
  readFileSync: vi.fn(),
@@ -84,6 +84,38 @@ sharedPaths:
84
84
  sharedPaths: ['packages/ui', 'packages/lexical'],
85
85
  });
86
86
  });
87
+ it('parses valid YAML with mixed string and object projectPaths', () => {
88
+ mockExistsSync.mockReturnValue(true);
89
+ mockReadFileSync.mockReturnValue(`apiVersion: v1
90
+ kind: RepositoryConfig
91
+ repository: github.com/org/monorepo
92
+ projectPaths:
93
+ Social: apps/social
94
+ Family iOS:
95
+ path: apps/family-ios
96
+ packageManager: none
97
+ buildCommand: "make build"
98
+ testCommand: "make test"
99
+ sharedPaths:
100
+ - packages/ui
101
+ `);
102
+ const result = loadRepositoryConfig('/some/repo');
103
+ expect(result).toEqual({
104
+ apiVersion: 'v1',
105
+ kind: 'RepositoryConfig',
106
+ repository: 'github.com/org/monorepo',
107
+ projectPaths: {
108
+ Social: 'apps/social',
109
+ 'Family iOS': {
110
+ path: 'apps/family-ios',
111
+ packageManager: 'none',
112
+ buildCommand: 'make build',
113
+ testCommand: 'make test',
114
+ },
115
+ },
116
+ sharedPaths: ['packages/ui'],
117
+ });
118
+ });
87
119
  it('throws on invalid schema — allowedProjects is not an array', () => {
88
120
  mockExistsSync.mockReturnValue(true);
89
121
  mockReadFileSync.mockReturnValue(`apiVersion: v1
@@ -122,7 +154,7 @@ describe('RepositoryConfigSchema', () => {
122
154
  kind: 'InvalidKind',
123
155
  })).toThrow();
124
156
  });
125
- it('validates config with projectPaths only', () => {
157
+ it('validates config with projectPaths only (string shorthand)', () => {
126
158
  const result = RepositoryConfigSchema.parse({
127
159
  apiVersion: 'v1',
128
160
  kind: 'RepositoryConfig',
@@ -135,6 +167,44 @@ describe('RepositoryConfigSchema', () => {
135
167
  expect(result.projectPaths).toEqual({ Social: 'apps/social', Family: 'apps/family' });
136
168
  expect(result.allowedProjects).toBeUndefined();
137
169
  });
170
+ it('validates config with projectPaths object form', () => {
171
+ const result = RepositoryConfigSchema.parse({
172
+ apiVersion: 'v1',
173
+ kind: 'RepositoryConfig',
174
+ projectPaths: {
175
+ Social: 'apps/social',
176
+ 'Family iOS': {
177
+ path: 'apps/family-ios',
178
+ packageManager: 'none',
179
+ buildCommand: 'make build',
180
+ testCommand: 'make test',
181
+ validateCommand: 'make build',
182
+ },
183
+ },
184
+ });
185
+ expect(result.projectPaths).toEqual({
186
+ Social: 'apps/social',
187
+ 'Family iOS': {
188
+ path: 'apps/family-ios',
189
+ packageManager: 'none',
190
+ buildCommand: 'make build',
191
+ testCommand: 'make test',
192
+ validateCommand: 'make build',
193
+ },
194
+ });
195
+ });
196
+ it('validates config with mixed string and object projectPaths', () => {
197
+ const result = RepositoryConfigSchema.parse({
198
+ apiVersion: 'v1',
199
+ kind: 'RepositoryConfig',
200
+ projectPaths: {
201
+ Social: 'apps/social',
202
+ Family: 'apps/family',
203
+ 'Family iOS': { path: 'apps/family-ios', packageManager: 'none' },
204
+ },
205
+ });
206
+ expect(Object.keys(result.projectPaths)).toEqual(['Social', 'Family', 'Family iOS']);
207
+ });
138
208
  it('validates config with projectPaths and sharedPaths', () => {
139
209
  const result = RepositoryConfigSchema.parse({
140
210
  apiVersion: 'v1',
@@ -246,4 +316,235 @@ describe('getEffectiveAllowedProjects', () => {
246
316
  });
247
317
  expect(getEffectiveAllowedProjects(config)).toBeUndefined();
248
318
  });
319
+ it('returns keys from mixed string/object projectPaths', () => {
320
+ const config = RepositoryConfigSchema.parse({
321
+ apiVersion: 'v1',
322
+ kind: 'RepositoryConfig',
323
+ projectPaths: {
324
+ Social: 'apps/social',
325
+ 'Family iOS': { path: 'apps/family-ios', packageManager: 'none' },
326
+ },
327
+ });
328
+ expect(getEffectiveAllowedProjects(config)).toEqual(['Social', 'Family iOS']);
329
+ });
330
+ });
331
+ describe('getProjectConfig', () => {
332
+ it('returns null when projectPaths is not set', () => {
333
+ const config = RepositoryConfigSchema.parse({
334
+ apiVersion: 'v1',
335
+ kind: 'RepositoryConfig',
336
+ });
337
+ expect(getProjectConfig(config, 'Social')).toBeNull();
338
+ });
339
+ it('returns null for unknown project', () => {
340
+ const config = RepositoryConfigSchema.parse({
341
+ apiVersion: 'v1',
342
+ kind: 'RepositoryConfig',
343
+ projectPaths: { Social: 'apps/social' },
344
+ });
345
+ expect(getProjectConfig(config, 'Unknown')).toBeNull();
346
+ });
347
+ it('normalizes string shorthand to ProjectConfig', () => {
348
+ const config = RepositoryConfigSchema.parse({
349
+ apiVersion: 'v1',
350
+ kind: 'RepositoryConfig',
351
+ projectPaths: { Social: 'apps/social' },
352
+ });
353
+ const result = getProjectConfig(config, 'Social');
354
+ expect(result).toEqual({
355
+ path: 'apps/social',
356
+ packageManager: undefined,
357
+ buildCommand: undefined,
358
+ testCommand: undefined,
359
+ validateCommand: undefined,
360
+ });
361
+ });
362
+ it('returns per-project overrides from object form', () => {
363
+ const config = RepositoryConfigSchema.parse({
364
+ apiVersion: 'v1',
365
+ kind: 'RepositoryConfig',
366
+ projectPaths: {
367
+ 'Family iOS': {
368
+ path: 'apps/family-ios',
369
+ packageManager: 'none',
370
+ buildCommand: 'make build',
371
+ testCommand: 'make test',
372
+ validateCommand: 'make build',
373
+ },
374
+ },
375
+ });
376
+ const result = getProjectConfig(config, 'Family iOS');
377
+ expect(result).toEqual({
378
+ path: 'apps/family-ios',
379
+ packageManager: 'none',
380
+ buildCommand: 'make build',
381
+ testCommand: 'make test',
382
+ validateCommand: 'make build',
383
+ });
384
+ });
385
+ it('falls back to repo-wide defaults when per-project overrides are not set', () => {
386
+ const config = RepositoryConfigSchema.parse({
387
+ apiVersion: 'v1',
388
+ kind: 'RepositoryConfig',
389
+ buildCommand: 'pnpm build',
390
+ testCommand: 'pnpm test',
391
+ projectPaths: { Social: 'apps/social' },
392
+ });
393
+ const result = getProjectConfig(config, 'Social');
394
+ expect(result).toEqual({
395
+ path: 'apps/social',
396
+ packageManager: undefined,
397
+ buildCommand: 'pnpm build',
398
+ testCommand: 'pnpm test',
399
+ validateCommand: undefined,
400
+ });
401
+ });
402
+ it('per-project overrides take precedence over repo-wide defaults', () => {
403
+ const config = RepositoryConfigSchema.parse({
404
+ apiVersion: 'v1',
405
+ kind: 'RepositoryConfig',
406
+ buildCommand: 'pnpm build',
407
+ testCommand: 'pnpm test',
408
+ packageManager: 'pnpm',
409
+ projectPaths: {
410
+ 'Family iOS': {
411
+ path: 'apps/family-ios',
412
+ packageManager: 'none',
413
+ buildCommand: 'make build',
414
+ testCommand: 'make test',
415
+ },
416
+ },
417
+ });
418
+ const result = getProjectConfig(config, 'Family iOS');
419
+ expect(result).toEqual({
420
+ path: 'apps/family-ios',
421
+ packageManager: 'none',
422
+ buildCommand: 'make build',
423
+ testCommand: 'make test',
424
+ validateCommand: undefined,
425
+ });
426
+ });
427
+ });
428
+ describe('getProjectPath', () => {
429
+ it('returns path from string shorthand', () => {
430
+ const config = RepositoryConfigSchema.parse({
431
+ apiVersion: 'v1',
432
+ kind: 'RepositoryConfig',
433
+ projectPaths: { Social: 'apps/social' },
434
+ });
435
+ expect(getProjectPath(config, 'Social')).toBe('apps/social');
436
+ });
437
+ it('returns path from object form', () => {
438
+ const config = RepositoryConfigSchema.parse({
439
+ apiVersion: 'v1',
440
+ kind: 'RepositoryConfig',
441
+ projectPaths: { 'Family iOS': { path: 'apps/family-ios' } },
442
+ });
443
+ expect(getProjectPath(config, 'Family iOS')).toBe('apps/family-ios');
444
+ });
445
+ it('returns undefined for unknown project', () => {
446
+ const config = RepositoryConfigSchema.parse({
447
+ apiVersion: 'v1',
448
+ kind: 'RepositoryConfig',
449
+ projectPaths: { Social: 'apps/social' },
450
+ });
451
+ expect(getProjectPath(config, 'Unknown')).toBeUndefined();
452
+ });
453
+ it('returns undefined when projectPaths is not set', () => {
454
+ const config = RepositoryConfigSchema.parse({
455
+ apiVersion: 'v1',
456
+ kind: 'RepositoryConfig',
457
+ });
458
+ expect(getProjectPath(config, 'Social')).toBeUndefined();
459
+ });
460
+ });
461
+ describe('ProvidersConfigSchema', () => {
462
+ it('validates a complete providers config', () => {
463
+ const result = ProvidersConfigSchema.parse({
464
+ default: 'codex',
465
+ byWorkType: { qa: 'amp', development: 'claude' },
466
+ byProject: { Social: 'codex' },
467
+ });
468
+ expect(result.default).toBe('codex');
469
+ expect(result.byWorkType).toEqual({ qa: 'amp', development: 'claude' });
470
+ expect(result.byProject).toEqual({ Social: 'codex' });
471
+ });
472
+ it('validates an empty object', () => {
473
+ const result = ProvidersConfigSchema.parse({});
474
+ expect(result.default).toBeUndefined();
475
+ expect(result.byWorkType).toBeUndefined();
476
+ expect(result.byProject).toBeUndefined();
477
+ });
478
+ it('rejects invalid provider names', () => {
479
+ expect(() => ProvidersConfigSchema.parse({ default: 'invalid' })).toThrow();
480
+ });
481
+ it('rejects invalid provider names in byWorkType', () => {
482
+ expect(() => ProvidersConfigSchema.parse({ byWorkType: { qa: 'invalid' } })).toThrow();
483
+ });
484
+ });
485
+ describe('RepositoryConfigSchema with providers', () => {
486
+ it('validates config with providers field', () => {
487
+ const result = RepositoryConfigSchema.parse({
488
+ apiVersion: 'v1',
489
+ kind: 'RepositoryConfig',
490
+ providers: {
491
+ default: 'codex',
492
+ byWorkType: { qa: 'amp' },
493
+ },
494
+ });
495
+ expect(result.providers).toEqual({
496
+ default: 'codex',
497
+ byWorkType: { qa: 'amp' },
498
+ });
499
+ });
500
+ it('validates config without providers field', () => {
501
+ const result = RepositoryConfigSchema.parse({
502
+ apiVersion: 'v1',
503
+ kind: 'RepositoryConfig',
504
+ });
505
+ expect(result.providers).toBeUndefined();
506
+ });
507
+ });
508
+ describe('loadRepositoryConfig with providers', () => {
509
+ beforeEach(() => {
510
+ vi.clearAllMocks();
511
+ });
512
+ afterEach(() => {
513
+ vi.restoreAllMocks();
514
+ });
515
+ it('parses config with providers section', () => {
516
+ mockExistsSync.mockReturnValue(true);
517
+ mockReadFileSync.mockReturnValue(`apiVersion: v1
518
+ kind: RepositoryConfig
519
+ providers:
520
+ default: codex
521
+ byWorkType:
522
+ qa: amp
523
+ byProject:
524
+ Social: claude
525
+ `);
526
+ const result = loadRepositoryConfig('/some/repo');
527
+ expect(result?.providers).toEqual({
528
+ default: 'codex',
529
+ byWorkType: { qa: 'amp' },
530
+ byProject: { Social: 'claude' },
531
+ });
532
+ });
533
+ });
534
+ describe('getProvidersConfig', () => {
535
+ it('returns providers config when present', () => {
536
+ const config = RepositoryConfigSchema.parse({
537
+ apiVersion: 'v1',
538
+ kind: 'RepositoryConfig',
539
+ providers: { default: 'codex' },
540
+ });
541
+ expect(getProvidersConfig(config)).toEqual({ default: 'codex' });
542
+ });
543
+ it('returns undefined when providers not set', () => {
544
+ const config = RepositoryConfigSchema.parse({
545
+ apiVersion: 'v1',
546
+ kind: 'RepositoryConfig',
547
+ });
548
+ expect(getProvidersConfig(config)).toBeUndefined();
549
+ });
249
550
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=detect-work-type.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect-work-type.test.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/detect-work-type.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { detectWorkType } from './orchestrator.js';
3
+ describe('detectWorkType', () => {
4
+ describe('leaf issues (isParent=false)', () => {
5
+ it('maps Backlog status to development', () => {
6
+ expect(detectWorkType('Backlog', false)).toBe('development');
7
+ });
8
+ it('maps Finished status to qa', () => {
9
+ expect(detectWorkType('Finished', false)).toBe('qa');
10
+ });
11
+ it('maps Delivered status to acceptance', () => {
12
+ expect(detectWorkType('Delivered', false)).toBe('acceptance');
13
+ });
14
+ it('maps Rejected status to refinement', () => {
15
+ expect(detectWorkType('Rejected', false)).toBe('refinement');
16
+ });
17
+ it('maps Icebox status to research', () => {
18
+ expect(detectWorkType('Icebox', false)).toBe('research');
19
+ });
20
+ it('maps Started status to inflight', () => {
21
+ expect(detectWorkType('Started', false)).toBe('inflight');
22
+ });
23
+ it('defaults unknown status to development', () => {
24
+ expect(detectWorkType('SomeUnknownStatus', false)).toBe('development');
25
+ });
26
+ });
27
+ describe('parent issues (isParent=true)', () => {
28
+ it('upgrades Backlog/development to coordination', () => {
29
+ expect(detectWorkType('Backlog', true)).toBe('coordination');
30
+ });
31
+ it('upgrades Finished/qa to qa-coordination', () => {
32
+ expect(detectWorkType('Finished', true)).toBe('qa-coordination');
33
+ });
34
+ it('upgrades Delivered/acceptance to acceptance-coordination', () => {
35
+ expect(detectWorkType('Delivered', true)).toBe('acceptance-coordination');
36
+ });
37
+ it('upgrades Rejected/refinement to refinement-coordination', () => {
38
+ expect(detectWorkType('Rejected', true)).toBe('refinement-coordination');
39
+ });
40
+ it('does not upgrade research (Icebox) — no coordination variant', () => {
41
+ expect(detectWorkType('Icebox', true)).toBe('research');
42
+ });
43
+ it('does not upgrade inflight (Started) — no coordination variant', () => {
44
+ expect(detectWorkType('Started', true)).toBe('inflight');
45
+ });
46
+ it('upgrades unknown status (defaults to development → coordination)', () => {
47
+ expect(detectWorkType('SomeUnknownStatus', true)).toBe('coordination');
48
+ });
49
+ });
50
+ describe('post-refinement rework scenario (SUP-1116 bug)', () => {
51
+ it('parent issue returning to Backlog after refinement gets coordination, not development', () => {
52
+ // This is the exact scenario that caused SUP-1116 to fail:
53
+ // After refinement-coordination completed, the parent moved to Backlog.
54
+ // The orchestrator's run() previously hardcoded 'development', which loaded
55
+ // the wrong template and the agent asked for human input instead of
56
+ // autonomously dispatching sub-agents.
57
+ const workType = detectWorkType('Backlog', true);
58
+ expect(workType).toBe('coordination');
59
+ expect(workType).not.toBe('development');
60
+ });
61
+ });
62
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=heartbeat-writer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeat-writer.test.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/heartbeat-writer.test.ts"],"names":[],"mappings":""}