@ikenga/contract 0.12.0 → 0.13.0
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/action-frontmatter.d.ts +389 -0
- package/dist/action-frontmatter.d.ts.map +1 -0
- package/dist/action-frontmatter.js +215 -0
- package/dist/action-frontmatter.js.map +1 -0
- package/dist/artifact.d.ts +110 -110
- package/dist/browser.d.ts +2 -2
- package/dist/engine/adapter.d.ts +6 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +175 -150
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +5 -0
- package/dist/manifest.js.map +1 -1
- package/dist/registry.d.ts +301 -285
- package/dist/registry.d.ts.map +1 -1
- package/dist/window.d.ts +8 -8
- package/package.json +7 -2
- package/src/action-frontmatter.test.ts +219 -0
- package/src/action-frontmatter.ts +250 -0
- package/src/index.ts +1 -0
- package/src/manifest.ts +5 -0
package/dist/registry.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,uBAAuB,EAAG,CAAU,CAAC;AAIlD;;;;GAIG;AACH,eAAO,MAAM,YAAY;IACvB,4DAA4D;;IAE5D,0DAA0D;;;;;;;;EAE1D,CAAC;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;EAGjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,gBAAgB;IAC3B,4BAA4B;;IAE5B,iDAAiD;;IAEjD,yEAAyE;;IAEzE,2DAA2D;;IAE3D,mFAAmF;;IAEnF,4DAA4D
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAE5D,gFAAgF;;YA/BhF,4DAA4D;;YAE5D,0DAA0D;;;;;;;;;QA+B1D;;;;;;WAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAaH,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAIxD,eAAO,MAAM,mBAAmB;IAC9B,qBAAqB;;IAErB,qBAAqB;;IAErB;;;;;OAKG;;IAEH,4EAA4E;;IAE5E,2EAA2E;;IAE3E;;;;OAIG;;IAEH;;;;;;;OAOG;;;;;;;;;;;;;;;;;;EAEH,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,mBAAmB;;IAE9B,wDAAwD;;;QAnCxD,qBAAqB;;QAErB,qBAAqB;;QAErB;;;;;WAKG;;QAEH,4EAA4E;;QAE5E,2EAA2E;;QAE3E;;;;WAIG;;QAEH;;;;;;;WAOG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUH,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD"}
|
package/dist/window.d.ts
CHANGED
|
@@ -24,14 +24,14 @@ export declare const WindowDescriptorSchema: z.ZodObject<{
|
|
|
24
24
|
*/
|
|
25
25
|
layout_key: z.ZodString;
|
|
26
26
|
}, "strict", z.ZodTypeAny, {
|
|
27
|
-
label: string;
|
|
28
27
|
kind: "primary" | "single-surface" | "pane-set" | "workspace";
|
|
28
|
+
label: string;
|
|
29
29
|
surface_set: string[];
|
|
30
30
|
project_id: string | null;
|
|
31
31
|
layout_key: string;
|
|
32
32
|
}, {
|
|
33
|
-
label: string;
|
|
34
33
|
kind: "primary" | "single-surface" | "pane-set" | "workspace";
|
|
34
|
+
label: string;
|
|
35
35
|
layout_key: string;
|
|
36
36
|
surface_set?: string[] | undefined;
|
|
37
37
|
project_id?: string | null | undefined;
|
|
@@ -52,11 +52,11 @@ export declare const WindowEventTargetSchema: z.ZodDiscriminatedUnion<"kind", [z
|
|
|
52
52
|
kind: z.ZodLiteral<"window">;
|
|
53
53
|
label: z.ZodString;
|
|
54
54
|
}, "strict", z.ZodTypeAny, {
|
|
55
|
-
label: string;
|
|
56
55
|
kind: "window";
|
|
57
|
-
}, {
|
|
58
56
|
label: string;
|
|
57
|
+
}, {
|
|
59
58
|
kind: "window";
|
|
59
|
+
label: string;
|
|
60
60
|
}>]>;
|
|
61
61
|
export type WindowEventTarget = z.infer<typeof WindowEventTargetSchema>;
|
|
62
62
|
/**
|
|
@@ -80,11 +80,11 @@ export declare const WindowEventEnvelopeSchema: z.ZodObject<{
|
|
|
80
80
|
kind: z.ZodLiteral<"window">;
|
|
81
81
|
label: z.ZodString;
|
|
82
82
|
}, "strict", z.ZodTypeAny, {
|
|
83
|
-
label: string;
|
|
84
83
|
kind: "window";
|
|
85
|
-
}, {
|
|
86
84
|
label: string;
|
|
85
|
+
}, {
|
|
87
86
|
kind: "window";
|
|
87
|
+
label: string;
|
|
88
88
|
}>]>;
|
|
89
89
|
payload: z.ZodUnknown;
|
|
90
90
|
}, "strict", z.ZodTypeAny, {
|
|
@@ -94,8 +94,8 @@ export declare const WindowEventEnvelopeSchema: z.ZodObject<{
|
|
|
94
94
|
target: {
|
|
95
95
|
kind: "broadcast";
|
|
96
96
|
} | {
|
|
97
|
-
label: string;
|
|
98
97
|
kind: "window";
|
|
98
|
+
label: string;
|
|
99
99
|
};
|
|
100
100
|
payload?: unknown;
|
|
101
101
|
}, {
|
|
@@ -105,8 +105,8 @@ export declare const WindowEventEnvelopeSchema: z.ZodObject<{
|
|
|
105
105
|
target: {
|
|
106
106
|
kind: "broadcast";
|
|
107
107
|
} | {
|
|
108
|
-
label: string;
|
|
109
108
|
kind: "window";
|
|
109
|
+
label: string;
|
|
110
110
|
};
|
|
111
111
|
payload?: unknown;
|
|
112
112
|
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikenga/contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Shared contract for the Ikenga pkg system: manifest schema, RPC types, Engine interface, capability scopes.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -59,6 +59,10 @@
|
|
|
59
59
|
"./window": {
|
|
60
60
|
"types": "./dist/window.d.ts",
|
|
61
61
|
"import": "./dist/window.js"
|
|
62
|
+
},
|
|
63
|
+
"./action-frontmatter": {
|
|
64
|
+
"types": "./dist/action-frontmatter.d.ts",
|
|
65
|
+
"import": "./dist/action-frontmatter.js"
|
|
62
66
|
}
|
|
63
67
|
},
|
|
64
68
|
"files": [
|
|
@@ -90,7 +94,8 @@
|
|
|
90
94
|
"react": "^19.0.0",
|
|
91
95
|
"react-dom": "^19.0.0",
|
|
92
96
|
"tsx": "^4.21.0",
|
|
93
|
-
"typescript": "^5.4.0"
|
|
97
|
+
"typescript": "^5.4.0",
|
|
98
|
+
"yaml": "^1.10.3"
|
|
94
99
|
},
|
|
95
100
|
"publishConfig": {
|
|
96
101
|
"access": "public"
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import YAML from 'yaml';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ActionFrontmatter,
|
|
11
|
+
CapabilityEnum,
|
|
12
|
+
DomainEnum,
|
|
13
|
+
RunBinding,
|
|
14
|
+
SetupSpec,
|
|
15
|
+
Trigger,
|
|
16
|
+
UxModeEnum,
|
|
17
|
+
} from './action-frontmatter.js';
|
|
18
|
+
|
|
19
|
+
// ─── unit: sub-schemas ──────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
test('DomainEnum: the eight domains + skill-core', () => {
|
|
22
|
+
for (const d of [
|
|
23
|
+
'tasks',
|
|
24
|
+
'mail',
|
|
25
|
+
'outbound',
|
|
26
|
+
'sales',
|
|
27
|
+
'finance',
|
|
28
|
+
'content',
|
|
29
|
+
'research',
|
|
30
|
+
'strategy',
|
|
31
|
+
'skill-core',
|
|
32
|
+
]) {
|
|
33
|
+
assert.equal(DomainEnum.parse(d), d);
|
|
34
|
+
}
|
|
35
|
+
assert.throws(() => DomainEnum.parse('marketing'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('UxModeEnum: exactly the five modes', () => {
|
|
39
|
+
for (const m of ['confirm', 'silent', 'form', 'streaming', 'approve']) {
|
|
40
|
+
assert.equal(UxModeEnum.parse(m), m);
|
|
41
|
+
}
|
|
42
|
+
assert.throws(() => UxModeEnum.parse('modal'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('RunBinding: chat_prompt requires a non-empty prompt', () => {
|
|
46
|
+
const r = RunBinding.parse({ kind: 'chat_prompt', prompt: '# do the thing' });
|
|
47
|
+
assert.equal(r.kind, 'chat_prompt');
|
|
48
|
+
assert.throws(() => RunBinding.parse({ kind: 'chat_prompt', prompt: '' }));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('RunBinding: sidecar defaults args to []', () => {
|
|
52
|
+
const r = RunBinding.parse({ kind: 'sidecar', sidecar_id: 'pa-x' });
|
|
53
|
+
assert.deepEqual(r.kind === 'sidecar' ? r.args : null, []);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('Trigger: schedule needs a cron; unknown kind rejected', () => {
|
|
57
|
+
const t = Trigger.parse({ kind: 'schedule', cron: '30 */4 * * *', label: 'x' });
|
|
58
|
+
assert.equal(t.kind, 'schedule');
|
|
59
|
+
assert.throws(() => Trigger.parse({ kind: 'timer', every: '5m' }));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('CapabilityEnum: closed set', () => {
|
|
63
|
+
for (const c of ['sqlite', 'mcp', 'sidecar', 'network', 'fs', 'secrets', 'chat']) {
|
|
64
|
+
assert.equal(CapabilityEnum.parse(c), c);
|
|
65
|
+
}
|
|
66
|
+
assert.throws(() => CapabilityEnum.parse('gpu'));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('SetupSpec: template_version must be a positive int', () => {
|
|
70
|
+
const s = SetupSpec.parse({ mode: 'interview', template_version: 1 });
|
|
71
|
+
assert.equal(s.template_version, 1);
|
|
72
|
+
assert.throws(() => SetupSpec.parse({ mode: 'interview', template_version: 0 }));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ─── unit: ActionFrontmatter refinements ────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
test('ActionFrontmatter: name must be kebab-case', () => {
|
|
78
|
+
const base = {
|
|
79
|
+
name: 'draft-reply',
|
|
80
|
+
description: 'x',
|
|
81
|
+
domain: 'mail',
|
|
82
|
+
ux_mode: 'approve',
|
|
83
|
+
run: { kind: 'chat_prompt', prompt: 'x' },
|
|
84
|
+
};
|
|
85
|
+
assert.equal(ActionFrontmatter.parse(base).name, 'draft-reply');
|
|
86
|
+
assert.throws(() => ActionFrontmatter.parse({ ...base, name: 'DraftReply' }));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('ActionFrontmatter: .strict rejects unknown top-level keys', () => {
|
|
90
|
+
assert.throws(() =>
|
|
91
|
+
ActionFrontmatter.parse({
|
|
92
|
+
name: 'x',
|
|
93
|
+
description: 'x',
|
|
94
|
+
domain: 'mail',
|
|
95
|
+
ux_mode: 'approve',
|
|
96
|
+
run: { kind: 'chat_prompt', prompt: 'x' },
|
|
97
|
+
bogus: true,
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('ActionFrontmatter: setup block only valid on the setup action', () => {
|
|
103
|
+
// Non-setup action carrying a setup block → rejected.
|
|
104
|
+
assert.throws(() =>
|
|
105
|
+
ActionFrontmatter.parse({
|
|
106
|
+
name: 'sweep',
|
|
107
|
+
description: 'x',
|
|
108
|
+
domain: 'tasks',
|
|
109
|
+
ux_mode: 'approve',
|
|
110
|
+
run: { kind: 'chat_prompt', prompt: 'x' },
|
|
111
|
+
setup: { mode: 'interview', template_version: 1 },
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
// setup action missing its setup block → rejected.
|
|
115
|
+
assert.throws(() =>
|
|
116
|
+
ActionFrontmatter.parse({
|
|
117
|
+
name: 'setup',
|
|
118
|
+
description: 'x',
|
|
119
|
+
domain: 'skill-core',
|
|
120
|
+
ux_mode: 'streaming',
|
|
121
|
+
run: { kind: 'chat_prompt', prompt: 'x' },
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
// setup action with its setup block → ok.
|
|
125
|
+
const ok = ActionFrontmatter.parse({
|
|
126
|
+
name: 'setup',
|
|
127
|
+
description: 'x',
|
|
128
|
+
domain: 'skill-core',
|
|
129
|
+
ux_mode: 'streaming',
|
|
130
|
+
run: { kind: 'chat_prompt', prompt: 'x' },
|
|
131
|
+
setup: { mode: 'ai_infer', template_version: 1, infer_sources: ['README.md'] },
|
|
132
|
+
});
|
|
133
|
+
assert.equal(ok.setup?.mode, 'ai_infer');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('ActionFrontmatter: depends_on refuses non-skill-core targets', () => {
|
|
137
|
+
assert.throws(() =>
|
|
138
|
+
ActionFrontmatter.parse({
|
|
139
|
+
name: 'x',
|
|
140
|
+
description: 'x',
|
|
141
|
+
domain: 'mail',
|
|
142
|
+
ux_mode: 'approve',
|
|
143
|
+
run: { kind: 'chat_prompt', prompt: 'x' },
|
|
144
|
+
depends_on: ['mail'],
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ─── conformance: parse every real installed action file ────────────────────
|
|
150
|
+
|
|
151
|
+
/** Extract the YAML frontmatter block between the leading `---` fences.
|
|
152
|
+
* Mirrors the Rust `extract_frontmatter` in skill_actions.rs. Returns null
|
|
153
|
+
* when the file does not open with a `---` fence (prose-only skill docs). */
|
|
154
|
+
function extractFrontmatter(body: string): string | null {
|
|
155
|
+
const src = body.replace(/^/, '');
|
|
156
|
+
const lines = src.split('\n');
|
|
157
|
+
if (lines[0]?.trim() !== '---') return null;
|
|
158
|
+
const close = lines.slice(1).findIndex((l) => l.replace(/\r$/, '').trim() === '---');
|
|
159
|
+
if (close === -1) return null;
|
|
160
|
+
return lines.slice(1, 1 + close).join('\n');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function collectActionFiles(): string[] {
|
|
164
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
165
|
+
// contract/src → ikenga-pkgs/packages/skills (sibling repos under the workspace).
|
|
166
|
+
const skillsRoot = join(here, '..', '..', 'ikenga-pkgs', 'packages', 'skills');
|
|
167
|
+
if (!existsSync(skillsRoot)) return [];
|
|
168
|
+
const out: string[] = [];
|
|
169
|
+
for (const domain of readdirSync(skillsRoot, { withFileTypes: true })) {
|
|
170
|
+
if (!domain.isDirectory()) continue;
|
|
171
|
+
const skillsDir = join(skillsRoot, domain.name, 'skills');
|
|
172
|
+
if (!existsSync(skillsDir)) continue;
|
|
173
|
+
for (const skill of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
174
|
+
if (!skill.isDirectory()) continue;
|
|
175
|
+
const actionsDir = join(skillsDir, skill.name, 'actions');
|
|
176
|
+
if (!existsSync(actionsDir)) continue;
|
|
177
|
+
for (const f of readdirSync(actionsDir)) {
|
|
178
|
+
if (f.endsWith('.md') && f.toLowerCase() !== 'readme.md') {
|
|
179
|
+
out.push(join(actionsDir, f));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return out.sort();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
test('conformance: every real action file with frontmatter parses against ActionFrontmatter', () => {
|
|
188
|
+
const files = collectActionFiles();
|
|
189
|
+
// The real action files live in the SIBLING ikenga-pkgs repo, present when the
|
|
190
|
+
// workspace is checked out whole (local dev) but absent in this repo's isolated
|
|
191
|
+
// CI checkout. Skip cleanly there — the schema unit tests above already cover
|
|
192
|
+
// the contract; this is the cross-repo integration layer, best-effort.
|
|
193
|
+
if (files.length === 0) {
|
|
194
|
+
console.error('# skip: ikenga-pkgs sibling repo not present (isolated CI checkout)');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let parsed = 0;
|
|
199
|
+
const failures: string[] = [];
|
|
200
|
+
for (const file of files) {
|
|
201
|
+
const fm = extractFrontmatter(readFileSync(file, 'utf8'));
|
|
202
|
+
if (fm === null) continue; // prose-only skill doc (groundwork/contribute) — not a structured action
|
|
203
|
+
const doc = YAML.parse(fm);
|
|
204
|
+
const res = ActionFrontmatter.safeParse(doc);
|
|
205
|
+
if (!res.success) {
|
|
206
|
+
failures.push(`${file}\n ${JSON.stringify(res.error.issues, null, 2)}`);
|
|
207
|
+
} else {
|
|
208
|
+
parsed += 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
assert.equal(
|
|
213
|
+
failures.length,
|
|
214
|
+
0,
|
|
215
|
+
`real action files failed the schema:\n${failures.join('\n')}`,
|
|
216
|
+
);
|
|
217
|
+
// Well above the "at least 3" bar — the full Atelier surface is ~32 structured actions.
|
|
218
|
+
assert.ok(parsed >= 3, `expected >= 3 structured action files, parsed ${parsed}`);
|
|
219
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Ikenga skill-action frontmatter schema — the source-of-truth shape for an
|
|
2
|
+
// Atelier skill *action*'s YAML frontmatter (the block between the leading
|
|
3
|
+
// `---` fences of an `actions/*.md` file).
|
|
4
|
+
//
|
|
5
|
+
// The Rust loader in `royalti-io/ikenga` at
|
|
6
|
+
// `src-tauri/src/pkg/skill_actions.rs` mirrors this schema. Field changes MUST
|
|
7
|
+
// be made in lockstep with that Rust struct — same convention as
|
|
8
|
+
// manifest.ts ↔ manifest.rs.
|
|
9
|
+
//
|
|
10
|
+
// Every field below is justified in plans/atelier/06-skill-action-contract.md.
|
|
11
|
+
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Identity & taxonomy (G-TAXONOMY)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The eight stateful domains plus the one stateless generic skill.
|
|
20
|
+
* `skill-core` is the ONLY legal `depends_on` target (one-way edge, G-04/E-14).
|
|
21
|
+
*/
|
|
22
|
+
export const DomainEnum = z.enum([
|
|
23
|
+
'tasks',
|
|
24
|
+
'mail',
|
|
25
|
+
'outbound',
|
|
26
|
+
'sales',
|
|
27
|
+
'finance',
|
|
28
|
+
'content',
|
|
29
|
+
'research',
|
|
30
|
+
'strategy',
|
|
31
|
+
'skill-core',
|
|
32
|
+
]);
|
|
33
|
+
export type Domain = z.infer<typeof DomainEnum>;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// UX modes (R5, E-11) — exactly five
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How an action presents itself to the operator.
|
|
41
|
+
* - confirm : preview the planned effect, single yes/no, then run.
|
|
42
|
+
* - silent : run with no prompt (still subject to capability scopes).
|
|
43
|
+
* - form : collect `inputs_schema` from the operator before running.
|
|
44
|
+
* - streaming : run with live token/log streaming surfaced in the dock.
|
|
45
|
+
* - approve : run-THEN-pause on a produced draft; operator approves/edits
|
|
46
|
+
* /rejects the artifact before any external side effect commits
|
|
47
|
+
* (the draft-review gate, E-11).
|
|
48
|
+
*/
|
|
49
|
+
export const UxModeEnum = z.enum([
|
|
50
|
+
'confirm',
|
|
51
|
+
'silent',
|
|
52
|
+
'form',
|
|
53
|
+
'streaming',
|
|
54
|
+
'approve',
|
|
55
|
+
]);
|
|
56
|
+
export type UxMode = z.infer<typeof UxModeEnum>;
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Run binding (R5, G-01) — how the action actually executes
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/** chat_prompt: hand a templated prompt to the engine in the dock chat. */
|
|
63
|
+
const ChatPromptRun = z.object({
|
|
64
|
+
kind: z.literal('chat_prompt'),
|
|
65
|
+
// Prompt template; may interpolate validated `inputs` via {{var}}.
|
|
66
|
+
prompt: z.string().min(1),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** sidecar: invoke a bundled CLI sidecar by its manifest-declared id. */
|
|
70
|
+
const SidecarRun = z.object({
|
|
71
|
+
kind: z.literal('sidecar'),
|
|
72
|
+
sidecar_id: z.string().min(1),
|
|
73
|
+
// Argv template; entries may interpolate validated `inputs`.
|
|
74
|
+
args: z.array(z.string()).default([]),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** mcp_tool: call a tool on a pkg-declared MCP server. */
|
|
78
|
+
const McpToolRun = z.object({
|
|
79
|
+
kind: z.literal('mcp_tool'),
|
|
80
|
+
server_id: z.string().min(1),
|
|
81
|
+
tool: z.string().min(1),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const RunBinding = z.discriminatedUnion('kind', [
|
|
85
|
+
ChatPromptRun,
|
|
86
|
+
SidecarRun,
|
|
87
|
+
McpToolRun,
|
|
88
|
+
]);
|
|
89
|
+
export type RunBinding = z.infer<typeof RunBinding>;
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Triggers (R5) — absorbs the 41 legacy crons. An action may be reachable by
|
|
93
|
+
// more than one trigger; `manual` is the implicit default if none declared.
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/** Operator-invoked from the command surface / dock. */
|
|
97
|
+
const ManualTrigger = z.object({
|
|
98
|
+
kind: z.literal('manual'),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/** Time-driven. `cron` is a standard 5-field crontab expression. */
|
|
102
|
+
const ScheduleTrigger = z.object({
|
|
103
|
+
kind: z.literal('schedule'),
|
|
104
|
+
cron: z.string().min(1),
|
|
105
|
+
// Optional human label surfaced in the schedule UI.
|
|
106
|
+
label: z.string().optional(),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** Inbound HTTP trigger routed through the shell bridge. */
|
|
110
|
+
const WebhookTrigger = z.object({
|
|
111
|
+
kind: z.literal('webhook'),
|
|
112
|
+
// Stable path segment; the shell namespaces it under the pkg id.
|
|
113
|
+
path: z.string().min(1),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/** Reacts to an internal shell/domain event by name. */
|
|
117
|
+
const EventTrigger = z.object({
|
|
118
|
+
kind: z.literal('event'),
|
|
119
|
+
event: z.string().min(1),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export const Trigger = z.discriminatedUnion('kind', [
|
|
123
|
+
ManualTrigger,
|
|
124
|
+
ScheduleTrigger,
|
|
125
|
+
WebhookTrigger,
|
|
126
|
+
EventTrigger,
|
|
127
|
+
]);
|
|
128
|
+
export type Trigger = z.infer<typeof Trigger>;
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Capabilities (G-08, R18/R19) — coarse permission grants an action requires.
|
|
132
|
+
// `sqlite` is the stateful-domain seam: state lives in the local ikenga.db,
|
|
133
|
+
// reached through the host dbQuery (SELECT-only) and dbExec (parameterized
|
|
134
|
+
// mutate) bridges. The *table scope* is NOT declared here — it is declared once
|
|
135
|
+
// at the pkg manifest as permissions["sqlite.tables"] and cross-checked at
|
|
136
|
+
// install time against the generated tables.json (the applied ikenga.db STRICT
|
|
137
|
+
// schema). The frontmatter only asserts "this action touches sqlite".
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
export const CapabilityEnum = z.enum([
|
|
141
|
+
'sqlite', // host.dbQuery / host.dbExec against ikenga.db (R18/R19)
|
|
142
|
+
'mcp', // call declared MCP tools
|
|
143
|
+
'sidecar', // spawn declared sidecars
|
|
144
|
+
'network', // outbound network
|
|
145
|
+
'fs', // host filesystem (scoped by manifest)
|
|
146
|
+
'secrets', // read Stronghold-vaulted secrets
|
|
147
|
+
'chat', // drive the dock chat engine
|
|
148
|
+
]);
|
|
149
|
+
export type Capability = z.infer<typeof CapabilityEnum>;
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Setup lifecycle (D-02, setup-lifecycle decision) — `setup` is an optional,
|
|
153
|
+
// well-known action on EVERY skill. It localises the skill per project by
|
|
154
|
+
// writing ${CLAUDE_PROJECT_DIR}/.atelier/<skill>/manifest.json. It runs IN
|
|
155
|
+
// CHAT, never as a form screen. Two modes:
|
|
156
|
+
// - ai_infer : the agent drafts the instance config from the repo/site, the
|
|
157
|
+
// operator confirms/edits in chat.
|
|
158
|
+
// - interview: the agent asks the operator a scripted set of questions.
|
|
159
|
+
// `template_version` carries a migrate path so an upgraded skill can migrate an
|
|
160
|
+
// older instance file forward.
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
export const SetupModeEnum = z.enum(['ai_infer', 'interview']);
|
|
164
|
+
export type SetupMode = z.infer<typeof SetupModeEnum>;
|
|
165
|
+
|
|
166
|
+
export const SetupSpec = z.object({
|
|
167
|
+
mode: SetupModeEnum,
|
|
168
|
+
// Bumped whenever the instance-file shape changes; drives the migrate path.
|
|
169
|
+
template_version: z.number().int().positive(),
|
|
170
|
+
// For ai_infer: where the agent should look to draft the instance config.
|
|
171
|
+
infer_sources: z.array(z.string()).optional(),
|
|
172
|
+
// For interview: ordered question ids the chat flow walks.
|
|
173
|
+
interview_questions: z.array(z.string()).optional(),
|
|
174
|
+
});
|
|
175
|
+
export type SetupSpec = z.infer<typeof SetupSpec>;
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// ActionFrontmatter — the locked shape.
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
export const ActionFrontmatter = z
|
|
182
|
+
.object({
|
|
183
|
+
// --- identity ---
|
|
184
|
+
/** Stable action id, unique within the skill. kebab-case. */
|
|
185
|
+
name: z
|
|
186
|
+
.string()
|
|
187
|
+
.min(1)
|
|
188
|
+
.regex(/^[a-z][a-z0-9-]*$/, 'name must be kebab-case'),
|
|
189
|
+
/** One-line human description shown in the command surface. */
|
|
190
|
+
description: z.string().min(1),
|
|
191
|
+
/** Owning domain (G-TAXONOMY). */
|
|
192
|
+
domain: DomainEnum,
|
|
193
|
+
|
|
194
|
+
// --- presentation & io ---
|
|
195
|
+
/** How the action presents to the operator (R5/E-11). */
|
|
196
|
+
ux_mode: UxModeEnum,
|
|
197
|
+
/**
|
|
198
|
+
* JSON-Schema describing the action's inputs. Kept as an opaque object here
|
|
199
|
+
* (validated as JSON-Schema at the destination); `form`/`approve` modes
|
|
200
|
+
* render it, `chat_prompt` runs interpolate validated values from it.
|
|
201
|
+
*/
|
|
202
|
+
inputs_schema: z.record(z.string(), z.unknown()).optional(),
|
|
203
|
+
|
|
204
|
+
// --- execution ---
|
|
205
|
+
/** What the action actually does (G-01). */
|
|
206
|
+
run: RunBinding,
|
|
207
|
+
/** How the action can be invoked. Empty ⇒ manual-only. */
|
|
208
|
+
triggers: z.array(Trigger).default([]),
|
|
209
|
+
|
|
210
|
+
// --- dependency & permissions ---
|
|
211
|
+
/**
|
|
212
|
+
* One-way dependency edge. The ONLY legal target is 'skill-core'
|
|
213
|
+
* (G-04/E-14); any other entry is rejected by the lint in
|
|
214
|
+
* 06-skill-action-contract.md §"depends_on lint". Modeled as a literal
|
|
215
|
+
* array so the schema itself refuses non-skill-core targets.
|
|
216
|
+
*/
|
|
217
|
+
depends_on: z.array(z.literal('skill-core')).default([]),
|
|
218
|
+
/** Coarse capability grants this action needs (G-08, R18/R19). */
|
|
219
|
+
requires_capabilities: z.array(CapabilityEnum).default([]),
|
|
220
|
+
|
|
221
|
+
// --- lifecycle ---
|
|
222
|
+
/**
|
|
223
|
+
* Present ONLY on the well-known `setup` action. Omitted on every other
|
|
224
|
+
* action. Refined below.
|
|
225
|
+
*/
|
|
226
|
+
setup: SetupSpec.optional(),
|
|
227
|
+
})
|
|
228
|
+
.strict()
|
|
229
|
+
.superRefine((fm, ctx) => {
|
|
230
|
+
// The `setup` block is allowed iff this IS the setup action.
|
|
231
|
+
if (fm.name === 'setup' && fm.setup === undefined) {
|
|
232
|
+
ctx.addIssue({
|
|
233
|
+
code: z.ZodIssueCode.custom,
|
|
234
|
+
path: ['setup'],
|
|
235
|
+
message: 'the "setup" action must declare a `setup` block',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (fm.name !== 'setup' && fm.setup !== undefined) {
|
|
239
|
+
ctx.addIssue({
|
|
240
|
+
code: z.ZodIssueCode.custom,
|
|
241
|
+
path: ['setup'],
|
|
242
|
+
message: '`setup` block is only valid on the "setup" action',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// An action that runs SQL must declare the `sqlite` capability so the
|
|
246
|
+
// pkg-manifest sqlite.tables cross-check has something to bind to.
|
|
247
|
+
// (The reverse — declaring sqlite without using it — is allowed/harmless.)
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
export type ActionFrontmatter = z.infer<typeof ActionFrontmatter>;
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './registry.js';
|
|
|
9
9
|
export * from './browser.js';
|
|
10
10
|
export * from './pa-actions.js';
|
|
11
11
|
export * from './window.js';
|
|
12
|
+
export * from './action-frontmatter.js';
|
|
12
13
|
|
|
13
14
|
/** This package's own version. */
|
|
14
15
|
export const CONTRACT_PACKAGE_VERSION = '0.5.0' as const;
|
package/src/manifest.ts
CHANGED
|
@@ -74,6 +74,11 @@ export const PermissionsSchema = z.object({
|
|
|
74
74
|
* without errors during the transition window. */
|
|
75
75
|
'supabase.tables': z.array(z.string()).default([]),
|
|
76
76
|
'vault.keys': z.array(z.string()).default([]),
|
|
77
|
+
/** Engine scopes exercisable from the pkg iframe. `"invoke"` gates the
|
|
78
|
+
* FE-side host.sendToActiveSession / host.startChatSession verbs
|
|
79
|
+
* (pkgDeclaresScope). Mirrors `Permissions.engine` in the shell's
|
|
80
|
+
* manifest.rs — keep in lockstep. */
|
|
81
|
+
engine: z.array(z.string()).default([]),
|
|
77
82
|
}).default({});
|
|
78
83
|
|
|
79
84
|
export const NavEntrySchema = z.object({
|