@malmhq/intermesh-cli 0.1.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/LICENSE +198 -0
- package/README.md +77 -0
- package/dist/generation/.opencode/AGENTS.md +38 -0
- package/dist/generation/.opencode/agents/generation-builder.md +9 -0
- package/dist/generation/.opencode/agents/generation-fidelity.md +9 -0
- package/dist/generation/.opencode/agents/generation-source.md +13 -0
- package/dist/generation/.opencode/prompts/analyze-capabilities.md +61 -0
- package/dist/generation/.opencode/prompts/analyze-source.md +41 -0
- package/dist/generation/.opencode/prompts/generate.md +9 -0
- package/dist/generation/.opencode/prompts/repair-capability-proposal.md +5 -0
- package/dist/generation/.opencode/skills/generation-building/SKILL.md +32 -0
- package/dist/generation/.opencode/skills/generation-package-review/SKILL.md +36 -0
- package/dist/generation/.opencode/skills/generation-source-analysis/SKILL.md +36 -0
- package/dist/generation/AGENTS.md +38 -0
- package/dist/generation/agent-skill-installer/index.ts +4 -0
- package/dist/generation/agent-skill-installer/install.ts +503 -0
- package/dist/generation/agent-skill-installer/lock.ts +128 -0
- package/dist/generation/agent-skill-installer/manifest.ts +271 -0
- package/dist/generation/agent-skill-installer/targets.ts +169 -0
- package/dist/generation/runtime-entrypoint.js +39980 -0
- package/dist/generation/scripts/check-package.mjs +45 -0
- package/dist/generation/scripts/installed-help.mjs +98 -0
- package/dist/generation/scripts/lead-input.mjs +584 -0
- package/dist/generation/scripts/prepare-opencode.mjs +33 -0
- package/dist/generation/scripts/prepared-work.mjs +197 -0
- package/dist/generation/scripts/repair-loop.mjs +519 -0
- package/dist/generation/scripts/run-reviewers.mjs +306 -0
- package/dist/generation/scripts/runtime-capture.mjs +119 -0
- package/dist/generation/scripts/stage-timings.mjs +53 -0
- package/dist/generation/scripts/validate-source-brief.mjs +220 -0
- package/dist/generation/scripts/verify-package.mjs +365 -0
- package/dist/generation/templates/npm-cli/CONTRIBUTING.md +20 -0
- package/dist/generation/templates/npm-cli/GENERATION-PATTERNS.md +143 -0
- package/dist/generation/templates/npm-cli/README.md +67 -0
- package/dist/generation/templates/npm-cli/capabilities.json +1 -0
- package/dist/generation/templates/npm-cli/mock/routes.ts +3 -0
- package/dist/generation/templates/npm-cli/mock/server.ts +15 -0
- package/dist/generation/templates/npm-cli/mock/types.ts +7 -0
- package/dist/generation/templates/npm-cli/package.json +36 -0
- package/dist/generation/templates/npm-cli/pnpm-lock.yaml +495 -0
- package/dist/generation/templates/npm-cli/pnpm-workspace.yaml +2 -0
- package/dist/generation/templates/npm-cli/skills/customer-cli-management/SKILL.md +22 -0
- package/dist/generation/templates/npm-cli/skills/customer-workflow/SKILL.md +22 -0
- package/dist/generation/templates/npm-cli/src/auth/commands.ts +77 -0
- package/dist/generation/templates/npm-cli/src/auth/credentials.ts +66 -0
- package/dist/generation/templates/npm-cli/src/auth/prompt.ts +27 -0
- package/dist/generation/templates/npm-cli/src/cli.ts +53 -0
- package/dist/generation/templates/npm-cli/src/commands.ts +6 -0
- package/dist/generation/templates/npm-cli/src/customer.ts +16 -0
- package/dist/generation/templates/npm-cli/src/index.ts +11 -0
- package/dist/generation/templates/npm-cli/src/lifecycle/commands.ts +344 -0
- package/dist/generation/templates/npm-cli/src/lifecycle/config.ts +32 -0
- package/dist/generation/templates/npm-cli/src/shared/dry-run.ts +32 -0
- package/dist/generation/templates/npm-cli/src/shared/errors.ts +162 -0
- package/dist/generation/templates/npm-cli/src/shared/files.ts +45 -0
- package/dist/generation/templates/npm-cli/src/shared/http.ts +125 -0
- package/dist/generation/templates/npm-cli/src/shared/output.ts +33 -0
- package/dist/generation/templates/npm-cli/test/auth.test.ts +113 -0
- package/dist/generation/templates/npm-cli/test/fixture-cli.ts +51 -0
- package/dist/generation/templates/npm-cli/test/foundation.test.ts +316 -0
- package/dist/generation/templates/npm-cli/test/helpers.ts +34 -0
- package/dist/generation/templates/npm-cli/test/mock-lifecycle.test.ts +86 -0
- package/dist/generation/templates/npm-cli/test/run-with-mock.ts +142 -0
- package/dist/generation/templates/npm-cli/tsconfig.build.json +5 -0
- package/dist/generation/templates/npm-cli/tsconfig.json +11 -0
- package/dist/generation/templates/opencode-bootstrap/package-lock.json +402 -0
- package/dist/generation/templates/opencode-bootstrap/package.json +7 -0
- package/dist/index.js +51965 -0
- package/dist/runtime/candidate-inspection/inspect.Dockerfile +4 -0
- package/dist/runtime/candidate-inspection/install.Dockerfile +5 -0
- package/dist/runtime/candidate-inspection/registry-proxy.mjs +37 -0
- package/dist/runtime/harbor/bootstrap/package-lock.json +402 -0
- package/dist/runtime/harbor/bootstrap/package.json +7 -0
- package/dist/runtime/harbor/evaluation.Dockerfile +31 -0
- package/dist/runtime/harbor/harbor_launcher.py +62 -0
- package/dist/runtime/harbor/intermesh_opencode.py +44 -0
- package/dist/runtime/harbor/model-forwarder.mjs +52 -0
- package/dist/runtime/live-task.md +13 -0
- package/package.json +44 -0
- package/skills/inter-cli-management/SKILL.md +38 -0
- package/skills/inter-cli-management/evals/evals.json +40 -0
- package/skills/inter-context/SKILL.md +72 -0
- package/skills/inter-context/evals/cases.json +82 -0
- package/skills/inter-eval-authoring/SKILL.md +70 -0
- package/skills/inter-eval-authoring/evals/evals.json +69 -0
- package/skills/inter-eval-authoring/evals/trigger-queries.json +34 -0
- package/skills/inter-eval-authoring/references/authoring-guide.md +70 -0
- package/skills/inter-eval-authoring/references/eval-plan.example.json +92 -0
- package/skills/inter-eval-authoring/references/eval-plan.schema.json +304 -0
- package/skills/inter-evaluation/SKILL.md +51 -0
- package/skills/inter-evaluation/evals/evals.json +49 -0
- package/skills/inter-evaluation/evals/trigger-queries.json +30 -0
- package/skills/inter-generation/SKILL.md +42 -0
- package/skills/inter-generation/evals/evals.json +81 -0
- package/skills/inter-generation/evals/trigger-queries.json +58 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
function fail(message) {
|
|
6
|
+
throw new TypeError(message);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function record(value, name) {
|
|
10
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
11
|
+
fail(`${name} is invalid`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function exactKeys(value, expected, name) {
|
|
16
|
+
const actual = Object.keys(value).sort();
|
|
17
|
+
if (actual.join(',') !== [...expected].sort().join(',')) fail(`${name} has unexpected fields`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function text(value, name) {
|
|
21
|
+
if (typeof value !== 'string' || value.trim() === '') fail(`${name} is invalid`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function operation(value, name, strict = true) {
|
|
26
|
+
const item = record(value, name);
|
|
27
|
+
if (strict) exactKeys(item, ['method', 'path'], name);
|
|
28
|
+
return { method: text(item.method, `${name}.method`), path: text(item.path, `${name}.path`) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function selectedOperation(value, name) {
|
|
32
|
+
const item = record(value, name);
|
|
33
|
+
exactKeys(item, ['method', 'path', 'effect'], name);
|
|
34
|
+
return {
|
|
35
|
+
...operation(item, name, false),
|
|
36
|
+
effect: text(item.effect, `${name}.effect`),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hash(value) {
|
|
41
|
+
return createHash('sha256').update(value).digest('hex');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function canonical(value) {
|
|
45
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
46
|
+
if (typeof value === 'object' && value !== null) {
|
|
47
|
+
return `{${Object.keys(value)
|
|
48
|
+
.sort()
|
|
49
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
50
|
+
.join(',')}}`;
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function validateSourceBrief(
|
|
56
|
+
requestPath,
|
|
57
|
+
briefPath,
|
|
58
|
+
inventoryPath,
|
|
59
|
+
expectedRequestHash,
|
|
60
|
+
) {
|
|
61
|
+
const requestBytes = await readFile(requestPath);
|
|
62
|
+
if (hash(requestBytes) !== expectedRequestHash) fail('Run request hash does not match');
|
|
63
|
+
const request = record(JSON.parse(requestBytes.toString('utf8')), 'Run request');
|
|
64
|
+
const workspace = dirname(dirname(resolve(requestPath)));
|
|
65
|
+
const inventory = record(
|
|
66
|
+
JSON.parse(await readFile(inventoryPath, 'utf8')),
|
|
67
|
+
'Operation inventory',
|
|
68
|
+
);
|
|
69
|
+
const brief = record(JSON.parse(await readFile(briefPath, 'utf8')), 'Source brief');
|
|
70
|
+
const inventoryHash = hash(canonical(inventory));
|
|
71
|
+
if (request.operation_inventory_hash !== inventoryHash)
|
|
72
|
+
fail('Operation inventory hash does not match the run request');
|
|
73
|
+
if (
|
|
74
|
+
resolve(workspace, text(request.operation_inventory, 'operation_inventory')) !==
|
|
75
|
+
resolve(inventoryPath)
|
|
76
|
+
)
|
|
77
|
+
fail('Run request points to a different operation inventory');
|
|
78
|
+
const openApiBytes = await readFile(resolve(workspace, text(request.openapi, 'openapi')));
|
|
79
|
+
const openApiHash = hash(openApiBytes);
|
|
80
|
+
if (request.openapi_hash !== openApiHash || inventory.source_hash !== openApiHash)
|
|
81
|
+
fail('OpenAPI hash does not match the operation inventory');
|
|
82
|
+
const profileBytes = await readFile(
|
|
83
|
+
resolve(workspace, text(request.customer_profile, 'customer_profile')),
|
|
84
|
+
);
|
|
85
|
+
if (request.customer_profile_hash !== hash(profileBytes))
|
|
86
|
+
fail('Customer profile hash does not match the run request');
|
|
87
|
+
const profile = record(JSON.parse(profileBytes.toString('utf8')), 'Customer profile');
|
|
88
|
+
if (!Array.isArray(profile.operations) || profile.operations.length === 0)
|
|
89
|
+
fail('Customer profile has no operations');
|
|
90
|
+
const approved = new Map();
|
|
91
|
+
for (const [index, item] of profile.operations.entries()) {
|
|
92
|
+
const value = selectedOperation(item, `customer_profile.operations[${index}]`);
|
|
93
|
+
const key = `${value.method} ${value.path}`;
|
|
94
|
+
if (approved.has(key)) fail(`Customer profile operation is duplicated: ${key}`);
|
|
95
|
+
approved.set(key, value.effect);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!['1.0.0', '2.0.0'].includes(brief.schema_version))
|
|
99
|
+
fail('Source brief schema version is invalid');
|
|
100
|
+
exactKeys(
|
|
101
|
+
brief,
|
|
102
|
+
[
|
|
103
|
+
'schema_version',
|
|
104
|
+
'operation_inventory_hash',
|
|
105
|
+
'capabilities',
|
|
106
|
+
'excluded_operations',
|
|
107
|
+
'questions',
|
|
108
|
+
],
|
|
109
|
+
'Source brief',
|
|
110
|
+
);
|
|
111
|
+
if (brief.operation_inventory_hash !== inventoryHash)
|
|
112
|
+
fail('Source brief does not reference the trusted operation inventory');
|
|
113
|
+
|
|
114
|
+
if (!Array.isArray(request.selected_operations) || request.selected_operations.length === 0)
|
|
115
|
+
fail('Run request has no selected operations');
|
|
116
|
+
const selected = new Set();
|
|
117
|
+
for (const [index, item] of request.selected_operations.entries()) {
|
|
118
|
+
const value = selectedOperation(item, `selected_operations[${index}]`);
|
|
119
|
+
const key = `${value.method} ${value.path}`;
|
|
120
|
+
if (selected.has(key)) fail(`Selected operation is duplicated: ${key}`);
|
|
121
|
+
if (approved.get(key) !== value.effect)
|
|
122
|
+
fail(`Selected operation is not approved by the customer profile: ${key}`);
|
|
123
|
+
selected.add(key);
|
|
124
|
+
}
|
|
125
|
+
if (selected.size !== approved.size)
|
|
126
|
+
fail('Selected operations do not match the customer profile');
|
|
127
|
+
const available = new Set(
|
|
128
|
+
Array.isArray(inventory.operations)
|
|
129
|
+
? inventory.operations.map((item, index) => {
|
|
130
|
+
const value = operation(item, `inventory.operations[${index}]`, false);
|
|
131
|
+
return `${value.method} ${value.path}`;
|
|
132
|
+
})
|
|
133
|
+
: fail('Operation inventory has no operations'),
|
|
134
|
+
);
|
|
135
|
+
if (!Array.isArray(request.context) || request.context.length === 0)
|
|
136
|
+
fail('Run request has no context');
|
|
137
|
+
const documents = new Map();
|
|
138
|
+
for (const [index, item] of request.context.entries()) {
|
|
139
|
+
const context = record(item, `context[${index}]`);
|
|
140
|
+
exactKeys(context, ['path', 'hash'], `context[${index}]`);
|
|
141
|
+
const path = resolve(workspace, text(context.path, `context[${index}].path`));
|
|
142
|
+
const content = await readFile(path, 'utf8');
|
|
143
|
+
if (hash(content) !== context.hash) fail(`Context hash does not match: ${path}`);
|
|
144
|
+
documents.set(basename(path), content);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const covered = new Set();
|
|
148
|
+
const commands = new Set();
|
|
149
|
+
if (!Array.isArray(brief.capabilities)) fail('Source brief capabilities are invalid');
|
|
150
|
+
for (const [index, value] of brief.capabilities.entries()) {
|
|
151
|
+
const capability = record(value, `capabilities[${index}]`);
|
|
152
|
+
const capabilityKeys = [
|
|
153
|
+
'operation',
|
|
154
|
+
'effect',
|
|
155
|
+
'business_purpose',
|
|
156
|
+
'proposed_command',
|
|
157
|
+
'context_evidence',
|
|
158
|
+
...(brief.schema_version === '2.0.0' && capability.response_pattern !== undefined
|
|
159
|
+
? ['response_pattern']
|
|
160
|
+
: []),
|
|
161
|
+
];
|
|
162
|
+
exactKeys(capability, capabilityKeys, `capabilities[${index}]`);
|
|
163
|
+
const ref = operation(capability.operation, `capabilities[${index}].operation`);
|
|
164
|
+
const key = `${ref.method} ${ref.path}`;
|
|
165
|
+
if (!available.has(key) || !selected.has(key))
|
|
166
|
+
fail(`Source brief references an unselected operation: ${key}`);
|
|
167
|
+
if (capability.effect !== approved.get(key))
|
|
168
|
+
fail(`Source brief effect does not match the customer profile: ${key}`);
|
|
169
|
+
if (covered.has(key)) fail(`Source brief operation is duplicated: ${key}`);
|
|
170
|
+
text(capability.business_purpose, `capabilities[${index}].business_purpose`);
|
|
171
|
+
const command = text(capability.proposed_command, `capabilities[${index}].proposed_command`);
|
|
172
|
+
if (commands.has(command)) fail(`Source brief command is duplicated: ${command}`);
|
|
173
|
+
commands.add(command);
|
|
174
|
+
if (capability.response_pattern !== undefined && capability.response_pattern !== 'file')
|
|
175
|
+
fail(`capabilities[${index}].response_pattern must be file`);
|
|
176
|
+
if (!Array.isArray(capability.context_evidence) || capability.context_evidence.length === 0)
|
|
177
|
+
fail(`capabilities[${index}].context_evidence is invalid`);
|
|
178
|
+
for (const [evidenceIndex, evidenceValue] of capability.context_evidence.entries()) {
|
|
179
|
+
const evidence = record(evidenceValue, `context_evidence[${evidenceIndex}]`);
|
|
180
|
+
exactKeys(evidence, ['document', 'location', 'excerpt'], 'context_evidence');
|
|
181
|
+
const document = text(evidence.document, 'context_evidence.document');
|
|
182
|
+
text(evidence.location, 'context_evidence.location');
|
|
183
|
+
const excerpt = text(evidence.excerpt, 'context_evidence.excerpt');
|
|
184
|
+
if (excerpt.length > 500) fail('Context evidence excerpt is too long');
|
|
185
|
+
const content = documents.get(document);
|
|
186
|
+
if (content === undefined)
|
|
187
|
+
fail(`Source brief references an unknown context document: ${document}`);
|
|
188
|
+
if (!content.includes(excerpt)) fail(`Context evidence was not found in ${document}`);
|
|
189
|
+
}
|
|
190
|
+
covered.add(key);
|
|
191
|
+
}
|
|
192
|
+
if (!Array.isArray(brief.excluded_operations)) fail('Source brief exclusions are invalid');
|
|
193
|
+
for (const [index, value] of brief.excluded_operations.entries()) {
|
|
194
|
+
const exclusion = record(value, `excluded_operations[${index}]`);
|
|
195
|
+
exactKeys(exclusion, ['operation', 'reason'], `excluded_operations[${index}]`);
|
|
196
|
+
const ref = operation(exclusion.operation, `excluded_operations[${index}].operation`);
|
|
197
|
+
const key = `${ref.method} ${ref.path}`;
|
|
198
|
+
if (!selected.has(key)) fail(`Source brief excludes an unselected operation: ${key}`);
|
|
199
|
+
if (covered.has(key)) fail(`Source brief operation is duplicated: ${key}`);
|
|
200
|
+
text(exclusion.reason, `excluded_operations[${index}].reason`);
|
|
201
|
+
covered.add(key);
|
|
202
|
+
}
|
|
203
|
+
if (brief.excluded_operations.length > 0)
|
|
204
|
+
fail('Source brief could not support every selected operation');
|
|
205
|
+
if (covered.size !== selected.size)
|
|
206
|
+
fail('Source brief does not account for every selected operation');
|
|
207
|
+
if (!Array.isArray(brief.questions) || brief.questions.some((value) => typeof value !== 'string'))
|
|
208
|
+
fail('Source brief questions are invalid');
|
|
209
|
+
if (brief.questions.length > 0) fail('Source brief has unresolved questions');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (
|
|
213
|
+
globalThis.__INTERMESH_BUNDLED__ !== true &&
|
|
214
|
+
process.argv[1] === new URL(import.meta.url).pathname
|
|
215
|
+
) {
|
|
216
|
+
const [requestPath, briefPath, inventoryPath, expectedRequestHash] = process.argv.slice(2);
|
|
217
|
+
if (!requestPath || !briefPath || !inventoryPath || !expectedRequestHash)
|
|
218
|
+
fail('Expected request, brief, inventory and request hash');
|
|
219
|
+
await validateSourceBrief(requestPath, briefPath, inventoryPath, expectedRequestHash);
|
|
220
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
|
|
7
|
+
const execute = promisify(execFile);
|
|
8
|
+
function fail(message) {
|
|
9
|
+
throw new TypeError(message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function record(value, name) {
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`${name} is invalid`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function text(value, name) {
|
|
18
|
+
if (typeof value !== 'string' || value.trim() === '') fail(`${name} is invalid`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function operation(value, name) {
|
|
23
|
+
const item = record(value, name);
|
|
24
|
+
return {
|
|
25
|
+
method: text(item.method, `${name}.method`),
|
|
26
|
+
path: text(item.path, `${name}.path`),
|
|
27
|
+
effect: text(item.effect, `${name}.effect`),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validateManifest(value, selected) {
|
|
32
|
+
const manifest = record(value, 'Capability manifest');
|
|
33
|
+
if (!Array.isArray(manifest.commands) || !Array.isArray(manifest.excluded))
|
|
34
|
+
fail('Capability manifest is invalid');
|
|
35
|
+
if (manifest.excluded.length !== 0) fail('Capability manifest has excluded approved operations');
|
|
36
|
+
const approved = new Map(
|
|
37
|
+
selected.map((item, index) => {
|
|
38
|
+
const entry = operation(item, `selected_operations[${index}]`);
|
|
39
|
+
return [`${entry.method} ${entry.path}`, entry.effect];
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
const commands = new Set();
|
|
43
|
+
const covered = new Set();
|
|
44
|
+
for (const [index, value] of manifest.commands.entries()) {
|
|
45
|
+
const item = record(value, `commands[${index}]`);
|
|
46
|
+
const command = text(item.command, `commands[${index}].command`);
|
|
47
|
+
const entry = operation(item, `commands[${index}]`);
|
|
48
|
+
const key = `${entry.method} ${entry.path}`;
|
|
49
|
+
if (commands.has(command)) fail(`Capability command is duplicated: ${command}`);
|
|
50
|
+
if (covered.has(key)) fail(`Capability operation is duplicated: ${key}`);
|
|
51
|
+
if (approved.get(key) !== entry.effect) fail(`Capability is not approved: ${key}`);
|
|
52
|
+
commands.add(command);
|
|
53
|
+
covered.add(key);
|
|
54
|
+
}
|
|
55
|
+
if (covered.size !== approved.size)
|
|
56
|
+
fail('Capability manifest does not cover every approved operation');
|
|
57
|
+
return [...commands];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function escapePattern(value) {
|
|
61
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateSkillFrontmatter(skill) {
|
|
65
|
+
const normalized = skill.replaceAll('\r\n', '\n');
|
|
66
|
+
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/u.exec(normalized);
|
|
67
|
+
if (!match) fail('Skill must start with YAML frontmatter');
|
|
68
|
+
const fields = new Map();
|
|
69
|
+
for (const line of match[1].split('\n')) {
|
|
70
|
+
const field = /^([a-z][a-z0-9_-]*):\s*(.+)$/iu.exec(line);
|
|
71
|
+
if (field) fields.set(field[1].toLowerCase(), field[2].trim());
|
|
72
|
+
}
|
|
73
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(fields.get('name') ?? ''))
|
|
74
|
+
fail('Skill frontmatter name is invalid');
|
|
75
|
+
if (!fields.get('description')) fail('Skill frontmatter description is invalid');
|
|
76
|
+
return { name: fields.get('name'), description: fields.get('description') };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function skillLocalLinks(skill) {
|
|
80
|
+
return [...skill.matchAll(/\[[^\]]+\]\(([^)]+)\)/gu)]
|
|
81
|
+
.map((match) => match[1].trim().replace(/^<|>$/gu, '').split(/\s+/u)[0])
|
|
82
|
+
.filter((target) => !/^(?:https?:|mailto:|#)/iu.test(target));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateReadmeCommands(readme, binary, commands) {
|
|
86
|
+
const escapedBinary = escapePattern(binary);
|
|
87
|
+
const invocations = [
|
|
88
|
+
...readme.matchAll(/`([^`\n]+)`/gu),
|
|
89
|
+
...readme.matchAll(new RegExp(`^\\s*(?:\\$\\s*)?(${escapedBinary}(?:\\s+.*)?)$`, 'gmu')),
|
|
90
|
+
]
|
|
91
|
+
.map((match) => match[1].trim())
|
|
92
|
+
.filter((value) => value === binary || value.startsWith(`${binary} `));
|
|
93
|
+
const allowed = ['', '--help', 'setup', 'login', 'status', 'update', 'logout', ...commands];
|
|
94
|
+
for (const invocation of invocations) {
|
|
95
|
+
const argumentsText = invocation.slice(binary.length).trim();
|
|
96
|
+
if (
|
|
97
|
+
!allowed.some(
|
|
98
|
+
(command) => argumentsText === command || argumentsText.startsWith(`${command} `),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
fail(`README documents an unknown command: ${invocation}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function validateDocumentationBoundary({
|
|
106
|
+
readme,
|
|
107
|
+
skills,
|
|
108
|
+
contributing,
|
|
109
|
+
binary,
|
|
110
|
+
packageName,
|
|
111
|
+
commands,
|
|
112
|
+
}) {
|
|
113
|
+
for (const [value, name] of [
|
|
114
|
+
[readme, 'README'],
|
|
115
|
+
[contributing, 'CONTRIBUTING'],
|
|
116
|
+
[binary, 'Binary'],
|
|
117
|
+
[packageName, 'Package name'],
|
|
118
|
+
])
|
|
119
|
+
text(value, name);
|
|
120
|
+
if (!Array.isArray(skills) || skills.length === 0) fail('Generated Skill suite is invalid');
|
|
121
|
+
for (const [index, skill] of skills.entries()) {
|
|
122
|
+
const item = record(skill, `Skill ${index}`);
|
|
123
|
+
text(item.path, `Skill ${index} path`);
|
|
124
|
+
text(item.content, `Skill ${index} content`);
|
|
125
|
+
}
|
|
126
|
+
if (!Array.isArray(commands) || commands.some((command) => typeof command !== 'string'))
|
|
127
|
+
fail('Generated commands are invalid');
|
|
128
|
+
if (
|
|
129
|
+
/\bpnpm\s+(?:install|check|test|build|pack|mock:run)\b|\.generation(?:\/|\b)|\.opencode(?:\/|\b)|\bmock[- ]server\b|\bnode\s+dist\/index\.js\b|\/tmp\//iu.test(
|
|
130
|
+
readme,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
fail('README contains maintainer or generation instructions');
|
|
134
|
+
for (const required of [
|
|
135
|
+
`npm install --global ${packageName}`,
|
|
136
|
+
`${binary} login`,
|
|
137
|
+
`${binary} setup`,
|
|
138
|
+
`${binary} status`,
|
|
139
|
+
`${binary} update`,
|
|
140
|
+
`${binary} logout`,
|
|
141
|
+
])
|
|
142
|
+
if (!readme.includes(required)) fail(`README is missing customer guidance: ${required}`);
|
|
143
|
+
if (commands.length > 0 && !commands.some((command) => readme.includes(`${binary} ${command}`)))
|
|
144
|
+
fail('README must document a generated command');
|
|
145
|
+
validateReadmeCommands(readme, binary, commands);
|
|
146
|
+
if (
|
|
147
|
+
/replace this|unfinished|\bTBD\b|\bTODO\b|__[A-Z0-9_]+__/iu.test(
|
|
148
|
+
`${readme}\n${skills.map((skill) => skill.content).join('\n')}`,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
fail('Customer documentation contains unfinished placeholders');
|
|
152
|
+
for (const skill of skills) validateSkillFrontmatter(skill.content);
|
|
153
|
+
for (const command of ['check', 'test', 'build', 'pack', 'mock:run'])
|
|
154
|
+
if (!contributing.includes(`pnpm ${command}`))
|
|
155
|
+
fail(`CONTRIBUTING is missing maintainer command: pnpm ${command}`);
|
|
156
|
+
return skills.flatMap((skill) =>
|
|
157
|
+
skillLocalLinks(skill.content).map((target) => ({ source: skill.path, target })),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function readSkillSuite(packagePath, binary) {
|
|
162
|
+
const skillsRoot = join(packagePath, 'skills');
|
|
163
|
+
const entries = await readdir(skillsRoot, { withFileTypes: true }).catch(() =>
|
|
164
|
+
fail('Generated package has no Skill suite'),
|
|
165
|
+
);
|
|
166
|
+
const skills = [];
|
|
167
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
168
|
+
if (!entry.isDirectory()) fail('Skill suite contains a file outside a Skill folder');
|
|
169
|
+
const path = `skills/${entry.name}/SKILL.md`;
|
|
170
|
+
const content = await readFile(join(packagePath, path), 'utf8').catch(() =>
|
|
171
|
+
fail(`Skill is missing its entrypoint: ${entry.name}`),
|
|
172
|
+
);
|
|
173
|
+
const frontmatter = validateSkillFrontmatter(content);
|
|
174
|
+
if (frontmatter.name !== entry.name) fail(`Skill folder and name do not match: ${entry.name}`);
|
|
175
|
+
skills.push({
|
|
176
|
+
name: entry.name,
|
|
177
|
+
path,
|
|
178
|
+
content,
|
|
179
|
+
description: frontmatter.description,
|
|
180
|
+
role:
|
|
181
|
+
entry.name === `${binary}-cli-management`
|
|
182
|
+
? 'management'
|
|
183
|
+
: entry.name === `${binary}-workflow`
|
|
184
|
+
? 'workflow'
|
|
185
|
+
: 'focused',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const management = skills.filter((skill) => skill.role === 'management');
|
|
189
|
+
const workflow = skills.filter((skill) => skill.role === 'workflow');
|
|
190
|
+
if (management.length !== 1) fail(`Skill suite must contain ${binary}-cli-management`);
|
|
191
|
+
if (workflow.length !== 1) fail(`Skill suite must contain ${binary}-workflow`);
|
|
192
|
+
if (new Set(skills.map((skill) => skill.description)).size !== skills.length)
|
|
193
|
+
fail('Skill descriptions must be distinct');
|
|
194
|
+
for (const focused of skills.filter((skill) => skill.role === 'focused')) {
|
|
195
|
+
const expected = `../${focused.name}/SKILL.md`;
|
|
196
|
+
if (!skillLocalLinks(workflow[0].content).some((link) => link.split('#')[0] === expected))
|
|
197
|
+
fail(`Workflow Skill does not route focused Skill: ${focused.name}`);
|
|
198
|
+
}
|
|
199
|
+
return skills;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function run(command, args, options) {
|
|
203
|
+
await execute(command, args, { ...options, timeout: 120_000, maxBuffer: 10 * 1024 * 1024 });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function verifyPackage(packageDirectory, requestPath, reportPath) {
|
|
207
|
+
const offlineArgs = [];
|
|
208
|
+
const packagePath = resolve(packageDirectory);
|
|
209
|
+
const request = record(JSON.parse(await readFile(requestPath, 'utf8')), 'Run request');
|
|
210
|
+
if (!Array.isArray(request.selected_operations) || request.selected_operations.length === 0)
|
|
211
|
+
fail('Run request has no selected operations');
|
|
212
|
+
const commands = validateManifest(
|
|
213
|
+
JSON.parse(await readFile(join(packagePath, 'capabilities.json'), 'utf8')),
|
|
214
|
+
request.selected_operations,
|
|
215
|
+
);
|
|
216
|
+
const packageDocument = record(
|
|
217
|
+
JSON.parse(await readFile(join(packagePath, 'package.json'), 'utf8')),
|
|
218
|
+
'Package',
|
|
219
|
+
);
|
|
220
|
+
if (packageDocument.private !== true) fail('Generated package must remain private');
|
|
221
|
+
const binaries = record(packageDocument.bin, 'Package binaries');
|
|
222
|
+
const binaryNames = Object.keys(binaries);
|
|
223
|
+
if (binaryNames.length !== 1 || typeof binaries[binaryNames[0]] !== 'string')
|
|
224
|
+
fail('Generated package must expose one binary');
|
|
225
|
+
const binary = binaryNames[0];
|
|
226
|
+
const skills = await readSkillSuite(packagePath, binary);
|
|
227
|
+
const skillLinks = validateDocumentationBoundary({
|
|
228
|
+
readme: await readFile(join(packagePath, 'README.md'), 'utf8'),
|
|
229
|
+
skills,
|
|
230
|
+
contributing: await readFile(join(packagePath, 'CONTRIBUTING.md'), 'utf8'),
|
|
231
|
+
binary,
|
|
232
|
+
packageName: text(packageDocument.name, 'Package name'),
|
|
233
|
+
commands,
|
|
234
|
+
});
|
|
235
|
+
const linkedFiles = [];
|
|
236
|
+
for (const link of skillLinks) {
|
|
237
|
+
let decoded;
|
|
238
|
+
try {
|
|
239
|
+
decoded = decodeURIComponent(link.target.split('#')[0]);
|
|
240
|
+
} catch {
|
|
241
|
+
fail(`Skill has an invalid local link: ${link.target}`);
|
|
242
|
+
}
|
|
243
|
+
const linkedPath = resolve(packagePath, dirname(link.source), decoded);
|
|
244
|
+
if (linkedPath === packagePath || !linkedPath.startsWith(`${packagePath}${sep}`))
|
|
245
|
+
fail(`Skill local link leaves the package: ${link.target}`);
|
|
246
|
+
await readFile(linkedPath).catch(() => fail(`Skill local link is missing: ${link.target}`));
|
|
247
|
+
linkedFiles.push(relative(packagePath, linkedPath).replaceAll(sep, '/'));
|
|
248
|
+
}
|
|
249
|
+
const excludedVariables = new Set(['OPENROUTER_API_KEY', 'DEEPSEEK_API_KEY']);
|
|
250
|
+
if (typeof request.customer_profile === 'string') {
|
|
251
|
+
const workspace = dirname(dirname(resolve(requestPath)));
|
|
252
|
+
const profile = record(
|
|
253
|
+
JSON.parse(await readFile(resolve(workspace, request.customer_profile), 'utf8')),
|
|
254
|
+
'Customer profile',
|
|
255
|
+
);
|
|
256
|
+
const api = record(profile.api, 'Customer profile API');
|
|
257
|
+
const auth = record(api.auth, 'Customer profile authentication');
|
|
258
|
+
excludedVariables.add(text(auth.environment_variable, 'Authentication variable'));
|
|
259
|
+
}
|
|
260
|
+
const environment = Object.fromEntries(
|
|
261
|
+
Object.entries(process.env).filter(([name]) => !excludedVariables.has(name)),
|
|
262
|
+
);
|
|
263
|
+
const installArgs = ['install', ...offlineArgs, '--frozen-lockfile', '--ignore-scripts'];
|
|
264
|
+
await run('pnpm', installArgs, {
|
|
265
|
+
cwd: packagePath,
|
|
266
|
+
env: environment,
|
|
267
|
+
});
|
|
268
|
+
const checks = [`pnpm ${installArgs.join(' ')}`];
|
|
269
|
+
for (const name of ['check', 'test', 'build']) {
|
|
270
|
+
await run('pnpm', [name], { cwd: packagePath, env: environment });
|
|
271
|
+
checks.push(`pnpm ${name}`);
|
|
272
|
+
}
|
|
273
|
+
const tarball = resolve(dirname(reportPath), 'candidate.tgz');
|
|
274
|
+
const packed = await execute('pnpm', ['pack', '--out', tarball, '--json'], {
|
|
275
|
+
cwd: packagePath,
|
|
276
|
+
env: environment,
|
|
277
|
+
timeout: 120_000,
|
|
278
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
279
|
+
});
|
|
280
|
+
const pack = record(JSON.parse(packed.stdout), 'Pack result');
|
|
281
|
+
if (!Array.isArray(pack.files)) fail('Pack result has no files');
|
|
282
|
+
const paths = pack.files.map((value, index) =>
|
|
283
|
+
text(record(value, `Pack file ${index}`).path, `Pack file ${index}.path`),
|
|
284
|
+
);
|
|
285
|
+
for (const required of ['README.md', 'capabilities.json', 'dist/index.js'])
|
|
286
|
+
if (!paths.includes(required)) fail(`Packed candidate is missing ${required}`);
|
|
287
|
+
for (const skill of skills)
|
|
288
|
+
if (!paths.includes(skill.path)) fail(`Packed candidate is missing ${skill.path}`);
|
|
289
|
+
for (const linkedFile of linkedFiles)
|
|
290
|
+
if (!paths.includes(linkedFile)) fail(`Packed candidate is missing Skill link ${linkedFile}`);
|
|
291
|
+
if (
|
|
292
|
+
paths.some((path) =>
|
|
293
|
+
/^(?:\.generation|\.opencode|inputs|node_modules|\.env|src|test|mock|CONTRIBUTING\.md)/u.test(
|
|
294
|
+
path,
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
fail('Packed candidate contains development or generation files');
|
|
299
|
+
const installation = await mkdtemp(join(dirname(reportPath), 'candidate-install-'));
|
|
300
|
+
try {
|
|
301
|
+
await writeFile(join(installation, 'package.json'), '{"private":true}');
|
|
302
|
+
await run('pnpm', ['add', ...offlineArgs, '--ignore-scripts', tarball], {
|
|
303
|
+
cwd: installation,
|
|
304
|
+
env: environment,
|
|
305
|
+
});
|
|
306
|
+
const executable = join(installation, 'node_modules/.bin', binary);
|
|
307
|
+
for (const args of [
|
|
308
|
+
['--help'],
|
|
309
|
+
['setup', '--help'],
|
|
310
|
+
['login', '--help'],
|
|
311
|
+
['status', '--help'],
|
|
312
|
+
['update', '--help'],
|
|
313
|
+
['logout', '--help'],
|
|
314
|
+
...commands.map((command) => [...command.split(/\s+/u), '--help']),
|
|
315
|
+
])
|
|
316
|
+
await run(executable, args, { cwd: installation, env: environment });
|
|
317
|
+
} finally {
|
|
318
|
+
await rm(installation, { recursive: true, force: true });
|
|
319
|
+
}
|
|
320
|
+
const tarballHash = createHash('sha256')
|
|
321
|
+
.update(await readFile(tarball))
|
|
322
|
+
.digest('hex');
|
|
323
|
+
const report = {
|
|
324
|
+
passed: true,
|
|
325
|
+
commands,
|
|
326
|
+
skills: await Promise.all(
|
|
327
|
+
skills.map(async (skill) => ({
|
|
328
|
+
name: skill.name,
|
|
329
|
+
path: skill.path,
|
|
330
|
+
sha256: createHash('sha256')
|
|
331
|
+
.update(await readFile(join(packagePath, skill.path)))
|
|
332
|
+
.digest('hex'),
|
|
333
|
+
role: skill.role,
|
|
334
|
+
})),
|
|
335
|
+
),
|
|
336
|
+
checks: [...checks, 'pnpm pack', 'clean install', 'installed help'],
|
|
337
|
+
tarball: 'candidate.tgz',
|
|
338
|
+
tarball_sha256: tarballHash,
|
|
339
|
+
};
|
|
340
|
+
await writeFile(reportPath, JSON.stringify(report, null, 2), { flag: 'wx' });
|
|
341
|
+
return report;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (
|
|
345
|
+
globalThis.__INTERMESH_BUNDLED__ !== true &&
|
|
346
|
+
process.argv[1] === new URL(import.meta.url).pathname
|
|
347
|
+
) {
|
|
348
|
+
const [packageDirectory, requestPath, reportPath] = process.argv.slice(2);
|
|
349
|
+
try {
|
|
350
|
+
if (!packageDirectory || !requestPath || !reportPath)
|
|
351
|
+
fail('Expected package, run request and report paths');
|
|
352
|
+
await verifyPackage(packageDirectory, requestPath, reportPath);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
const finding = error instanceof Error ? error.message : 'Candidate verification failed';
|
|
355
|
+
if (reportPath) {
|
|
356
|
+
await writeFile(
|
|
357
|
+
reportPath,
|
|
358
|
+
`${JSON.stringify({ passed: false, step: 'candidate_verification', finding }, null, 2)}\n`,
|
|
359
|
+
{ flag: 'wx' },
|
|
360
|
+
).catch(() => {});
|
|
361
|
+
}
|
|
362
|
+
console.error(finding);
|
|
363
|
+
process.exitCode = 1;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
Generation and automated tests remain offline and use synthetic fixtures.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm install --frozen-lockfile
|
|
7
|
+
pnpm check
|
|
8
|
+
pnpm test
|
|
9
|
+
pnpm build
|
|
10
|
+
pnpm mock:run -- node dist/index.js <command>
|
|
11
|
+
pnpm pack
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`mock:run` starts the synthetic API, runs one command and stops the API. The loopback-only
|
|
15
|
+
`--base-url` option never sends a stored token. Human-controlled live verification happens only
|
|
16
|
+
after the exact tarball has passed offline checks and clean installation.
|
|
17
|
+
|
|
18
|
+
Define stable error codes, messages, recovery hints, safe details, retryability and exit codes in
|
|
19
|
+
`src/shared/errors.ts`. Command and transport code must throw `new CliError(cliErrors.<name>)`
|
|
20
|
+
instead of defining error copy or exit behavior inline.
|