@madebywild/agent-harness-framework 1.1.0 → 1.3.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.
Files changed (97) hide show
  1. package/README.md +32 -32
  2. package/dist/cli/adapters/commander.d.ts.map +1 -1
  3. package/dist/cli/adapters/commander.js +76 -1
  4. package/dist/cli/adapters/commander.js.map +1 -1
  5. package/dist/cli/adapters/interactive.d.ts.map +1 -1
  6. package/dist/cli/adapters/interactive.js +111 -11
  7. package/dist/cli/adapters/interactive.js.map +1 -1
  8. package/dist/cli/command-registry.d.ts.map +1 -1
  9. package/dist/cli/command-registry.js +111 -4
  10. package/dist/cli/command-registry.js.map +1 -1
  11. package/dist/cli/contracts.d.ts +17 -3
  12. package/dist/cli/contracts.d.ts.map +1 -1
  13. package/dist/cli/handlers/entities.d.ts +4 -0
  14. package/dist/cli/handlers/entities.d.ts.map +1 -1
  15. package/dist/cli/handlers/entities.js +22 -0
  16. package/dist/cli/handlers/entities.js.map +1 -1
  17. package/dist/cli/handlers/init.d.ts +7 -1
  18. package/dist/cli/handlers/init.d.ts.map +1 -1
  19. package/dist/cli/handlers/init.js +41 -2
  20. package/dist/cli/handlers/init.js.map +1 -1
  21. package/dist/cli/handlers/preset.d.ts +13 -0
  22. package/dist/cli/handlers/preset.d.ts.map +1 -0
  23. package/dist/cli/handlers/preset.js +51 -0
  24. package/dist/cli/handlers/preset.js.map +1 -0
  25. package/dist/cli/renderers/text.d.ts.map +1 -1
  26. package/dist/cli/renderers/text.js +55 -0
  27. package/dist/cli/renderers/text.js.map +1 -1
  28. package/dist/delegated-init.d.ts +20 -0
  29. package/dist/delegated-init.d.ts.map +1 -0
  30. package/dist/delegated-init.js +71 -0
  31. package/dist/delegated-init.js.map +1 -0
  32. package/dist/engine/entities.d.ts +14 -1
  33. package/dist/engine/entities.d.ts.map +1 -1
  34. package/dist/engine/entities.js +263 -107
  35. package/dist/engine/entities.js.map +1 -1
  36. package/dist/engine/presets.d.ts +3 -0
  37. package/dist/engine/presets.d.ts.map +1 -0
  38. package/dist/engine/presets.js +237 -0
  39. package/dist/engine/presets.js.map +1 -0
  40. package/dist/engine/utils.d.ts +1 -0
  41. package/dist/engine/utils.d.ts.map +1 -1
  42. package/dist/engine/utils.js +12 -6
  43. package/dist/engine/utils.js.map +1 -1
  44. package/dist/engine.d.ts +13 -1
  45. package/dist/engine.d.ts.map +1 -1
  46. package/dist/engine.js +43 -1
  47. package/dist/engine.js.map +1 -1
  48. package/dist/entity-registries.d.ts +28 -2
  49. package/dist/entity-registries.d.ts.map +1 -1
  50. package/dist/entity-registries.js +209 -135
  51. package/dist/entity-registries.js.map +1 -1
  52. package/dist/loader.d.ts.map +1 -1
  53. package/dist/loader.js +123 -2
  54. package/dist/loader.js.map +1 -1
  55. package/dist/paths.d.ts +3 -0
  56. package/dist/paths.d.ts.map +1 -1
  57. package/dist/paths.js +8 -0
  58. package/dist/paths.js.map +1 -1
  59. package/dist/planner.d.ts.map +1 -1
  60. package/dist/planner.js +24 -0
  61. package/dist/planner.js.map +1 -1
  62. package/dist/preset-builtin.d.ts +3 -0
  63. package/dist/preset-builtin.d.ts.map +1 -0
  64. package/dist/preset-builtin.js +139 -0
  65. package/dist/preset-builtin.js.map +1 -0
  66. package/dist/preset-packages.d.ts +9 -0
  67. package/dist/preset-packages.d.ts.map +1 -0
  68. package/dist/preset-packages.js +109 -0
  69. package/dist/preset-packages.js.map +1 -0
  70. package/dist/presets.d.ts +12 -0
  71. package/dist/presets.d.ts.map +1 -0
  72. package/dist/presets.js +79 -0
  73. package/dist/presets.js.map +1 -0
  74. package/dist/provider-adapters/claude.d.ts.map +1 -1
  75. package/dist/provider-adapters/claude.js +44 -13
  76. package/dist/provider-adapters/claude.js.map +1 -1
  77. package/dist/provider-adapters/codex.d.ts.map +1 -1
  78. package/dist/provider-adapters/codex.js +10 -6
  79. package/dist/provider-adapters/codex.js.map +1 -1
  80. package/dist/provider-adapters/copilot.d.ts.map +1 -1
  81. package/dist/provider-adapters/copilot.js +16 -2
  82. package/dist/provider-adapters/copilot.js.map +1 -1
  83. package/dist/registry-validator.d.ts.map +1 -1
  84. package/dist/registry-validator.js +150 -7
  85. package/dist/registry-validator.js.map +1 -1
  86. package/dist/repository.d.ts.map +1 -1
  87. package/dist/repository.js +4 -0
  88. package/dist/repository.js.map +1 -1
  89. package/dist/types.d.ts +55 -3
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/types.js +2 -1
  92. package/dist/types.js.map +1 -1
  93. package/dist/utils.d.ts +10 -0
  94. package/dist/utils.d.ts.map +1 -1
  95. package/dist/utils.js +56 -0
  96. package/dist/utils.js.map +1 -1
  97. package/package.json +16 -2
