@renseiai/agentfactory 0.8.18 → 0.8.20

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 (117) hide show
  1. package/dist/src/config/repository-config.d.ts +7 -0
  2. package/dist/src/config/repository-config.d.ts.map +1 -1
  3. package/dist/src/config/repository-config.js +15 -1
  4. package/dist/src/config/repository-config.test.js +1 -1
  5. package/dist/src/governor/decision-engine-adapter.d.ts +43 -0
  6. package/dist/src/governor/decision-engine-adapter.d.ts.map +1 -0
  7. package/dist/src/governor/decision-engine-adapter.js +417 -0
  8. package/dist/src/governor/decision-engine-adapter.test.d.ts +2 -0
  9. package/dist/src/governor/decision-engine-adapter.test.d.ts.map +1 -0
  10. package/dist/src/governor/decision-engine-adapter.test.js +362 -0
  11. package/dist/src/governor/decision-engine.js +3 -7
  12. package/dist/src/governor/decision-engine.test.js +5 -5
  13. package/dist/src/governor/index.d.ts +1 -0
  14. package/dist/src/governor/index.d.ts.map +1 -1
  15. package/dist/src/governor/index.js +1 -0
  16. package/dist/src/index.d.ts +1 -0
  17. package/dist/src/index.d.ts.map +1 -1
  18. package/dist/src/index.js +1 -0
  19. package/dist/src/manifest/route-manifest.d.ts.map +1 -1
  20. package/dist/src/manifest/route-manifest.js +4 -0
  21. package/dist/src/merge-queue/adapters/local.d.ts +68 -0
  22. package/dist/src/merge-queue/adapters/local.d.ts.map +1 -0
  23. package/dist/src/merge-queue/adapters/local.js +136 -0
  24. package/dist/src/merge-queue/adapters/local.test.d.ts +2 -0
  25. package/dist/src/merge-queue/adapters/local.test.d.ts.map +1 -0
  26. package/dist/src/merge-queue/adapters/local.test.js +176 -0
  27. package/dist/src/merge-queue/index.d.ts +13 -5
  28. package/dist/src/merge-queue/index.d.ts.map +1 -1
  29. package/dist/src/merge-queue/index.js +13 -6
  30. package/dist/src/merge-queue/merge-queue.integration.test.js +19 -0
  31. package/dist/src/merge-queue/merge-worker.d.ts.map +1 -1
  32. package/dist/src/merge-queue/merge-worker.js +29 -0
  33. package/dist/src/merge-queue/types.d.ts +1 -1
  34. package/dist/src/merge-queue/types.d.ts.map +1 -1
  35. package/dist/src/orchestrator/index.d.ts +4 -0
  36. package/dist/src/orchestrator/index.d.ts.map +1 -1
  37. package/dist/src/orchestrator/index.js +3 -0
  38. package/dist/src/orchestrator/orchestrator.d.ts +58 -0
  39. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  40. package/dist/src/orchestrator/orchestrator.js +552 -97
  41. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
  42. package/dist/src/orchestrator/parse-work-result.js +3 -1
  43. package/dist/src/orchestrator/parse-work-result.test.js +6 -0
  44. package/dist/src/orchestrator/quality-baseline.d.ts +83 -0
  45. package/dist/src/orchestrator/quality-baseline.d.ts.map +1 -0
  46. package/dist/src/orchestrator/quality-baseline.js +313 -0
  47. package/dist/src/orchestrator/quality-baseline.test.d.ts +2 -0
  48. package/dist/src/orchestrator/quality-baseline.test.d.ts.map +1 -0
  49. package/dist/src/orchestrator/quality-baseline.test.js +448 -0
  50. package/dist/src/orchestrator/quality-ratchet.d.ts +70 -0
  51. package/dist/src/orchestrator/quality-ratchet.d.ts.map +1 -0
  52. package/dist/src/orchestrator/quality-ratchet.js +162 -0
  53. package/dist/src/orchestrator/quality-ratchet.test.d.ts +2 -0
  54. package/dist/src/orchestrator/quality-ratchet.test.d.ts.map +1 -0
  55. package/dist/src/orchestrator/quality-ratchet.test.js +335 -0
  56. package/dist/src/orchestrator/types.d.ts +2 -0
  57. package/dist/src/orchestrator/types.d.ts.map +1 -1
  58. package/dist/src/providers/claude-provider.d.ts.map +1 -1
  59. package/dist/src/providers/claude-provider.js +11 -0
  60. package/dist/src/providers/codex-app-server-provider.d.ts +237 -0
  61. package/dist/src/providers/codex-app-server-provider.d.ts.map +1 -0
  62. package/dist/src/providers/codex-app-server-provider.js +1041 -0
  63. package/dist/src/providers/codex-app-server-provider.test.d.ts +2 -0
  64. package/dist/src/providers/codex-app-server-provider.test.d.ts.map +1 -0
  65. package/dist/src/providers/codex-app-server-provider.test.js +589 -0
  66. package/dist/src/providers/codex-approval-bridge.d.ts +49 -0
  67. package/dist/src/providers/codex-approval-bridge.d.ts.map +1 -0
  68. package/dist/src/providers/codex-approval-bridge.js +117 -0
  69. package/dist/src/providers/codex-approval-bridge.test.d.ts +2 -0
  70. package/dist/src/providers/codex-approval-bridge.test.d.ts.map +1 -0
  71. package/dist/src/providers/codex-approval-bridge.test.js +188 -0
  72. package/dist/src/providers/codex-provider.d.ts +24 -4
  73. package/dist/src/providers/codex-provider.d.ts.map +1 -1
  74. package/dist/src/providers/codex-provider.js +58 -6
  75. package/dist/src/providers/index.d.ts +1 -0
  76. package/dist/src/providers/index.d.ts.map +1 -1
  77. package/dist/src/providers/index.js +1 -0
  78. package/dist/src/providers/types.d.ts +25 -0
  79. package/dist/src/providers/types.d.ts.map +1 -1
  80. package/dist/src/routing/observation-recorder.test.js +1 -1
  81. package/dist/src/routing/observation-store.d.ts +15 -1
  82. package/dist/src/routing/observation-store.d.ts.map +1 -1
  83. package/dist/src/routing/observation-store.test.js +17 -11
  84. package/dist/src/routing/types.d.ts +1 -1
  85. package/dist/src/templates/adapters.d.ts +25 -0
  86. package/dist/src/templates/adapters.d.ts.map +1 -1
  87. package/dist/src/templates/adapters.js +70 -0
  88. package/dist/src/templates/adapters.test.js +49 -0
  89. package/dist/src/templates/index.d.ts +3 -1
  90. package/dist/src/templates/index.d.ts.map +1 -1
  91. package/dist/src/templates/index.js +1 -0
  92. package/dist/src/templates/registry.d.ts +31 -0
  93. package/dist/src/templates/registry.d.ts.map +1 -1
  94. package/dist/src/templates/registry.js +91 -0
  95. package/dist/src/templates/schema.d.ts +31 -0
  96. package/dist/src/templates/schema.d.ts.map +1 -0
  97. package/dist/src/templates/schema.js +139 -0
  98. package/dist/src/templates/schema.test.d.ts +2 -0
  99. package/dist/src/templates/schema.test.d.ts.map +1 -0
  100. package/dist/src/templates/schema.test.js +215 -0
  101. package/dist/src/templates/types.d.ts +22 -0
  102. package/dist/src/templates/types.d.ts.map +1 -1
  103. package/dist/src/templates/types.js +12 -0
  104. package/dist/src/tools/index.d.ts +2 -0
  105. package/dist/src/tools/index.d.ts.map +1 -1
  106. package/dist/src/tools/index.js +1 -0
  107. package/dist/src/tools/registry.d.ts +9 -1
  108. package/dist/src/tools/registry.d.ts.map +1 -1
  109. package/dist/src/tools/registry.js +13 -1
  110. package/dist/src/tools/stdio-server-entry.d.ts +25 -0
  111. package/dist/src/tools/stdio-server-entry.d.ts.map +1 -0
  112. package/dist/src/tools/stdio-server-entry.js +205 -0
  113. package/dist/src/tools/stdio-server.d.ts +87 -0
  114. package/dist/src/tools/stdio-server.d.ts.map +1 -0
  115. package/dist/src/tools/stdio-server.js +138 -0
  116. package/dist/src/workflow/workflow-types.d.ts +3 -3
  117. package/package.json +3 -2
