@renseiai/agentfactory 0.8.0 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/config/index.d.ts +2 -2
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/index.js +1 -1
- package/dist/src/config/repository-config.d.ts +39 -1
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +57 -2
- package/dist/src/config/repository-config.test.js +213 -2
- package/dist/src/orchestrator/orchestrator.d.ts +1 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +20 -9
- package/dist/src/providers/claude-provider.d.ts.map +1 -1
- package/dist/src/providers/claude-provider.js +6 -0
- package/package.json +12 -22
|
@@ -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, getEffectiveAllowedProjects, getProjectConfig, getProjectPath } 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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AACzK,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA"}
|
package/dist/src/config/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { loadRepositoryConfig, RepositoryConfigSchema, getEffectiveAllowedProjects } from './repository-config.js';
|
|
1
|
+
export { loadRepositoryConfig, RepositoryConfigSchema, ProjectConfigSchema, getEffectiveAllowedProjects, getProjectConfig, getProjectPath } from './repository-config.js';
|
|
@@ -7,12 +7,39 @@
|
|
|
7
7
|
* - Project allowlisting for the orchestrator (allowedProjects field)
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
+
/** Per-project configuration (object form of projectPaths values) */
|
|
11
|
+
export declare const ProjectConfigSchema: z.ZodObject<{
|
|
12
|
+
path: z.ZodString;
|
|
13
|
+
packageManager: z.ZodOptional<z.ZodEnum<{
|
|
14
|
+
pnpm: "pnpm";
|
|
15
|
+
npm: "npm";
|
|
16
|
+
yarn: "yarn";
|
|
17
|
+
bun: "bun";
|
|
18
|
+
none: "none";
|
|
19
|
+
}>>;
|
|
20
|
+
buildCommand: z.ZodOptional<z.ZodString>;
|
|
21
|
+
testCommand: z.ZodOptional<z.ZodString>;
|
|
22
|
+
validateCommand: z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export type ProjectConfig = z.infer<typeof ProjectConfigSchema>;
|
|
10
25
|
export declare const RepositoryConfigSchema: z.ZodObject<{
|
|
11
26
|
apiVersion: z.ZodString;
|
|
12
27
|
kind: z.ZodLiteral<"RepositoryConfig">;
|
|
13
28
|
repository: z.ZodOptional<z.ZodString>;
|
|
14
29
|
allowedProjects: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
15
|
-
projectPaths: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString
|
|
30
|
+
projectPaths: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
31
|
+
path: z.ZodString;
|
|
32
|
+
packageManager: z.ZodOptional<z.ZodEnum<{
|
|
33
|
+
pnpm: "pnpm";
|
|
34
|
+
npm: "npm";
|
|
35
|
+
yarn: "yarn";
|
|
36
|
+
bun: "bun";
|
|
37
|
+
none: "none";
|
|
38
|
+
}>>;
|
|
39
|
+
buildCommand: z.ZodOptional<z.ZodString>;
|
|
40
|
+
testCommand: z.ZodOptional<z.ZodString>;
|
|
41
|
+
validateCommand: z.ZodOptional<z.ZodString>;
|
|
42
|
+
}, z.core.$strip>]>>>;
|
|
16
43
|
sharedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
17
44
|
linearCli: z.ZodOptional<z.ZodString>;
|
|
18
45
|
packageManager: z.ZodOptional<z.ZodEnum<{
|
|
@@ -33,6 +60,17 @@ export type RepositoryConfig = z.infer<typeof RepositoryConfigSchema>;
|
|
|
33
60
|
* Otherwise falls back to `allowedProjects`.
|
|
34
61
|
*/
|
|
35
62
|
export declare function getEffectiveAllowedProjects(config: RepositoryConfig): string[] | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Returns the normalized ProjectConfig for a given project name.
|
|
65
|
+
* Handles both string shorthand and object form of projectPaths values.
|
|
66
|
+
* Per-project overrides take precedence over repo-wide defaults.
|
|
67
|
+
*/
|
|
68
|
+
export declare function getProjectConfig(config: RepositoryConfig, projectName: string): ProjectConfig | null;
|
|
69
|
+
/**
|
|
70
|
+
* Returns just the path string for a given project name.
|
|
71
|
+
* Handles both string shorthand and object form.
|
|
72
|
+
*/
|
|
73
|
+
export declare function getProjectPath(config: RepositoryConfig, projectName: string): string | undefined;
|
|
36
74
|
/**
|
|
37
75
|
* Load and validate .agentfactory/config.yaml from the given git root.
|
|
38
76
|
*
|
|
@@ -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
|
|
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,qEAAqE;AACrE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAW9B,CAAA;AAEF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAQ/D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8ClC,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;AAMD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAQ7E"}
|
|
@@ -13,13 +13,35 @@ 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]);
|
|
16
34
|
export const RepositoryConfigSchema = z.object({
|
|
17
35
|
apiVersion: z.string(),
|
|
18
36
|
kind: z.literal('RepositoryConfig'),
|
|
19
37
|
repository: z.string().optional(),
|
|
20
38
|
allowedProjects: z.array(z.string()).optional(),
|
|
21
|
-
/**
|
|
22
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Maps Linear project names to their root directory or full config.
|
|
41
|
+
* String shorthand: { Family: 'apps/family' }
|
|
42
|
+
* Object form: { 'Family iOS': { path: 'apps/family-ios', packageManager: 'none', buildCommand: 'make build' } }
|
|
43
|
+
*/
|
|
44
|
+
projectPaths: z.record(z.string(), ProjectPathValueSchema).optional(),
|
|
23
45
|
/** Shared directories that any project's agent may modify (e.g., ['packages/ui']) */
|
|
24
46
|
sharedPaths: z.array(z.string()).optional(),
|
|
25
47
|
/**
|
|
@@ -67,6 +89,39 @@ export function getEffectiveAllowedProjects(config) {
|
|
|
67
89
|
}
|
|
68
90
|
return config.allowedProjects;
|
|
69
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Returns the normalized ProjectConfig for a given project name.
|
|
94
|
+
* Handles both string shorthand and object form of projectPaths values.
|
|
95
|
+
* Per-project overrides take precedence over repo-wide defaults.
|
|
96
|
+
*/
|
|
97
|
+
export function getProjectConfig(config, projectName) {
|
|
98
|
+
if (!config.projectPaths)
|
|
99
|
+
return null;
|
|
100
|
+
const value = config.projectPaths[projectName];
|
|
101
|
+
if (value === undefined)
|
|
102
|
+
return null;
|
|
103
|
+
// Normalize string shorthand to object form
|
|
104
|
+
const projectConfig = typeof value === 'string' ? { path: value } : value;
|
|
105
|
+
return {
|
|
106
|
+
path: projectConfig.path,
|
|
107
|
+
packageManager: projectConfig.packageManager ?? config.packageManager,
|
|
108
|
+
buildCommand: projectConfig.buildCommand ?? config.buildCommand,
|
|
109
|
+
testCommand: projectConfig.testCommand ?? config.testCommand,
|
|
110
|
+
validateCommand: projectConfig.validateCommand ?? config.validateCommand,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Returns just the path string for a given project name.
|
|
115
|
+
* Handles both string shorthand and object form.
|
|
116
|
+
*/
|
|
117
|
+
export function getProjectPath(config, projectName) {
|
|
118
|
+
if (!config.projectPaths)
|
|
119
|
+
return undefined;
|
|
120
|
+
const value = config.projectPaths[projectName];
|
|
121
|
+
if (value === undefined)
|
|
122
|
+
return undefined;
|
|
123
|
+
return typeof value === 'string' ? value : value.path;
|
|
124
|
+
}
|
|
70
125
|
// ---------------------------------------------------------------------------
|
|
71
126
|
// Loader
|
|
72
127
|
// ---------------------------------------------------------------------------
|
|
@@ -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 } 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,145 @@ 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
|
+
});
|
|
249
460
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA+BH,OAAO,EAML,KAAK,aAAa,EAQnB,MAAM,+BAA+B,CAAA;
|
|
1
|
+
{"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/orchestrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,7 +20,7 @@ import { createActivityEmitter } from './activity-emitter.js';
|
|
|
20
20
|
import { createApiActivityEmitter } from './api-activity-emitter.js';
|
|
21
21
|
import { createLogger } from '../logger.js';
|
|
22
22
|
import { TemplateRegistry, createToolPermissionAdapter } from '../templates/index.js';
|
|
23
|
-
import { loadRepositoryConfig } from '../config/index.js';
|
|
23
|
+
import { loadRepositoryConfig, getProjectConfig } from '../config/index.js';
|
|
24
24
|
import { ToolRegistry, linearPlugin } from '../tools/index.js';
|
|
25
25
|
// Default inactivity timeout: 5 minutes
|
|
26
26
|
const DEFAULT_INACTIVITY_TIMEOUT_MS = 300000;
|
|
@@ -695,6 +695,8 @@ export class AgentOrchestrator {
|
|
|
695
695
|
templateRegistry;
|
|
696
696
|
// Allowlisted project names from .agentfactory/config.yaml
|
|
697
697
|
allowedProjects;
|
|
698
|
+
// Full repository config from .agentfactory/config.yaml
|
|
699
|
+
repoConfig;
|
|
698
700
|
// Project-to-path mapping from .agentfactory/config.yaml (monorepo support)
|
|
699
701
|
projectPaths;
|
|
700
702
|
// Shared paths from .agentfactory/config.yaml (monorepo support)
|
|
@@ -772,6 +774,7 @@ export class AgentOrchestrator {
|
|
|
772
774
|
if (repoRoot) {
|
|
773
775
|
const repoConfig = loadRepositoryConfig(repoRoot);
|
|
774
776
|
if (repoConfig) {
|
|
777
|
+
this.repoConfig = repoConfig;
|
|
775
778
|
// Use repository from config as fallback if not set in OrchestratorConfig
|
|
776
779
|
if (!this.config.repository && repoConfig.repository) {
|
|
777
780
|
this.config.repository = repoConfig.repository;
|
|
@@ -779,21 +782,25 @@ export class AgentOrchestrator {
|
|
|
779
782
|
}
|
|
780
783
|
// Store allowedProjects for backlog filtering
|
|
781
784
|
if (repoConfig.projectPaths) {
|
|
782
|
-
|
|
785
|
+
// Resolve projectPaths to plain path strings (handles both string and object forms)
|
|
786
|
+
this.projectPaths = Object.fromEntries(Object.entries(repoConfig.projectPaths).map(([name, value]) => [
|
|
787
|
+
name,
|
|
788
|
+
typeof value === 'string' ? value : value.path,
|
|
789
|
+
]));
|
|
783
790
|
this.sharedPaths = repoConfig.sharedPaths;
|
|
784
791
|
this.allowedProjects = Object.keys(repoConfig.projectPaths);
|
|
785
792
|
}
|
|
786
793
|
else if (repoConfig.allowedProjects) {
|
|
787
794
|
this.allowedProjects = repoConfig.allowedProjects;
|
|
788
795
|
}
|
|
789
|
-
// Store non-Node project config
|
|
796
|
+
// Store non-Node project config (repo-wide defaults)
|
|
790
797
|
if (repoConfig.linearCli) {
|
|
791
798
|
this.linearCli = repoConfig.linearCli;
|
|
792
799
|
}
|
|
793
800
|
if (repoConfig.packageManager) {
|
|
794
801
|
this.packageManager = repoConfig.packageManager;
|
|
795
802
|
}
|
|
796
|
-
// Store configurable build/test/validate commands
|
|
803
|
+
// Store configurable build/test/validate commands (repo-wide defaults)
|
|
797
804
|
if (repoConfig.buildCommand) {
|
|
798
805
|
this.buildCommand = repoConfig.buildCommand;
|
|
799
806
|
}
|
|
@@ -1596,17 +1603,21 @@ ORCHESTRATOR_INSTALL=1 exec pnpm add "$@"
|
|
|
1596
1603
|
prompt = customPrompt;
|
|
1597
1604
|
}
|
|
1598
1605
|
else if (this.templateRegistry?.hasTemplate(workType)) {
|
|
1606
|
+
// Resolve per-project config overrides (falls back to repo-wide defaults)
|
|
1607
|
+
const perProject = projectName && this.repoConfig
|
|
1608
|
+
? getProjectConfig(this.repoConfig, projectName)
|
|
1609
|
+
: null;
|
|
1599
1610
|
const context = {
|
|
1600
1611
|
identifier,
|
|
1601
1612
|
repository: this.config.repository,
|
|
1602
|
-
projectPath: this.projectPaths?.[projectName ?? ''],
|
|
1613
|
+
projectPath: perProject?.path ?? this.projectPaths?.[projectName ?? ''],
|
|
1603
1614
|
sharedPaths: this.sharedPaths,
|
|
1604
1615
|
useToolPlugins: this.provider.name === 'claude',
|
|
1605
1616
|
linearCli: this.linearCli ?? 'pnpm af-linear',
|
|
1606
|
-
packageManager: this.packageManager ?? 'pnpm',
|
|
1607
|
-
buildCommand: this.buildCommand,
|
|
1608
|
-
testCommand: this.testCommand,
|
|
1609
|
-
validateCommand: this.validateCommand,
|
|
1617
|
+
packageManager: perProject?.packageManager ?? this.packageManager ?? 'pnpm',
|
|
1618
|
+
buildCommand: perProject?.buildCommand ?? this.buildCommand,
|
|
1619
|
+
testCommand: perProject?.testCommand ?? this.testCommand,
|
|
1620
|
+
validateCommand: perProject?.validateCommand ?? this.validateCommand,
|
|
1610
1621
|
};
|
|
1611
1622
|
const rendered = this.templateRegistry.renderPrompt(workType, context);
|
|
1612
1623
|
prompt = rendered ?? generatePromptForWorkType(identifier, workType);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-provider.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-provider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,WAAW,EAEZ,MAAM,YAAY,CAAA;AA0EnB,qBAAa,cAAe,YAAW,aAAa;IAClD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAS;IAEjC,KAAK,CAAC,MAAM,EAAE,gBAAgB,GAAG,WAAW;IAI5C,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,WAAW;IAIhE,OAAO,CAAC,YAAY;
|
|
1
|
+
{"version":3,"file":"claude-provider.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-provider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,WAAW,EAEZ,MAAM,YAAY,CAAA;AA0EnB,qBAAa,cAAe,YAAW,aAAa;IAClD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAS;IAEjC,KAAK,CAAC,MAAM,EAAE,gBAAgB,GAAG,WAAW;IAI5C,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,WAAW;IAIhE,OAAO,CAAC,YAAY;CA4KrB;AAsND;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,cAAc,CAErD"}
|
|
@@ -137,6 +137,12 @@ export class ClaudeProvider {
|
|
|
137
137
|
'Bash(cargo:*)',
|
|
138
138
|
'Bash(rustc:*)',
|
|
139
139
|
'Bash(go:*)',
|
|
140
|
+
// iOS/Apple development tools
|
|
141
|
+
'Bash(xcodebuild:*)',
|
|
142
|
+
'Bash(xcrun:*)',
|
|
143
|
+
'Bash(xcodegen:*)',
|
|
144
|
+
'Bash(swift:*)',
|
|
145
|
+
'Bash(swiftlint:*)',
|
|
140
146
|
'Bash(bash:*)',
|
|
141
147
|
'Bash(sh:*)',
|
|
142
148
|
'Bash(./:*)',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@renseiai/agentfactory",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-agent fleet management for coding agents — orchestrator, providers, crash recovery",
|
|
6
6
|
"author": "Rensei AI (https://rensei.ai)",
|
|
@@ -10,24 +10,15 @@
|
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/
|
|
13
|
+
"url": "git+https://github.com/RenseiAI/agentfactory.git",
|
|
14
14
|
"directory": "packages/core"
|
|
15
15
|
},
|
|
16
|
-
"homepage": "https://github.com/
|
|
16
|
+
"homepage": "https://github.com/RenseiAI/agentfactory/tree/main/packages/core",
|
|
17
17
|
"bugs": {
|
|
18
|
-
"url": "https://github.com/
|
|
18
|
+
"url": "https://github.com/RenseiAI/agentfactory/issues"
|
|
19
19
|
},
|
|
20
20
|
"publishConfig": {
|
|
21
|
-
"access": "public"
|
|
22
|
-
"exports": {
|
|
23
|
-
".": {
|
|
24
|
-
"types": "./dist/src/index.d.ts",
|
|
25
|
-
"import": "./dist/src/index.js",
|
|
26
|
-
"default": "./dist/src/index.js"
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
"main": "./dist/src/index.js",
|
|
30
|
-
"types": "./dist/src/index.d.ts"
|
|
21
|
+
"access": "public"
|
|
31
22
|
},
|
|
32
23
|
"keywords": [
|
|
33
24
|
"agent",
|
|
@@ -43,7 +34,7 @@
|
|
|
43
34
|
"types": "./dist/src/index.d.ts",
|
|
44
35
|
"exports": {
|
|
45
36
|
".": {
|
|
46
|
-
"types": "./src/index.ts",
|
|
37
|
+
"types": "./dist/src/index.d.ts",
|
|
47
38
|
"import": "./dist/src/index.js",
|
|
48
39
|
"default": "./dist/src/index.js"
|
|
49
40
|
}
|
|
@@ -55,24 +46,23 @@
|
|
|
55
46
|
],
|
|
56
47
|
"dependencies": {
|
|
57
48
|
"@anthropic-ai/claude-agent-sdk": "^0.2.7",
|
|
58
|
-
"@renseiai/agentfactory-linear": "workspace:*",
|
|
59
49
|
"dotenv": "^17.2.3",
|
|
60
50
|
"handlebars": "^4.7.8",
|
|
61
51
|
"yaml": "^2.8.2",
|
|
62
|
-
"zod": "^4.3.6"
|
|
52
|
+
"zod": "^4.3.6",
|
|
53
|
+
"@renseiai/agentfactory-linear": "0.8.3"
|
|
63
54
|
},
|
|
64
55
|
"devDependencies": {
|
|
65
|
-
"@renseiai/create-agentfactory-app": "workspace:*",
|
|
66
56
|
"@types/node": "^22.5.4",
|
|
67
57
|
"typescript": "^5.7.3",
|
|
68
|
-
"vitest": "^3.2.3"
|
|
58
|
+
"vitest": "^3.2.3",
|
|
59
|
+
"@renseiai/create-agentfactory-app": "0.8.3"
|
|
69
60
|
},
|
|
70
61
|
"scripts": {
|
|
71
62
|
"build": "tsc",
|
|
72
63
|
"typecheck": "tsc --noEmit",
|
|
73
64
|
"test": "vitest run --passWithNoTests",
|
|
74
65
|
"test:watch": "vitest",
|
|
75
|
-
"clean": "rm -rf dist"
|
|
76
|
-
"prepublishOnly": "pnpm clean && pnpm build"
|
|
66
|
+
"clean": "rm -rf dist"
|
|
77
67
|
}
|
|
78
|
-
}
|
|
68
|
+
}
|