@@ -1,11 +1,12 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import * as TOML from "@iarna/toml";
3
4
  import { DEFAULT_REGISTRY_ID, providerIdSchema } from "@madebywild/agent-harness-manifest";
4
5
  import { fetchEntityFromRegistry } from "../entity-registries.js";
5
- import { DEFAULT_PROMPT_SOURCE_PATH, defaultCommandOverridePath, defaultCommandSourcePath, defaultHookOverridePath, defaultHookSourcePath, defaultMcpOverridePath, defaultMcpSourcePath, defaultPromptOverridePath, defaultSkillOverridePath, defaultSkillSourcePath, defaultSubagentOverridePath, defaultSubagentSourcePath, resolveHarnessPaths, } from "../paths.js";
6
+ import { DEFAULT_PROMPT_SOURCE_PATH, defaultCommandOverridePath, defaultCommandSourcePath, defaultHookOverridePath, defaultHookSourcePath, defaultMcpOverridePath, defaultMcpSourcePath, defaultPromptOverridePath, defaultSettingsSourcePath, defaultSkillOverridePath, defaultSkillSourcePath, defaultSubagentOverridePath, defaultSubagentSourcePath, resolveHarnessPaths, } from "../paths.js";
6
7
  import { listFilesRecursively, removeIfExists, writeLock, writeManifest } from "../repository.js";
7
8
  import { CLI_ENTITY_TO_MANIFEST_ENTITY, CLI_ENTITY_TYPES } from "../types.js";
8
- import { ensureParentDir, exists, normalizeRelativePath, nowIso, sha256, stableStringify } from "../utils.js";
9
+ import { ensureParentDir, exists, normalizeRelativePath, nowIso, parseJsonAsRecord, parseTomlAsRecord, sha256, stableStringify, withSingleTrailingNewline, } from "../utils.js";
9
10
  import { readLockOrDefault, readManifestOrThrow, removeLockEntityRecord, setLockEntityRecord, upsertLockEntityRecord, writeManagedSourceIndex, } from "./state.js";
10
11
  import { computeSkillSourceSha, isSkillOverrideFile, manifestEntityTypeToCliEntityType, registryIdFromInput, resolveEntityRegistrySelection, resolveRemoveTargetId, sortEntities, validateEntityId, } from "./utils.js";
11
12
  export async function ensureOverrideFiles(cwd, entityType, entityId, existing) {
@@ -58,6 +59,12 @@ export async function readCurrentSourceSha(cwd, entity) {
58
59
  }
59
60
  return sha256(stableStringify(parsed));
60
61
  }
