@instruktlabs/kiln 1.0.0 → 1.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +48 -3
- package/CONTRIBUTING.md +16 -7
- package/README.md +95 -381
- package/dist/agent-run.mjs +1 -1
- package/dist/build.json +40 -28
- package/dist/cli.mjs +18 -14
- package/dist/mcp-engine.mjs +15 -11
- package/dist/mcp-server.mjs +1 -1
- package/docs/architecture.md +6 -3
- package/docs/chatgpt.md +20 -5
- package/docs/examples.md +40 -170
- package/docs/install.md +100 -35
- package/docs/migration.md +20 -0
- package/docs/sdk.md +26 -5
- package/lib/asset-widget-uri.d.ts +9 -0
- package/lib/asset-widget-uri.js +9 -0
- package/lib/cli-render-mode.js +10 -3
- package/lib/discovery/portable.d.ts +9 -0
- package/lib/discovery/portable.js +6 -0
- package/lib/engine-identity.d.ts +1 -1
- package/lib/engine-identity.js +1 -1
- package/lib/evaluator/isolation.js +19 -5
- package/lib/evaluator/probe-invariants.d.ts +11 -0
- package/lib/evaluator/probe-invariants.js +36 -0
- package/lib/evaluator/probe-worker.js +16 -4
- package/lib/loopback.d.ts +6 -0
- package/lib/loopback.js +6 -0
- package/lib/mcp-core.d.ts +209 -0
- package/lib/mcp-core.js +278 -0
- package/lib/mcp-engine.d.ts +57 -0
- package/lib/mcp-engine.js +284 -0
- package/lib/mcp-manifest.d.ts +15 -0
- package/lib/mcp-manifest.js +65 -0
- package/lib/mcp.d.ts +5 -0
- package/lib/mcp.js +2 -0
- package/lib/render-service-host.js +1 -1
- package/package.json +22 -1
- package/plugin.json +1 -1
- package/plugins/kiln-engine/.claude-plugin/icon.png +0 -0
- package/plugins/kiln-engine/.claude-plugin/plugin.json +9 -3
- package/plugins/kiln-engine/PRIVACY.md +100 -0
- package/plugins/kiln-engine/README.md +103 -18
- package/plugins/kiln-engine/{scripts/setup-workspace.mjs → bin/kiln-setup-workspace.mjs} +37 -9
- package/plugins/kiln-engine/package-provenance.json +15 -8
- package/plugins/kiln-engine/plugin.json +10 -4
- package/plugins/kiln-engine/runtime.json +10 -1
- package/plugins/kiln-engine/skills/kiln-setup-workspace/SKILL.md +17 -18
- package/plugins/kiln-engine/skills/kiln-setup-workspace/references/plugin-install.md +28 -8
- package/plugins/kiln-engine/skills/kiln-setup-workspace/references/renderer-setup.md +11 -0
- package/scripts/create-workspace.mjs +71 -13
- package/scripts/workspace-config.mjs +239 -0
- package/scripts/workspace-project.mjs +384 -0
- package/scripts/workspace-transaction.mjs +504 -0
- package/skills/kiln-setup-workspace/SKILL.md +17 -18
- package/skills/kiln-setup-workspace/references/plugin-install.md +28 -8
- package/skills/kiln-setup-workspace/references/renderer-setup.md +11 -0
- package/src/cli-render-mode.ts +9 -9
- package/src/cli.ts +3 -3
- package/src/discovery/portable.ts +9 -0
- package/src/engine-identity.ts +1 -1
- package/src/evaluator/isolation.ts +27 -6
- package/src/evaluator/probe-invariants.ts +50 -0
- package/src/evaluator/probe-worker.ts +22 -4
- package/src/mcp.ts +5 -0
- package/src/render-service-host.ts +1 -1
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/** Shared project adoption and maintenance, independent of any one agent client. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { getNodeValue, parseTree } from 'jsonc-parser';
|
|
6
|
+
import { mergeJsonConfig, mergeTomlConfig } from './workspace-config.mjs';
|
|
7
|
+
import { applyProjectEdits, readProjectFile } from './workspace-transaction.mjs';
|
|
8
|
+
|
|
9
|
+
const hash = (value) =>
|
|
10
|
+
value === undefined ? undefined : createHash('sha256').update(value).digest('hex');
|
|
11
|
+
const digestPattern = /^[a-f0-9]{64}$/;
|
|
12
|
+
const text = (value, name) => {
|
|
13
|
+
try {
|
|
14
|
+
return value === undefined ? '' : new TextDecoder('utf-8', { fatal: true }).decode(value);
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(`Invalid UTF-8 project configuration: ${name}`);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const descriptorKey = (value) => JSON.stringify([value.file, value.path]);
|
|
20
|
+
const object = (value) => value && typeof value === 'object' && !Array.isArray(value);
|
|
21
|
+
|
|
22
|
+
function configProjection(file, body) {
|
|
23
|
+
if (file === '.codex/config.toml')
|
|
24
|
+
return [{ file, kind: 'toml', path: ['mcp_servers', 'kiln_workspace'], fragment: body }];
|
|
25
|
+
if (file === 'opencode.json') {
|
|
26
|
+
const value = JSON.parse(body);
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
file,
|
|
30
|
+
kind: 'json',
|
|
31
|
+
path: ['mcp', 'kiln_workspace'],
|
|
32
|
+
value: value.mcp.kiln_workspace,
|
|
33
|
+
comments: true,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
file,
|
|
37
|
+
kind: 'json-list',
|
|
38
|
+
path: ['skills', 'paths'],
|
|
39
|
+
value: value.skills.paths[0],
|
|
40
|
+
comments: true,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
if (['.mcp.json', '.cursor/mcp.json', '.agents/mcp_config.json'].includes(file))
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
file,
|
|
48
|
+
kind: 'json',
|
|
49
|
+
path: ['mcpServers', 'kiln_workspace'],
|
|
50
|
+
value: JSON.parse(body).mcpServers.kiln_workspace,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validatePrevious(previous, allowedNames, skills, knownConfigs, allowedHarnesses) {
|
|
57
|
+
if (!previous) return;
|
|
58
|
+
if (![1, 2].includes(previous.schemaVersion))
|
|
59
|
+
throw new Error('Unsupported workspace manifest version.');
|
|
60
|
+
if (!allowedHarnesses.includes(previous.harness))
|
|
61
|
+
throw new Error('Invalid workspace harness manifest.');
|
|
62
|
+
const ownedName = (name) => {
|
|
63
|
+
if (
|
|
64
|
+
typeof name !== 'string' ||
|
|
65
|
+
name.includes('\\') ||
|
|
66
|
+
name.includes(':') ||
|
|
67
|
+
name.split('/').some((p) => !p || p === '.' || p === '..')
|
|
68
|
+
)
|
|
69
|
+
return false;
|
|
70
|
+
if (allowedNames.has(name)) return true;
|
|
71
|
+
for (const prefix of ['skills/', '.claude/skills/', '.agents/skills/']) {
|
|
72
|
+
if (name.startsWith(prefix) && skills.includes(name.slice(prefix.length).split('/')[0]))
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
for (const map of [previous.ownedFiles, previous.managedHashes, previous.generated]) {
|
|
78
|
+
if (
|
|
79
|
+
map !== undefined &&
|
|
80
|
+
(!object(map) ||
|
|
81
|
+
Object.entries(map).some(
|
|
82
|
+
([name, value]) =>
|
|
83
|
+
!ownedName(name) || typeof value !== 'string' || !digestPattern.test(value),
|
|
84
|
+
))
|
|
85
|
+
)
|
|
86
|
+
throw new Error('Invalid managed-file manifest.');
|
|
87
|
+
}
|
|
88
|
+
if (previous.schemaVersion === 2) {
|
|
89
|
+
if (!Array.isArray(previous.configEntries) || previous.configEntries.length > 32)
|
|
90
|
+
throw new Error('Invalid configuration ownership manifest.');
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
for (const entry of previous.configEntries) {
|
|
93
|
+
const key = descriptorKey(entry);
|
|
94
|
+
if (!knownConfigs.has(key) || entry.kind !== knownConfigs.get(key).kind || seen.has(key))
|
|
95
|
+
throw new Error('Invalid configuration ownership manifest.');
|
|
96
|
+
seen.add(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function listValue(content, path) {
|
|
102
|
+
const errors = [];
|
|
103
|
+
const tree = parseTree(content, errors, { allowTrailingComma: true });
|
|
104
|
+
if (errors.length || tree?.type !== 'object') throw new Error('Invalid JSON configuration.');
|
|
105
|
+
let value = getNodeValue(tree);
|
|
106
|
+
for (const part of path) {
|
|
107
|
+
if (!object(value)) throw new Error('Configuration conflict at skills.paths.');
|
|
108
|
+
value = value[part];
|
|
109
|
+
if (value === undefined) return [];
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string'))
|
|
112
|
+
throw new Error('Configuration conflict at skills.paths.');
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** All side effects go through one validated transaction after the complete plan is built. */
|
|
117
|
+
export async function setupProject({
|
|
118
|
+
root,
|
|
119
|
+
runtime,
|
|
120
|
+
harness,
|
|
121
|
+
previous,
|
|
122
|
+
metadata,
|
|
123
|
+
exists,
|
|
124
|
+
options,
|
|
125
|
+
projectFiles,
|
|
126
|
+
skillFiles,
|
|
127
|
+
guide,
|
|
128
|
+
startGuide,
|
|
129
|
+
allowedHarnesses,
|
|
130
|
+
}) {
|
|
131
|
+
if (
|
|
132
|
+
previous?.schemaVersion === 2 &&
|
|
133
|
+
(!Array.isArray(previous.harnesses) ||
|
|
134
|
+
!previous.harnesses.includes(previous.harness) ||
|
|
135
|
+
previous.harnesses.some((name) => !allowedHarnesses.includes(name)) ||
|
|
136
|
+
new Set(previous.harnesses).size !== previous.harnesses.length)
|
|
137
|
+
)
|
|
138
|
+
throw new Error('Invalid workspace harness manifest.');
|
|
139
|
+
const selected =
|
|
140
|
+
previous?.schemaVersion === 2 ? [...previous.harnesses] : previous ? [previous.harness] : [];
|
|
141
|
+
if (options.adopt || !previous) selected.push(harness);
|
|
142
|
+
const registrations = [...new Set(selected)].sort();
|
|
143
|
+
const projection = new Map();
|
|
144
|
+
const desired = {};
|
|
145
|
+
const runtimeFiles = new Set();
|
|
146
|
+
const knownConfigs = new Map();
|
|
147
|
+
const allowedNames = new Set([
|
|
148
|
+
'AGENTS.md',
|
|
149
|
+
'CLAUDE.md',
|
|
150
|
+
'START.md',
|
|
151
|
+
'.kiln/AGENTS.md',
|
|
152
|
+
'.kiln/START.md',
|
|
153
|
+
'.kiln/.gitignore',
|
|
154
|
+
]);
|
|
155
|
+
for (const name of allowedHarnesses) {
|
|
156
|
+
for (const [file, body] of Object.entries(projectFiles(name))) {
|
|
157
|
+
allowedNames.add(file);
|
|
158
|
+
for (const descriptor of configProjection(file, body))
|
|
159
|
+
knownConfigs.set(descriptorKey(descriptor), descriptor);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
validatePrevious(previous, allowedNames, metadata.skills, knownConfigs, allowedHarnesses);
|
|
163
|
+
const snapshots = new Map();
|
|
164
|
+
const read = async (name) => {
|
|
165
|
+
if (!snapshots.has(name))
|
|
166
|
+
snapshots.set(name, exists ? await readProjectFile(root, name) : undefined);
|
|
167
|
+
return snapshots.get(name);
|
|
168
|
+
};
|
|
169
|
+
for (const name of registrations) {
|
|
170
|
+
for (const [file, body] of Object.entries(projectFiles(name))) {
|
|
171
|
+
const descriptors = configProjection(file, body);
|
|
172
|
+
if (!descriptors.length) {
|
|
173
|
+
desired[file] = body;
|
|
174
|
+
runtimeFiles.add(file);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
for (const descriptor of descriptors) {
|
|
178
|
+
const key = descriptorKey(descriptor);
|
|
179
|
+
if (
|
|
180
|
+
projection.has(key) &&
|
|
181
|
+
JSON.stringify(projection.get(key)) !== JSON.stringify(descriptor)
|
|
182
|
+
)
|
|
183
|
+
throw new Error(
|
|
184
|
+
`The selected clients require conflicting configuration at ${file}. Keep their registrations separate until a compatible adapter is available.`,
|
|
185
|
+
);
|
|
186
|
+
projection.set(key, descriptor);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const configFiles = new Set([...knownConfigs.values()].map((entry) => entry.file));
|
|
191
|
+
const original =
|
|
192
|
+
previous?.schemaVersion === 2
|
|
193
|
+
? { ...previous.ownedFiles }
|
|
194
|
+
: { ...previous?.generated, ...previous?.managedHashes };
|
|
195
|
+
if (previous?.schemaVersion === 1) {
|
|
196
|
+
for (const [name, value] of Object.entries(previous.skillHashes ?? {})) {
|
|
197
|
+
if (typeof value !== 'string' || !digestPattern.test(value))
|
|
198
|
+
throw new Error('Invalid skill manifest.');
|
|
199
|
+
for (const folder of ['skills', '.claude/skills', '.agents/skills']) {
|
|
200
|
+
const path = `${folder}/${name}`;
|
|
201
|
+
// Validate before reading paths from an older manifest.
|
|
202
|
+
validatePrevious(
|
|
203
|
+
{ schemaVersion: 1, harness: previous.harness, generated: { [path]: value } },
|
|
204
|
+
allowedNames,
|
|
205
|
+
metadata.skills,
|
|
206
|
+
knownConfigs,
|
|
207
|
+
allowedHarnesses,
|
|
208
|
+
);
|
|
209
|
+
if ((await read(path)) !== undefined) original[path] ??= value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const file of configFiles) delete original[file];
|
|
214
|
+
desired['.kiln/AGENTS.md'] = guide;
|
|
215
|
+
desired['.kiln/START.md'] = registrations.map((name) => startGuide(name)).join('\n---\n\n');
|
|
216
|
+
desired['.kiln/.gitignore'] = 'programs/\n';
|
|
217
|
+
const preserved = [];
|
|
218
|
+
for (const [file, body] of Object.entries({
|
|
219
|
+
'AGENTS.md': guide,
|
|
220
|
+
'CLAUDE.md':
|
|
221
|
+
(await read('AGENTS.md')) === undefined || original['AGENTS.md']
|
|
222
|
+
? '@AGENTS.md\n'
|
|
223
|
+
: '@AGENTS.md\n@.kiln/AGENTS.md\n',
|
|
224
|
+
'START.md': desired['.kiln/START.md'],
|
|
225
|
+
})) {
|
|
226
|
+
if ((await read(file)) === undefined || original[file]) desired[file] = body;
|
|
227
|
+
else preserved.push(file);
|
|
228
|
+
}
|
|
229
|
+
const registries = new Set(['skills']);
|
|
230
|
+
for (const name of registrations) {
|
|
231
|
+
if (name === 'claude') registries.add('.claude/skills');
|
|
232
|
+
else if (name !== 'opencode') registries.add('.agents/skills');
|
|
233
|
+
}
|
|
234
|
+
const skillHashes = {};
|
|
235
|
+
for (const [name, body] of Object.entries(await skillFiles()).sort(([a], [b]) =>
|
|
236
|
+
a.localeCompare(b),
|
|
237
|
+
)) {
|
|
238
|
+
skillHashes[name] = hash(body);
|
|
239
|
+
for (const folder of registries) desired[`${folder}/${name}`] = body;
|
|
240
|
+
}
|
|
241
|
+
const edits = [],
|
|
242
|
+
files = [],
|
|
243
|
+
ownedFiles = {};
|
|
244
|
+
for (const name of [...new Set([...Object.keys(original), ...Object.keys(desired)])].sort()) {
|
|
245
|
+
if (options.repair && previous && !runtimeFiles.has(name)) {
|
|
246
|
+
if (original[name] !== undefined) ownedFiles[name] = original[name];
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const before = await read(name);
|
|
250
|
+
const after = desired[name] === undefined ? undefined : Buffer.from(desired[name]);
|
|
251
|
+
const localHash = hash(before),
|
|
252
|
+
desiredHash = hash(after),
|
|
253
|
+
previousHash = original[name];
|
|
254
|
+
const status =
|
|
255
|
+
localHash === desiredHash
|
|
256
|
+
? 'current'
|
|
257
|
+
: before === undefined
|
|
258
|
+
? 'missing'
|
|
259
|
+
: localHash === previousHash
|
|
260
|
+
? after === undefined
|
|
261
|
+
? 'retired'
|
|
262
|
+
: 'outdated'
|
|
263
|
+
: previousHash === desiredHash && after !== undefined
|
|
264
|
+
? 'customized'
|
|
265
|
+
: 'conflict';
|
|
266
|
+
if (after !== undefined) ownedFiles[name] = desiredHash;
|
|
267
|
+
if (status !== 'current') files.push({ path: name, status });
|
|
268
|
+
if (!['current', 'customized', 'conflict'].includes(status))
|
|
269
|
+
edits.push({ path: name, before, after });
|
|
270
|
+
}
|
|
271
|
+
const previousEntries = new Map(
|
|
272
|
+
(previous?.configEntries ?? []).map((value) => [descriptorKey(value), value]),
|
|
273
|
+
);
|
|
274
|
+
const configEntries = [...projection.values()];
|
|
275
|
+
for (const file of [...new Set(configEntries.map((entry) => entry.file))].sort()) {
|
|
276
|
+
const before = await read(file);
|
|
277
|
+
try {
|
|
278
|
+
let merged = text(before, file);
|
|
279
|
+
const legacyOwned =
|
|
280
|
+
previous?.schemaVersion === 1 &&
|
|
281
|
+
before !== undefined &&
|
|
282
|
+
hash(before) === (previous.generated?.[file] ?? previous.managedHashes?.[file]);
|
|
283
|
+
for (const entry of configEntries.filter((value) => value.file === file)) {
|
|
284
|
+
let prior = previousEntries.get(descriptorKey(entry));
|
|
285
|
+
if (entry.kind === 'toml') {
|
|
286
|
+
if (legacyOwned) merged = '';
|
|
287
|
+
merged = mergeTomlConfig(merged, {
|
|
288
|
+
path: entry.path,
|
|
289
|
+
fragment: entry.fragment,
|
|
290
|
+
...(prior ? { previous: prior.fragment } : {}),
|
|
291
|
+
});
|
|
292
|
+
} else if (entry.kind === 'json') {
|
|
293
|
+
if (legacyOwned) {
|
|
294
|
+
let value = JSON.parse(text(before, file));
|
|
295
|
+
for (const part of entry.path) value = value?.[part];
|
|
296
|
+
if (value !== undefined) prior = { value };
|
|
297
|
+
}
|
|
298
|
+
merged = mergeJsonConfig(
|
|
299
|
+
merged,
|
|
300
|
+
[{ path: entry.path, value: entry.value, ...(prior ? { previous: prior.value } : {}) }],
|
|
301
|
+
{ comments: entry.comments === true },
|
|
302
|
+
);
|
|
303
|
+
} else {
|
|
304
|
+
const current = listValue(merged, entry.path);
|
|
305
|
+
// Schema 1 owns the unchanged generated config as a whole. Its single
|
|
306
|
+
// skill path belongs to the original workspace, which may have moved.
|
|
307
|
+
// Never infer this ownership from a modified or adopted user config.
|
|
308
|
+
const oldPath =
|
|
309
|
+
prior?.value ?? (legacyOwned && current.length === 1 ? current[0] : undefined);
|
|
310
|
+
const value = current.filter((item) => item !== oldPath || oldPath === entry.value);
|
|
311
|
+
if (!value.includes(entry.value)) value.push(entry.value);
|
|
312
|
+
merged = mergeJsonConfig(merged, [{ path: entry.path, value, previous: current }], {
|
|
313
|
+
comments: true,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (before === undefined || !before.equals(Buffer.from(merged))) {
|
|
318
|
+
files.push({ path: file, status: before === undefined ? 'missing' : 'outdated' });
|
|
319
|
+
edits.push({ path: file, before, after: merged });
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
files.push({ path: file, status: 'conflict' });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const next = {
|
|
326
|
+
schemaVersion: 2,
|
|
327
|
+
root,
|
|
328
|
+
harness: previous?.harness ?? harness,
|
|
329
|
+
harnesses: registrations,
|
|
330
|
+
runtime,
|
|
331
|
+
runtimeVersion: metadata.runtimeVersion,
|
|
332
|
+
buildIdentity: metadata.buildIdentity,
|
|
333
|
+
runtimeHashes: metadata.runtimeHashes,
|
|
334
|
+
node: metadata.node,
|
|
335
|
+
skills: metadata.skills,
|
|
336
|
+
skillHashes: options.repair && previous ? previous.skillHashes : skillHashes,
|
|
337
|
+
ownedFiles,
|
|
338
|
+
configEntries,
|
|
339
|
+
};
|
|
340
|
+
const manifestBefore = await read('.kiln/workspace.json');
|
|
341
|
+
const manifestAfter = JSON.stringify(next);
|
|
342
|
+
const runtimeChanged =
|
|
343
|
+
!!previous &&
|
|
344
|
+
(previous.runtime !== runtime ||
|
|
345
|
+
previous.node !== next.node ||
|
|
346
|
+
previous.runtimeVersion !== next.runtimeVersion ||
|
|
347
|
+
(previous.buildIdentity ?? previous.runtimeIdentity) !== next.buildIdentity ||
|
|
348
|
+
JSON.stringify(previous.runtimeHashes) !== JSON.stringify(next.runtimeHashes) ||
|
|
349
|
+
(previous.schemaVersion === 2 && previous.root !== root));
|
|
350
|
+
const manifestChanged =
|
|
351
|
+
manifestBefore === undefined || !manifestBefore.equals(Buffer.from(manifestAfter));
|
|
352
|
+
if (manifestChanged)
|
|
353
|
+
edits.push({ path: '.kiln/workspace.json', before: manifestBefore, after: manifestAfter });
|
|
354
|
+
const conflicts = files.filter((file) => file.status === 'conflict');
|
|
355
|
+
const report = {
|
|
356
|
+
root,
|
|
357
|
+
runtime,
|
|
358
|
+
harness: next.harness,
|
|
359
|
+
harnesses: registrations,
|
|
360
|
+
store: join(root, '.kiln', 'programs'),
|
|
361
|
+
server: join(runtime, 'dist', 'mcp-server.mjs'),
|
|
362
|
+
instructions: join(root, '.kiln', 'AGENTS.md'),
|
|
363
|
+
status: edits.length || conflicts.length || runtimeChanged ? 'update-required' : 'current',
|
|
364
|
+
runtimeChanged,
|
|
365
|
+
files,
|
|
366
|
+
preserved,
|
|
367
|
+
next: 'Stop active agent/MCP sessions before setup changes. Restart afterward and accept the normal project and MCP trust prompts.',
|
|
368
|
+
};
|
|
369
|
+
if (options.check) {
|
|
370
|
+
if (exists && !conflicts.length)
|
|
371
|
+
await applyProjectEdits(root, edits, { stateDirectory: options.stateDirectory, check: true });
|
|
372
|
+
return report;
|
|
373
|
+
}
|
|
374
|
+
if (conflicts.length)
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Project setup has conflicts; no files were changed: ${conflicts.map((file) => file.path).join(', ')}. Preserve and resolve these files before retrying.`,
|
|
377
|
+
);
|
|
378
|
+
if (!exists) await mkdir(root, { recursive: true });
|
|
379
|
+
await applyProjectEdits(root, edits, {
|
|
380
|
+
stateDirectory: options.stateDirectory,
|
|
381
|
+
onStep: options.onStep,
|
|
382
|
+
});
|
|
383
|
+
return { ...report, status: 'current', changed: edits.map((edit) => edit.path) };
|
|
384
|
+
}
|