@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.
- package/dist/src/config/repository-config.d.ts +7 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +15 -1
- package/dist/src/config/repository-config.test.js +1 -1
- package/dist/src/governor/decision-engine-adapter.d.ts +43 -0
- package/dist/src/governor/decision-engine-adapter.d.ts.map +1 -0
- package/dist/src/governor/decision-engine-adapter.js +417 -0
- package/dist/src/governor/decision-engine-adapter.test.d.ts +2 -0
- package/dist/src/governor/decision-engine-adapter.test.d.ts.map +1 -0
- package/dist/src/governor/decision-engine-adapter.test.js +362 -0
- package/dist/src/governor/decision-engine.js +3 -7
- package/dist/src/governor/decision-engine.test.js +5 -5
- package/dist/src/governor/index.d.ts +1 -0
- package/dist/src/governor/index.d.ts.map +1 -1
- package/dist/src/governor/index.js +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/manifest/route-manifest.d.ts.map +1 -1
- package/dist/src/manifest/route-manifest.js +4 -0
- package/dist/src/merge-queue/adapters/local.d.ts +68 -0
- package/dist/src/merge-queue/adapters/local.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/local.js +136 -0
- package/dist/src/merge-queue/adapters/local.test.d.ts +2 -0
- package/dist/src/merge-queue/adapters/local.test.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/local.test.js +176 -0
- package/dist/src/merge-queue/index.d.ts +13 -5
- package/dist/src/merge-queue/index.d.ts.map +1 -1
- package/dist/src/merge-queue/index.js +13 -6
- package/dist/src/merge-queue/merge-queue.integration.test.js +19 -0
- package/dist/src/merge-queue/merge-worker.d.ts.map +1 -1
- package/dist/src/merge-queue/merge-worker.js +29 -0
- package/dist/src/merge-queue/types.d.ts +1 -1
- package/dist/src/merge-queue/types.d.ts.map +1 -1
- package/dist/src/orchestrator/index.d.ts +4 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -1
- package/dist/src/orchestrator/index.js +3 -0
- package/dist/src/orchestrator/orchestrator.d.ts +58 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +552 -97
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
- package/dist/src/orchestrator/parse-work-result.js +3 -1
- package/dist/src/orchestrator/parse-work-result.test.js +6 -0
- package/dist/src/orchestrator/quality-baseline.d.ts +83 -0
- package/dist/src/orchestrator/quality-baseline.d.ts.map +1 -0
- package/dist/src/orchestrator/quality-baseline.js +313 -0
- package/dist/src/orchestrator/quality-baseline.test.d.ts +2 -0
- package/dist/src/orchestrator/quality-baseline.test.d.ts.map +1 -0
- package/dist/src/orchestrator/quality-baseline.test.js +448 -0
- package/dist/src/orchestrator/quality-ratchet.d.ts +70 -0
- package/dist/src/orchestrator/quality-ratchet.d.ts.map +1 -0
- package/dist/src/orchestrator/quality-ratchet.js +162 -0
- package/dist/src/orchestrator/quality-ratchet.test.d.ts +2 -0
- package/dist/src/orchestrator/quality-ratchet.test.d.ts.map +1 -0
- package/dist/src/orchestrator/quality-ratchet.test.js +335 -0
- package/dist/src/orchestrator/types.d.ts +2 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -1
- package/dist/src/providers/claude-provider.d.ts.map +1 -1
- package/dist/src/providers/claude-provider.js +11 -0
- package/dist/src/providers/codex-app-server-provider.d.ts +237 -0
- package/dist/src/providers/codex-app-server-provider.d.ts.map +1 -0
- package/dist/src/providers/codex-app-server-provider.js +1041 -0
- package/dist/src/providers/codex-app-server-provider.test.d.ts +2 -0
- package/dist/src/providers/codex-app-server-provider.test.d.ts.map +1 -0
- package/dist/src/providers/codex-app-server-provider.test.js +589 -0
- package/dist/src/providers/codex-approval-bridge.d.ts +49 -0
- package/dist/src/providers/codex-approval-bridge.d.ts.map +1 -0
- package/dist/src/providers/codex-approval-bridge.js +117 -0
- package/dist/src/providers/codex-approval-bridge.test.d.ts +2 -0
- package/dist/src/providers/codex-approval-bridge.test.d.ts.map +1 -0
- package/dist/src/providers/codex-approval-bridge.test.js +188 -0
- package/dist/src/providers/codex-provider.d.ts +24 -4
- package/dist/src/providers/codex-provider.d.ts.map +1 -1
- package/dist/src/providers/codex-provider.js +58 -6
- package/dist/src/providers/index.d.ts +1 -0
- package/dist/src/providers/index.d.ts.map +1 -1
- package/dist/src/providers/index.js +1 -0
- package/dist/src/providers/types.d.ts +25 -0
- package/dist/src/providers/types.d.ts.map +1 -1
- package/dist/src/routing/observation-recorder.test.js +1 -1
- package/dist/src/routing/observation-store.d.ts +15 -1
- package/dist/src/routing/observation-store.d.ts.map +1 -1
- package/dist/src/routing/observation-store.test.js +17 -11
- package/dist/src/routing/types.d.ts +1 -1
- package/dist/src/templates/adapters.d.ts +25 -0
- package/dist/src/templates/adapters.d.ts.map +1 -1
- package/dist/src/templates/adapters.js +70 -0
- package/dist/src/templates/adapters.test.js +49 -0
- package/dist/src/templates/index.d.ts +3 -1
- package/dist/src/templates/index.d.ts.map +1 -1
- package/dist/src/templates/index.js +1 -0
- package/dist/src/templates/registry.d.ts +31 -0
- package/dist/src/templates/registry.d.ts.map +1 -1
- package/dist/src/templates/registry.js +91 -0
- package/dist/src/templates/schema.d.ts +31 -0
- package/dist/src/templates/schema.d.ts.map +1 -0
- package/dist/src/templates/schema.js +139 -0
- package/dist/src/templates/schema.test.d.ts +2 -0
- package/dist/src/templates/schema.test.d.ts.map +1 -0
- package/dist/src/templates/schema.test.js +215 -0
- package/dist/src/templates/types.d.ts +22 -0
- package/dist/src/templates/types.d.ts.map +1 -1
- package/dist/src/templates/types.js +12 -0
- package/dist/src/tools/index.d.ts +2 -0
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +1 -0
- package/dist/src/tools/registry.d.ts +9 -1
- package/dist/src/tools/registry.d.ts.map +1 -1
- package/dist/src/tools/registry.js +13 -1
- package/dist/src/tools/stdio-server-entry.d.ts +25 -0
- package/dist/src/tools/stdio-server-entry.d.ts.map +1 -0
- package/dist/src/tools/stdio-server-entry.js +205 -0
- package/dist/src/tools/stdio-server.d.ts +87 -0
- package/dist/src/tools/stdio-server.d.ts.map +1 -0
- package/dist/src/tools/stdio-server.js +138 -0
- package/dist/src/workflow/workflow-types.d.ts +3 -3
- 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;
|
|
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;
|
|
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;
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"schema.test.d.ts","sourceRoot":"","sources":["../../../src/templates/schema.test.ts"],"names":[],"mappings":""}
|