62
+ if (entity.type === "settings") {
63
+ const text = await fs.readFile(sourceAbs, "utf8");
64
+ const provider = resolveSettingsProviderOrThrow(entity.id);
65
+ const parsed = parseSettingsPayloadFromText(provider, text, entity.sourcePath);
66
+ return sha256(stableStringify(parsed));
67
+ }
61
68
  if (entity.type === "command") {
62
69
  const text = await fs.readFile(sourceAbs, "utf8");
63
70
  return sha256(text);
@@ -80,6 +87,29 @@ export async function loadSkillSourceHashes(skillRootAbs) {
80
87
  output.sort((left, right) => left.path.localeCompare(right.path));
81
88
  return output;
82
89
  }
90
+ function resolveSettingsProviderOrThrow(id) {
91
+ try {
92
+ return providerIdSchema.parse(id);
93
+ }
94
+ catch {
95
+ throw new Error(`Settings id must be one of: ${providerIdSchema.options.join(", ")}`);
96
+ }
97
+ }
98
+ function parseSettingsPayloadFromText(provider, text, sourcePath) {
99
+ try {
100
+ return provider === "codex" ? parseTomlAsRecord(text, TOML) : parseJsonAsRecord(text);
101
+ }
102
+ catch (error) {
103
+ const format = provider === "codex" ? "TOML" : "JSON";
104
+ throw new Error(`Settings source '${sourcePath}' is invalid ${format}: ${error instanceof Error ? error.message : "unknown error"}`);
105
+ }
106
+ }
107
+ function serializeSettingsPayload(provider, payload) {
108
+ if (provider === "codex") {
109
+ return withSingleTrailingNewline(TOML.stringify(payload));
110
+ }
111
+ return stableStringify(payload);
112
+ }
83
113
  export async function materializeFetchedEntity(cwd, entity, fetched) {
84
114
  if (entity.type === "prompt" && fetched.type === "prompt") {
85
115
  const sourceAbs = path.join(cwd, entity.sourcePath);
@@ -105,6 +135,12 @@ export async function materializeFetchedEntity(cwd, entity, fetched) {
105
135
  await fs.writeFile(sourceAbs, fetched.sourceText, "utf8");
106
136
  return;
107
137
  }
138
+ if (entity.type === "settings" && fetched.type === "settings") {
139
+ const sourceAbs = path.join(cwd, entity.sourcePath);
140
+ await ensureParentDir(sourceAbs);
141
+ await fs.writeFile(sourceAbs, serializeSettingsPayload(fetched.provider, fetched.sourcePayload), "utf8");
142
+ return;
143
+ }
108
144
  if (entity.type === "command" && fetched.type === "command") {
109
145
  const sourceAbs = path.join(cwd, entity.sourcePath);
110
146
  await ensureParentDir(sourceAbs);
@@ -146,18 +182,29 @@ export async function addPromptEntity(cwd, options) {
146
182
  if (await exists(sourceAbs)) {
147
183
  throw new Error(`Cannot add prompt because '${sourcePath}' already exists`);
148
184
  }
149
- const registry = resolveEntityRegistrySelection(manifest, options?.registry);
150
- let sourceText = "# System Prompt\n\nDescribe the core behavior for the assistant.\n";
185
+ let sourceText;
186
+ let registryId;
151
187
  let importedSourceSha256;
152
188
  let registryRevision;
153
- if (registry.definition.type === "git") {
154
- const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "prompt", "system");
155
- if (fetched.type !== "prompt") {
156
- throw new Error(`REGISTRY_FETCH_FAILED: expected prompt from registry '${registry.id}'`);
189
+ if (options?.sourceText) {
190
+ sourceText = options.sourceText;
191
+ registryId = DEFAULT_REGISTRY_ID;
192
+ }
193
+ else {
194
+ const registry = resolveEntityRegistrySelection(manifest, options?.registry);
195
+ registryId = registry.id;
196
+ if (registry.definition.type === "git") {
197
+ const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "prompt", "system");
198
+ if (fetched.type !== "prompt") {
199
+ throw new Error(`REGISTRY_FETCH_FAILED: expected prompt from registry '${registry.id}'`);
200
+ }
201
+ sourceText = fetched.sourceText;
202
+ importedSourceSha256 = fetched.importedSourceSha256;
203
+ registryRevision = fetched.registryRevision;
204
+ }
205
+ else {
206
+ sourceText = "# System Prompt\n\nDescribe the core behavior for the assistant.\n";
157
207
  }
158
- sourceText = fetched.sourceText;
159
- importedSourceSha256 = fetched.importedSourceSha256;
160
- registryRevision = fetched.registryRevision;
161
208
  }
162
209
  await ensureParentDir(sourceAbs);
163
210
  await fs.writeFile(sourceAbs, sourceText, "utf8");
@@ -165,7 +212,7 @@ export async function addPromptEntity(cwd, options) {
165
212
  manifest.entities.push({
166
213
  id: "system",
167
214
  type: "prompt",
168
- registry: registry.id,
215
+ registry: registryId,
169
216
  sourcePath,
170
217
  overrides,
171
218
  enabled: true,
@@ -176,7 +223,7 @@ export async function addPromptEntity(cwd, options) {
176
223
  await upsertLockEntityRecord(paths, manifest, {
177
224
  id: "system",
178
225
  type: "prompt",
179
- registry: registry.id,
226
+ registry: registryId,
180
227
  sourceSha256: sha256(sourceText),
181
228
  overrideSha256ByProvider: overrideShaByProvider,
182
229
  importedSourceSha256,
@@ -196,35 +243,48 @@ export async function addSkillEntity(cwd, skillId, options) {
196
243
  if (await exists(skillRootAbs)) {
197
244
  throw new Error(`Cannot add skill because '${skillRootRel}' already exists`);
198
245
  }
199
- const registry = resolveEntityRegistrySelection(manifest, options?.registry);
200
246
  let sourceSha256;
247
+ let registryId;
201
248
  let importedSourceSha256;
202
249
  let registryRevision;
203
250
  let skillFiles;
204
- if (registry.definition.type === "git") {
205
- const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "skill", skillId);
206
- if (fetched.type !== "skill") {
207
- throw new Error(`REGISTRY_FETCH_FAILED: expected skill '${skillId}' from registry '${registry.id}'`);
208
- }
209
- skillFiles = fetched.files.map((file) => ({
210
- path: file.path,
251
+ if (options?.files) {
252
+ skillFiles = options.files.map((file) => ({
253
+ path: normalizeRelativePath(file.path),
211
254
  content: file.content,
212
- sha256: file.sha256,
255
+ sha256: sha256(file.content),
213
256
  }));
214
- sourceSha256 = fetched.importedSourceSha256;
215
- importedSourceSha256 = fetched.importedSourceSha256;
216
- registryRevision = fetched.registryRevision;
257
+ sourceSha256 = computeSkillSourceSha(skillFiles.map((file) => ({ path: file.path, sha256: file.sha256 })));
258
+ registryId = DEFAULT_REGISTRY_ID;
217
259
  }
218
260
  else {
219
- const content = `---\nname: ${skillId}\ndescription: Describe what this skill does.\n---\n\n# ${skillId}\n\nAdd usage guidance here.\n`;
220
- skillFiles = [
221
- {
222
- path: "SKILL.md",
223
- content,
224
- sha256: sha256(content),
225
- },
226
- ];
227
- sourceSha256 = computeSkillSourceSha(skillFiles.map((file) => ({ path: file.path, sha256: file.sha256 })));
261
+ const registry = resolveEntityRegistrySelection(manifest, options?.registry);
262
+ registryId = registry.id;
263
+ if (registry.definition.type === "git") {
264
+ const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "skill", skillId);
265
+ if (fetched.type !== "skill") {
266
+ throw new Error(`REGISTRY_FETCH_FAILED: expected skill '${skillId}' from registry '${registry.id}'`);
267
+ }
268
+ skillFiles = fetched.files.map((file) => ({
269
+ path: file.path,
270
+ content: file.content,
271
+ sha256: file.sha256,
272
+ }));
273
+ sourceSha256 = fetched.importedSourceSha256;
274
+ importedSourceSha256 = fetched.importedSourceSha256;
275
+ registryRevision = fetched.registryRevision;
276
+ }
277
+ else {
278
+ const content = `---\nname: ${skillId}\ndescription: Describe what this skill does.\n---\n\n# ${skillId}\n\nAdd usage guidance here.\n`;
279
+ skillFiles = [
280
+ {
281
+ path: "SKILL.md",
282
+ content,
283
+ sha256: sha256(content),
284
+ },
285
+ ];
286
+ sourceSha256 = computeSkillSourceSha(skillFiles.map((file) => ({ path: file.path, sha256: file.sha256 })));
287
+ }
228
288
  }
229
289
  for (const file of skillFiles) {
230
290
  const absolute = path.join(skillRootAbs, file.path);
@@ -235,7 +295,7 @@ export async function addSkillEntity(cwd, skillId, options) {
235
295
  manifest.entities.push({
236
296
  id: skillId,
237
297
  type: "skill",
238
- registry: registry.id,
298
+ registry: registryId,
239
299
  sourcePath,
240
300
  overrides,
241
301
  enabled: true,
@@ -246,7 +306,7 @@ export async function addSkillEntity(cwd, skillId, options) {
246
306
  await upsertLockEntityRecord(paths, manifest, {
247
307
  id: skillId,
248
308
  type: "skill",
249
- registry: registry.id,
309
+ registry: registryId,
250
310
  sourceSha256,
251
311
  overrideSha256ByProvider: overrideShaByProvider,
252
312
  importedSourceSha256,
@@ -265,28 +325,36 @@ export async function addMcpEntity(cwd, configId, options) {
265
325
  if (await exists(sourceAbs)) {
266
326
  throw new Error(`Cannot add MCP config because '${sourcePath}' already exists`);
267
327
  }
268
- const registry = resolveEntityRegistrySelection(manifest, options?.registry);
269
328
  let sourceJson;
329
+ let registryId;
270
330
  let importedSourceSha256;
271
331
  let registryRevision;
272
- if (registry.definition.type === "git") {
273
- const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "mcp_config", configId);
274
- if (fetched.type !== "mcp_config") {
275
- throw new Error(`REGISTRY_FETCH_FAILED: expected mcp config '${configId}' from registry '${registry.id}'`);
276
- }
277
- sourceJson = fetched.sourceJson;
278
- importedSourceSha256 = fetched.importedSourceSha256;
279
- registryRevision = fetched.registryRevision;
332
+ if (options?.sourceJson) {
333
+ sourceJson = options.sourceJson;
334
+ registryId = DEFAULT_REGISTRY_ID;
280
335
  }
281
336
  else {
282
- sourceJson = {
283
- servers: {
284
- [configId]: {
285
- command: "echo",
286
- args: ["configure-this-mcp-server"],
337
+ const registry = resolveEntityRegistrySelection(manifest, options?.registry);
338
+ registryId = registry.id;
339
+ if (registry.definition.type === "git") {
340
+ const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "mcp_config", configId);
341
+ if (fetched.type !== "mcp_config") {
342
+ throw new Error(`REGISTRY_FETCH_FAILED: expected mcp config '${configId}' from registry '${registry.id}'`);
343
+ }
344
+ sourceJson = fetched.sourceJson;
345
+ importedSourceSha256 = fetched.importedSourceSha256;
346
+ registryRevision = fetched.registryRevision;
347
+ }
348
+ else {
349
+ sourceJson = {
350
+ servers: {
351
+ [configId]: {
352
+ command: "echo",
353
+ args: ["configure-this-mcp-server"],
354
+ },
287
355
  },
288
- },
289
- };
356
+ };
357
+ }
290
358
  }
291
359
  const sourceContent = stableStringify(sourceJson);
292
360
  await ensureParentDir(sourceAbs);
@@ -295,7 +363,7 @@ export async function addMcpEntity(cwd, configId, options) {
295
363
  manifest.entities.push({
296
364
  id: configId,
297
365
  type: "mcp_config",
298
- registry: registry.id,
366
+ registry: registryId,
299
367
  sourcePath,
300
368
  overrides,
301
369
  enabled: true,
@@ -306,7 +374,7 @@ export async function addMcpEntity(cwd, configId, options) {
306
374
  await upsertLockEntityRecord(paths, manifest, {
307
375
  id: configId,
308
376
  type: "mcp_config",
309
- registry: registry.id,
377
+ registry: registryId,
310
378
  sourceSha256: sha256(stableStringify(sourceJson)),
311
379
  overrideSha256ByProvider: overrideShaByProvider,
312
380
  importedSourceSha256,
@@ -325,23 +393,31 @@ export async function addSubagentEntity(cwd, subagentId, options) {
325
393
  if (await exists(sourceAbs)) {
326
394
  throw new Error(`Cannot add subagent because '${sourcePath}' already exists`);
327
395
  }
328
- const registry = resolveEntityRegistrySelection(manifest, options?.registry);
329
396
  let sourceText;
397
+ let registryId;
330
398
  let importedSourceSha256;
331
399
  let registryRevision;
332
- if (registry.definition.type === "git") {
333
- const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "subagent", subagentId);
334
- if (fetched.type !== "subagent") {
335
- throw new Error(`REGISTRY_FETCH_FAILED: expected subagent '${subagentId}' from registry '${registry.id}'`);
336
- }
337
- sourceText = fetched.sourceText;
338
- importedSourceSha256 = fetched.importedSourceSha256;
339
- registryRevision = fetched.registryRevision;
400
+ if (options?.sourceText) {
401
+ sourceText = options.sourceText;
402
+ registryId = DEFAULT_REGISTRY_ID;
340
403
  }
341
404
  else {
342
- sourceText =
343
- `---\nname: ${subagentId}\ndescription: Describe what this subagent does.\n---\n\n` +
344
- `You are the ${subagentId} subagent.\n\nAdd instructions here.\n`;
405
+ const registry = resolveEntityRegistrySelection(manifest, options?.registry);
406
+ registryId = registry.id;
407
+ if (registry.definition.type === "git") {
408
+ const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "subagent", subagentId);
409
+ if (fetched.type !== "subagent") {
410
+ throw new Error(`REGISTRY_FETCH_FAILED: expected subagent '${subagentId}' from registry '${registry.id}'`);
411
+ }
412
+ sourceText = fetched.sourceText;
413
+ importedSourceSha256 = fetched.importedSourceSha256;
414
+ registryRevision = fetched.registryRevision;
415
+ }
416
+ else {
417
+ sourceText =
418
+ `---\nname: ${subagentId}\ndescription: Describe what this subagent does.\n---\n\n` +
419
+ `You are the ${subagentId} subagent.\n\nAdd instructions here.\n`;
420
+ }
345
421
  }
346
422
  await ensureParentDir(sourceAbs);
347
423
  await fs.writeFile(sourceAbs, sourceText, "utf8");
@@ -349,7 +425,7 @@ export async function addSubagentEntity(cwd, subagentId, options) {
349
425
  manifest.entities.push({
350
426
  id: subagentId,
351
427
  type: "subagent",
352
- registry: registry.id,
428
+ registry: registryId,
353
429
  sourcePath,
354
430
  overrides,
355
431
  enabled: true,
@@ -360,7 +436,7 @@ export async function addSubagentEntity(cwd, subagentId, options) {
360
436
  await upsertLockEntityRecord(paths, manifest, {
361
437
  id: subagentId,
362
438
  type: "subagent",
363
- registry: registry.id,
439
+ registry: registryId,
364
440
  sourceSha256: sha256(sourceText),
365
441
  overrideSha256ByProvider: overrideShaByProvider,
366
442
  importedSourceSha256,
@@ -379,31 +455,39 @@ export async function addHookEntity(cwd, hookId, options) {
379
455
  if (await exists(sourceAbs)) {
380
456
  throw new Error(`Cannot add hook because '${sourcePath}' already exists`);
381
457
  }
382
- const registry = resolveEntityRegistrySelection(manifest, options?.registry);
383
458
  let sourceJson;
459
+ let registryId;
384
460
  let importedSourceSha256;
385
461
  let registryRevision;
386
- if (registry.definition.type === "git") {
387
- const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "hook", hookId);
388
- if (fetched.type !== "hook") {
389
- throw new Error(`REGISTRY_FETCH_FAILED: expected hook '${hookId}' from registry '${registry.id}'`);
390
- }
391
- sourceJson = fetched.sourceJson;
392
- importedSourceSha256 = fetched.importedSourceSha256;
393
- registryRevision = fetched.registryRevision;
462
+ if (options?.sourceJson) {
463
+ sourceJson = options.sourceJson;
464
+ registryId = DEFAULT_REGISTRY_ID;
394
465
  }
395
466
  else {
396
- sourceJson = {
397
- mode: "best_effort",
398
- events: {
399
- pre_tool_use: [
400
- {
401
- type: "command",
402
- command: "echo 'replace-with-hook-command'",
403
- },
404
- ],
405
- },
406
- };
467
+ const registry = resolveEntityRegistrySelection(manifest, options?.registry);
468
+ registryId = registry.id;
469
+ if (registry.definition.type === "git") {
470
+ const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "hook", hookId);
471
+ if (fetched.type !== "hook") {
472
+ throw new Error(`REGISTRY_FETCH_FAILED: expected hook '${hookId}' from registry '${registry.id}'`);
473
+ }
474
+ sourceJson = fetched.sourceJson;
475
+ importedSourceSha256 = fetched.importedSourceSha256;
476
+ registryRevision = fetched.registryRevision;
477
+ }
478
+ else {
479
+ sourceJson = {
480
+ mode: "best_effort",
481
+ events: {
482
+ pre_tool_use: [
483
+ {
484
+ type: "command",
485
+ command: "echo 'replace-with-hook-command'",
486
+ },
487
+ ],
488
+ },
489
+ };
490
+ }
407
491
  }
408
492
  const sourceContent = stableStringify(sourceJson);
409
493
  await ensureParentDir(sourceAbs);
@@ -412,7 +496,7 @@ export async function addHookEntity(cwd, hookId, options) {
412
496
  manifest.entities.push({
413
497
  id: hookId,
414
498
  type: "hook",
415
- registry: registry.id,
499
+ registry: registryId,
416
500
  sourcePath,
417
501
  overrides,
418
502
  enabled: true,
@@ -423,13 +507,70 @@ export async function addHookEntity(cwd, hookId, options) {
423
507
  await upsertLockEntityRecord(paths, manifest, {
424
508
  id: hookId,
425
509
  type: "hook",
426
- registry: registry.id,
510
+ registry: registryId,
427
511
  sourceSha256: sha256(stableStringify(sourceJson)),
428
512
  overrideSha256ByProvider: overrideShaByProvider,
429
513
  importedSourceSha256,
430
514
  registryRevision,
431
515
  });
432
516
  }
517
+ export async function addSettingsEntity(cwd, provider, options) {
518
+ const settingsProvider = providerIdSchema.parse(provider);
519
+ const paths = resolveHarnessPaths(cwd);
520
+ const manifest = await readManifestOrThrow(paths);
521
+ if (manifest.entities.some((entity) => entity.type === "settings" && entity.id === settingsProvider)) {
522
+ throw new Error(`Settings '${settingsProvider}' already exists`);
523
+ }
524
+ const sourcePath = defaultSettingsSourcePath(settingsProvider);
525
+ const sourceAbs = path.join(cwd, sourcePath);
526
+ if (await exists(sourceAbs)) {
527
+ throw new Error(`Cannot add settings because '${sourcePath}' already exists`);
528
+ }
529
+ let sourcePayload;
530
+ let registryId;
531
+ let importedSourceSha256;
532
+ let registryRevision;
533
+ if (options?.sourcePayload) {
534
+ sourcePayload = options.sourcePayload;
535
+ registryId = DEFAULT_REGISTRY_ID;
536
+ }
537
+ else {
538
+ const registry = resolveEntityRegistrySelection(manifest, options?.registry);
539
+ registryId = registry.id;
540
+ sourcePayload = {};
541
+ if (registry.definition.type === "git") {
542
+ const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "settings", settingsProvider);
543
+ if (fetched.type !== "settings") {
544
+ throw new Error(`REGISTRY_FETCH_FAILED: expected settings '${settingsProvider}' from registry '${registry.id}'`);
545
+ }
546
+ sourcePayload = fetched.sourcePayload;
547
+ importedSourceSha256 = fetched.importedSourceSha256;
548
+ registryRevision = fetched.registryRevision;
549
+ }
550
+ }
551
+ const sourceContent = serializeSettingsPayload(settingsProvider, sourcePayload);
552
+ await ensureParentDir(sourceAbs);
553
+ await fs.writeFile(sourceAbs, sourceContent, "utf8");
554
+ manifest.entities.push({
555
+ id: settingsProvider,
556
+ type: "settings",
557
+ registry: registryId,
558
+ sourcePath,
559
+ enabled: true,
560
+ });
561
+ manifest.entities = sortEntities(manifest.entities);
562
+ await writeManifest(paths, manifest);
563
+ await writeManagedSourceIndex(paths, manifest);
564
+ await upsertLockEntityRecord(paths, manifest, {
565
+ id: settingsProvider,
566
+ type: "settings",
567
+ registry: registryId,
568
+ sourceSha256: sha256(sourceContent),
569
+ overrideSha256ByProvider: {},
570
+ importedSourceSha256,
571
+ registryRevision,
572
+ });
573
+ }
433
574
  export async function addCommandEntity(cwd, commandId, options) {
434
575
  validateEntityId(commandId, "command");
435
576
  const paths = resolveHarnessPaths(cwd);
@@ -442,21 +583,29 @@ export async function addCommandEntity(cwd, commandId, options) {
442
583
  if (await exists(sourceAbs)) {
443
584
  throw new Error(`Cannot add command because '${sourcePath}' already exists`);
444
585
  }
445
- const registry = resolveEntityRegistrySelection(manifest, options?.registry);
446
586
  let sourceText;
587
+ let registryId;
447
588
  let importedSourceSha256;
448
589
  let registryRevision;
449
- if (registry.definition.type === "git") {
450
- const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "command", commandId);
451
- if (fetched.type !== "command") {
452
- throw new Error(`REGISTRY_FETCH_FAILED: expected command '${commandId}' from registry '${registry.id}'`);
453
- }
454
- sourceText = fetched.sourceText;
455
- importedSourceSha256 = fetched.importedSourceSha256;
456
- registryRevision = fetched.registryRevision;
590
+ if (options?.sourceText) {
591
+ sourceText = options.sourceText;
592
+ registryId = DEFAULT_REGISTRY_ID;
457
593
  }
458
594
  else {
459
- sourceText = `---\ndescription: "Describe what this command does"\n---\n\n# ${commandId}\n\nDescribe the task here. Use $ARGUMENTS to reference arguments passed to this command.\n`;
595
+ const registry = resolveEntityRegistrySelection(manifest, options?.registry);
596
+ registryId = registry.id;
597
+ if (registry.definition.type === "git") {
598
+ const fetched = await fetchEntityFromRegistry(registry.id, registry.definition, "command", commandId);
599
+ if (fetched.type !== "command") {
600
+ throw new Error(`REGISTRY_FETCH_FAILED: expected command '${commandId}' from registry '${registry.id}'`);
601
+ }
602
+ sourceText = fetched.sourceText;
603
+ importedSourceSha256 = fetched.importedSourceSha256;
604
+ registryRevision = fetched.registryRevision;
605
+ }
606
+ else {
607
+ sourceText = `---\ndescription: "Describe what this command does"\n---\n\n# ${commandId}\n\nDescribe the task here. Use $ARGUMENTS to reference arguments passed to this command.\n`;
608
+ }
460
609
  }
461
610
  await ensureParentDir(sourceAbs);
462
611
  await fs.writeFile(sourceAbs, sourceText, "utf8");
@@ -464,7 +613,7 @@ export async function addCommandEntity(cwd, commandId, options) {
464
613
  manifest.entities.push({
465
614
  id: commandId,
466
615
  type: "command",
467
- registry: registry.id,
616
+ registry: registryId,
468
617
  sourcePath,
469
618
  overrides,
470
619
  enabled: true,
@@ -475,7 +624,7 @@ export async function addCommandEntity(cwd, commandId, options) {
475
624
  await upsertLockEntityRecord(paths, manifest, {
476
625
  id: commandId,
477
626
  type: "command",
478
- registry: registry.id,
627
+ registry: registryId,
479
628
  sourceSha256: sha256(sourceText),
480
629
  overrideSha256ByProvider: overrideShaByProvider,
481
630
  importedSourceSha256,
@@ -532,15 +681,22 @@ export async function pullRegistryEntities(cwd, options) {
532
681
  for (const planned of plannedUpdates) {
533
682
  const { entity, fetched } = planned;
534
683
  await materializeFetchedEntity(cwd, entity, fetched);
535
- const ensuredOverrides = await ensureOverrideFiles(cwd, entity.type, entity.id, entity.overrides);
536
- entity.overrides = ensuredOverrides.overrides;
537
- manifestMutated = true;
684
+ let overrideShaByProvider = {};
685
+ if (entity.type !== "settings") {
686
+ const ensuredOverrides = await ensureOverrideFiles(cwd, entity.type, entity.id, entity.overrides);
687
+ entity.overrides = ensuredOverrides.overrides;
688
+ overrideShaByProvider = ensuredOverrides.overrideShaByProvider;
689
+ manifestMutated = true;
690
+ }
691
+ const sourceSha256ForLock = entity.type === "settings" && fetched.type === "settings"
692
+ ? sha256(serializeSettingsPayload(fetched.provider, fetched.sourcePayload))
693
+ : fetched.importedSourceSha256;
538
694
  setLockEntityRecord(lock, {
539
695
  id: entity.id,
540
696
  type: entity.type,
541
697
  registry: entity.registry,
542
- sourceSha256: fetched.importedSourceSha256,
543
- overrideSha256ByProvider: ensuredOverrides.overrideShaByProvider,
698
+ sourceSha256: sourceSha256ForLock,
699
+ overrideSha256ByProvider: overrideShaByProvider,
544
700
  importedSourceSha256: fetched.importedSourceSha256,
545
701
  registryRevision: fetched.registryRevision,
546
702
  });