@kitman-sh/cli 0.7.3 → 0.7.5

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.
@@ -13,10 +13,13 @@ import {
13
13
  none,
14
14
  isNone,
15
15
  isSome,
16
+ match,
16
17
  getOrUndefined,
18
+ flatMap,
17
19
  Service,
18
20
  get,
19
21
  merge,
22
+ Reference,
20
23
  succeed,
21
24
  fail,
22
25
  isResult,
@@ -36,11 +39,14 @@ import {
36
39
  isSuccess2,
37
40
  isFailure2,
38
41
  makeUnsafe,
42
+ make6,
39
43
  _await,
44
+ fail3,
40
45
  isDoneUnsafe,
46
+ succeed3,
41
47
  doneUnsafe,
42
48
  Scope,
43
- make6,
49
+ make7,
44
50
  provide,
45
51
  addFinalizer,
46
52
  close,
@@ -61,7 +67,7 @@ import {
61
67
  fail4,
62
68
  die3,
63
69
  withFiber,
64
- flatMap,
70
+ flatMap2,
65
71
  andThen,
66
72
  tap,
67
73
  result,
@@ -88,6 +94,7 @@ import {
88
94
  matchEffect,
89
95
  context,
90
96
  contextWith,
97
+ provide3,
91
98
  provideContext,
92
99
  updateContext,
93
100
  provideService,
@@ -106,28 +113,38 @@ import {
106
113
  logError,
107
114
  interrupt3,
108
115
  makeUnsafe3,
116
+ unbounded,
117
+ offer,
109
118
  makeUnsafe4,
110
- make9,
119
+ make11,
120
+ fromQueue,
111
121
  map4,
112
122
  filter,
113
123
  decodeText,
114
124
  splitLines,
115
125
  runHead,
126
+ runForEach,
116
127
  FileSystem,
117
128
  Path,
118
129
  ChildProcessSpawner,
119
- make11,
130
+ make13,
120
131
  Crypto,
121
132
  value,
122
133
  is,
134
+ decodeUnknownEffect,
135
+ decodeUnknownOption,
123
136
  optionalKey,
137
+ optional,
124
138
  Literal,
139
+ Unknown,
125
140
  String2,
126
141
  Boolean2,
127
142
  Struct,
128
143
  ArraySchema,
129
144
  Union,
130
145
  Literals,
146
+ NullOr,
147
+ Finite,
131
148
  Int,
132
149
  Date2,
133
150
  TaggedError2,
@@ -136,6 +153,7 @@ import {
136
153
  findForbiddenRepoBindingSecretPath,
137
154
  runnerAgentSandboxPolicy,
138
155
  agentNetworkAllowlist,
156
+ AgentHarnessIds,
139
157
  findForbiddenAgentEnvironmentSecretPath,
140
158
  redactedOutputTail,
141
159
  canonicalSha256,
@@ -147,12 +165,17 @@ import {
147
165
  encodePrettyJsonString,
148
166
  decodeJsonStringSync,
149
167
  encodeUnknownJsonStringSync,
168
+ schemaBodyJson,
169
+ filterStatusOk,
170
+ get4,
171
+ layer4,
150
172
  digestSha256,
151
173
  RunSettled,
152
174
  CommandFailed2,
153
175
  runCommand2,
154
176
  shellOutputTailChars2,
155
177
  runProcess2,
178
+ CommandRunner2,
156
179
  runShell2,
157
180
  optionalEnv,
158
181
  detectHarnesses2,
@@ -161,7 +184,7 @@ import {
161
184
  herdrRunnerCapabilitiesForRepoBinding2,
162
185
  herdrRunnerCapabilities2,
163
186
  assertHerdrRunnerCapability2
164
- } from "./chunk-8g2twwm8.js";
187
+ } from "./chunk-p2wk2v39.js";
165
188
  import {
166
189
  make4,
167
190
  readerString,
@@ -172,17 +195,17 @@ import {
172
195
  SocketUpgradeError,
173
196
  SocketCloseError,
174
197
  SocketError
175
- } from "./chunk-84t0s0s2.js";
176
- import"./chunk-0296rete.js";
198
+ } from "./chunk-tk0gy9wq.js";
199
+ import"./chunk-a44szh8v.js";
177
200
  import {
178
201
  programLease,
179
202
  programHostsEnv,
180
203
  undeclaredHostsEnv
181
- } from "./chunk-1n61xbaq.js";
182
- import"./chunk-c5nwtn11.js";
204
+ } from "./chunk-4gc7b9qt.js";
205
+ import"./chunk-1f7qe00x.js";
183
206
  import {
184
207
  nextInt
185
- } from "./chunk-x6j2j12j.js";
208
+ } from "./chunk-sq2brwmh.js";
186
209
 
187
210
  // packages/core/src/runner-trust.ts
188
211
  import { createHash } from "crypto";
@@ -195,129 +218,6 @@ function sha256(value) {
195
218
  return createHash("sha256").update(value, "utf8").digest("hex");
196
219
  }
197
220
 
198
- // packages/core/src/engineering-resources.ts
199
- var marketplace = {
200
- source: "kitman-engineering-loop",
201
- publisher: "Kitman Platform",
202
- visibility: "public",
203
- trusted: true,
204
- reviewStatus: "approved",
205
- installable: true,
206
- rollout: { channel: "default", installDefault: true, preservesExistingInstallations: true }
207
- };
208
- var kit = (input) => ({
209
- ...input,
210
- metadata: { marketplace }
211
- });
212
- var MattPocockProcessKit = kit({
213
- name: "Matt Pocock Process",
214
- slug: "matt-pocock-process",
215
- kind: "process",
216
- description: "Adversarial, type-safe implementation and review practice for Kitman Engineering.",
217
- contentMarkdown: [
218
- "# Matt Pocock Process",
219
- "",
220
- "Use fresh-context review, TDD, Effect-native boundaries, and explicit type contracts.",
221
- "",
222
- "## Complete skill collection",
223
- "",
224
- "diagnosing-bugs, tdd, effect-ts, codebase-design, api-and-interface-design, improve-codebase-architecture, doubt-driven-development, documentation-and-adrs, postgres-drizzle, security-and-hardening, source-driven-development, test-driven-development, research."
225
- ].join(`
226
- `),
227
- agentEnvironment: {
228
- skills: [
229
- {
230
- name: "research",
231
- policy: "required",
232
- source: {
233
- kind: "git",
234
- url: "https://github.com/mattpocock/skills.git",
235
- ref: "49dd158d1076134a641b33efb035946536778336",
236
- path: "skills/engineering/research"
237
- }
238
- }
239
- ]
240
- },
241
- agentInstructions: "Prefer the smallest correct change, preserve evidence, and stop on ambiguous public-interface decisions.",
242
- doNotDo: "Do not execute user-controlled code in the API or Workflow worker."
243
- });
244
- var AddyOsmaniProcessKit = kit({
245
- name: "Addy Osmani Process",
246
- slug: "addy-osmani-process",
247
- kind: "process",
248
- description: "Incremental, accessible, observable, and production-focused delivery practice for Kitman Engineering.",
249
- contentMarkdown: [
250
- "# Addy Osmani Process",
251
- "",
252
- "Use incremental implementation, browser evidence, accessibility checks, CI discipline, and simplification.",
253
- "",
254
- "## Complete skill collection",
255
- "",
256
- "incremental-implementation, frontend-ui-engineering, browser-testing-with-devtools, web-design-guidelines, ci-cd-and-automation, code-review-and-quality, code-simplification, observability-and-instrumentation, performance-optimization, shipping-and-launch, git-workflow-and-versioning."
257
- ].join(`
258
- `),
259
- agentInstructions: "Prefer verifiable slices, accessible interfaces, and evidence that survives handoff.",
260
- doNotDo: "Do not hide failures behind broad fallbacks or claim production evidence from local fixtures."
261
- });
262
- var PonytailProcessKit = kit({
263
- name: "Ponytail",
264
- slug: "ponytail",
265
- kind: "process",
266
- description: "Removes unnecessary code and complexity before implementation and review complete.",
267
- contentMarkdown: `# Ponytail
268
-
269
- Prefer deletion, existing code, and the smallest correct change. Avoid speculative abstractions.`,
270
- agentEnvironment: {
271
- plugins: [
272
- {
273
- name: "ponytail",
274
- policy: "required",
275
- source: {
276
- kind: "package",
277
- packageName: "@dietrichgebert/ponytail",
278
- version: "4.9.0"
279
- }
280
- }
281
- ]
282
- },
283
- agentInstructions: "Use the required Ponytail skill before completing implementation or review.",
284
- doNotDo: "Do not add an abstraction, dependency, or configuration without a present requirement."
285
- });
286
- var EngineeringTechnologyKitCatalog = [
287
- kit({
288
- name: "Backend",
289
- slug: "backend",
290
- kind: "technology",
291
- description: "Effect, Elysia, PostgreSQL, Drizzle, Better Auth, and backend practices.",
292
- contentMarkdown: `# Backend
293
-
294
- Effect, Elysia, PostgreSQL, Drizzle, Better Auth, API contracts, security, and observability.`
295
- }),
296
- kit({
297
- name: "Frontend",
298
- slug: "frontend",
299
- kind: "technology",
300
- description: "Nuxt, Vue, Nuxt UI, TanStack Query, VueUse, accessibility, and browser testing.",
301
- contentMarkdown: `# Frontend
302
-
303
- Nuxt, Vue, Nuxt UI, TanStack Query, VueUse, accessibility, responsive UI, and browser testing.`
304
- }),
305
- kit({
306
- name: "DevOps",
307
- slug: "devops",
308
- kind: "technology",
309
- description: "Kubernetes, Kustomize, GitLab CI, and multi-stage container builds.",
310
- contentMarkdown: `# DevOps
311
-
312
- Kubernetes, Kustomize, GitLab CI, multi-stage container builds, and operational verification.`
313
- })
314
- ];
315
- var EngineeringProcessKitCatalog = [
316
- MattPocockProcessKit,
317
- AddyOsmaniProcessKit,
318
- PonytailProcessKit
319
- ];
320
-
321
221
  // packages/agent-runtime/src/agent-environment.ts
322
222
  class AgentEnvironmentResolutionError extends TaggedError("AgentEnvironmentResolutionError") {
323
223
  get message() {
@@ -647,7 +547,7 @@ var optionalString = (value, label) => gen(function* () {
647
547
  return yield* invalid(`${label} must be a non-empty string when provided`);
648
548
  });
649
549
  // packages/agent-runtime/src/config-convergence.ts
650
- var writeAgentEnvironmentConfigFile = fnUntraced(function* (file, content) {
550
+ var writeAgentEnvironmentConfigFile = fnUntraced(function* (file, content, keep = {}) {
651
551
  const fs = yield* FileSystem;
652
552
  const paths = {
653
553
  path: yield* Path,
@@ -660,14 +560,16 @@ var writeAgentEnvironmentConfigFile = fnUntraced(function* (file, content) {
660
560
  return mergeCodexAgents(existing, content);
661
561
  if (file.endsWith("opencode.json")) {
662
562
  const managed = yield* readManagedNamesForConfig(paths, file);
663
- return mergeOpenCodeConfig(existing, content, managed.mcpNames, managed.pluginNames);
563
+ return mergeOpenCodeConfig(existing, content, managed.mcpNames, without(managed.packageNames, keep.packages));
664
564
  }
665
565
  if (file.endsWith(".codex/config.toml")) {
666
566
  const managed = yield* readManagedNamesForConfig(paths, file);
667
- return mergeCodexConfig(existing, content, managed.mcpNames, managed.skillPaths);
567
+ return mergeCodexConfig(existing, content, managed.mcpNames, managed.skillPaths, keep.kitmanPlugins === true);
568
+ }
569
+ if (isPiConfigPath(paths.path, file, "settings.json")) {
570
+ const managed = yield* readManagedNamesForConfig(paths, file);
571
+ return mergePiSettingsConfig(existing, content, without(managed.packageNames, keep.packages));
668
572
  }
669
- if (isPiConfigPath(paths.path, file, "settings.json"))
670
- return mergePiSettingsConfig(existing, content);
671
573
  if (isPiConfigPath(paths.path, file, "mcp.json")) {
672
574
  const managed = yield* readManagedNamesForConfig(paths, file);
673
575
  return mergePiMcpConfig(existing, content, managed.mcpNames);
@@ -676,6 +578,7 @@ var writeAgentEnvironmentConfigFile = fnUntraced(function* (file, content) {
676
578
  });
677
579
  yield* fs.writeFileString(file, merged);
678
580
  });
581
+ var without = (names, kept = []) => new Set([...names].filter((name) => !kept.includes(name)));
679
582
  var KitmanAgentsStartMarker = "<!-- KITMAN_AGENT_ENVIRONMENT_START -->";
680
583
  var KitmanAgentsEndMarker = "<!-- KITMAN_AGENT_ENVIRONMENT_END -->";
681
584
  var KitmanGeneratedAgentsPrefix = "# Kitman AgentEnvironment\n\nThis file is generated by `kitman sync`";
@@ -698,27 +601,34 @@ ${KitmanAgentsEndMarker}`;
698
601
  ${block}
699
602
  `;
700
603
  }
701
- var readExistingText = (file) => flatMap(FileSystem, (fs) => fs.readFileString(file).pipe(catchReason("PlatformError", "NotFound", () => succeed5(""))));
604
+ var readExistingText = (file) => flatMap2(FileSystem, (fs) => fs.readFileString(file).pipe(catchReason("PlatformError", "NotFound", () => succeed5(""))));
702
605
  var readManagedNamesForConfig = (paths, file) => gen(function* () {
703
606
  const manifest = parseJsonObject(yield* readExistingText(yield* managedManifestPathForConfig(paths, file)));
704
607
  const servers = Array.isArray(manifest.mcpServers) ? manifest.mcpServers : [];
705
608
  const plugins = Array.isArray(manifest.plugins) ? manifest.plugins : [];
706
- const materializedSkills = Array.isArray(manifest.materializedSkills) ? manifest.materializedSkills : [];
707
609
  return {
708
610
  mcpNames: new Set(servers.flatMap((entry) => {
709
611
  const item = objectValue(objectValue(entry).item);
710
612
  return isString2(item.name) ? [item.name] : [];
711
613
  })),
712
- pluginNames: new Set(plugins.flatMap((entry) => {
713
- const item = objectValue(objectValue(entry).item);
714
- return isString2(item.name) ? [item.name] : [];
614
+ packageNames: new Set(plugins.flatMap((entry) => {
615
+ const { source: legacySource, sources } = objectValue(objectValue(entry).item);
616
+ return (Array.isArray(sources) ? sources : legacySource === undefined ? [] : [legacySource]).flatMap((source) => {
617
+ const { kind, packageName } = objectValue(source);
618
+ return kind === "package" && isString2(packageName) ? [packageName] : [];
619
+ });
715
620
  })),
716
- skillPaths: new Set(materializedSkills.flatMap((entry) => {
717
- const item = objectValue(entry);
718
- return isString2(item.skillPath) ? [item.skillPath] : [];
719
- }))
621
+ skillPaths: manifestSkillPaths(manifest)
720
622
  };
721
623
  });
624
+ function manifestSkillPaths(manifest) {
625
+ const materializedSkills = Array.isArray(manifest.materializedSkills) ? manifest.materializedSkills : [];
626
+ return new Set(materializedSkills.flatMap((entry) => {
627
+ const item = objectValue(entry);
628
+ return isString2(item.skillPath) ? [item.skillPath] : [];
629
+ }));
630
+ }
631
+ var readManifestSkillPaths = (manifestPath) => map3(readExistingText(manifestPath), (text) => manifestSkillPaths(parseJsonObject(text)));
722
632
  var managedManifestPathForConfig = (paths, file) => gen(function* () {
723
633
  const fs = yield* FileSystem;
724
634
  const manifestRoot = managedManifestRootPath(paths, file);
@@ -778,13 +688,13 @@ function manifestFileForHarness(harness) {
778
688
  return "agent-environment.manifest.json";
779
689
  return `agent-environment.${harness}.manifest.json`;
780
690
  }
781
- function mergeOpenCodeConfig(existingText, generatedText, managedMcpNames, managedPluginNames) {
691
+ function mergeOpenCodeConfig(existingText, generatedText, managedMcpNames, managedPackageNames) {
782
692
  const existing = parseJsonObject(existingText);
783
693
  const generated = parseJsonObject(generatedText);
784
694
  const existingMcp = { ...objectValue(existing.mcp) };
785
695
  for (const name of managedMcpNames)
786
696
  delete existingMcp[name];
787
- const existingPlugins = uniqueStrings(arrayValue(existing.plugin).filter((entry) => !pluginReferenceMatchesManaged(entry, managedPluginNames)));
697
+ const existingPlugins = uniqueStrings(arrayValue(existing.plugin).filter((entry) => !isManagedPackageReference(entry, managedPackageNames)));
788
698
  const merged = {
789
699
  ...existing,
790
700
  ...generated,
@@ -802,22 +712,25 @@ function mergeOpenCodeConfig(existingText, generatedText, managedMcpNames, manag
802
712
  return `${JSON.stringify(merged, null, 2)}
803
713
  `;
804
714
  }
805
- function pluginReferenceMatchesManaged(entry, managedPluginNames) {
715
+ function isManagedPackageReference(entry, managedPackageNames, prefix = "") {
806
716
  if (!isString2(entry))
807
717
  return false;
808
- for (const name of managedPluginNames) {
809
- if (entry === name || entry.startsWith(`${name}@`))
718
+ for (const name of managedPackageNames) {
719
+ if (entry === `${prefix}${name}` || entry.startsWith(`${prefix}${name}@`))
810
720
  return true;
811
721
  }
812
722
  return false;
813
723
  }
814
- function mergePiSettingsConfig(existingText, generatedText) {
724
+ function mergePiSettingsConfig(existingText, generatedText, managedPackageNames) {
815
725
  const existing = parseJsonObject(existingText);
816
726
  const generated = parseJsonObject(generatedText);
817
727
  const merged = {
818
728
  ...existing,
819
729
  ...generated,
820
- packages: uniquePackageEntries([...packageEntries(existing.packages), ...packageEntries(generated.packages)])
730
+ packages: uniquePackageEntries([
731
+ ...packageEntries(existing.packages).filter((entry) => !isManagedPackageReference(entry, managedPackageNames, "npm:")),
732
+ ...packageEntries(generated.packages)
733
+ ])
821
734
  };
822
735
  return `${JSON.stringify(merged, null, 2)}
823
736
  `;
@@ -855,8 +768,8 @@ function isPiConfigPath(path, file, name) {
855
768
  const parent = path.dirname(file);
856
769
  return path.basename(file) === name && (path.basename(parent) === ".pi" || path.basename(parent) === "agent" && path.basename(path.dirname(parent)) === ".pi");
857
770
  }
858
- function mergeCodexConfig(existingText, generatedText, managedMcpNames, managedSkillPaths = new Set) {
859
- const preserved = stripTomlTables(existingText, new Set([...managedMcpNames, ...mcpServerNames(generatedText)]), new Set([...managedSkillPaths, ...codexSkillPaths(generatedText)])).trimEnd();
771
+ function mergeCodexConfig(existingText, generatedText, managedMcpNames, managedSkillPaths, keepKitmanPlugins) {
772
+ const preserved = stripTomlTables(existingText, new Set([...managedMcpNames, ...mcpServerNames(generatedText)]), new Set([...managedSkillPaths, ...codexSkillPaths(generatedText)]), keepKitmanPlugins ? undefined : kitmanPluginNames(generatedText)).trimEnd();
860
773
  const generated = generatedText.trim();
861
774
  if (preserved.length === 0)
862
775
  return `${generated}
@@ -869,13 +782,17 @@ function mergeCodexConfig(existingText, generatedText, managedMcpNames, managedS
869
782
  ${generated}
870
783
  `;
871
784
  }
872
- function stripTomlTables(text, removeMcpServerNames, removeSkillPaths) {
785
+ function stripTomlTables(text, removeMcpServerNames, removeSkillPaths, keptKitmanPlugins) {
873
786
  const lines = text.split(`
874
787
  `);
788
+ const staleHookState = (key) => {
789
+ const plugin = /^([^@]+)@kitman:/u.exec(key ?? "")?.[1];
790
+ return keptKitmanPlugins !== undefined && plugin !== undefined && !keptKitmanPlugins.has(plugin);
791
+ };
875
792
  const kept = [];
876
793
  for (let index = 0;index < lines.length; ) {
877
794
  const line = lines[index] ?? "";
878
- const table = /^(\[\[?)([^\]]+)\]\]?/.exec(line)?.[2];
795
+ const table = tomlTableHeader(line);
879
796
  if (table === undefined) {
880
797
  kept.push(line);
881
798
  index += 1;
@@ -883,23 +800,65 @@ function stripTomlTables(text, removeMcpServerNames, removeSkillPaths) {
883
800
  }
884
801
  const block = [line];
885
802
  index += 1;
886
- while (index < lines.length && !/^\[\[?[^\]]+\]\]?/.test(lines[index] ?? "")) {
803
+ while (index < lines.length && tomlTableHeader(lines[index] ?? "") === undefined) {
887
804
  block.push(lines[index] ?? "");
888
805
  index += 1;
889
806
  }
890
807
  const skillPath = table === "skills.config" ? codexSkillPathFromBlock(block) : undefined;
891
- const drop = codexMcpServerName(table, removeMcpServerNames) !== undefined || skillPath !== undefined && removeSkillPaths.has(skillPath);
892
- if (!drop)
808
+ const [first, second, hookKey, ...rest] = tomlKeyPath(table);
809
+ const hookState = first === "hooks" && second === "state";
810
+ const drop = keptKitmanPlugins !== undefined && isKitmanPluginTable([first ?? "", second ?? ""]) || hookState && rest.length === 0 && staleHookState(hookKey) || codexMcpServerName(table, removeMcpServerNames) !== undefined || skillPath !== undefined && removeSkillPaths.has(skillPath);
811
+ if (drop)
812
+ continue;
813
+ if (hookState && hookKey === undefined)
814
+ kept.push(...block.filter((entry) => !staleHookState(tomlKeyPath(/^([^=]+)=/u.exec(entry)?.[1] ?? "")[0])));
815
+ else
893
816
  kept.push(...block);
894
817
  }
895
818
  return kept.join(`
896
819
  `);
897
820
  }
821
+ function tomlTableHeader(line) {
822
+ return /^\s*\[\[?((?:"(?:[^"\\]|\\.)*"|'[^']*'|[^"'[\]#])+)\]\]?\s*(?:#.*)?$/u.exec(line)?.[1];
823
+ }
824
+ function kitmanPluginNames(toml) {
825
+ return new Set(toml.split(`
826
+ `).flatMap((line) => {
827
+ const [table, name] = tomlKeyPath(tomlTableHeader(line) ?? "");
828
+ return table === "plugins" && name?.endsWith("@kitman") === true ? [name.slice(0, -"@kitman".length)] : [];
829
+ }));
830
+ }
831
+ function isKitmanPluginTable([table, name]) {
832
+ return table === "marketplaces" && name === "kitman" || table === "plugins" && name?.endsWith("@kitman") === true;
833
+ }
834
+ function tomlKeyPath(key) {
835
+ const part = /\s*(?:"((?:[^"\\]|\\.)*)"|'([^']*)'|([\w-]+))\s*(?:\.|$)/uy;
836
+ const parts = [];
837
+ while (part.lastIndex < key.length) {
838
+ const match = part.exec(key);
839
+ if (match === null)
840
+ return [];
841
+ parts.push(match[3] ?? match[2] ?? unescapeTomlString(match[1] ?? ""));
842
+ }
843
+ return parts;
844
+ }
845
+ function unescapeTomlString(text) {
846
+ const escapes = new Map([
847
+ ["b", "\b"],
848
+ ["t", "\t"],
849
+ ["n", `
850
+ `],
851
+ ["f", "\f"],
852
+ ["r", "\r"],
853
+ ["e", "\x1B"]
854
+ ]);
855
+ return text.replace(/\\(?:u([\da-fA-F]{4})|U([\da-fA-F]{8})|(.))/gu, (_, short, long, char) => short !== undefined || long !== undefined ? String.fromCodePoint(Number.parseInt(short ?? long, 16)) : escapes.get(char) ?? char);
856
+ }
898
857
  function mcpServerNames(toml) {
899
858
  const names = new Set;
900
859
  for (const line of toml.split(`
901
860
  `)) {
902
- const table = /^\[([^\]]+)\]/.exec(line)?.[1];
861
+ const table = tomlTableHeader(line);
903
862
  if (table === undefined)
904
863
  continue;
905
864
  const name = codexMcpServerTableName(table);
@@ -914,13 +873,13 @@ function codexSkillPaths(toml) {
914
873
  `);
915
874
  for (let index = 0;index < lines.length; ) {
916
875
  const line = lines[index] ?? "";
917
- if (!line.startsWith("[[skills.config]]")) {
876
+ if (!/^\s*\[\[\s*skills\.config\s*\]\]/u.test(line)) {
918
877
  index += 1;
919
878
  continue;
920
879
  }
921
880
  const block = [line];
922
881
  index += 1;
923
- while (index < lines.length && !/^\[\[?[^\]]+\]\]?/.test(lines[index] ?? "")) {
882
+ while (index < lines.length && tomlTableHeader(lines[index] ?? "") === undefined) {
924
883
  block.push(lines[index] ?? "");
925
884
  index += 1;
926
885
  }
@@ -961,6 +920,46 @@ function uniqueStrings(values) {
961
920
  return [...new Set(values)];
962
921
  }
963
922
 
923
+ // packages/agent-runtime/src/source-resolver.ts
924
+ import { tmpdir } from "os";
925
+ class SourceResolutionFailed extends TaggedError("SourceResolutionFailed") {
926
+ get message() {
927
+ return this.reason;
928
+ }
929
+ }
930
+ var isCommitHash = (ref) => ref !== undefined && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(ref);
931
+ var DistTags = Struct({ latest: String2 });
932
+ var gitCommit = (url, ref) => gen(function* () {
933
+ const patterns = ref === undefined ? ["HEAD"] : [ref, `${ref}^{}`];
934
+ const output = yield* runCommand2("git", ["ls-remote", "--", url, ...patterns], {
935
+ cwd: tmpdir(),
936
+ timeout: "1 minute"
937
+ }).pipe(mapError((error) => new SourceResolutionFailed({ reason: error.message })));
938
+ const lines = output.split(`
939
+ `).map((line) => line.split("\t"));
940
+ const names = ref === undefined ? ["HEAD"] : ref.startsWith("refs/") ? [ref] : [`refs/heads/${ref}`, `refs/tags/${ref}`];
941
+ const commit = (lines.find(([, name]) => names.some((candidate) => name === `${candidate}^{}`)) ?? lines.find(([, name]) => names.includes(name ?? "")))?.[0];
942
+ if (!isCommitHash(commit))
943
+ return yield* new SourceResolutionFailed({ reason: `${url} has no ${ref ?? "default branch"}` });
944
+ return commit;
945
+ });
946
+ var npmLatest = (packageName) => get4(`https://registry.npmjs.org/-/package/${packageName.replace("/", "%2F")}/dist-tags`).pipe(flatMap2(filterStatusOk), flatMap2(schemaBodyJson(DistTags)), map3((tags) => tags.latest), provide3(layer4), mapError((error) => new SourceResolutionFailed({ reason: `npm has no latest ${packageName}: ${error.message}` })));
947
+ var SourceResolver = Reference("kitman/agent-runtime/SourceResolver", {
948
+ defaultValue: () => ({ gitCommit, npmLatest })
949
+ });
950
+ var withSourceResolutionCache = (effect) => gen(function* () {
951
+ const resolver = yield* SourceResolver;
952
+ const answers = new Map;
953
+ const once = (key, resolve) => suspend(() => answers.get(key) ?? resolve.pipe(exit, flatMap2((exit) => {
954
+ answers.set(key, exit);
955
+ return exit;
956
+ })));
957
+ return yield* effect.pipe(provideService(SourceResolver, {
958
+ gitCommit: (url, ref) => once(`git:${url}#${ref ?? ""}`, resolver.gitCommit(url, ref)),
959
+ npmLatest: (packageName) => once(`npm:${packageName}`, resolver.npmLatest(packageName))
960
+ }));
961
+ });
962
+
964
963
  // packages/agent-runtime/src/config-writer.ts
965
964
  var Sources = {
966
965
  opencodeMcp: "https://opencode.ai/docs/mcp-servers/",
@@ -1005,16 +1004,17 @@ var previewAgentEnvironmentConfigSync = fnUntraced(function* (previewInput) {
1005
1004
  const syncKitPath = input.syncKit === undefined ? undefined : findForbiddenAgentEnvironmentSecretPath(input.syncKit);
1006
1005
  if (syncKitPath !== undefined)
1007
1006
  plan.issues.push({ message: `Sync Kit contains forbidden secret-like value at ${syncKitPath}` });
1008
- if (input.environment.harness === "opencode")
1009
- renderOpenCode(input, plan);
1010
- else if (input.environment.harness === "codex")
1011
- renderCodex(input, plan);
1012
- else if (input.environment.harness === "claude-code")
1013
- renderClaudeCode(input, plan);
1014
- else if (input.environment.harness === "pi")
1015
- renderPi(input, plan);
1016
- renderSkillFiles(input, plan);
1017
- renderManifest(input, plan);
1007
+ const context = { ...input, ...yield* resolvePackagePlugins(input, plan) };
1008
+ if (context.environment.harness === "opencode")
1009
+ renderOpenCode(context, plan);
1010
+ else if (context.environment.harness === "codex")
1011
+ renderCodex(context, plan);
1012
+ else if (context.environment.harness === "claude-code")
1013
+ renderClaudeCode(context, plan);
1014
+ else if (context.environment.harness === "pi")
1015
+ renderPi(context, plan);
1016
+ renderSkillFiles(context, plan);
1017
+ renderManifest(context, plan);
1018
1018
  if (plan.operations.some((operation) => isUnsafeConfigWritePath(operation.path))) {
1019
1019
  plan.issues.push({ message: "AgentEnvironment config sync produced an unsafe relative write path." });
1020
1020
  }
@@ -1027,13 +1027,53 @@ var previewAgentEnvironmentConfigSync = fnUntraced(function* (previewInput) {
1027
1027
  ...input.syncKit === undefined ? undefined : { syncKitHash: input.syncKit.syncKitHash, kitRefs: input.syncKit.kitRefs }
1028
1028
  };
1029
1029
  });
1030
+ var resolvePackagePlugins = (input, plan) => gen(function* () {
1031
+ const harness = input.environment.harness;
1032
+ if (input.noInstall === true || harness !== "opencode" && harness !== "pi")
1033
+ return { environment: input.environment, unresolvedPlugins: [] };
1034
+ const resolver = yield* SourceResolver;
1035
+ const unresolvedPlugins = [];
1036
+ const plugins = yield* forEach(input.environment.plugins, (plugin) => {
1037
+ const source = packageSource(plugin);
1038
+ if (source === undefined || source.version !== undefined && input.versions !== "latest")
1039
+ return succeed5([plugin]);
1040
+ return resolver.npmLatest(source.packageName).pipe(map3((version) => [
1041
+ {
1042
+ ...plugin,
1043
+ item: {
1044
+ ...plugin.item,
1045
+ sources: [
1046
+ { ...source, version },
1047
+ ...plugin.item.sources.filter((candidate) => candidate !== source)
1048
+ ]
1049
+ }
1050
+ }
1051
+ ]), catch_((error) => sync(() => {
1052
+ const issue = {
1053
+ message: `Kitman could not resolve plugin ${plugin.item.name} from npm package ${source.packageName}: ${error.message}`,
1054
+ source: sourceLabel2(plugin.source)
1055
+ };
1056
+ if (plugin.item.policy === "required")
1057
+ plan.issues.push(issue);
1058
+ else {
1059
+ plan.warnings.push(issue);
1060
+ unresolvedPlugins.push(plugin);
1061
+ }
1062
+ return [];
1063
+ })));
1064
+ });
1065
+ return { environment: { ...input.environment, plugins: plugins.flat() }, unresolvedPlugins };
1066
+ });
1030
1067
  var applyAgentEnvironmentConfigSync = fnUntraced(function* (plan) {
1031
1068
  if (plan.operations.some((operation) => isUnsafeConfigWritePath(operation.path)))
1032
1069
  return yield* new AgentEnvironmentConfigSyncError({
1033
1070
  issues: [{ message: "AgentEnvironment config apply rejected an unsafe relative write path." }]
1034
1071
  });
1035
1072
  for (const operation of plan.operations)
1036
- yield* writeAgentEnvironmentConfigFile(operation.path, operation.content);
1073
+ yield* writeAgentEnvironmentConfigFile(operation.path, operation.content, {
1074
+ kitmanPlugins: operation.keepKitmanPlugins === true,
1075
+ ...operation.keepPackages === undefined ? undefined : { packages: operation.keepPackages }
1076
+ });
1037
1077
  return plan;
1038
1078
  });
1039
1079
  var SafeModelReference = /^(?!.*(?:https?:\/\/|ftp:\/\/|file:\/\/|Bearer\s|secret|token|password))(?!.*(?:^|[\s])(?:\/|~\/|[A-Za-z]:\\\\))[A-Za-z0-9][A-Za-z0-9_.:/@-]{0,239}$/iu;
@@ -1096,6 +1136,7 @@ function renderOpenCode(input, plan) {
1096
1136
  kind: "write_file",
1097
1137
  path: targetPath(input, "opencode.json"),
1098
1138
  description: "OpenCode project config for Kitman AgentEnvironment MCP servers and package plugins.",
1139
+ ...keepUnresolvedPackages(input),
1099
1140
  content: json({
1100
1141
  $schema: "https://opencode.ai/config.json",
1101
1142
  ...model === undefined ? undefined : { model },
@@ -1115,14 +1156,18 @@ function renderCodex(input, plan) {
1115
1156
  if (block !== undefined)
1116
1157
  serverBlocks.push(block);
1117
1158
  }
1118
- renderUnsupportedPlugins(input.environment.plugins, input.environment.harness, plan);
1159
+ const materializedPlugins = input.materializedPlugins ?? [];
1160
+ const pluginBlocks = renderCodexPlugins(materializedPlugins);
1161
+ for (const plugin of unhandledPlugins(input))
1162
+ reportUnsupported(plan, plugin.item.policy, `plugin ${plugin.item.name}`, input.environment.harness, input.scope !== "user" && plugin.item.sources.some((source) => source.kind === "git") ? "Codex git plugins install only at user scope" : unsupportedPluginReason(plugin), sourceLabel2(plugin.source));
1119
1163
  renderCodexInstructions(input, plan);
1120
1164
  plan.operations.push({
1121
1165
  kind: "write_file",
1122
1166
  path: targetPath(input, ".codex/config.toml"),
1123
1167
  description: "Codex project config.toml MCP server entries for Kitman AgentEnvironment.",
1124
- content: serverBlocks.length === 0 && model === undefined ? `# Kitman Codex MCP config generated by kitman sync
1125
- ` : `${[...model === undefined ? [] : [`model = ${tomlString(model)}`], ...serverBlocks].join(`
1168
+ ...(input.skippedPlugins ?? []).length === 0 ? undefined : { keepKitmanPlugins: true },
1169
+ content: serverBlocks.length === 0 && pluginBlocks.length === 0 && model === undefined ? `# Kitman Codex MCP config generated by kitman sync
1170
+ ` : `${[...model === undefined ? [] : [`model = ${tomlString(model)}`], ...serverBlocks, ...pluginBlocks].join(`
1126
1171
 
1127
1172
  `)}
1128
1173
  `,
@@ -1150,7 +1195,7 @@ function renderCodexInstructions(input, plan) {
1150
1195
  "",
1151
1196
  "## Plugins",
1152
1197
  "",
1153
- ...markdownList(input.environment.plugins.map((entry) => `${entry.item.name} (${entry.item.policy}, ${entry.item.source.kind})`)),
1198
+ ...markdownList(input.environment.plugins.map((entry) => `${entry.item.name} (${entry.item.policy}, ${sourceKinds(entry, ", ")})`)),
1154
1199
  "",
1155
1200
  "## Harness Settings",
1156
1201
  "",
@@ -1193,7 +1238,7 @@ function renderCodexInstructions(input, plan) {
1193
1238
  }
1194
1239
  function renderClaudeCode(input, plan) {
1195
1240
  const mcpServers = objectMap(input.environment.mcpServers, (server) => renderClaudeMcpServer(input, server, plan));
1196
- renderUnsupportedPlugins(input.environment.plugins, input.environment.harness, plan);
1241
+ renderUnsupportedPlugins(unhandledPlugins(input), input.environment.harness, plan);
1197
1242
  const model = renderModelSetting(input, plan);
1198
1243
  const skillOverrides = Object.fromEntries((input.materializedSkills ?? []).map((skill) => [skill.name, "on"]));
1199
1244
  if (Object.keys(mcpServers).length > 0 || Object.keys(skillOverrides).length > 0) {
@@ -1226,18 +1271,17 @@ function renderPi(input, plan) {
1226
1271
  ...Object.keys(mcpServers).length === 0 ? [] : ["npm:pi-mcp-adapter"],
1227
1272
  ...renderPiPlugins(input.environment.plugins, plan)
1228
1273
  ];
1229
- if (packages.length > 0 || model !== undefined) {
1230
- plan.operations.push({
1231
- kind: "write_file",
1232
- path: targetPath(input, ".pi/settings.json"),
1233
- description: "Pi project settings generated from resolved non-secret MCP and model intent.",
1234
- content: json({
1235
- ...packages.length === 0 ? undefined : { packages },
1236
- ...model === undefined ? undefined : { model }
1237
- }),
1238
- sources: [Sources.piReadme, Sources.piMcpAdapter]
1239
- });
1240
- }
1274
+ plan.operations.push({
1275
+ kind: "write_file",
1276
+ path: targetPath(input, ".pi/settings.json"),
1277
+ description: "Pi project settings generated from resolved non-secret MCP and model intent.",
1278
+ ...keepUnresolvedPackages(input),
1279
+ content: json({
1280
+ ...packages.length === 0 ? undefined : { packages },
1281
+ ...model === undefined ? undefined : { model }
1282
+ }),
1283
+ sources: [Sources.piReadme, Sources.piMcpAdapter]
1284
+ });
1241
1285
  plan.operations.push({
1242
1286
  kind: "write_file",
1243
1287
  path: targetPath(input, ".pi/mcp.json"),
@@ -1249,8 +1293,9 @@ function renderPi(input, plan) {
1249
1293
  function renderPiPlugins(plugins, plan) {
1250
1294
  const packages = [];
1251
1295
  for (const plugin of plugins) {
1252
- if (plugin.item.source.kind === "package") {
1253
- packages.push(`npm:${packageReference(plugin.item.source.packageName, plugin.item.source.version)}`);
1296
+ const source = packageSource(plugin);
1297
+ if (source) {
1298
+ packages.push(`npm:${packageReference(source.packageName, source.version)}`);
1254
1299
  continue;
1255
1300
  }
1256
1301
  reportUnsupported(plan, plugin.item.policy, `plugin ${plugin.item.name}`, "pi", "Pi package plugins require an npm package source", sourceLabel2(plugin.source));
@@ -1373,20 +1418,52 @@ function hasNameArgument(args) {
1373
1418
  function renderOpenCodePlugins(plugins, plan) {
1374
1419
  const packages = [];
1375
1420
  for (const plugin of plugins) {
1376
- if (plugin.item.source.kind === "package") {
1377
- packages.push(packageReference(plugin.item.source.packageName, plugin.item.source.version));
1421
+ const source = packageSource(plugin);
1422
+ if (source) {
1423
+ packages.push(packageReference(source.packageName, source.version));
1378
1424
  continue;
1379
1425
  }
1380
1426
  reportUnsupported(plan, plugin.item.policy, `plugin ${plugin.item.name}`, "opencode", "this writer only has source-verified OpenCode plugin config for package sources", sourceLabel2(plugin.source));
1381
1427
  }
1382
1428
  return packages;
1383
1429
  }
1430
+ function keepUnresolvedPackages(input) {
1431
+ const packages = (input.unresolvedPlugins ?? []).flatMap((plugin) => packageSource(plugin)?.packageName ?? []);
1432
+ return packages.length === 0 ? undefined : { keepPackages: packages };
1433
+ }
1434
+ function unhandledPlugins(input) {
1435
+ const handled = new Set([
1436
+ ...(input.materializedPlugins ?? []).map((plugin) => plugin.name),
1437
+ ...input.skippedPlugins ?? []
1438
+ ]);
1439
+ return input.environment.plugins.filter((plugin) => !handled.has(plugin.item.name));
1440
+ }
1441
+ function renderCodexPlugins(plugins) {
1442
+ const marketplaceRoot = plugins[0]?.marketplaceRoot;
1443
+ if (marketplaceRoot === undefined)
1444
+ return [];
1445
+ return [
1446
+ `[marketplaces.kitman]
1447
+ source_type = "local"
1448
+ source = ${tomlString(marketplaceRoot)}`,
1449
+ ...plugins.map((plugin) => `[plugins.${tomlString(`${plugin.name}@kitman`)}]
1450
+ enabled = true`)
1451
+ ];
1452
+ }
1384
1453
  function renderUnsupportedPlugins(plugins, harness, plan) {
1385
1454
  for (const plugin of plugins) {
1386
- const sourceKind = plugin.item.source.kind === "package" ? "package sources" : `${plugin.item.source.kind} sources`;
1387
- reportUnsupported(plan, plugin.item.policy, `plugin ${plugin.item.name}`, harness, `this writer has no source-verified plugin installer for ${sourceKind}`, sourceLabel2(plugin.source));
1455
+ reportUnsupported(plan, plugin.item.policy, `plugin ${plugin.item.name}`, harness, unsupportedPluginReason(plugin), sourceLabel2(plugin.source));
1388
1456
  }
1389
1457
  }
1458
+ function unsupportedPluginReason(plugin) {
1459
+ return `this writer has no source-verified plugin installer for ${sourceKinds(plugin, " or ")} sources`;
1460
+ }
1461
+ function packageSource(plugin) {
1462
+ return plugin.item.sources.find((source) => source.kind === "package");
1463
+ }
1464
+ function sourceKinds(plugin, separator) {
1465
+ return [...new Set(plugin.item.sources.map((source) => source.kind))].join(separator);
1466
+ }
1390
1467
  function renderSkillFiles(input, plan) {
1391
1468
  const materializedSkillNames = new Set((input.materializedSkills ?? []).map((skill) => skill.name));
1392
1469
  for (const skill of input.environment.skills) {
@@ -1435,7 +1512,7 @@ function renderManifest(input, plan) {
1435
1512
  generatedBy: "kitman",
1436
1513
  harness: input.environment.harness,
1437
1514
  skills: input.environment.skills,
1438
- plugins: input.environment.plugins,
1515
+ plugins: [...input.environment.plugins, ...input.unresolvedPlugins ?? []],
1439
1516
  mcpServers: input.environment.mcpServers,
1440
1517
  harnessSettings: input.environment.harnessSettings,
1441
1518
  providerAuthRefs: input.environment.providerAuthRefs,
@@ -1719,8 +1796,36 @@ function repositoryUrl(repoBinding) {
1719
1796
  return repoBinding.repositoryUrl ?? `https://${repoBinding.host}/${repoBinding.owner}/${repoBinding.name}.git`;
1720
1797
  }
1721
1798
  // packages/agent-runtime/src/skill-materializer.ts
1722
- var gitSourceKey = (source) => `${source.url}#${source.ref}:${source.path ?? ""}`;
1723
- var reviewedGitSkillSources = new Set([...EngineeringProcessKitCatalog, ...EngineeringTechnologyKitCatalog].flatMap((kit) => kit.agentEnvironment?.skills ?? []).flatMap((skill) => skill.source.kind === "git" ? [gitSourceKey(skill.source)] : []));
1799
+ var gitSourceKey = (source) => `${source.url}#${source.ref ?? ""}:${source.path ?? ""}`;
1800
+ var approvedCommits = (approved, source) => approved.flatMap((entry) => {
1801
+ const [commit, path] = entry.startsWith(`${source.url}#`) ? entry.slice(source.url.length + 1).split(/:(.*)/su) : [];
1802
+ return isCommitHash(commit) && path === (source.path ?? "") ? [commit] : [];
1803
+ });
1804
+ var resolveGitSource = (subject, source, input) => gen(function* () {
1805
+ if (!source.url.startsWith("https://"))
1806
+ return { problem: `${subject} must come from an https URL, not ${source.url}.` };
1807
+ const resolver = yield* SourceResolver;
1808
+ const latest = input.versions === "latest";
1809
+ const ref = latest ? undefined : source.ref;
1810
+ const resolved = (isCommitHash(ref) ? succeed5(ref) : resolver.gitCommit(source.url, ref)).pipe(map3((commit) => ({ commit })), catch_((error) => succeed5({
1811
+ problem: `Kitman could not resolve ${subject} from ${source.url}${ref === undefined ? "" : ` at ${ref}`}: ${error.message}`
1812
+ })));
1813
+ if (input.requireReviewedSources !== true)
1814
+ return yield* resolved;
1815
+ const approved = approvedCommits(input.approvedSkillSources ?? [], source);
1816
+ const wanted = isCommitHash(ref) ? { commit: ref } : undefined;
1817
+ if (ref === undefined && !latest && approved.length > 0)
1818
+ return { commit: approved.at(-1) ?? "" };
1819
+ const candidate = wanted ?? (yield* resolved);
1820
+ if ("problem" in candidate)
1821
+ return { rejected: true, problem: `${candidate.problem} Under review Kitman keeps no copy it cannot check.` };
1822
+ if (approved.includes(candidate.commit))
1823
+ return candidate;
1824
+ return {
1825
+ rejected: true,
1826
+ problem: `${subject} is not approved at ${candidate.commit}; approve it by adding ${source.url}#${candidate.commit}:${source.path ?? ""} to approvedSkillSources in your Kitman user config, or remove it from the Kit.`
1827
+ };
1828
+ });
1724
1829
 
1725
1830
  class AgentEnvironmentMaterializationError extends TaggedError("AgentEnvironmentMaterializationError") {
1726
1831
  get message() {
@@ -1731,6 +1836,7 @@ class AgentEnvironmentMaterializationError extends TaggedError("AgentEnvironment
1731
1836
  class SkillMaterializationRefused extends TaggedError("SkillMaterializationRefused") {
1732
1837
  }
1733
1838
  var previewAgentEnvironmentMaterialization = fnUntraced(function* (input) {
1839
+ const fs = yield* FileSystem;
1734
1840
  const path = yield* Path;
1735
1841
  const home = input.rootDir ?? (yield* optionalEnv("HOME")) ?? ".";
1736
1842
  const harness = input.environment.harness;
@@ -1738,43 +1844,70 @@ var previewAgentEnvironmentMaterialization = fnUntraced(function* (input) {
1738
1844
  const targetRoot = skillBasePath2(path, harness, input.scope, input.rootDir, home);
1739
1845
  const operations = [];
1740
1846
  const materializedSkills = [];
1847
+ const materializedPlugins = [];
1848
+ const skippedPlugins = [];
1741
1849
  const warnings = [];
1850
+ const unresolvedLinks = [];
1742
1851
  for (const skill of input.environment.skills) {
1743
1852
  const skillKey = skillIdentity(skill.item);
1853
+ const targetPath = path.join(targetRoot, `${skillSlug2(skill.item.name)}-${hashSegment(skillKey)}`);
1744
1854
  const source = skill.item.source;
1745
1855
  const fromKit = skill.source.label === undefined ? "" : ` (from Kit ${skill.source.label})`;
1746
- const problem = source.kind === "kitman" ? source.contentMarkdown === undefined ? `Kitman skill ${skill.item.name} has no contentMarkdown to materialize.` : undefined : input.noInstall === true ? `Skipping materialization for ${skill.item.name} because sync was run with noInstall.` : source.kind !== "git" ? `Skill ${skill.item.name}${fromKit} comes from ${source.kind}, which Kitman cannot install yet; only Kitman and git skills are installed. Remove it from the Kit or make it recommended.` : !source.url.startsWith("https://") ? `Git skill ${skill.item.name} must come from an https URL, not ${source.url}.` : !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(source.ref) ? `Git skill ${skill.item.name} must be pinned to a commit, not ${source.ref}, so Kitman installs only the reviewed version.` : !reviewedGitSkillSources.has(gitSourceKey(source)) && !(input.approvedSkillSources ?? []).includes(gitSourceKey(source)) ? `Git skill ${skill.item.name}${fromKit} at ${gitSourceKey(source)} is not one Kitman reviewed; approve it by adding that source to approvedSkillSources in your Kitman user config, or remove it from the Kit.` : undefined;
1747
- if (problem !== undefined) {
1748
- const issue = { message: problem, skillName: skill.item.name, skillKey };
1856
+ const gitSkillPath = path.join(sourceRoot, "git", hashSegment(skillKey), skillSlug2(skill.item.name));
1857
+ const problem = source.kind === "kitman" ? source.contentMarkdown === undefined ? `Kitman skill ${skill.item.name} has no contentMarkdown to materialize.` : undefined : input.noInstall === true ? `Skipping materialization for ${skill.item.name} because sync was run with noInstall.` : source.kind !== "git" ? `Skill ${skill.item.name}${fromKit} comes from ${source.kind}, which Kitman cannot install yet; only Kitman and git skills are installed. Remove it from the Kit or make it recommended.` : undefined;
1858
+ const resolution = problem === undefined && source.kind === "git" ? yield* resolveGitSource(`Git skill ${skill.item.name}${fromKit}`, source, input) : undefined;
1859
+ const issueMessage = problem ?? (resolution !== undefined && "problem" in resolution ? resolution.problem : undefined);
1860
+ if (issueMessage !== undefined) {
1861
+ const issue = { message: issueMessage, skillName: skill.item.name, skillKey };
1749
1862
  if (skill.item.policy === "required")
1750
1863
  return yield* new AgentEnvironmentMaterializationError({ issues: [issue] });
1751
1864
  warnings.push(issue);
1865
+ if (resolution !== undefined && "rejected" in resolution)
1866
+ continue;
1867
+ unresolvedLinks.push(targetPath);
1868
+ if (source.kind === "git" && (yield* fs.exists(gitSkillPath).pipe(orElseSucceed(() => false))))
1869
+ materializedSkills.push({
1870
+ name: skill.item.name,
1871
+ key: skillKey,
1872
+ harness,
1873
+ source,
1874
+ skillPath: gitSkillPath,
1875
+ managed: true
1876
+ });
1752
1877
  continue;
1753
1878
  }
1879
+ const commit = resolution !== undefined && "commit" in resolution ? resolution.commit : undefined;
1754
1880
  const expose = (skillPath) => {
1755
1881
  operations.push({
1756
1882
  kind: "symlink_skill",
1757
1883
  skillName: skill.item.name,
1758
1884
  skillKey,
1759
1885
  sourcePath: skillPath,
1760
- targetPath: path.join(targetRoot, `${skillSlug2(skill.item.name)}-${hashSegment(skillKey)}`),
1886
+ targetPath,
1761
1887
  sourceRoot,
1762
1888
  targetRoot
1763
1889
  });
1764
- materializedSkills.push({ name: skill.item.name, key: skillKey, harness, source, skillPath, managed: true });
1890
+ materializedSkills.push({
1891
+ name: skill.item.name,
1892
+ key: skillKey,
1893
+ harness,
1894
+ source,
1895
+ skillPath,
1896
+ managed: true,
1897
+ ...commit === undefined ? undefined : { commit }
1898
+ });
1765
1899
  };
1766
- if (source.kind === "git") {
1767
- const skillPath = path.join(sourceRoot, "git", hashSegment(skillKey), skillSlug2(skill.item.name));
1900
+ if (source.kind === "git" && commit !== undefined) {
1768
1901
  operations.push({
1769
1902
  kind: "fetch_git_skill",
1770
1903
  skillName: skill.item.name,
1771
1904
  skillKey,
1772
1905
  url: source.url,
1773
- ref: source.ref,
1906
+ ref: commit,
1774
1907
  ...source.path === undefined ? undefined : { repositoryPath: source.path },
1775
- path: skillPath
1908
+ path: gitSkillPath
1776
1909
  });
1777
- expose(skillPath);
1910
+ expose(gitSkillPath);
1778
1911
  } else if (source.kind === "kitman" && source.contentMarkdown !== undefined) {
1779
1912
  const skillPath = path.join(sourceRoot, "kitman", hashSegment(source.skillId ?? source.name ?? skill.item.name), skillSlug2(skill.item.name));
1780
1913
  operations.push({
@@ -1787,26 +1920,164 @@ var previewAgentEnvironmentMaterialization = fnUntraced(function* (input) {
1787
1920
  expose(skillPath);
1788
1921
  }
1789
1922
  }
1790
- return { harness, operations, materializedSkills, warnings };
1923
+ const codexMarketplace = harness === "codex" && input.scope === "user";
1924
+ const marketplaceRoot = path.join(sourceRoot, "codex-marketplace");
1925
+ const claudePluginsRoot = path.join(sourceRoot, "git-plugins");
1926
+ for (const plugin of harness === "claude-code" || codexMarketplace ? input.environment.plugins : []) {
1927
+ const source = plugin.item.sources.find((candidate) => candidate.kind === "git");
1928
+ if (source === undefined)
1929
+ continue;
1930
+ const name = plugin.item.name;
1931
+ const pluginKey = `plugin:git:${gitSourceKey(source)}:${name}`;
1932
+ const fromKit = plugin.source.label === undefined ? "" : ` (from Kit ${plugin.source.label})`;
1933
+ if (input.noInstall === true) {
1934
+ skippedPlugins.push(name);
1935
+ warnings.push({
1936
+ message: `Skipping materialization for plugin ${name} because sync was run with noInstall.`,
1937
+ skillName: name,
1938
+ skillKey: pluginKey
1939
+ });
1940
+ continue;
1941
+ }
1942
+ const directoryName = `${skillSlug2(name)}-${hashSegment(pluginKey)}`;
1943
+ const [pluginPath, manifest] = harness === "codex" ? [path.join(marketplaceRoot, "plugins", directoryName), ".codex-plugin/plugin.json"] : [path.join(claudePluginsRoot, hashSegment(pluginKey), skillSlug2(name)), ".claude-plugin/plugin.json"];
1944
+ const resolution = yield* resolveGitSource(`Git plugin ${name}${fromKit}`, source, input);
1945
+ if ("problem" in resolution) {
1946
+ const issue = { message: resolution.problem, skillName: name, skillKey: pluginKey };
1947
+ if (plugin.item.policy === "required")
1948
+ return yield* new AgentEnvironmentMaterializationError({ issues: [issue] });
1949
+ warnings.push(issue);
1950
+ if ("rejected" in resolution)
1951
+ continue;
1952
+ unresolvedLinks.push(path.join(targetRoot, directoryName));
1953
+ if (yield* fs.exists(path.join(pluginPath, manifest)).pipe(orElseSucceed(() => false)))
1954
+ materializedPlugins.push({
1955
+ name,
1956
+ key: pluginKey,
1957
+ harness,
1958
+ source,
1959
+ pluginPath,
1960
+ ...harness === "codex" ? { marketplaceRoot } : undefined
1961
+ });
1962
+ continue;
1963
+ }
1964
+ operations.push({
1965
+ kind: "fetch_git_plugin",
1966
+ pluginName: name,
1967
+ pluginKey,
1968
+ url: source.url,
1969
+ ref: resolution.commit,
1970
+ manifest,
1971
+ path: pluginPath,
1972
+ sourceRoot
1973
+ });
1974
+ if (harness === "codex") {
1975
+ materializedPlugins.push({
1976
+ name,
1977
+ key: pluginKey,
1978
+ harness,
1979
+ source,
1980
+ pluginPath,
1981
+ marketplaceRoot,
1982
+ commit: resolution.commit
1983
+ });
1984
+ continue;
1985
+ }
1986
+ operations.push({
1987
+ kind: "symlink_skill",
1988
+ skillName: name,
1989
+ skillKey: pluginKey,
1990
+ sourcePath: pluginPath,
1991
+ targetPath: path.join(targetRoot, directoryName),
1992
+ sourceRoot,
1993
+ targetRoot
1994
+ });
1995
+ materializedPlugins.push({ name, key: pluginKey, harness, source, pluginPath, commit: resolution.commit });
1996
+ }
1997
+ if (input.noInstall !== true)
1998
+ operations.push({
1999
+ kind: "unlink_stale_links",
2000
+ path: targetRoot,
2001
+ sourceRoot,
2002
+ keepPaths: [
2003
+ ...operations.flatMap((operation) => operation.kind === "symlink_skill" ? [operation.targetPath] : []),
2004
+ ...unresolvedLinks
2005
+ ],
2006
+ sharedWith: AgentHarnessIds.filter((other) => other !== harness && skillBasePath2(path, other, input.scope, input.rootDir, home) === targetRoot).map((other) => ({
2007
+ harness: other,
2008
+ manifestPath: path.join(input.scope === "user" ? home : input.rootDir ?? ".", ".kitman", `agent-environment.${other}.manifest.json`)
2009
+ }))
2010
+ });
2011
+ const marketplacePath = path.join(marketplaceRoot, ".agents", "plugins", "marketplace.json");
2012
+ if (codexMarketplace && input.noInstall !== true)
2013
+ operations.push({
2014
+ kind: "write_codex_marketplace",
2015
+ path: marketplacePath,
2016
+ content: `${yield* encodePrettyJsonString({
2017
+ name: "kitman",
2018
+ interface: { displayName: "Kitman" },
2019
+ plugins: materializedPlugins.map((plugin) => ({
2020
+ name: plugin.name,
2021
+ source: { source: "local", path: `./${path.relative(marketplaceRoot, plugin.pluginPath)}` },
2022
+ policy: { installation: "INSTALLED_BY_DEFAULT", authentication: "ON_INSTALL" },
2023
+ category: "Productivity"
2024
+ }))
2025
+ }).pipe(orDie)}
2026
+ `,
2027
+ sourceRoot
2028
+ });
2029
+ return {
2030
+ harness,
2031
+ operations,
2032
+ materializedSkills,
2033
+ materializedPlugins,
2034
+ skippedPlugins,
2035
+ warnings,
2036
+ ...codexMarketplace && materializedPlugins.length > 0 ? { codexHookTrust: { home, marketplacePath } } : undefined
2037
+ };
2038
+ });
2039
+ var gitCheckouts = gen(function* () {
2040
+ const scope = yield* Scope;
2041
+ const trees = new Map;
2042
+ const checkout = (url, ref) => gen(function* () {
2043
+ const key = `${url}#${ref}`;
2044
+ const known = trees.get(key);
2045
+ if (known !== undefined)
2046
+ return known;
2047
+ const tree = yield* checkoutGitTree(url, ref).pipe(provide(scope));
2048
+ trees.set(key, tree);
2049
+ return tree;
2050
+ });
2051
+ return checkout;
1791
2052
  });
1792
- var applyAgentEnvironmentMaterialization = fnUntraced(function* (plan) {
2053
+ var applyAgentEnvironmentMaterialization = (plan, checkout) => gen(function* () {
2054
+ const checkoutTree = checkout ?? (yield* gitCheckouts);
1793
2055
  for (const operation of plan.operations) {
1794
2056
  if (operation.kind === "write_skill")
1795
2057
  yield* writeSkill(plan, operation);
1796
2058
  else if (operation.kind === "fetch_git_skill")
1797
- yield* fetchGitSkill(plan, operation);
2059
+ yield* fetchGitSkill(plan, operation, checkoutTree);
2060
+ else if (operation.kind === "fetch_git_plugin")
2061
+ yield* fetchGitPlugin(operation, checkoutTree);
2062
+ else if (operation.kind === "write_codex_marketplace")
2063
+ yield* writeCodexMarketplace(operation);
2064
+ else if (operation.kind === "unlink_stale_links")
2065
+ yield* unlinkStaleLinks(operation);
1798
2066
  else
1799
2067
  yield* exposeSkill(operation);
1800
2068
  }
1801
2069
  return plan;
1802
- });
2070
+ }).pipe(scoped);
1803
2071
  var refuse = (message) => fail4(new SkillMaterializationRefused({ message }));
1804
2072
  var managedSkillPath = (plan, skillKey, file) => gen(function* () {
1805
- const path = yield* Path;
1806
2073
  const boundary = plan.operations.find((candidate) => candidate.kind === "symlink_skill" && candidate.skillKey === skillKey);
1807
2074
  if (boundary === undefined)
1808
2075
  return yield* refuse("SKILL_SYMLINK_BOUNDARY_MISSING");
1809
- const root = path.resolve(boundary.sourceRoot);
2076
+ return yield* managedPath(boundary.sourceRoot, file);
2077
+ });
2078
+ var managedPath = (sourceRoot, file) => gen(function* () {
2079
+ const path = yield* Path;
2080
+ const root = path.resolve(sourceRoot);
1810
2081
  const target = path.resolve(file);
1811
2082
  if (!isPathWithin(path, root, target) || target === root)
1812
2083
  return yield* refuse("SKILL_WRITE_OUTSIDE_ROOT");
@@ -1822,18 +2093,34 @@ var writeSkill = (plan, operation) => gen(function* () {
1822
2093
  yield* fs.makeDirectory(path.dirname(target), { recursive: true });
1823
2094
  yield* fs.writeFileString(target, operation.content);
1824
2095
  });
1825
- var fetchGitSkill = (plan, operation) => gen(function* () {
2096
+ var checkoutGitTree = (url, ref) => gen(function* () {
1826
2097
  const fs = yield* FileSystem;
1827
2098
  const path = yield* Path;
1828
- const target = yield* managedSkillPath(plan, operation.skillKey, operation.path);
1829
- const scratch = yield* fs.makeTempDirectoryScoped({ prefix: "kitman-git-skill-" });
2099
+ const scratch = yield* fs.makeTempDirectoryScoped({ prefix: "kitman-git-" });
1830
2100
  const repository = path.join(scratch, "repository");
1831
2101
  const tree = path.join(scratch, "tree");
1832
2102
  const git = (...args) => runCommand2("git", ["--git-dir", repository, ...args]);
1833
2103
  yield* fs.makeDirectory(tree);
1834
2104
  yield* runCommand2("git", ["init", "--quiet", "--bare", repository]);
1835
- yield* git("fetch", "--quiet", "--depth", "1", "--", operation.url, operation.ref);
2105
+ yield* git("fetch", "--quiet", "--depth", "1", "--", url, ref);
1836
2106
  yield* git("--work-tree", tree, "checkout", "FETCH_HEAD", "--", ".");
2107
+ return tree;
2108
+ });
2109
+ var replaceWithCopy = (directory, target) => gen(function* () {
2110
+ const fs = yield* FileSystem;
2111
+ const path = yield* Path;
2112
+ const staged = `${target}.partial`;
2113
+ yield* fs.remove(staged, { recursive: true, force: true });
2114
+ yield* fs.makeDirectory(path.dirname(target), { recursive: true });
2115
+ yield* fs.copy(directory, staged);
2116
+ yield* fs.remove(target, { recursive: true, force: true });
2117
+ yield* fs.rename(staged, target);
2118
+ });
2119
+ var fetchGitSkill = (plan, operation, checkout) => gen(function* () {
2120
+ const fs = yield* FileSystem;
2121
+ const path = yield* Path;
2122
+ const target = yield* managedSkillPath(plan, operation.skillKey, operation.path);
2123
+ const tree = yield* checkout(operation.url, operation.ref);
1837
2124
  const skillDirectory = path.resolve(tree, operation.repositoryPath ?? ".");
1838
2125
  if (!isPathWithin(path, tree, skillDirectory))
1839
2126
  return yield* refuse("SKILL_SOURCE_OUTSIDE_ROOT");
@@ -1841,13 +2128,44 @@ var fetchGitSkill = (plan, operation) => gen(function* () {
1841
2128
  return yield* refuse(`Git skill ${operation.skillName} links to a file outside itself in ${operation.url}@${operation.ref}; Kitman does not install linked files.`);
1842
2129
  if (!(yield* fs.exists(path.join(skillDirectory, "SKILL.md"))))
1843
2130
  return yield* refuse(`Git skill ${operation.skillName} has no SKILL.md at ${operation.repositoryPath ?? "the repository root"} in ${operation.url}@${operation.ref}.`);
1844
- const staged = `${target}.partial`;
1845
- yield* fs.remove(staged, { recursive: true, force: true });
2131
+ yield* replaceWithCopy(skillDirectory, target);
2132
+ });
2133
+ var writeCodexMarketplace = (operation) => gen(function* () {
2134
+ const fs = yield* FileSystem;
2135
+ const path = yield* Path;
2136
+ const target = yield* managedPath(operation.sourceRoot, operation.path);
2137
+ if (yield* isSymbolicLink(target))
2138
+ return yield* refuse("SKILL_WRITE_SYMLINK");
1846
2139
  yield* fs.makeDirectory(path.dirname(target), { recursive: true });
1847
- yield* fs.copy(skillDirectory, staged);
1848
- yield* fs.remove(target, { recursive: true, force: true });
1849
- yield* fs.rename(staged, target);
1850
- }).pipe(scoped);
2140
+ yield* fs.writeFileString(target, operation.content);
2141
+ });
2142
+ var fetchGitPlugin = (operation, checkout) => gen(function* () {
2143
+ const fs = yield* FileSystem;
2144
+ const path = yield* Path;
2145
+ const target = yield* managedPath(operation.sourceRoot, operation.path);
2146
+ const tree = yield* checkout(operation.url, operation.ref);
2147
+ if (yield* containsSymbolicLink(tree, tree))
2148
+ return yield* refuse(`Git plugin ${operation.pluginName} links to a file in ${operation.url}@${operation.ref}; Kitman does not install linked files.`);
2149
+ if (!(yield* fs.exists(path.join(tree, operation.manifest))))
2150
+ return yield* refuse(`Git plugin ${operation.pluginName} has no ${operation.manifest} at the root of ${operation.url}@${operation.ref}.`);
2151
+ yield* replaceWithCopy(tree, target);
2152
+ });
2153
+ var unlinkStaleLinks = (operation) => gen(function* () {
2154
+ const fs = yield* FileSystem;
2155
+ const path = yield* Path;
2156
+ if (!(yield* fs.exists(operation.path)))
2157
+ return;
2158
+ yield* assertNoSymlinkComponents(operation.path, "SKILL_TARGET_SYMLINK_PARENT");
2159
+ const sharedSkillPaths = new Set((yield* forEach(operation.sharedWith, (shared) => readManifestSkillPaths(shared.manifestPath))).flatMap((paths) => [...paths].map((skillPath) => path.resolve(skillPath))));
2160
+ for (const entry of yield* fs.readDirectory(operation.path)) {
2161
+ const link = path.join(operation.path, entry);
2162
+ if (operation.keepPaths.includes(link) || !(yield* isSymbolicLink(link)))
2163
+ continue;
2164
+ const linkTarget = path.resolve(operation.path, yield* fs.readLink(link));
2165
+ if (isPathWithin(path, operation.sourceRoot, linkTarget) && !sharedSkillPaths.has(linkTarget))
2166
+ yield* fs.remove(link);
2167
+ }
2168
+ });
1851
2169
  var containsSymbolicLink = (root, directory) => gen(function* () {
1852
2170
  const fs = yield* FileSystem;
1853
2171
  const path = yield* Path;
@@ -1894,8 +2212,8 @@ var exposeSkill = (operation) => gen(function* () {
1894
2212
  }
1895
2213
  yield* fs.symlink(path.resolve(operation.sourcePath), targetPath);
1896
2214
  });
1897
- var isSymbolicLink = (file) => flatMap(FileSystem, (fs) => fs.readLink(file).pipe(as(true), orElseSucceed(() => false)));
1898
- var realPathOr = (file, reason) => flatMap(FileSystem, (fs) => fs.realPath(file).pipe(mapError(() => new SkillMaterializationRefused({ message: reason }))));
2215
+ var isSymbolicLink = (file) => flatMap2(FileSystem, (fs) => fs.readLink(file).pipe(as(true), orElseSucceed(() => false)));
2216
+ var realPathOr = (file, reason) => flatMap2(FileSystem, (fs) => fs.realPath(file).pipe(mapError(() => new SkillMaterializationRefused({ message: reason }))));
1899
2217
  var assertNoSymlinkComponents = (file, reason) => gen(function* () {
1900
2218
  const path = yield* Path;
1901
2219
  for (let current = path.resolve(file);; current = path.dirname(current)) {
@@ -1916,7 +2234,7 @@ function skillIdentity(skill) {
1916
2234
  if (source.kind === "kitman")
1917
2235
  return `kitman:${source.skillId ?? source.name ?? skill.name}:${skill.name}`;
1918
2236
  if (source.kind === "git")
1919
- return `git:${source.url}#${source.ref}:${source.path ?? ""}:${skill.name}`;
2237
+ return `git:${source.url}#${source.ref ?? ""}:${source.path ?? ""}:${skill.name}`;
1920
2238
  if (source.kind === "package")
1921
2239
  return `package:${source.packageName}@${source.version}:${source.path ?? ""}:${skill.name}`;
1922
2240
  if (source.kind === "url")
@@ -1930,7 +2248,9 @@ var ProjectSkillDirectories = {
1930
2248
  pi: ".pi/skills"
1931
2249
  };
1932
2250
  function skillBasePath2(path, harness, scope, rootDir, home) {
1933
- return scope === "user" ? path.join(home, ".agents", "skills") : path.join(rootDir ?? ".", ProjectSkillDirectories[harness]);
2251
+ if (scope !== "user")
2252
+ return path.join(rootDir ?? ".", ProjectSkillDirectories[harness]);
2253
+ return harness === "claude-code" ? path.join(home, ".claude", "skills") : path.join(home, ".agents", "skills");
1934
2254
  }
1935
2255
  function skillMarkdown(name, contentMarkdown) {
1936
2256
  const content = contentMarkdown.trimEnd();
@@ -1985,7 +2305,7 @@ var closeSocket = (conn, isOpen) => {
1985
2305
  var makeNet = (options) => fromDuplex(contextWith((context) => {
1986
2306
  let conn;
1987
2307
  let isOpen = false;
1988
- return flatMap(addFinalizer(get(context, Scope), sync(() => {
2308
+ return flatMap2(addFinalizer(get(context, Scope), sync(() => {
1989
2309
  if (!conn)
1990
2310
  return;
1991
2311
  closeSocket(conn, isOpen);
@@ -2387,7 +2707,7 @@ class HerdrSocketClient {
2387
2707
  });
2388
2708
  }
2389
2709
  sessionSnapshot() {
2390
- return flatMap(this.request("session.snapshot", {}), (result) => {
2710
+ return flatMap2(this.request("session.snapshot", {}), (result) => {
2391
2711
  const snapshot = isRecord(result) && result.type === "session_snapshot" ? result.snapshot : undefined;
2392
2712
  if (!isRecord(snapshot) || !isString2(snapshot.version) || !isNumber(snapshot.protocol) || !Number.isInteger(snapshot.protocol) || !Array.isArray(snapshot.workspaces) || !Array.isArray(snapshot.tabs) || !Array.isArray(snapshot.panes) || !Array.isArray(snapshot.layouts) || !Array.isArray(snapshot.agents))
2393
2713
  return fail4(fail2("herdr_session_snapshot_invalid"));
@@ -2502,7 +2822,7 @@ function isRecord(value) {
2502
2822
  // packages/agent-runtime/src/herdr-server-supervisor.ts
2503
2823
  class HerdrServerError extends TaggedError("HerdrServerError") {
2504
2824
  }
2505
- var fail3 = (message) => new HerdrServerError({ message });
2825
+ var fail5 = (message) => new HerdrServerError({ message });
2506
2826
 
2507
2827
  class HerdrServerSupervisor {
2508
2828
  runnerId;
@@ -2534,9 +2854,9 @@ class HerdrServerSupervisor {
2534
2854
  }
2535
2855
  start = fnUntraced(function* () {
2536
2856
  if (this.started && this.client !== undefined)
2537
- return yield* this.client.sessionSnapshot().pipe(mapError((error) => fail3(error.message)));
2857
+ return yield* this.client.sessionSnapshot().pipe(mapError((error) => fail5(error.message)));
2538
2858
  if (!["linux", "darwin"].includes(this.options.platform ?? process.platform))
2539
- return yield* fail3("herdr_platform_unsupported");
2859
+ return yield* fail5("herdr_platform_unsupported");
2540
2860
  const defaultSession = this.options.session === "default";
2541
2861
  const runtime = defaultSession ? undefined : yield* this.prepareRuntime();
2542
2862
  const socketClientOptions = this.options.socketClientOptions ?? {};
@@ -2577,7 +2897,7 @@ class HerdrServerSupervisor {
2577
2897
  }
2578
2898
  const snapshot = yield* awaitReadiness(socketClient, this.readinessTimeoutMs);
2579
2899
  if (snapshot.protocol !== HERDR_SOCKET_PROTOCOL)
2580
- return yield* fail3("herdr_protocol_mismatch");
2900
+ return yield* fail5("herdr_protocol_mismatch");
2581
2901
  return snapshot;
2582
2902
  });
2583
2903
  attachToDefaultSession = fnUntraced(function* (socketPath, socketClient) {
@@ -2585,7 +2905,7 @@ class HerdrServerSupervisor {
2585
2905
  if (isFailure(running)) {
2586
2906
  const message = running.failure.message;
2587
2907
  if (message !== "herdr_socket_connect_failed" && message !== "herdr_socket_closed")
2588
- return yield* fail3(message);
2908
+ return yield* fail5(message);
2589
2909
  yield* (this.options.spawnServer ?? spawnServer)({
2590
2910
  command: this.options.herdrCommand ?? "herdr",
2591
2911
  args: ["server"],
@@ -2594,7 +2914,7 @@ class HerdrServerSupervisor {
2594
2914
  }
2595
2915
  const snapshot = isSuccess(running) ? running.success : yield* awaitReadiness(socketClient, this.readinessTimeoutMs);
2596
2916
  if (snapshot.protocol !== HERDR_SOCKET_PROTOCOL)
2597
- return yield* fail3("herdr_protocol_mismatch");
2917
+ return yield* fail5("herdr_protocol_mismatch");
2598
2918
  return snapshot;
2599
2919
  });
2600
2920
  prepareRuntime = fnUntraced(function* () {
@@ -2606,7 +2926,7 @@ class HerdrServerSupervisor {
2606
2926
  fs.makeDirectory(path.dirname(directory), { recursive: true, mode: 448 }),
2607
2927
  fs.makeDirectory(directory, { recursive: true, mode: 448 }),
2608
2928
  fs.chmod(directory, 448)
2609
- ]), () => fail3("herdr_runtime_unavailable"));
2929
+ ]), () => fail5("herdr_runtime_unavailable"));
2610
2930
  const sessionName = `kitman-herdr-${digest(this.runnerId)}`;
2611
2931
  const runtime = {
2612
2932
  directory,
@@ -2640,7 +2960,7 @@ var reclaimStaleRuntime = (runtime, runnerId, isAlive) => gen(function* () {
2640
2960
  if (!socketExists && owner === undefined)
2641
2961
  return false;
2642
2962
  if (owner === undefined || owner.runnerId !== runnerId || owner.socketPath !== runtime.socketPath)
2643
- return yield* fail3(`herdr_socket_ownership_unknown:${socketExists ? "socket_present" : "socket_missing"}:${owner === undefined ? "owner_missing" : "owner_mismatch"}`);
2963
+ return yield* fail5(`herdr_socket_ownership_unknown:${socketExists ? "socket_present" : "socket_missing"}:${owner === undefined ? "owner_missing" : "owner_mismatch"}`);
2644
2964
  if (yield* isAlive(owner.pid))
2645
2965
  return true;
2646
2966
  yield* removeRuntimeFiles(runtime);
@@ -2651,8 +2971,8 @@ var awaitReadiness = (client, readinessTimeoutMs) => gen(function* () {
2651
2971
  for (;; ) {
2652
2972
  const remaining = deadline - (yield* currentTimeMillis);
2653
2973
  if (remaining <= 0)
2654
- return yield* fail3("herdr_socket_connect_failed");
2655
- const attempt = yield* result(client.sessionSnapshot().pipe(mapError((error) => fail3(error.message)), timeoutOrElse({ duration: remaining, orElse: () => fail4(fail3("herdr_readiness_timeout")) })));
2974
+ return yield* fail5("herdr_socket_connect_failed");
2975
+ const attempt = yield* result(client.sessionSnapshot().pipe(mapError((error) => fail5(error.message)), timeoutOrElse({ duration: remaining, orElse: () => fail4(fail5("herdr_readiness_timeout")) })));
2656
2976
  if (isSuccess(attempt))
2657
2977
  return attempt.success;
2658
2978
  const message = attempt.failure.message;
@@ -2680,15 +3000,15 @@ var homeDirectory = map3(optionalEnv("HOME"), (home) => home ?? "/");
2680
3000
  var temporaryDirectory = map3(optionalEnv("TMPDIR"), (tmp) => tmp ?? "/tmp");
2681
3001
  var spawnServer = (input) => gen(function* () {
2682
3002
  const spawner = yield* ChildProcessSpawner;
2683
- const scope = yield* make6();
2684
- const handle = yield* spawner.spawn(make11(input.command, [...input.args], {
3003
+ const scope = yield* make7();
3004
+ const handle = yield* spawner.spawn(make13(input.command, [...input.args], {
2685
3005
  env: { HERDR_SOCKET_PATH: input.socketPath },
2686
3006
  extendEnv: true,
2687
3007
  detached: true,
2688
3008
  stdin: "ignore",
2689
3009
  stdout: "ignore",
2690
3010
  stderr: "ignore"
2691
- })).pipe(provide(scope), mapError(() => fail3("herdr_server_start_failed")));
3011
+ })).pipe(provide(scope), mapError(() => fail5("herdr_server_start_failed")));
2692
3012
  yield* ignore(handle.unref);
2693
3013
  return {
2694
3014
  pid: handle.pid,
@@ -2708,7 +3028,7 @@ var writeOwnerMarker = (path, marker) => gen(function* () {
2708
3028
  yield* fs.writeFileString(path, `${encodeJsonStringSync(marker)}
2709
3029
  `, { mode: 384 });
2710
3030
  yield* fs.chmod(path, 384);
2711
- }).pipe(mapError(() => fail3("herdr_runtime_unavailable")));
3031
+ }).pipe(mapError(() => fail5("herdr_runtime_unavailable")));
2712
3032
  var readOwnerMarker = (path) => gen(function* () {
2713
3033
  const fs = yield* FileSystem;
2714
3034
  const text = yield* fs.readFileString(path);
@@ -2757,7 +3077,7 @@ function shortId(value) {
2757
3077
  function isRecord2(value) {
2758
3078
  return isObject(value) && value !== null && !Array.isArray(value);
2759
3079
  }
2760
- var requestRecord = (client, method, params, expectedType) => flatMap(client.request(method, params), (response) => {
3080
+ var requestRecord = (client, method, params, expectedType) => flatMap2(client.request(method, params), (response) => {
2761
3081
  const result = recordValue(response);
2762
3082
  return result === undefined || result.type !== expectedType ? fail4(new HerdrHarnessError({ message: `${method}_response_invalid` })) : succeed5(result);
2763
3083
  });
@@ -2907,7 +3227,7 @@ var HarnessAdapters = [
2907
3227
  auth: ["~/.codex"],
2908
3228
  providerHosts: ["api.openai.com", "auth.openai.com", "chatgpt.com"],
2909
3229
  effortArgs: ["-c", 'model_reasoning_effort="{effort}"'],
2910
- autoPermissionArgs: ["--dangerously-bypass-approvals-and-sandbox"],
3230
+ autoPermissionArgs: ["--dangerously-bypass-approvals-and-sandbox", "--dangerously-bypass-hook-trust"],
2911
3231
  firstMessage: "herdr_prompt",
2912
3232
  mcp: { kind: "toml_overrides", flag: "-c" },
2913
3233
  skills: { kind: "skills_config", root: "skills" },
@@ -3476,7 +3796,7 @@ var executeHerdrStepAttempt = (input) => gen(function* () {
3476
3796
  });
3477
3797
  yield* input.harness.prompt({ targetId: launched.targetId, text: firstMessage(input.agent, worktreePath) });
3478
3798
  const steering = input.control.steering;
3479
- const deliveries = steering === undefined ? undefined : yield* forkScoped(forever(flatMap(steering.next, (note) => input.harness.prompt({ targetId: launched.targetId, text: note.text }).pipe(matchEffect({
3799
+ const deliveries = steering === undefined ? undefined : yield* forkScoped(forever(flatMap2(steering.next, (note) => input.harness.prompt({ targetId: launched.targetId, text: note.text }).pipe(matchEffect({
3480
3800
  onSuccess: () => steering.report(note, true),
3481
3801
  onFailure: () => steering.report(note, false)
3482
3802
  })))));
@@ -3887,7 +4207,7 @@ var findSession = (adapter, place) => gen(function* () {
3887
4207
  }).pipe(orElseSucceed(() => {
3888
4208
  return;
3889
4209
  }));
3890
- var packSession = (path) => map3(flatMap(FileSystem, (fs) => fs.readFile(path)), (content) => new Uint8Array(gzipSync(content)));
4210
+ var packSession = (path) => map3(flatMap2(FileSystem, (fs) => fs.readFile(path)), (content) => new Uint8Array(gzipSync(content)));
3891
4211
  var restoreSession = (adapter, place) => gen(function* () {
3892
4212
  const fs = yield* FileSystem;
3893
4213
  const location = adapter.session?.location;
@@ -4158,8 +4478,8 @@ function runReplica(control, executor, signals) {
4158
4478
  if (work === undefined)
4159
4479
  return { status: "idle" };
4160
4480
  const lease = leaseInput(work);
4161
- const workHeartbeat = sleep(heartbeatInterval(work)).pipe(flatMap(() => call("heartbeatWork", () => control.heartbeatWork(work.id, lease))), repeat({ until: (heartbeat) => heartbeat?.settled === true }), as({ status: "settled" }));
4162
- const pushedStop = signals === undefined ? never : signals.stopped(work.id).pipe(flatMap((stop) => stop === "canceled" ? fail4(new RunnerReplicaCancellation) : succeed5({ status: "settled" })), ensuring(signals.forget(work.id)));
4481
+ const workHeartbeat = sleep(heartbeatInterval(work)).pipe(flatMap2(() => call("heartbeatWork", () => control.heartbeatWork(work.id, lease))), repeat({ until: (heartbeat) => heartbeat?.settled === true }), as({ status: "settled" }));
4482
+ const pushedStop = signals === undefined ? never : signals.stopped(work.id).pipe(flatMap2((stop) => stop === "canceled" ? fail4(new RunnerReplicaCancellation) : succeed5({ status: "settled" })), ensuring(signals.forget(work.id)));
4163
4483
  let completed = false;
4164
4484
  let leaseLost = false;
4165
4485
  return yield* gen(function* () {
@@ -4543,7 +4863,101 @@ function runnerStepAttemptFromAgentStep(work) {
4543
4863
  }
4544
4864
  };
4545
4865
  }
4866
+ // packages/agent-runtime/src/codex-hook-trust.ts
4867
+ class CodexAppServerFailed extends TaggedError("CodexAppServerFailed") {
4868
+ get message() {
4869
+ return this.reason;
4870
+ }
4871
+ }
4872
+ var RpcResponse = Struct({
4873
+ id: Finite,
4874
+ result: optional(Unknown),
4875
+ error: optional(Struct({ message: String2 }))
4876
+ });
4877
+ var HooksList = Struct({
4878
+ data: ArraySchema(Struct({
4879
+ hooks: ArraySchema(Struct({
4880
+ key: String2,
4881
+ pluginId: optional(NullOr(String2)),
4882
+ currentHash: String2,
4883
+ trustStatus: String2
4884
+ }))
4885
+ }))
4886
+ });
4887
+ var codexAppServer = (binary = "codex") => (env) => gen(function* () {
4888
+ const spawner = yield* ChildProcessSpawner;
4889
+ const input = yield* unbounded();
4890
+ const pending = new Map;
4891
+ let lastId = 0;
4892
+ const handle = yield* spawner.spawn(make13(binary, ["app-server"], {
4893
+ cwd: env.home,
4894
+ extendEnv: true,
4895
+ env: { HOME: env.home, CODEX_HOME: env.codexHome },
4896
+ stdin: fromQueue(input),
4897
+ stderr: "ignore"
4898
+ })).pipe(catchReason("PlatformError", "NotFound", () => fail4(new CodexAppServerFailed({ reason: `${binary} is not on the PATH` }))), mapError((error) => new CodexAppServerFailed({ reason: error.message })));
4899
+ const settle = (line) => match(flatMap(decodeJsonStringOption(line), decodeUnknownOption(RpcResponse)), {
4900
+ onNone: () => void_2,
4901
+ onSome: (response) => {
4902
+ const deferred = pending.get(response.id);
4903
+ if (deferred === undefined)
4904
+ return void_2;
4905
+ pending.delete(response.id);
4906
+ return response.error === undefined ? succeed3(deferred, response.result) : fail3(deferred, new CodexAppServerFailed({ reason: response.error.message }));
4907
+ }
4908
+ });
4909
+ yield* handle.stdout.pipe(decodeText, splitLines, runForEach(settle), ignore, ensuring(forEach(pending.values(), (deferred) => fail3(deferred, new CodexAppServerFailed({ reason: `${binary} app-server exited` })))), forkScoped);
4910
+ const send = (message) => offer(input, new TextEncoder().encode(`${encodeJsonStringSync(message)}
4911
+ `));
4912
+ return {
4913
+ request: (method, params) => gen(function* () {
4914
+ const id = ++lastId;
4915
+ const deferred = yield* make6();
4916
+ pending.set(id, deferred);
4917
+ yield* send({ id, method, params });
4918
+ return yield* _await(deferred).pipe(mapError((error) => new CodexAppServerFailed({ reason: `${method}: ${error.message}` })));
4919
+ }),
4920
+ notify: (method) => asVoid(send({ method }))
4921
+ };
4922
+ });
4923
+ var CodexAppServer = Reference("kitman/agent-runtime/CodexAppServer", {
4924
+ defaultValue: () => codexAppServer()
4925
+ });
4926
+ var trustCodexPluginHooks = (input) => gen(function* () {
4927
+ const launch = yield* CodexAppServer;
4928
+ const codex = yield* launch({ home: input.home, codexHome: `${input.home}/.codex` });
4929
+ const pluginIds = new Set(input.plugins.map((plugin) => `${plugin}@kitman`));
4930
+ yield* codex.request("initialize", { clientInfo: { name: "kitman", title: "Kitman", version: "1" } });
4931
+ yield* codex.notify("initialized");
4932
+ for (const pluginName of input.plugins)
4933
+ yield* codex.request("plugin/install", { marketplacePath: input.marketplacePath, pluginName });
4934
+ const listed = yield* decodeUnknownEffect(HooksList)(yield* codex.request("hooks/list", { cwds: [input.home] })).pipe(mapError(() => new CodexAppServerFailed({ reason: "hooks/list: unexpected response" })));
4935
+ const untrusted = listed.data.flatMap((entry) => entry.hooks).filter((hook) => pluginIds.has(hook.pluginId ?? "") && (hook.trustStatus === "untrusted" || hook.trustStatus === "modified"));
4936
+ if (untrusted.length > 0)
4937
+ yield* codex.request("config/batchWrite", {
4938
+ edits: [
4939
+ {
4940
+ keyPath: "hooks.state",
4941
+ mergeStrategy: "upsert",
4942
+ value: Object.fromEntries(untrusted.map((hook) => [hook.key, { trusted_hash: hook.currentHash }]))
4943
+ }
4944
+ ],
4945
+ reloadUserConfig: true
4946
+ });
4947
+ return [];
4948
+ }).pipe(scoped, timeoutOrElse({
4949
+ duration: "1 minute",
4950
+ orElse: () => fail4(new CodexAppServerFailed({ reason: "timed out" }))
4951
+ }), catch_((error) => succeed5(input.plugins.map((plugin) => `Kitman could not record trust for ${plugin}'s hooks (${error.message}); Codex will ask in /hooks.`))));
4952
+
4546
4953
  // packages/agent-runtime/src/materialization-plan.ts
4954
+ var HarnessUpdaters = {
4955
+ "claude-code": ["claude", "update"],
4956
+ codex: ["codex", "update"],
4957
+ opencode: ["opencode", "upgrade"],
4958
+ pi: ["pi", "update", "--self"]
4959
+ };
4960
+
4547
4961
  class AgentEnvironmentPlanApplyError extends TaggedError("AgentEnvironmentPlanApplyError") {
4548
4962
  get message() {
4549
4963
  return `AgentEnvironment plan apply failed during ${this.phase}: ${this.cause.message}`;
@@ -4556,11 +4970,17 @@ var previewAgentEnvironmentPlan = fnUntraced(function* (input) {
4556
4970
  ...input.scope === undefined ? undefined : { scope: input.scope },
4557
4971
  ...input.cacheRoot === undefined ? undefined : { cacheRoot: input.cacheRoot },
4558
4972
  ...input.noInstall === undefined ? undefined : { noInstall: input.noInstall },
4559
- ...input.approvedSkillSources === undefined ? undefined : { approvedSkillSources: input.approvedSkillSources }
4973
+ ...input.approvedSkillSources === undefined ? undefined : { approvedSkillSources: input.approvedSkillSources },
4974
+ ...input.requireReviewedSources === undefined ? undefined : { requireReviewedSources: input.requireReviewedSources },
4975
+ ...input.versions === undefined ? undefined : { versions: input.versions }
4560
4976
  });
4561
4977
  const harnessConfig = yield* previewAgentEnvironmentConfigSync({
4562
4978
  environment: input.environment,
4563
4979
  materializedSkills: skills.materializedSkills,
4980
+ materializedPlugins: skills.materializedPlugins,
4981
+ skippedPlugins: skills.skippedPlugins,
4982
+ ...input.noInstall === undefined ? undefined : { noInstall: input.noInstall },
4983
+ ...input.versions === undefined ? undefined : { versions: input.versions },
4564
4984
  ...input.syncKit === undefined ? undefined : { syncKit: input.syncKit },
4565
4985
  ...input.rootDir === undefined ? undefined : { rootDir: input.rootDir },
4566
4986
  ...input.scope === undefined ? undefined : { scope: input.scope },
@@ -4571,19 +4991,50 @@ var previewAgentEnvironmentPlan = fnUntraced(function* (input) {
4571
4991
  ...input.syncKit === undefined ? undefined : { syncKitHash: input.syncKit.syncKitHash },
4572
4992
  skills,
4573
4993
  harnessConfig,
4994
+ ...input.versions === "latest" && input.noInstall !== true ? { harnessUpdater: HarnessUpdaters[input.environment.harness] } : undefined,
4574
4995
  operationCount: skills.operations.length + harnessConfig.operations.length,
4575
4996
  warnings: [...skills.warnings, ...harnessConfig.warnings].map((item) => item.message)
4576
4997
  };
4577
4998
  });
4578
4999
  function composeAgentEnvironmentSyncPlan(targets) {
4579
- return { version: 1, planId: canonicalSha256(targets), targets };
5000
+ const unlinks = targets.flatMap((target) => target.skills.operations.flatMap((operation) => operation.kind === "unlink_stale_links" ? [operation] : []));
5001
+ const composed = targets.map((target) => ({
5002
+ ...target,
5003
+ skills: {
5004
+ ...target.skills,
5005
+ operations: target.skills.operations.map((operation) => operation.kind === "unlink_stale_links" ? {
5006
+ ...operation,
5007
+ keepPaths: unlinks.flatMap((unlink) => unlink.path === operation.path ? unlink.keepPaths : []),
5008
+ sharedWith: operation.sharedWith.filter((shared) => !targets.some((other) => other.harness === shared.harness))
5009
+ } : operation)
5010
+ }
5011
+ }));
5012
+ return { version: 1, planId: canonicalSha256(composed), targets: composed };
4580
5013
  }
5014
+ var updateHarness = (argv) => gen(function* () {
5015
+ const run = yield* CommandRunner2;
5016
+ const failure = yield* run(argv).pipe(map3((result) => result.exitCode === 0 ? undefined : result.output.trim().split(`
5017
+ `).at(-1) ?? `exit code ${result.exitCode}`), catch_((error) => succeed5(error.message)));
5018
+ return failure === undefined ? [] : [`Kitman could not update ${argv[0]} with \`${argv.join(" ")}\`: ${failure}`];
5019
+ });
4581
5020
  var applyAgentEnvironmentPlans = fnUntraced(function* (plan) {
4582
- for (const target of plan.targets)
4583
- yield* applyAgentEnvironmentMaterialization(target.skills).pipe(mapError((cause) => new AgentEnvironmentPlanApplyError({ phase: "skills", cause })));
5021
+ yield* gen(function* () {
5022
+ const checkout = yield* gitCheckouts;
5023
+ for (const target of plan.targets)
5024
+ yield* applyAgentEnvironmentMaterialization(target.skills, checkout).pipe(mapError((cause) => new AgentEnvironmentPlanApplyError({ phase: "skills", cause })));
5025
+ }).pipe(scoped);
5026
+ const updateWarnings = yield* forEach(plan.targets, (target) => target.harnessUpdater === undefined ? succeed5([]) : updateHarness(target.harnessUpdater));
4584
5027
  for (const target of plan.targets)
4585
5028
  yield* applyAgentEnvironmentConfigSync(target.harnessConfig).pipe(mapError((cause) => new AgentEnvironmentPlanApplyError({ phase: "harness_config", cause })));
4586
- return plan;
5029
+ const targets = yield* forEach(plan.targets, (target, index) => map3(target.skills.codexHookTrust === undefined ? succeed5([]) : trustCodexPluginHooks({
5030
+ ...target.skills.codexHookTrust,
5031
+ plugins: target.skills.materializedPlugins.map((plugin) => plugin.name)
5032
+ }), (trustWarnings) => [...updateWarnings[index] ?? [], ...trustWarnings]).pipe(map3((warnings) => ({
5033
+ ...target,
5034
+ skills: { ...target.skills, warnings: [...target.skills.warnings, ...warnings.map((message) => ({ message }))] },
5035
+ warnings: [...target.warnings, ...warnings]
5036
+ }))));
5037
+ return { ...plan, targets };
4587
5038
  });
4588
5039
  // packages/agent-runtime/src/herdr-executor-surface.ts
4589
5040
  class HerdrExecutorSurfaceError extends TaggedError("HerdrExecutorSurfaceError") {
@@ -4619,7 +5070,7 @@ function herdrExecutorSurface(input) {
4619
5070
  executionClient: {
4620
5071
  request: (method, params) => supervisor.executionClient.request(method, params).pipe(tap((response) => record(method, response)))
4621
5072
  },
4622
- start: () => supervisor.start().pipe(mapError((error) => new HerdrExecutorSurfaceError({ reason: "start_failed", message: error.message })), flatMap((readiness) => readiness.protocol === HERDR_SOCKET_PROTOCOL ? void_2 : fail4(new HerdrExecutorSurfaceError({ reason: "start_failed", message: "herdr_protocol_mismatch" })))),
5073
+ start: () => supervisor.start().pipe(mapError((error) => new HerdrExecutorSurfaceError({ reason: "start_failed", message: error.message })), flatMap2((readiness) => readiness.protocol === HERDR_SOCKET_PROTOCOL ? void_2 : fail4(new HerdrExecutorSurfaceError({ reason: "start_failed", message: "herdr_protocol_mismatch" })))),
4623
5074
  stop: () => andThen(prune, supervisor.stop())
4624
5075
  };
4625
5076
  }
@@ -4639,10 +5090,10 @@ var handshake = [
4639
5090
  `).join("");
4640
5091
  var kitmanMcpPreflight = (mcp) => gen(function* () {
4641
5092
  const spawner = yield* ChildProcessSpawner;
4642
- const server = yield* spawner.spawn(make11(mcp.command, [...mcp.args], {
5093
+ const server = yield* spawner.spawn(make13(mcp.command, [...mcp.args], {
4643
5094
  extendEnv: true,
4644
5095
  env: mcp.env,
4645
- stdin: { stream: make9(new TextEncoder().encode(handshake)), endOnDone: false },
5096
+ stdin: { stream: make11(new TextEncoder().encode(handshake)), endOnDone: false },
4646
5097
  stderr: "ignore"
4647
5098
  }));
4648
5099
  const reply = yield* server.stdout.pipe(decodeText, splitLines, map4((line) => recordValue(getOrUndefined(decodeJsonStringOption(line)))), filter((message) => message?.id === 2), runHead);
@@ -4714,8 +5165,8 @@ var programProcessExecutor = (input) => (work) => gen(function* () {
4714
5165
  };
4715
5166
  const exitCode = sandbox === undefined ? yield* gen(function* () {
4716
5167
  const spawner = yield* ChildProcessSpawner;
4717
- const handle = yield* spawner.spawn(make11(input.bun ?? process.execPath, [file], {
4718
- stdin: make9(new TextEncoder().encode(stdin)),
5168
+ const handle = yield* spawner.spawn(make13(input.bun ?? process.execPath, [file], {
5169
+ stdin: make11(new TextEncoder().encode(stdin)),
4719
5170
  stdout: "inherit",
4720
5171
  stderr: "inherit",
4721
5172
  extendEnv: false,
@@ -4867,6 +5318,7 @@ export {
4867
5318
  AgentRuntimePackageName,
4868
5319
  BundleRefused,
4869
5320
  CommandFailed2 as CommandFailed,
5321
+ CommandRunner2 as CommandRunner,
4870
5322
  ContainerError,
4871
5323
  EgressProxyImage,
4872
5324
  HERDR_SOCKET_PROTOCOL,
@@ -4903,6 +5355,7 @@ export {
4903
5355
  dockerContainerRunner,
4904
5356
  dockerMissing,
4905
5357
  executeHerdrStepAttempt,
5358
+ gitCheckouts,
4906
5359
  harnessAdapter,
4907
5360
  herdrAgentBackendLayer,
4908
5361
  herdrExecutionExtension2 as herdrExecutionExtension,
@@ -4933,6 +5386,7 @@ export {
4933
5386
  previewAgentEnvironmentPlan,
4934
5387
  probeSandboxHost,
4935
5388
  programProcessExecutor,
5389
+ readManifestSkillPaths,
4936
5390
  recordList,
4937
5391
  recordValue,
4938
5392
  refuseAgentRequest,
@@ -4968,6 +5422,7 @@ export {
4968
5422
  stringValue,
4969
5423
  superviseReplica,
4970
5424
  verificationExecutor,
5425
+ withSourceResolutionCache,
4971
5426
  writeAgentEnvironmentConfigFile,
4972
5427
  writeSrtSettings
4973
5428
  };