@@ -41,10 +41,16 @@ function createInMemoryStore() {
41
41
  if (opts.since) {
42
42
  result = result.filter((o) => o.timestamp >= opts.since);
43
43
  }
44
+ if (opts.from) {
45
+ result = result.filter((o) => o.timestamp >= opts.from);
46
+ }
47
+ if (opts.to) {
48
+ result = result.filter((o) => o.timestamp <= opts.to);
49
+ }
44
50
  if (opts.limit) {
45
51
  result = result.slice(0, opts.limit);
46
52
  }
47
- return result;
53
+ return { observations: result };
48
54
  },
49
55
  async getRecentObservations(provider, workType, windowSize) {
50
56
  return observations
@@ -67,8 +73,8 @@ describe('ObservationStore interface', () => {
67
73
  const obs = makeObservation();
68
74
  await store.recordObservation(obs);
69
75
  const all = await store.getObservations({});
70
- expect(all).toHaveLength(1);
71
- expect(all[0]).toEqual(obs);
76
+ expect(all.observations).toHaveLength(1);
77
+ expect(all.observations[0]).toEqual(obs);
72
78
  });
73
79
  it('getObservations filters by provider', async () => {
74
80
  const store = createInMemoryStore();
@@ -76,16 +82,16 @@ describe('ObservationStore interface', () => {
76
82
  await store.recordObservation(makeObservation({ provider: 'codex' }));
77
83
  await store.recordObservation(makeObservation({ provider: 'claude' }));
78
84
  const result = await store.getObservations({ provider: 'claude' });
79
- expect(result).toHaveLength(2);
80
- expect(result.every((o) => o.provider === 'claude')).toBe(true);
85
+ expect(result.observations).toHaveLength(2);
86
+ expect(result.observations.every((o) => o.provider === 'claude')).toBe(true);
81
87
  });
82
88
  it('getObservations filters by workType', async () => {
83
89
  const store = createInMemoryStore();
84
90
  await store.recordObservation(makeObservation({ workType: 'development' }));
85
91
  await store.recordObservation(makeObservation({ workType: 'qa' }));
86
92
  const result = await store.getObservations({ workType: 'qa' });
87
- expect(result).toHaveLength(1);
88
- expect(result[0].workType).toBe('qa');
93
+ expect(result.observations).toHaveLength(1);
94
+ expect(result.observations[0].workType).toBe('qa');
89
95
  });
90
96
  it('getObservations filters by since', async () => {
91
97
  const store = createInMemoryStore();
@@ -93,8 +99,8 @@ describe('ObservationStore interface', () => {
93
99
  await store.recordObservation(makeObservation({ timestamp: 2000 }));
94
100
  await store.recordObservation(makeObservation({ timestamp: 3000 }));
95
101
  const result = await store.getObservations({ since: 2000 });
96
- expect(result).toHaveLength(2);
97
- expect(result.every((o) => o.timestamp >= 2000)).toBe(true);
102
+ expect(result.observations).toHaveLength(2);
103
+ expect(result.observations.every((o) => o.timestamp >= 2000)).toBe(true);
98
104
  });
99
105
  it('getObservations respects limit', async () => {
100
106
  const store = createInMemoryStore();
@@ -102,7 +108,7 @@ describe('ObservationStore interface', () => {
102
108
  await store.recordObservation(makeObservation({ timestamp: i }));
103
109
  }
104
110
  const result = await store.getObservations({ limit: 3 });
105
- expect(result).toHaveLength(3);
111
+ expect(result.observations).toHaveLength(3);
106
112
  });
107
113
  it('getRecentObservations returns newest-first for a provider+workType pair', async () => {
108
114
  const store = createInMemoryStore();
@@ -128,7 +134,7 @@ describe('ObservationStore interface', () => {
128
134
  const store = createInMemoryStore();
129
135
  await store.recordObservation(makeObservation({ provider: 'claude' }));
130
136
  const result = await store.getObservations({ provider: 'codex' });
131
- expect(result).toEqual([]);
137
+ expect(result.observations).toEqual([]);
132
138
  });
133
139
  it('getRecentObservations returns empty array when no observations match', async () => {
134
140
  const store = createInMemoryStore();
@@ -49,8 +49,8 @@ export declare const RoutingObservationSchema: z.ZodObject<{
49
49
  prCreated: z.ZodBoolean;
50
50
  qaResult: z.ZodEnum<{
51
51
  failed: "failed";
52
- passed: "passed";
53
52
  unknown: "unknown";
53
+ passed: "passed";
54
54
  }>;
55
55
  totalCostUsd: z.ZodNumber;
56
56
  wallClockMs: z.ZodNumber;
@@ -5,6 +5,23 @@
5
5
  */
6
6
  import type { ToolPermission, ToolPermissionAdapter } from './types.js';
7
7
  import type { AgentProviderName } from '../providers/types.js';
8
+ /**
9
+ * Structured permission config consumed by the Codex approval bridge.
10
+ * Produced by `CodexToolPermissionAdapter.buildPermissionConfig()`.
11
+ */
12
+ export interface CodexPermissionConfig {
13
+ /** Allowed command patterns from `tools.allow` */
14
+ allowedCommandPatterns: RegExp[];
15
+ /** Denied command patterns from `tools.disallow` (additive to safety defaults) */
16
+ deniedCommandPatterns: Array<{
17
+ pattern: RegExp;
18
+ reason: string;
19
+ }>;
20
+ /** Whether file edits are allowed (default: true) */
21
+ allowFileEdits: boolean;
22
+ /** Whether file writes are allowed (default: true) */
23
+ allowFileWrites: boolean;
24
+ }
8
25
  /**
9
26
  * Claude Code tool permission adapter.
10
27
  *
@@ -30,6 +47,14 @@ export declare class ClaudeToolPermissionAdapter implements ToolPermissionAdapte
30
47
  */
31
48
  export declare class CodexToolPermissionAdapter implements ToolPermissionAdapter {
32
49
  translatePermissions(permissions: ToolPermission[]): string[];
50
+ /**
51
+ * Build a structured permission config for the Codex approval bridge (SUP-1748).
52
+ *
53
+ * Translates abstract `tools.allow` and `tools.disallow` from templates
54
+ * into regex patterns consumed by `evaluateCommandApproval()` and
55
+ * `evaluateFileChangeApproval()` in the approval bridge.
56
+ */
57
+ buildPermissionConfig(allow: ToolPermission[], disallow: ToolPermission[]): CodexPermissionConfig;
33
58
  private translateOne;
34
59
  }
35
60
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../../../src/templates/adapters.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAE9D;;;;;;;GAOG;AACH,qBAAa,2BAA4B,YAAW,qBAAqB;IACvE,oBAAoB,CAAC,WAAW,EAAE,cAAc,EAAE,GAAG,MAAM,EAAE;IAI7D,OAAO,CAAC,YAAY;CAwBrB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,0BAA2B,YAAW,qBAAqB;IACtE,oBAAoB,CAAC,WAAW,EAAE,cAAc,EAAE,GAAG,MAAM,EAAE;IAI7D,OAAO,CAAC,YAAY;CAWrB;AAED;;;;;;;GAOG;AACH,qBAAa,6BAA8B,YAAW,qBAAqB;IACzE,oBAAoB,CAAC,WAAW,EAAE,cAAc,EAAE,GAAG,MAAM,EAAE;IAI7D,OAAO,CAAC,YAAY;CAWrB;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,iBAAiB,GAAG,qBAAqB,CAa9F"}
1
+ {"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../../../src/templates/adapters.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAM9D;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,kDAAkD;IAClD,sBAAsB,EAAE,MAAM,EAAE,CAAA;IAChC,kFAAkF;IAClF,qBAAqB,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACjE,qDAAqD;IACrD,cAAc,EAAE,OAAO,CAAA;IACvB,sDAAsD;IACtD,eAAe,EAAE,OAAO,CAAA;CACzB;AAED;;;;;;;GAOG;AACH,qBAAa,2BAA4B,YAAW,qBAAqB;IACvE,oBAAoB,CAAC,WAAW,EAAE,cAAc,EAAE,GAAG,MAAM,EAAE;IAI7D,OAAO,CAAC,YAAY;CAwBrB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,0BAA2B,YAAW,qBAAqB;IACtE,oBAAoB,CAAC,WAAW,EAAE,cAAc,EAAE,GAAG,MAAM,EAAE;IAI7D;;;;;;OAMG;IACH,qBAAqB,CACnB,KAAK,EAAE,cAAc,EAAE,EACvB,QAAQ,EAAE,cAAc,EAAE,GACzB,qBAAqB;IAsCxB,OAAO,CAAC,YAAY;CAWrB;AAkCD;;;;;;;GAOG;AACH,qBAAa,6BAA8B,YAAW,qBAAqB;IACzE,oBAAoB,CAAC,WAAW,EAAE,cAAc,EAAE,GAAG,MAAM,EAAE;IAI7D,OAAO,CAAC,YAAY;CAWrB;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,iBAAiB,GAAG,qBAAqB,CAa9F"}
@@ -53,6 +53,49 @@ export class CodexToolPermissionAdapter {
53
53
  translatePermissions(permissions) {
54
54
  return permissions.map(p => this.translateOne(p));
55
55
  }
56
+ /**
57
+ * Build a structured permission config for the Codex approval bridge (SUP-1748).
58
+ *
59
+ * Translates abstract `tools.allow` and `tools.disallow` from templates
60
+ * into regex patterns consumed by `evaluateCommandApproval()` and
61
+ * `evaluateFileChangeApproval()` in the approval bridge.
62
+ */
63
+ buildPermissionConfig(allow, disallow) {
64
+ const allowedCommandPatterns = [];
65
+ const deniedCommandPatterns = [];
66
+ let allowFileEdits = true;
67
+ let allowFileWrites = true;
68
+ // Process allow list → allowed command patterns
69
+ for (const permission of allow) {
70
+ if (typeof permission !== 'string' && 'shell' in permission) {
71
+ allowedCommandPatterns.push(shellGlobToRegex(permission.shell));
72
+ }
73
+ }
74
+ // Process disallow list → denied patterns + file permission flags
75
+ for (const permission of disallow) {
76
+ if (typeof permission === 'string') {
77
+ if (permission === 'file-edit') {
78
+ allowFileEdits = false;
79
+ }
80
+ else if (permission === 'file-write') {
81
+ allowFileWrites = false;
82
+ }
83
+ // 'user-input' is a no-op for Codex (non-interactive)
84
+ }
85
+ else if ('shell' in permission) {
86
+ deniedCommandPatterns.push({
87
+ pattern: shellGlobToRegex(permission.shell),
88
+ reason: `${permission.shell} blocked by template`,
89
+ });
90
+ }
91
+ }
92
+ return {
93
+ allowedCommandPatterns,
94
+ deniedCommandPatterns,
95
+ allowFileEdits,
96
+ allowFileWrites,
97
+ };
98
+ }
56
99
  translateOne(permission) {
57
100
  if (typeof permission === 'string') {
58
101
  return permission;
@@ -63,6 +106,33 @@ export class CodexToolPermissionAdapter {
63
106
  return String(permission);
64
107
  }
65
108
  }
109
+ /**
110
+ * Convert a shell glob pattern (e.g., "pnpm *", "git commit *") to a regex.
111
+ *
112
+ * The pattern "pnpm *" matches any command starting with "pnpm".
113
+ * The pattern "git commit *" matches "git commit" followed by anything.
114
+ */
115
+ function shellGlobToRegex(glob) {
116
+ // Split on the last space to separate command from glob
117
+ const lastSpace = glob.lastIndexOf(' ');
118
+ if (lastSpace === -1) {
119
+ // No glob part — match command prefix (e.g., "pnpm" matches "pnpm install")
120
+ return new RegExp(`^${escapeRegex(glob)}\\b`);
121
+ }
122
+ const command = glob.substring(0, lastSpace);
123
+ const globPart = glob.substring(lastSpace + 1);
124
+ if (globPart === '*') {
125
+ // "git commit *" → matches "git commit" followed by anything
126
+ return new RegExp(`^${escapeRegex(command)}\\b`);
127
+ }
128
+ // More specific globs: escape and convert * to .*
129
+ const escapedCommand = escapeRegex(command);
130
+ const regexGlob = escapeRegex(globPart).replace(/\\\*/g, '.*');
131
+ return new RegExp(`^${escapedCommand}\\s+${regexGlob}`);
132
+ }
133
+ function escapeRegex(str) {
134
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
135
+ }
66
136
  /**
67
137
  * Spring AI tool permission adapter.
68
138
  *
@@ -77,6 +77,55 @@ describe('CodexToolPermissionAdapter', () => {
77
77
  const result = adapter.translatePermissions([]);
78
78
  expect(result).toEqual([]);
79
79
  });
80
+ describe('buildPermissionConfig (SUP-1748)', () => {
81
+ it('builds permission config from allow and disallow lists', () => {
82
+ const config = adapter.buildPermissionConfig([{ shell: 'pnpm *' }, { shell: 'git commit *' }], [{ shell: 'git checkout *' }, 'user-input']);
83
+ expect(config.allowedCommandPatterns).toHaveLength(2);
84
+ expect(config.deniedCommandPatterns).toHaveLength(1);
85
+ expect(config.allowFileEdits).toBe(true);
86
+ expect(config.allowFileWrites).toBe(true);
87
+ });
88
+ it('allowed patterns match expected commands', () => {
89
+ const config = adapter.buildPermissionConfig([{ shell: 'pnpm *' }], []);
90
+ expect(config.allowedCommandPatterns[0].test('pnpm install')).toBe(true);
91
+ expect(config.allowedCommandPatterns[0].test('pnpm run build')).toBe(true);
92
+ expect(config.allowedCommandPatterns[0].test('npm install')).toBe(false);
93
+ });
94
+ it('denied patterns match expected commands', () => {
95
+ const config = adapter.buildPermissionConfig([], [{ shell: 'git *' }]);
96
+ expect(config.deniedCommandPatterns[0].pattern.test('git push')).toBe(true);
97
+ expect(config.deniedCommandPatterns[0].pattern.test('git commit')).toBe(true);
98
+ expect(config.deniedCommandPatterns[0].reason).toContain('blocked by template');
99
+ });
100
+ it('sets allowFileEdits to false when file-edit is in disallow', () => {
101
+ const config = adapter.buildPermissionConfig([], ['file-edit']);
102
+ expect(config.allowFileEdits).toBe(false);
103
+ expect(config.allowFileWrites).toBe(true);
104
+ });
105
+ it('sets allowFileWrites to false when file-write is in disallow', () => {
106
+ const config = adapter.buildPermissionConfig([], ['file-write']);
107
+ expect(config.allowFileEdits).toBe(true);
108
+ expect(config.allowFileWrites).toBe(false);
109
+ });
110
+ it('ignores user-input in disallow (non-interactive)', () => {
111
+ const config = adapter.buildPermissionConfig([], ['user-input']);
112
+ expect(config.deniedCommandPatterns).toHaveLength(0);
113
+ expect(config.allowFileEdits).toBe(true);
114
+ expect(config.allowFileWrites).toBe(true);
115
+ });
116
+ it('handles empty allow and disallow lists', () => {
117
+ const config = adapter.buildPermissionConfig([], []);
118
+ expect(config.allowedCommandPatterns).toHaveLength(0);
119
+ expect(config.deniedCommandPatterns).toHaveLength(0);
120
+ expect(config.allowFileEdits).toBe(true);
121
+ expect(config.allowFileWrites).toBe(true);
122
+ });
123
+ it('multi-word commands create correct regex', () => {
124
+ const config = adapter.buildPermissionConfig([{ shell: 'git commit *' }], []);
125
+ expect(config.allowedCommandPatterns[0].test('git commit -m "message"')).toBe(true);
126
+ expect(config.allowedCommandPatterns[0].test('git push origin main')).toBe(false);
127
+ });
128
+ });
80
129
  });
81
130
  describe('SpringAiToolPermissionAdapter', () => {
82
131
  const adapter = new SpringAiToolPermissionAdapter();
@@ -5,9 +5,11 @@
5
5
  */
6
6
  export type { WorkflowTemplate, PartialTemplate, TemplateContext, ToolPermission, ToolPermissionAdapter, TemplateRegistryConfig, } from './types.js';
7
7
  export { WorkflowTemplateSchema, PartialTemplateSchema, TemplateContextSchema, AgentWorkTypeSchema, ToolPermissionSchema, validateWorkflowTemplate, validatePartialTemplate, } from './types.js';
8
- export { TemplateRegistry } from './registry.js';
8
+ export { TemplateRegistry, type TemplateValidationResult } from './registry.js';
9
+ export { generateTemplateSchema, extractTemplateVariables, type TemplateSchemaOptions } from './schema.js';
9
10
  export { loadTemplatesFromDir, loadTemplateFile, loadPartialsFromDir, getBuiltinDefaultsDir, getBuiltinPartialsDir, } from './loader.js';
10
11
  export { ClaudeToolPermissionAdapter, CodexToolPermissionAdapter, createToolPermissionAdapter } from './adapters.js';
12
+ export type { CodexPermissionConfig } from './adapters.js';
11
13
  export { renderPromptWithFallback } from './renderer.js';
12
14
  export type { AgentDefinition, AgentDefinitionFrontmatter } from './agent-definition.js';
13
15
  export { parseAgentDefinition, parseAgentDefinitionFile, AgentDefinitionFrontmatterSchema } from './agent-definition.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,cAAc,EACd,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAEhD,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAA;AAEpH,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAA;AAExD,YAAY,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAA;AACxF,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,gCAAgC,EAAE,MAAM,uBAAuB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,cAAc,EACd,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,gBAAgB,EAAE,KAAK,wBAAwB,EAAE,MAAM,eAAe,CAAA;AAE/E,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAE1G,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAA;AACpH,YAAY,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAA;AAExD,YAAY,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAA;AACxF,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,gCAAgC,EAAE,MAAM,uBAAuB,CAAA"}
@@ -5,6 +5,7 @@
5
5
  */
6
6
  export { WorkflowTemplateSchema, PartialTemplateSchema, TemplateContextSchema, AgentWorkTypeSchema, ToolPermissionSchema, validateWorkflowTemplate, validatePartialTemplate, } from './types.js';
7
7
  export { TemplateRegistry } from './registry.js';
8
+ export { generateTemplateSchema, extractTemplateVariables } from './schema.js';
8
9
  export { loadTemplatesFromDir, loadTemplateFile, loadPartialsFromDir, getBuiltinDefaultsDir, getBuiltinPartialsDir, } from './loader.js';
9
10
  export { ClaudeToolPermissionAdapter, CodexToolPermissionAdapter, createToolPermissionAdapter } from './adapters.js';
10
11
  export { renderPromptWithFallback } from './renderer.js';
@@ -5,8 +5,10 @@
5
5
  * Supports layered resolution with built-in defaults, project overrides,
6
6
  * and inline config overrides.
7
7
  */
8
+ import type { JSONSchema7 } from 'json-schema';
8
9
  import type { AgentWorkType } from '../orchestrator/work-types.js';
9
10
  import type { WorkflowTemplate, TemplateContext, TemplateRegistryConfig, ToolPermission, ToolPermissionAdapter } from './types.js';
11
+ import { type TemplateSchemaOptions } from './schema.js';
10
12
  /**
11
13
  * Template Registry manages workflow templates and renders prompts.
12
14
  *
@@ -72,9 +74,38 @@ export declare class TemplateRegistry {
72
74
  * Get disallowed tools for a work type (optionally with strategy).
73
75
  */
74
76
  getDisallowedTools(workType: AgentWorkType, strategy?: string): ToolPermission[] | undefined;
77
+ /**
78
+ * Get the raw allow and disallow permission arrays for a work type (SUP-1748).
79
+ * Used by the orchestrator to build Codex permission configs via the adapter.
80
+ */
81
+ getRawToolPermissions(workType: AgentWorkType, strategy?: string): {
82
+ allow: ToolPermission[];
83
+ disallow: ToolPermission[];
84
+ };
75
85
  /**
76
86
  * Register a partial template for use in Handlebars rendering.
77
87
  */
78
88
  registerPartial(name: string, content: string): void;
89
+ /**
90
+ * Get JSON Schema 7 for a template's parameters.
91
+ *
92
+ * The schema is derived from the template's prompt by extracting
93
+ * Handlebars expression references and mapping them to the known
94
+ * TemplateContext fields. Returns null if no template is registered.
95
+ */
96
+ getSchema(templateName: string, options?: TemplateSchemaOptions): JSONSchema7 | null;
97
+ /**
98
+ * Validate a config object against a template's JSON Schema.
99
+ *
100
+ * Template expressions ({{ }}) in string values are treated as valid
101
+ * placeholders and are not validated against the schema type.
102
+ *
103
+ * Returns a validation result with `valid` boolean and any `errors`.
104
+ */
105
+ validateConfig(templateName: string, config: Record<string, unknown>): TemplateValidationResult;
106
+ }
107
+ export interface TemplateValidationResult {
108
+ valid: boolean;
109
+ errors: string[];
79
110
  }
80
111
  //# sourceMappingURL=registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/templates/registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAClE,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,qBAAqB,EACtB,MAAM,YAAY,CAAA;AAQnB;;;;;;GAMG;AACH,qBAAa,gBAAgB;IAC3B;;;OAGG;IACH,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,qBAAqB,CAAC,CAAuB;;IAcrD;;OAEG;IACH,MAAM,CAAC,MAAM,CAAC,MAAM,GAAE,sBAA2B,GAAG,gBAAgB;IAMpE;;;;;;OAMG;IACH,UAAU,CAAC,MAAM,GAAE,sBAA2B,GAAG,IAAI;IA4CrD;;OAEG;IACH,wBAAwB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAI9D;;;;;;;;OAQG;IACH,WAAW,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IASrF;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO;IAQhE;;OAEG;IACH,sBAAsB,IAAI,MAAM,EAAE;IAIlC;;;;OAIG;IACH,YAAY,CAAC,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAkBjG;;;OAGG;IACH,kBAAkB,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS;IAgBpF;;OAEG;IACH,kBAAkB,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,GAAG,SAAS;IAK5F;;OAEG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;CAGrD"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/templates/registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAClE,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,qBAAqB,EACtB,MAAM,YAAY,CAAA;AAOnB,OAAO,EAA0B,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAEhF;;;;;;GAMG;AACH,qBAAa,gBAAgB;IAC3B;;;OAGG;IACH,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,qBAAqB,CAAC,CAAuB;;IAcrD;;OAEG;IACH,MAAM,CAAC,MAAM,CAAC,MAAM,GAAE,sBAA2B,GAAG,gBAAgB;IAMpE;;;;;;OAMG;IACH,UAAU,CAAC,MAAM,GAAE,sBAA2B,GAAG,IAAI;IA4CrD;;OAEG;IACH,wBAAwB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAI9D;;;;;;;;OAQG;IACH,WAAW,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IASrF;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO;IAQhE;;OAEG;IACH,sBAAsB,IAAI,MAAM,EAAE;IAIlC;;;;OAIG;IACH,YAAY,CAAC,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAkBjG;;;OAGG;IACH,kBAAkB,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS;IAgBpF;;OAEG;IACH,kBAAkB,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,GAAG,SAAS;IAK5F;;;OAGG;IACH,qBAAqB,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG;QACjE,KAAK,EAAE,cAAc,EAAE,CAAA;QACvB,QAAQ,EAAE,cAAc,EAAE,CAAA;KAC3B;IAQD;;OAEG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAIpD;;;;;;OAMG;IACH,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,WAAW,GAAG,IAAI;IAMpF;;;;;;;OAOG;IACH,cAAc,CACZ,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,wBAAwB;CAS5B;AAMD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,OAAO,CAAA;IACd,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB"}
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import Handlebars from 'handlebars';
9
9
  import { loadTemplatesFromDir, loadPartialsFromDir, getBuiltinDefaultsDir, getBuiltinPartialsDir, } from './loader.js';
10
+ import { generateTemplateSchema } from './schema.js';
10
11
  /**
11
12
  * Template Registry manages workflow templates and renders prompts.
12
13
  *
@@ -168,10 +169,100 @@ export class TemplateRegistry {
168
169
  const template = this.getTemplate(workType, strategy);
169
170
  return template?.tools?.disallow;
170
171
  }
172
+ /**
173
+ * Get the raw allow and disallow permission arrays for a work type (SUP-1748).
174
+ * Used by the orchestrator to build Codex permission configs via the adapter.
175
+ */
176
+ getRawToolPermissions(workType, strategy) {
177
+ const template = this.getTemplate(workType, strategy);
178
+ return {
179
+ allow: template?.tools?.allow ?? [],
180
+ disallow: template?.tools?.disallow ?? [],
181
+ };
182
+ }
171
183
  /**
172
184
  * Register a partial template for use in Handlebars rendering.
173
185
  */
174
186
  registerPartial(name, content) {
175
187
  this.handlebars.registerPartial(name, content);
176
188
  }
189
+ /**
190
+ * Get JSON Schema 7 for a template's parameters.
191
+ *
192
+ * The schema is derived from the template's prompt by extracting
193
+ * Handlebars expression references and mapping them to the known
194
+ * TemplateContext fields. Returns null if no template is registered.
195
+ */
196
+ getSchema(templateName, options) {
197
+ const template = this.templates.get(templateName);
198
+ if (!template)
199
+ return null;
200
+ return generateTemplateSchema(template, options);
201
+ }
202
+ /**
203
+ * Validate a config object against a template's JSON Schema.
204
+ *
205
+ * Template expressions ({{ }}) in string values are treated as valid
206
+ * placeholders and are not validated against the schema type.
207
+ *
208
+ * Returns a validation result with `valid` boolean and any `errors`.
209
+ */
210
+ validateConfig(templateName, config) {
211
+ const template = this.templates.get(templateName);
212
+ if (!template) {
213
+ return { valid: false, errors: [`Template "${templateName}" not found`] };
214
+ }
215
+ const schema = generateTemplateSchema(template);
216
+ return validateConfigAgainstSchema(config, schema);
217
+ }
218
+ }
219
+ /** Handlebars expression pattern: {{ ... }} */
220
+ const TEMPLATE_EXPRESSION_RE = /^\{\{.*\}\}$/s;
221
+ /**
222
+ * Validate a config object against a JSON Schema 7.
223
+ * Template expressions ({{ }}) are treated as valid for any field.
224
+ */
225
+ function validateConfigAgainstSchema(config, schema) {
226
+ const errors = [];
227
+ const properties = schema.properties ?? {};
228
+ const required = schema.required ?? [];
229
+ // Check required fields
230
+ for (const field of required) {
231
+ if (!(field in config)) {
232
+ errors.push(`Missing required field: "${field}"`);
233
+ }
234
+ }
235
+ // Type-check provided fields
236
+ for (const [key, value] of Object.entries(config)) {
237
+ const fieldSchema = properties[key];
238
+ if (!fieldSchema || typeof fieldSchema === 'boolean')
239
+ continue;
240
+ // Template expressions are always valid
241
+ if (typeof value === 'string' && TEMPLATE_EXPRESSION_RE.test(value.trim())) {
242
+ continue;
243
+ }
244
+ const typeError = checkType(key, value, fieldSchema);
245
+ if (typeError)
246
+ errors.push(typeError);
247
+ }
248
+ return { valid: errors.length === 0, errors };
249
+ }
250
+ function checkType(key, value, schema) {
251
+ const schemaType = schema.type;
252
+ if (!schemaType)
253
+ return null;
254
+ // Normalize to array of type strings
255
+ const types = Array.isArray(schemaType) ? schemaType : [schemaType];
256
+ const valueType = Array.isArray(value) ? 'array' : typeof value;
257
+ // null is a valid JSON Schema type
258
+ if (value === null && types.includes('null'))
259
+ return null;
260
+ if (value === null)
261
+ return `Field "${key}" is null but expected ${types.join(' | ')}`;
262
+ // 'integer' matches typeof 'number' in JS
263
+ const effectiveTypes = types.map(t => t === 'integer' ? 'number' : t);
264
+ if (!effectiveTypes.includes(valueType)) {
265
+ return `Field "${key}" has type "${valueType}" but expected ${types.join(' | ')}`;
266
+ }
267
+ return null;
177
268
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Template JSON Schema Generation
3
+ *
4
+ * Generates JSON Schema 7 definitions from WorkflowTemplate objects.
5
+ * Schema is derived from TemplateContext fields referenced in the template's
6
+ * prompt via Handlebars expressions.
7
+ *
8
+ * @see SUP-1758
9
+ */
10
+ import type { JSONSchema7 } from 'json-schema';
11
+ import type { WorkflowTemplate } from './types.js';
12
+ export interface TemplateSchemaOptions {
13
+ /** Include all TemplateContext fields, not just those referenced in the prompt */
14
+ includeAllFields?: boolean;
15
+ }
16
+ /**
17
+ * Extract Handlebars variable references from a template prompt.
18
+ * Matches {{ varName }}, {{ varName.property }}, and handles
19
+ * conditionals like {{#if varName}} and {{#unless varName}}.
20
+ *
21
+ * Returns the set of top-level variable names referenced.
22
+ */
23
+ export declare function extractTemplateVariables(prompt: string): Set<string>;
24
+ /**
25
+ * Generate a JSON Schema 7 definition for a WorkflowTemplate's parameters.
26
+ *
27
+ * By default, only includes fields that are referenced in the template's
28
+ * prompt. Set `includeAllFields: true` to include all known TemplateContext fields.
29
+ */
30
+ export declare function generateTemplateSchema(template: WorkflowTemplate, options?: TemplateSchemaOptions): JSONSchema7;
31
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/templates/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAyB,MAAM,aAAa,CAAA;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAMlD,MAAM,WAAW,qBAAqB;IACpC,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AA4DD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA2BpE;AAMD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,WAAW,CAqCb"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Template JSON Schema Generation
3
+ *
4
+ * Generates JSON Schema 7 definitions from WorkflowTemplate objects.
5
+ * Schema is derived from TemplateContext fields referenced in the template's
6
+ * prompt via Handlebars expressions.
7
+ *
8
+ * @see SUP-1758
9
+ */
10
+ // ---------------------------------------------------------------------------
11
+ // Known TemplateContext field definitions
12
+ // ---------------------------------------------------------------------------
13
+ /**
14
+ * Maps TemplateContext field names to their JSON Schema 7 definitions.
15
+ * This is the source of truth for schema generation.
16
+ */
17
+ const CONTEXT_FIELD_SCHEMAS = {
18
+ identifier: { type: 'string', description: 'Issue identifier, e.g., "SUP-123"' },
19
+ mentionContext: { type: 'string', description: 'Optional user mention text providing additional context' },
20
+ startStatus: { type: 'string', description: 'Status to show when agent starts, e.g., "Started"' },
21
+ completeStatus: { type: 'string', description: 'Status to show when agent completes, e.g., "Finished"' },
22
+ parentContext: { type: 'string', description: 'Pre-built enriched prompt for parent issues with sub-issues' },
23
+ subIssueList: { type: 'string', description: 'Formatted list of sub-issues with statuses' },
24
+ cycleCount: { type: 'integer', description: 'Current escalation cycle count' },
25
+ strategy: { type: 'string', description: 'Current escalation strategy' },
26
+ failureSummary: { type: 'string', description: 'Accumulated failure summary across cycles' },
27
+ attemptNumber: { type: 'integer', description: 'Attempt number within current phase' },
28
+ previousFailureReasons: {
29
+ type: 'array',
30
+ items: { type: 'string' },
31
+ description: 'List of previous failure reasons',
32
+ },
33
+ totalCostUsd: { type: 'number', description: 'Total cost in USD across all attempts' },
34
+ blockerIdentifier: { type: 'string', description: 'Blocker issue identifier' },
35
+ team: { type: 'string', description: 'Team name' },
36
+ repository: { type: 'string', description: 'Git repository URL pattern' },
37
+ projectPath: { type: 'string', description: 'Root directory for this project within the repo' },
38
+ sharedPaths: {
39
+ type: 'array',
40
+ items: { type: 'string' },
41
+ description: 'Shared directories that any project agent may modify',
42
+ },
43
+ useToolPlugins: { type: 'boolean', description: 'When true, agents use in-process tools instead of CLI' },
44
+ linearCli: { type: 'string', description: 'Command to invoke the Linear CLI (default: "pnpm af-linear")' },
45
+ packageManager: { type: 'string', description: 'Package manager used by the project (default: "pnpm")' },
46
+ buildCommand: { type: 'string', description: 'Build command override' },
47
+ testCommand: { type: 'string', description: 'Test command override' },
48
+ validateCommand: { type: 'string', description: 'Validation command override' },
49
+ phaseOutputs: {
50
+ type: 'object',
51
+ additionalProperties: {
52
+ type: 'object',
53
+ additionalProperties: true,
54
+ },
55
+ description: 'Collected outputs from upstream phases',
56
+ },
57
+ agentBugBacklog: { type: 'string', description: 'Linear project name for agent-improvement issues' },
58
+ };
59
+ /** Fields that are always required in a template config */
60
+ const ALWAYS_REQUIRED = ['identifier'];
61
+ // ---------------------------------------------------------------------------
62
+ // Handlebars expression extraction
63
+ // ---------------------------------------------------------------------------
64
+ /**
65
+ * Extract Handlebars variable references from a template prompt.
66
+ * Matches {{ varName }}, {{ varName.property }}, and handles
67
+ * conditionals like {{#if varName}} and {{#unless varName}}.
68
+ *
69
+ * Returns the set of top-level variable names referenced.
70
+ */
71
+ export function extractTemplateVariables(prompt) {
72
+ const vars = new Set();
73
+ // Match {{ expr }}, {{#if expr}}, {{#unless expr}}, {{> partial}}, etc.
74
+ const patterns = [
75
+ /\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)\s*\}\}/g, // {{ varName }} or {{ var.prop }}
76
+ /\{\{#(?:if|unless)\s+([a-zA-Z_][a-zA-Z0-9_.]*)/g, // {{#if varName}}
77
+ /\{\{#(?:each)\s+([a-zA-Z_][a-zA-Z0-9_.]*)/g, // {{#each varName}}
78
+ /\{\{#(?:with)\s+([a-zA-Z_][a-zA-Z0-9_.]*)/g, // {{#with varName}}
79
+ /\{\{\s*(?:eq|neq)\s+([a-zA-Z_][a-zA-Z0-9_.]*)/g, // {{ eq varName "value" }}
80
+ /\((?:eq|neq)\s+([a-zA-Z_][a-zA-Z0-9_.]*)/g, // (eq varName "value") — subexpression
81
+ ];
82
+ for (const pattern of patterns) {
83
+ let match;
84
+ while ((match = pattern.exec(prompt)) !== null) {
85
+ const varName = match[1];
86
+ // Extract the top-level variable name (before any dot)
87
+ const topLevel = varName.split('.')[0];
88
+ // Skip Handlebars built-ins and partials
89
+ if (topLevel !== 'this' && topLevel !== 'else') {
90
+ vars.add(topLevel);
91
+ }
92
+ }
93
+ }
94
+ return vars;
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // Schema generation
98
+ // ---------------------------------------------------------------------------
99
+ /**
100
+ * Generate a JSON Schema 7 definition for a WorkflowTemplate's parameters.
101
+ *
102
+ * By default, only includes fields that are referenced in the template's
103
+ * prompt. Set `includeAllFields: true` to include all known TemplateContext fields.
104
+ */
105
+ export function generateTemplateSchema(template, options) {
106
+ const includeAll = options?.includeAllFields ?? false;
107
+ const properties = {};
108
+ const required = [];
109
+ if (includeAll) {
110
+ // Include all known fields
111
+ for (const [field, schemaDef] of Object.entries(CONTEXT_FIELD_SCHEMAS)) {
112
+ properties[field] = schemaDef;
113
+ }
114
+ }
115
+ else {
116
+ // Only include fields referenced in the template prompt
117
+ const referencedVars = extractTemplateVariables(template.prompt);
118
+ for (const varName of referencedVars) {
119
+ if (varName in CONTEXT_FIELD_SCHEMAS) {
120
+ properties[varName] = CONTEXT_FIELD_SCHEMAS[varName];
121
+ }
122
+ }
123
+ }
124
+ // Always include 'identifier' as required
125
+ for (const field of ALWAYS_REQUIRED) {
126
+ if (field in properties && !required.includes(field)) {
127
+ required.push(field);
128
+ }
129
+ }
130
+ return {
131
+ $schema: 'http://json-schema.org/draft-07/schema#',
132
+ type: 'object',
133
+ title: `${template.metadata.name} config`,
134
+ description: template.metadata.description ?? `Configuration schema for ${template.metadata.name} template`,
135
+ properties,
136
+ required: required.length > 0 ? required : undefined,
137
+ additionalProperties: true,
138
+ };
139
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=schema.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.test.d.ts","sourceRoot":"","sources":["../../../src/templates/schema.test.ts"],"names":[],"mappings":""}