@kitman-sh/cli 0.7.2 → 0.7.4

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-704ws2q7.js";
187
+ } from "./chunk-jd09xfx0.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-dwgta18y.js";
176
- import"./chunk-ztmwmk8t.js";
198
+ } from "./chunk-q6hk9ceb.js";
199
+ import"./chunk-h12rxyy9.js";
177
200
  import {
178
201
  programLease,
179
202
  programHostsEnv,
180
203
  undeclaredHostsEnv
181
- } from "./chunk-6kdj8psf.js";
182
- import"./chunk-ae7gx971.js";
204
+ } from "./chunk-7f20xrc0.js";
205
+ import"./chunk-j2psmzr4.js";
183
206
  import {
184
207
  nextInt
185
- } from "./chunk-6f4anyec.js";
208
+ } from "./chunk-20nwe9zd.js";
186
209
 
187
210
  // packages/core/src/runner-trust.ts
188
211
  import { createHash } from "crypto";
@@ -524,7 +547,7 @@ var optionalString = (value, label) => gen(function* () {
524
547
  return yield* invalid(`${label} must be a non-empty string when provided`);
525
548
  });
526
549
  // packages/agent-runtime/src/config-convergence.ts
527
- var writeAgentEnvironmentConfigFile = fnUntraced(function* (file, content) {
550
+ var writeAgentEnvironmentConfigFile = fnUntraced(function* (file, content, keep = {}) {
528
551
  const fs = yield* FileSystem;
529
552
  const paths = {
530
553
  path: yield* Path,
@@ -537,14 +560,16 @@ var writeAgentEnvironmentConfigFile = fnUntraced(function* (file, content) {
537
560
  return mergeCodexAgents(existing, content);
538
561
  if (file.endsWith("opencode.json")) {
539
562
  const managed = yield* readManagedNamesForConfig(paths, file);
540
- return mergeOpenCodeConfig(existing, content, managed.mcpNames, managed.pluginNames);
563
+ return mergeOpenCodeConfig(existing, content, managed.mcpNames, without(managed.packageNames, keep.packages));
541
564
  }
542
565
  if (file.endsWith(".codex/config.toml")) {
543
566
  const managed = yield* readManagedNamesForConfig(paths, file);
544
- 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));
545
572
  }
546
- if (isPiConfigPath(paths.path, file, "settings.json"))
547
- return mergePiSettingsConfig(existing, content);
548
573
  if (isPiConfigPath(paths.path, file, "mcp.json")) {
549
574
  const managed = yield* readManagedNamesForConfig(paths, file);
550
575
  return mergePiMcpConfig(existing, content, managed.mcpNames);
@@ -553,6 +578,7 @@ var writeAgentEnvironmentConfigFile = fnUntraced(function* (file, content) {
553
578
  });
554
579
  yield* fs.writeFileString(file, merged);
555
580
  });
581
+ var without = (names, kept = []) => new Set([...names].filter((name) => !kept.includes(name)));
556
582
  var KitmanAgentsStartMarker = "<!-- KITMAN_AGENT_ENVIRONMENT_START -->";
557
583
  var KitmanAgentsEndMarker = "<!-- KITMAN_AGENT_ENVIRONMENT_END -->";
558
584
  var KitmanGeneratedAgentsPrefix = "# Kitman AgentEnvironment\n\nThis file is generated by `kitman sync`";
@@ -575,27 +601,34 @@ ${KitmanAgentsEndMarker}`;
575
601
  ${block}
576
602
  `;
577
603
  }
578
- 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(""))));
579
605
  var readManagedNamesForConfig = (paths, file) => gen(function* () {
580
606
  const manifest = parseJsonObject(yield* readExistingText(yield* managedManifestPathForConfig(paths, file)));
581
607
  const servers = Array.isArray(manifest.mcpServers) ? manifest.mcpServers : [];
582
608
  const plugins = Array.isArray(manifest.plugins) ? manifest.plugins : [];
583
- const materializedSkills = Array.isArray(manifest.materializedSkills) ? manifest.materializedSkills : [];
584
609
  return {
585
610
  mcpNames: new Set(servers.flatMap((entry) => {
586
611
  const item = objectValue(objectValue(entry).item);
587
612
  return isString2(item.name) ? [item.name] : [];
588
613
  })),
589
- pluginNames: new Set(plugins.flatMap((entry) => {
590
- const item = objectValue(objectValue(entry).item);
591
- 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
+ });
592
620
  })),
593
- skillPaths: new Set(materializedSkills.flatMap((entry) => {
594
- const item = objectValue(entry);
595
- return isString2(item.skillPath) ? [item.skillPath] : [];
596
- }))
621
+ skillPaths: manifestSkillPaths(manifest)
597
622
  };
598
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)));
599
632
  var managedManifestPathForConfig = (paths, file) => gen(function* () {
600
633
  const fs = yield* FileSystem;
601
634
  const manifestRoot = managedManifestRootPath(paths, file);
@@ -655,13 +688,13 @@ function manifestFileForHarness(harness) {
655
688
  return "agent-environment.manifest.json";
656
689
  return `agent-environment.${harness}.manifest.json`;
657
690
  }
658
- function mergeOpenCodeConfig(existingText, generatedText, managedMcpNames, managedPluginNames) {
691
+ function mergeOpenCodeConfig(existingText, generatedText, managedMcpNames, managedPackageNames) {
659
692
  const existing = parseJsonObject(existingText);
660
693
  const generated = parseJsonObject(generatedText);
661
694
  const existingMcp = { ...objectValue(existing.mcp) };
662
695
  for (const name of managedMcpNames)
663
696
  delete existingMcp[name];
664
- const existingPlugins = uniqueStrings(arrayValue(existing.plugin).filter((entry) => !pluginReferenceMatchesManaged(entry, managedPluginNames)));
697
+ const existingPlugins = uniqueStrings(arrayValue(existing.plugin).filter((entry) => !isManagedPackageReference(entry, managedPackageNames)));
665
698
  const merged = {
666
699
  ...existing,
667
700
  ...generated,
@@ -679,22 +712,25 @@ function mergeOpenCodeConfig(existingText, generatedText, managedMcpNames, manag
679
712
  return `${JSON.stringify(merged, null, 2)}
680
713
  `;
681
714
  }
682
- function pluginReferenceMatchesManaged(entry, managedPluginNames) {
715
+ function isManagedPackageReference(entry, managedPackageNames, prefix = "") {
683
716
  if (!isString2(entry))
684
717
  return false;
685
- for (const name of managedPluginNames) {
686
- if (entry === name || entry.startsWith(`${name}@`))
718
+ for (const name of managedPackageNames) {
719
+ if (entry === `${prefix}${name}` || entry.startsWith(`${prefix}${name}@`))
687
720
  return true;
688
721
  }
689
722
  return false;
690
723
  }
691
- function mergePiSettingsConfig(existingText, generatedText) {
724
+ function mergePiSettingsConfig(existingText, generatedText, managedPackageNames) {
692
725
  const existing = parseJsonObject(existingText);
693
726
  const generated = parseJsonObject(generatedText);
694
727
  const merged = {
695
728
  ...existing,
696
729
  ...generated,
697
- 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
+ ])
698
734
  };
699
735
  return `${JSON.stringify(merged, null, 2)}
700
736
  `;
@@ -732,8 +768,8 @@ function isPiConfigPath(path, file, name) {
732
768
  const parent = path.dirname(file);
733
769
  return path.basename(file) === name && (path.basename(parent) === ".pi" || path.basename(parent) === "agent" && path.basename(path.dirname(parent)) === ".pi");
734
770
  }
735
- function mergeCodexConfig(existingText, generatedText, managedMcpNames, managedSkillPaths = new Set) {
736
- 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();
737
773
  const generated = generatedText.trim();
738
774
  if (preserved.length === 0)
739
775
  return `${generated}
@@ -746,13 +782,17 @@ function mergeCodexConfig(existingText, generatedText, managedMcpNames, managedS
746
782
  ${generated}
747
783
  `;
748
784
  }
749
- function stripTomlTables(text, removeMcpServerNames, removeSkillPaths) {
785
+ function stripTomlTables(text, removeMcpServerNames, removeSkillPaths, keptKitmanPlugins) {
750
786
  const lines = text.split(`
751
787
  `);
788
+ const staleHookState = (key) => {
789
+ const plugin = /^([^@]+)@kitman:/u.exec(key ?? "")?.[1];
790
+ return keptKitmanPlugins !== undefined && plugin !== undefined && !keptKitmanPlugins.has(plugin);
791
+ };
752
792
  const kept = [];
753
793
  for (let index = 0;index < lines.length; ) {
754
794
  const line = lines[index] ?? "";
755
- const table = /^(\[\[?)([^\]]+)\]\]?/.exec(line)?.[2];
795
+ const table = tomlTableHeader(line);
756
796
  if (table === undefined) {
757
797
  kept.push(line);
758
798
  index += 1;
@@ -760,23 +800,65 @@ function stripTomlTables(text, removeMcpServerNames, removeSkillPaths) {
760
800
  }
761
801
  const block = [line];
762
802
  index += 1;
763
- while (index < lines.length && !/^\[\[?[^\]]+\]\]?/.test(lines[index] ?? "")) {
803
+ while (index < lines.length && tomlTableHeader(lines[index] ?? "") === undefined) {
764
804
  block.push(lines[index] ?? "");
765
805
  index += 1;
766
806
  }
767
807
  const skillPath = table === "skills.config" ? codexSkillPathFromBlock(block) : undefined;
768
- const drop = codexMcpServerName(table, removeMcpServerNames) !== undefined || skillPath !== undefined && removeSkillPaths.has(skillPath);
769
- 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
770
816
  kept.push(...block);
771
817
  }
772
818
  return kept.join(`
773
819
  `);
774
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
+ }
775
857
  function mcpServerNames(toml) {
776
858
  const names = new Set;
777
859
  for (const line of toml.split(`
778
860
  `)) {
779
- const table = /^\[([^\]]+)\]/.exec(line)?.[1];
861
+ const table = tomlTableHeader(line);
780
862
  if (table === undefined)
781
863
  continue;
782
864
  const name = codexMcpServerTableName(table);
@@ -791,13 +873,13 @@ function codexSkillPaths(toml) {
791
873
  `);
792
874
  for (let index = 0;index < lines.length; ) {
793
875
  const line = lines[index] ?? "";
794
- if (!line.startsWith("[[skills.config]]")) {
876
+ if (!/^\s*\[\[\s*skills\.config\s*\]\]/u.test(line)) {
795
877
  index += 1;
796
878
  continue;
797
879
  }
798
880
  const block = [line];
799
881
  index += 1;
800
- while (index < lines.length && !/^\[\[?[^\]]+\]\]?/.test(lines[index] ?? "")) {
882
+ while (index < lines.length && tomlTableHeader(lines[index] ?? "") === undefined) {
801
883
  block.push(lines[index] ?? "");
802
884
  index += 1;
803
885
  }
@@ -838,6 +920,46 @@ function uniqueStrings(values) {
838
920
  return [...new Set(values)];
839
921
  }
840
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
+
841
963
  // packages/agent-runtime/src/config-writer.ts
842
964
  var Sources = {
843
965
  opencodeMcp: "https://opencode.ai/docs/mcp-servers/",
@@ -882,16 +1004,17 @@ var previewAgentEnvironmentConfigSync = fnUntraced(function* (previewInput) {
882
1004
  const syncKitPath = input.syncKit === undefined ? undefined : findForbiddenAgentEnvironmentSecretPath(input.syncKit);
883
1005
  if (syncKitPath !== undefined)
884
1006
  plan.issues.push({ message: `Sync Kit contains forbidden secret-like value at ${syncKitPath}` });
885
- if (input.environment.harness === "opencode")
886
- renderOpenCode(input, plan);
887
- else if (input.environment.harness === "codex")
888
- renderCodex(input, plan);
889
- else if (input.environment.harness === "claude-code")
890
- renderClaudeCode(input, plan);
891
- else if (input.environment.harness === "pi")
892
- renderPi(input, plan);
893
- renderSkillFiles(input, plan);
894
- 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);
895
1018
  if (plan.operations.some((operation) => isUnsafeConfigWritePath(operation.path))) {
896
1019
  plan.issues.push({ message: "AgentEnvironment config sync produced an unsafe relative write path." });
897
1020
  }
@@ -904,13 +1027,53 @@ var previewAgentEnvironmentConfigSync = fnUntraced(function* (previewInput) {
904
1027
  ...input.syncKit === undefined ? undefined : { syncKitHash: input.syncKit.syncKitHash, kitRefs: input.syncKit.kitRefs }
905
1028
  };
906
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
+ });
907
1067
  var applyAgentEnvironmentConfigSync = fnUntraced(function* (plan) {
908
1068
  if (plan.operations.some((operation) => isUnsafeConfigWritePath(operation.path)))
909
1069
  return yield* new AgentEnvironmentConfigSyncError({
910
1070
  issues: [{ message: "AgentEnvironment config apply rejected an unsafe relative write path." }]
911
1071
  });
912
1072
  for (const operation of plan.operations)
913
- 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
+ });
914
1077
  return plan;
915
1078
  });
916
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;
@@ -973,6 +1136,7 @@ function renderOpenCode(input, plan) {
973
1136
  kind: "write_file",
974
1137
  path: targetPath(input, "opencode.json"),
975
1138
  description: "OpenCode project config for Kitman AgentEnvironment MCP servers and package plugins.",
1139
+ ...keepUnresolvedPackages(input),
976
1140
  content: json({
977
1141
  $schema: "https://opencode.ai/config.json",
978
1142
  ...model === undefined ? undefined : { model },
@@ -992,14 +1156,18 @@ function renderCodex(input, plan) {
992
1156
  if (block !== undefined)
993
1157
  serverBlocks.push(block);
994
1158
  }
995
- 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));
996
1163
  renderCodexInstructions(input, plan);
997
1164
  plan.operations.push({
998
1165
  kind: "write_file",
999
1166
  path: targetPath(input, ".codex/config.toml"),
1000
1167
  description: "Codex project config.toml MCP server entries for Kitman AgentEnvironment.",
1001
- content: serverBlocks.length === 0 && model === undefined ? `# Kitman Codex MCP config generated by kitman sync
1002
- ` : `${[...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(`
1003
1171
 
1004
1172
  `)}
1005
1173
  `,
@@ -1027,7 +1195,7 @@ function renderCodexInstructions(input, plan) {
1027
1195
  "",
1028
1196
  "## Plugins",
1029
1197
  "",
1030
- ...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, ", ")})`)),
1031
1199
  "",
1032
1200
  "## Harness Settings",
1033
1201
  "",
@@ -1070,7 +1238,7 @@ function renderCodexInstructions(input, plan) {
1070
1238
  }
1071
1239
  function renderClaudeCode(input, plan) {
1072
1240
  const mcpServers = objectMap(input.environment.mcpServers, (server) => renderClaudeMcpServer(input, server, plan));
1073
- renderUnsupportedPlugins(input.environment.plugins, input.environment.harness, plan);
1241
+ renderUnsupportedPlugins(unhandledPlugins(input), input.environment.harness, plan);
1074
1242
  const model = renderModelSetting(input, plan);
1075
1243
  const skillOverrides = Object.fromEntries((input.materializedSkills ?? []).map((skill) => [skill.name, "on"]));
1076
1244
  if (Object.keys(mcpServers).length > 0 || Object.keys(skillOverrides).length > 0) {
@@ -1103,18 +1271,17 @@ function renderPi(input, plan) {
1103
1271
  ...Object.keys(mcpServers).length === 0 ? [] : ["npm:pi-mcp-adapter"],
1104
1272
  ...renderPiPlugins(input.environment.plugins, plan)
1105
1273
  ];
1106
- if (packages.length > 0 || model !== undefined) {
1107
- plan.operations.push({
1108
- kind: "write_file",
1109
- path: targetPath(input, ".pi/settings.json"),
1110
- description: "Pi project settings generated from resolved non-secret MCP and model intent.",
1111
- content: json({
1112
- ...packages.length === 0 ? undefined : { packages },
1113
- ...model === undefined ? undefined : { model }
1114
- }),
1115
- sources: [Sources.piReadme, Sources.piMcpAdapter]
1116
- });
1117
- }
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
+ });
1118
1285
  plan.operations.push({
1119
1286
  kind: "write_file",
1120
1287
  path: targetPath(input, ".pi/mcp.json"),
@@ -1126,8 +1293,9 @@ function renderPi(input, plan) {
1126
1293
  function renderPiPlugins(plugins, plan) {
1127
1294
  const packages = [];
1128
1295
  for (const plugin of plugins) {
1129
- if (plugin.item.source.kind === "package") {
1130
- 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)}`);
1131
1299
  continue;
1132
1300
  }
1133
1301
  reportUnsupported(plan, plugin.item.policy, `plugin ${plugin.item.name}`, "pi", "Pi package plugins require an npm package source", sourceLabel2(plugin.source));
@@ -1250,20 +1418,52 @@ function hasNameArgument(args) {
1250
1418
  function renderOpenCodePlugins(plugins, plan) {
1251
1419
  const packages = [];
1252
1420
  for (const plugin of plugins) {
1253
- if (plugin.item.source.kind === "package") {
1254
- 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));
1255
1424
  continue;
1256
1425
  }
1257
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));
1258
1427
  }
1259
1428
  return packages;
1260
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
+ }
1261
1453
  function renderUnsupportedPlugins(plugins, harness, plan) {
1262
1454
  for (const plugin of plugins) {
1263
- const sourceKind = plugin.item.source.kind === "package" ? "package sources" : `${plugin.item.source.kind} sources`;
1264
- 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));
1265
1456
  }
1266
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
+ }
1267
1467
  function renderSkillFiles(input, plan) {
1268
1468
  const materializedSkillNames = new Set((input.materializedSkills ?? []).map((skill) => skill.name));
1269
1469
  for (const skill of input.environment.skills) {
@@ -1312,7 +1512,7 @@ function renderManifest(input, plan) {
1312
1512
  generatedBy: "kitman",
1313
1513
  harness: input.environment.harness,
1314
1514
  skills: input.environment.skills,
1315
- plugins: input.environment.plugins,
1515
+ plugins: [...input.environment.plugins, ...input.unresolvedPlugins ?? []],
1316
1516
  mcpServers: input.environment.mcpServers,
1317
1517
  harnessSettings: input.environment.harnessSettings,
1318
1518
  providerAuthRefs: input.environment.providerAuthRefs,
@@ -1596,6 +1796,37 @@ function repositoryUrl(repoBinding) {
1596
1796
  return repoBinding.repositoryUrl ?? `https://${repoBinding.host}/${repoBinding.owner}/${repoBinding.name}.git`;
1597
1797
  }
1598
1798
  // packages/agent-runtime/src/skill-materializer.ts
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
+ });
1829
+
1599
1830
  class AgentEnvironmentMaterializationError extends TaggedError("AgentEnvironmentMaterializationError") {
1600
1831
  get message() {
1601
1832
  return `AgentEnvironment materialization failed: ${this.issues.map((issue) => issue.message).join("; ")}`;
@@ -1605,6 +1836,7 @@ class AgentEnvironmentMaterializationError extends TaggedError("AgentEnvironment
1605
1836
  class SkillMaterializationRefused extends TaggedError("SkillMaterializationRefused") {
1606
1837
  }
1607
1838
  var previewAgentEnvironmentMaterialization = fnUntraced(function* (input) {
1839
+ const fs = yield* FileSystem;
1608
1840
  const path = yield* Path;
1609
1841
  const home = input.rootDir ?? (yield* optionalEnv("HOME")) ?? ".";
1610
1842
  const harness = input.environment.harness;
@@ -1612,68 +1844,341 @@ var previewAgentEnvironmentMaterialization = fnUntraced(function* (input) {
1612
1844
  const targetRoot = skillBasePath2(path, harness, input.scope, input.rootDir, home);
1613
1845
  const operations = [];
1614
1846
  const materializedSkills = [];
1847
+ const materializedPlugins = [];
1848
+ const skippedPlugins = [];
1615
1849
  const warnings = [];
1850
+ const unresolvedLinks = [];
1616
1851
  for (const skill of input.environment.skills) {
1617
1852
  const skillKey = skillIdentity(skill.item);
1853
+ const targetPath = path.join(targetRoot, `${skillSlug2(skill.item.name)}-${hashSegment(skillKey)}`);
1618
1854
  const source = skill.item.source;
1619
- const skip = (message) => {
1620
- const issue = { message, skillName: skill.item.name, skillKey };
1855
+ const fromKit = skill.source.label === undefined ? "" : ` (from Kit ${skill.source.label})`;
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 };
1621
1862
  if (skill.item.policy === "required")
1622
- return new AgentEnvironmentMaterializationError({ issues: [issue] });
1863
+ return yield* new AgentEnvironmentMaterializationError({ issues: [issue] });
1623
1864
  warnings.push(issue);
1624
- return;
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
+ });
1877
+ continue;
1878
+ }
1879
+ const commit = resolution !== undefined && "commit" in resolution ? resolution.commit : undefined;
1880
+ const expose = (skillPath) => {
1881
+ operations.push({
1882
+ kind: "symlink_skill",
1883
+ skillName: skill.item.name,
1884
+ skillKey,
1885
+ sourcePath: skillPath,
1886
+ targetPath,
1887
+ sourceRoot,
1888
+ targetRoot
1889
+ });
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
+ });
1625
1899
  };
1626
- const refusal = source.kind !== "kitman" ? skip(input.noInstall === true ? `Skipping materialization for ${skill.item.name} because sync was run with noInstall.` : `Materializing ${skill.item.name} from ${source.kind} is not supported; only Kitman skills are materialized.`) : source.contentMarkdown === undefined ? skip(`Kitman skill ${skill.item.name} has no contentMarkdown to materialize.`) : undefined;
1627
- if (refusal !== undefined)
1628
- return yield* refusal;
1629
- if (source.kind !== "kitman" || source.contentMarkdown === undefined)
1900
+ if (source.kind === "git" && commit !== undefined) {
1901
+ operations.push({
1902
+ kind: "fetch_git_skill",
1903
+ skillName: skill.item.name,
1904
+ skillKey,
1905
+ url: source.url,
1906
+ ref: commit,
1907
+ ...source.path === undefined ? undefined : { repositoryPath: source.path },
1908
+ path: gitSkillPath
1909
+ });
1910
+ expose(gitSkillPath);
1911
+ } else if (source.kind === "kitman" && source.contentMarkdown !== undefined) {
1912
+ const skillPath = path.join(sourceRoot, "kitman", hashSegment(source.skillId ?? source.name ?? skill.item.name), skillSlug2(skill.item.name));
1913
+ operations.push({
1914
+ kind: "write_skill",
1915
+ skillName: skill.item.name,
1916
+ skillKey,
1917
+ path: path.join(skillPath, "SKILL.md"),
1918
+ content: skillMarkdown(skill.item.name, source.contentMarkdown)
1919
+ });
1920
+ expose(skillPath);
1921
+ }
1922
+ }
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
+ });
1630
1984
  continue;
1631
- const skillPath = path.join(sourceRoot, "kitman", hashSegment(source.skillId ?? source.name ?? skill.item.name), skillSlug2(skill.item.name));
1985
+ }
1632
1986
  operations.push({
1633
- kind: "write_skill",
1634
- skillName: skill.item.name,
1635
- skillKey,
1636
- path: path.join(skillPath, "SKILL.md"),
1637
- content: skillMarkdown(skill.item.name, source.contentMarkdown)
1638
- }, {
1639
1987
  kind: "symlink_skill",
1640
- skillName: skill.item.name,
1641
- skillKey,
1642
- sourcePath: skillPath,
1643
- targetPath: path.join(targetRoot, `${skillSlug2(skill.item.name)}-${hashSegment(skillKey)}`),
1988
+ skillName: name,
1989
+ skillKey: pluginKey,
1990
+ sourcePath: pluginPath,
1991
+ targetPath: path.join(targetRoot, directoryName),
1644
1992
  sourceRoot,
1645
1993
  targetRoot
1646
1994
  });
1647
- materializedSkills.push({ name: skill.item.name, key: skillKey, harness, source, skillPath, managed: true });
1995
+ materializedPlugins.push({ name, key: pluginKey, harness, source, pluginPath, commit: resolution.commit });
1648
1996
  }
1649
- return { harness, operations, materializedSkills, warnings };
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
+ };
1650
2038
  });
1651
- var applyAgentEnvironmentMaterialization = fnUntraced(function* (plan) {
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;
2052
+ });
2053
+ var applyAgentEnvironmentMaterialization = (plan, checkout) => gen(function* () {
2054
+ const checkoutTree = checkout ?? (yield* gitCheckouts);
1652
2055
  for (const operation of plan.operations) {
1653
2056
  if (operation.kind === "write_skill")
1654
2057
  yield* writeSkill(plan, operation);
2058
+ else if (operation.kind === "fetch_git_skill")
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);
1655
2066
  else
1656
2067
  yield* exposeSkill(operation);
1657
2068
  }
1658
2069
  return plan;
1659
- });
2070
+ }).pipe(scoped);
1660
2071
  var refuse = (message) => fail4(new SkillMaterializationRefused({ message }));
1661
- var writeSkill = (plan, operation) => gen(function* () {
1662
- const fs = yield* FileSystem;
1663
- const path = yield* Path;
1664
- const boundary = plan.operations.find((candidate) => candidate.kind === "symlink_skill" && candidate.skillKey === operation.skillKey);
2072
+ var managedSkillPath = (plan, skillKey, file) => gen(function* () {
2073
+ const boundary = plan.operations.find((candidate) => candidate.kind === "symlink_skill" && candidate.skillKey === skillKey);
1665
2074
  if (boundary === undefined)
1666
2075
  return yield* refuse("SKILL_SYMLINK_BOUNDARY_MISSING");
1667
- const root = path.resolve(boundary.sourceRoot);
1668
- const target = path.resolve(operation.path);
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);
2081
+ const target = path.resolve(file);
1669
2082
  if (!isPathWithin(path, root, target) || target === root)
1670
2083
  return yield* refuse("SKILL_WRITE_OUTSIDE_ROOT");
1671
2084
  yield* assertNoSymlinkComponents(path.dirname(target), "SKILL_WRITE_OUTSIDE_ROOT");
2085
+ return target;
2086
+ });
2087
+ var writeSkill = (plan, operation) => gen(function* () {
2088
+ const fs = yield* FileSystem;
2089
+ const path = yield* Path;
2090
+ const target = yield* managedSkillPath(plan, operation.skillKey, operation.path);
1672
2091
  if (yield* isSymbolicLink(target))
1673
2092
  return yield* refuse("SKILL_WRITE_SYMLINK");
1674
2093
  yield* fs.makeDirectory(path.dirname(target), { recursive: true });
1675
2094
  yield* fs.writeFileString(target, operation.content);
1676
2095
  });
2096
+ var checkoutGitTree = (url, ref) => gen(function* () {
2097
+ const fs = yield* FileSystem;
2098
+ const path = yield* Path;
2099
+ const scratch = yield* fs.makeTempDirectoryScoped({ prefix: "kitman-git-" });
2100
+ const repository = path.join(scratch, "repository");
2101
+ const tree = path.join(scratch, "tree");
2102
+ const git = (...args) => runCommand2("git", ["--git-dir", repository, ...args]);
2103
+ yield* fs.makeDirectory(tree);
2104
+ yield* runCommand2("git", ["init", "--quiet", "--bare", repository]);
2105
+ yield* git("fetch", "--quiet", "--depth", "1", "--", url, ref);
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);
2124
+ const skillDirectory = path.resolve(tree, operation.repositoryPath ?? ".");
2125
+ if (!isPathWithin(path, tree, skillDirectory))
2126
+ return yield* refuse("SKILL_SOURCE_OUTSIDE_ROOT");
2127
+ if (yield* containsSymbolicLink(tree, skillDirectory))
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.`);
2129
+ if (!(yield* fs.exists(path.join(skillDirectory, "SKILL.md"))))
2130
+ return yield* refuse(`Git skill ${operation.skillName} has no SKILL.md at ${operation.repositoryPath ?? "the repository root"} in ${operation.url}@${operation.ref}.`);
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");
2139
+ yield* fs.makeDirectory(path.dirname(target), { recursive: true });
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
+ });
2169
+ var containsSymbolicLink = (root, directory) => gen(function* () {
2170
+ const fs = yield* FileSystem;
2171
+ const path = yield* Path;
2172
+ for (let current = directory;current !== root; current = path.dirname(current))
2173
+ if (yield* isSymbolicLink(current))
2174
+ return true;
2175
+ if (!(yield* fs.exists(directory)))
2176
+ return false;
2177
+ for (const entry of yield* fs.readDirectory(directory, { recursive: true }))
2178
+ if (yield* isSymbolicLink(path.join(directory, entry)))
2179
+ return true;
2180
+ return false;
2181
+ });
1677
2182
  var exposeSkill = (operation) => gen(function* () {
1678
2183
  const fs = yield* FileSystem;
1679
2184
  const path = yield* Path;
@@ -1707,8 +2212,8 @@ var exposeSkill = (operation) => gen(function* () {
1707
2212
  }
1708
2213
  yield* fs.symlink(path.resolve(operation.sourcePath), targetPath);
1709
2214
  });
1710
- var isSymbolicLink = (file) => flatMap(FileSystem, (fs) => fs.readLink(file).pipe(as(true), orElseSucceed(() => false)));
1711
- 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 }))));
1712
2217
  var assertNoSymlinkComponents = (file, reason) => gen(function* () {
1713
2218
  const path = yield* Path;
1714
2219
  for (let current = path.resolve(file);; current = path.dirname(current)) {
@@ -1729,7 +2234,7 @@ function skillIdentity(skill) {
1729
2234
  if (source.kind === "kitman")
1730
2235
  return `kitman:${source.skillId ?? source.name ?? skill.name}:${skill.name}`;
1731
2236
  if (source.kind === "git")
1732
- return `git:${source.url}#${source.ref}:${source.path ?? ""}:${skill.name}`;
2237
+ return `git:${source.url}#${source.ref ?? ""}:${source.path ?? ""}:${skill.name}`;
1733
2238
  if (source.kind === "package")
1734
2239
  return `package:${source.packageName}@${source.version}:${source.path ?? ""}:${skill.name}`;
1735
2240
  if (source.kind === "url")
@@ -1743,7 +2248,9 @@ var ProjectSkillDirectories = {
1743
2248
  pi: ".pi/skills"
1744
2249
  };
1745
2250
  function skillBasePath2(path, harness, scope, rootDir, home) {
1746
- 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");
1747
2254
  }
1748
2255
  function skillMarkdown(name, contentMarkdown) {
1749
2256
  const content = contentMarkdown.trimEnd();
@@ -1798,7 +2305,7 @@ var closeSocket = (conn, isOpen) => {
1798
2305
  var makeNet = (options) => fromDuplex(contextWith((context) => {
1799
2306
  let conn;
1800
2307
  let isOpen = false;
1801
- return flatMap(addFinalizer(get(context, Scope), sync(() => {
2308
+ return flatMap2(addFinalizer(get(context, Scope), sync(() => {
1802
2309
  if (!conn)
1803
2310
  return;
1804
2311
  closeSocket(conn, isOpen);
@@ -2200,7 +2707,7 @@ class HerdrSocketClient {
2200
2707
  });
2201
2708
  }
2202
2709
  sessionSnapshot() {
2203
- return flatMap(this.request("session.snapshot", {}), (result) => {
2710
+ return flatMap2(this.request("session.snapshot", {}), (result) => {
2204
2711
  const snapshot = isRecord(result) && result.type === "session_snapshot" ? result.snapshot : undefined;
2205
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))
2206
2713
  return fail4(fail2("herdr_session_snapshot_invalid"));
@@ -2315,7 +2822,7 @@ function isRecord(value) {
2315
2822
  // packages/agent-runtime/src/herdr-server-supervisor.ts
2316
2823
  class HerdrServerError extends TaggedError("HerdrServerError") {
2317
2824
  }
2318
- var fail3 = (message) => new HerdrServerError({ message });
2825
+ var fail5 = (message) => new HerdrServerError({ message });
2319
2826
 
2320
2827
  class HerdrServerSupervisor {
2321
2828
  runnerId;
@@ -2347,9 +2854,9 @@ class HerdrServerSupervisor {
2347
2854
  }
2348
2855
  start = fnUntraced(function* () {
2349
2856
  if (this.started && this.client !== undefined)
2350
- return yield* this.client.sessionSnapshot().pipe(mapError((error) => fail3(error.message)));
2857
+ return yield* this.client.sessionSnapshot().pipe(mapError((error) => fail5(error.message)));
2351
2858
  if (!["linux", "darwin"].includes(this.options.platform ?? process.platform))
2352
- return yield* fail3("herdr_platform_unsupported");
2859
+ return yield* fail5("herdr_platform_unsupported");
2353
2860
  const defaultSession = this.options.session === "default";
2354
2861
  const runtime = defaultSession ? undefined : yield* this.prepareRuntime();
2355
2862
  const socketClientOptions = this.options.socketClientOptions ?? {};
@@ -2390,7 +2897,7 @@ class HerdrServerSupervisor {
2390
2897
  }
2391
2898
  const snapshot = yield* awaitReadiness(socketClient, this.readinessTimeoutMs);
2392
2899
  if (snapshot.protocol !== HERDR_SOCKET_PROTOCOL)
2393
- return yield* fail3("herdr_protocol_mismatch");
2900
+ return yield* fail5("herdr_protocol_mismatch");
2394
2901
  return snapshot;
2395
2902
  });
2396
2903
  attachToDefaultSession = fnUntraced(function* (socketPath, socketClient) {
@@ -2398,7 +2905,7 @@ class HerdrServerSupervisor {
2398
2905
  if (isFailure(running)) {
2399
2906
  const message = running.failure.message;
2400
2907
  if (message !== "herdr_socket_connect_failed" && message !== "herdr_socket_closed")
2401
- return yield* fail3(message);
2908
+ return yield* fail5(message);
2402
2909
  yield* (this.options.spawnServer ?? spawnServer)({
2403
2910
  command: this.options.herdrCommand ?? "herdr",
2404
2911
  args: ["server"],
@@ -2407,7 +2914,7 @@ class HerdrServerSupervisor {
2407
2914
  }
2408
2915
  const snapshot = isSuccess(running) ? running.success : yield* awaitReadiness(socketClient, this.readinessTimeoutMs);
2409
2916
  if (snapshot.protocol !== HERDR_SOCKET_PROTOCOL)
2410
- return yield* fail3("herdr_protocol_mismatch");
2917
+ return yield* fail5("herdr_protocol_mismatch");
2411
2918
  return snapshot;
2412
2919
  });
2413
2920
  prepareRuntime = fnUntraced(function* () {
@@ -2419,7 +2926,7 @@ class HerdrServerSupervisor {
2419
2926
  fs.makeDirectory(path.dirname(directory), { recursive: true, mode: 448 }),
2420
2927
  fs.makeDirectory(directory, { recursive: true, mode: 448 }),
2421
2928
  fs.chmod(directory, 448)
2422
- ]), () => fail3("herdr_runtime_unavailable"));
2929
+ ]), () => fail5("herdr_runtime_unavailable"));
2423
2930
  const sessionName = `kitman-herdr-${digest(this.runnerId)}`;
2424
2931
  const runtime = {
2425
2932
  directory,
@@ -2453,7 +2960,7 @@ var reclaimStaleRuntime = (runtime, runnerId, isAlive) => gen(function* () {
2453
2960
  if (!socketExists && owner === undefined)
2454
2961
  return false;
2455
2962
  if (owner === undefined || owner.runnerId !== runnerId || owner.socketPath !== runtime.socketPath)
2456
- 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"}`);
2457
2964
  if (yield* isAlive(owner.pid))
2458
2965
  return true;
2459
2966
  yield* removeRuntimeFiles(runtime);
@@ -2464,8 +2971,8 @@ var awaitReadiness = (client, readinessTimeoutMs) => gen(function* () {
2464
2971
  for (;; ) {
2465
2972
  const remaining = deadline - (yield* currentTimeMillis);
2466
2973
  if (remaining <= 0)
2467
- return yield* fail3("herdr_socket_connect_failed");
2468
- 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")) })));
2469
2976
  if (isSuccess(attempt))
2470
2977
  return attempt.success;
2471
2978
  const message = attempt.failure.message;
@@ -2493,15 +3000,15 @@ var homeDirectory = map3(optionalEnv("HOME"), (home) => home ?? "/");
2493
3000
  var temporaryDirectory = map3(optionalEnv("TMPDIR"), (tmp) => tmp ?? "/tmp");
2494
3001
  var spawnServer = (input) => gen(function* () {
2495
3002
  const spawner = yield* ChildProcessSpawner;
2496
- const scope = yield* make6();
2497
- 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], {
2498
3005
  env: { HERDR_SOCKET_PATH: input.socketPath },
2499
3006
  extendEnv: true,
2500
3007
  detached: true,
2501
3008
  stdin: "ignore",
2502
3009
  stdout: "ignore",
2503
3010
  stderr: "ignore"
2504
- })).pipe(provide(scope), mapError(() => fail3("herdr_server_start_failed")));
3011
+ })).pipe(provide(scope), mapError(() => fail5("herdr_server_start_failed")));
2505
3012
  yield* ignore(handle.unref);
2506
3013
  return {
2507
3014
  pid: handle.pid,
@@ -2521,7 +3028,7 @@ var writeOwnerMarker = (path, marker) => gen(function* () {
2521
3028
  yield* fs.writeFileString(path, `${encodeJsonStringSync(marker)}
2522
3029
  `, { mode: 384 });
2523
3030
  yield* fs.chmod(path, 384);
2524
- }).pipe(mapError(() => fail3("herdr_runtime_unavailable")));
3031
+ }).pipe(mapError(() => fail5("herdr_runtime_unavailable")));
2525
3032
  var readOwnerMarker = (path) => gen(function* () {
2526
3033
  const fs = yield* FileSystem;
2527
3034
  const text = yield* fs.readFileString(path);
@@ -2570,7 +3077,7 @@ function shortId(value) {
2570
3077
  function isRecord2(value) {
2571
3078
  return isObject(value) && value !== null && !Array.isArray(value);
2572
3079
  }
2573
- 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) => {
2574
3081
  const result = recordValue(response);
2575
3082
  return result === undefined || result.type !== expectedType ? fail4(new HerdrHarnessError({ message: `${method}_response_invalid` })) : succeed5(result);
2576
3083
  });
@@ -2720,7 +3227,7 @@ var HarnessAdapters = [
2720
3227
  auth: ["~/.codex"],
2721
3228
  providerHosts: ["api.openai.com", "auth.openai.com", "chatgpt.com"],
2722
3229
  effortArgs: ["-c", 'model_reasoning_effort="{effort}"'],
2723
- autoPermissionArgs: ["--dangerously-bypass-approvals-and-sandbox"],
3230
+ autoPermissionArgs: ["--dangerously-bypass-approvals-and-sandbox", "--dangerously-bypass-hook-trust"],
2724
3231
  firstMessage: "herdr_prompt",
2725
3232
  mcp: { kind: "toml_overrides", flag: "-c" },
2726
3233
  skills: { kind: "skills_config", root: "skills" },
@@ -3289,7 +3796,7 @@ var executeHerdrStepAttempt = (input) => gen(function* () {
3289
3796
  });
3290
3797
  yield* input.harness.prompt({ targetId: launched.targetId, text: firstMessage(input.agent, worktreePath) });
3291
3798
  const steering = input.control.steering;
3292
- 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({
3293
3800
  onSuccess: () => steering.report(note, true),
3294
3801
  onFailure: () => steering.report(note, false)
3295
3802
  })))));
@@ -3700,7 +4207,7 @@ var findSession = (adapter, place) => gen(function* () {
3700
4207
  }).pipe(orElseSucceed(() => {
3701
4208
  return;
3702
4209
  }));
3703
- 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)));
3704
4211
  var restoreSession = (adapter, place) => gen(function* () {
3705
4212
  const fs = yield* FileSystem;
3706
4213
  const location = adapter.session?.location;
@@ -3971,8 +4478,8 @@ function runReplica(control, executor, signals) {
3971
4478
  if (work === undefined)
3972
4479
  return { status: "idle" };
3973
4480
  const lease = leaseInput(work);
3974
- const workHeartbeat = sleep(heartbeatInterval(work)).pipe(flatMap(() => call("heartbeatWork", () => control.heartbeatWork(work.id, lease))), repeat({ until: (heartbeat) => heartbeat?.settled === true }), as({ status: "settled" }));
3975
- 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)));
3976
4483
  let completed = false;
3977
4484
  let leaseLost = false;
3978
4485
  return yield* gen(function* () {
@@ -4356,7 +4863,101 @@ function runnerStepAttemptFromAgentStep(work) {
4356
4863
  }
4357
4864
  };
4358
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
+
4359
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
+
4360
4961
  class AgentEnvironmentPlanApplyError extends TaggedError("AgentEnvironmentPlanApplyError") {
4361
4962
  get message() {
4362
4963
  return `AgentEnvironment plan apply failed during ${this.phase}: ${this.cause.message}`;
@@ -4368,11 +4969,18 @@ var previewAgentEnvironmentPlan = fnUntraced(function* (input) {
4368
4969
  ...input.rootDir === undefined ? undefined : { rootDir: input.rootDir },
4369
4970
  ...input.scope === undefined ? undefined : { scope: input.scope },
4370
4971
  ...input.cacheRoot === undefined ? undefined : { cacheRoot: input.cacheRoot },
4371
- ...input.noInstall === undefined ? undefined : { noInstall: input.noInstall }
4972
+ ...input.noInstall === undefined ? undefined : { noInstall: input.noInstall },
4973
+ ...input.approvedSkillSources === undefined ? undefined : { approvedSkillSources: input.approvedSkillSources },
4974
+ ...input.requireReviewedSources === undefined ? undefined : { requireReviewedSources: input.requireReviewedSources },
4975
+ ...input.versions === undefined ? undefined : { versions: input.versions }
4372
4976
  });
4373
4977
  const harnessConfig = yield* previewAgentEnvironmentConfigSync({
4374
4978
  environment: input.environment,
4375
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 },
4376
4984
  ...input.syncKit === undefined ? undefined : { syncKit: input.syncKit },
4377
4985
  ...input.rootDir === undefined ? undefined : { rootDir: input.rootDir },
4378
4986
  ...input.scope === undefined ? undefined : { scope: input.scope },
@@ -4383,19 +4991,50 @@ var previewAgentEnvironmentPlan = fnUntraced(function* (input) {
4383
4991
  ...input.syncKit === undefined ? undefined : { syncKitHash: input.syncKit.syncKitHash },
4384
4992
  skills,
4385
4993
  harnessConfig,
4994
+ ...input.versions === "latest" && input.noInstall !== true ? { harnessUpdater: HarnessUpdaters[input.environment.harness] } : undefined,
4386
4995
  operationCount: skills.operations.length + harnessConfig.operations.length,
4387
4996
  warnings: [...skills.warnings, ...harnessConfig.warnings].map((item) => item.message)
4388
4997
  };
4389
4998
  });
4390
4999
  function composeAgentEnvironmentSyncPlan(targets) {
4391
- 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 };
4392
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
+ });
4393
5020
  var applyAgentEnvironmentPlans = fnUntraced(function* (plan) {
4394
- for (const target of plan.targets)
4395
- 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));
4396
5027
  for (const target of plan.targets)
4397
5028
  yield* applyAgentEnvironmentConfigSync(target.harnessConfig).pipe(mapError((cause) => new AgentEnvironmentPlanApplyError({ phase: "harness_config", cause })));
4398
- 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 };
4399
5038
  });
4400
5039
  // packages/agent-runtime/src/herdr-executor-surface.ts
4401
5040
  class HerdrExecutorSurfaceError extends TaggedError("HerdrExecutorSurfaceError") {
@@ -4431,7 +5070,7 @@ function herdrExecutorSurface(input) {
4431
5070
  executionClient: {
4432
5071
  request: (method, params) => supervisor.executionClient.request(method, params).pipe(tap((response) => record(method, response)))
4433
5072
  },
4434
- 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" })))),
4435
5074
  stop: () => andThen(prune, supervisor.stop())
4436
5075
  };
4437
5076
  }
@@ -4451,10 +5090,10 @@ var handshake = [
4451
5090
  `).join("");
4452
5091
  var kitmanMcpPreflight = (mcp) => gen(function* () {
4453
5092
  const spawner = yield* ChildProcessSpawner;
4454
- const server = yield* spawner.spawn(make11(mcp.command, [...mcp.args], {
5093
+ const server = yield* spawner.spawn(make13(mcp.command, [...mcp.args], {
4455
5094
  extendEnv: true,
4456
5095
  env: mcp.env,
4457
- stdin: { stream: make9(new TextEncoder().encode(handshake)), endOnDone: false },
5096
+ stdin: { stream: make11(new TextEncoder().encode(handshake)), endOnDone: false },
4458
5097
  stderr: "ignore"
4459
5098
  }));
4460
5099
  const reply = yield* server.stdout.pipe(decodeText, splitLines, map4((line) => recordValue(getOrUndefined(decodeJsonStringOption(line)))), filter((message) => message?.id === 2), runHead);
@@ -4526,8 +5165,8 @@ var programProcessExecutor = (input) => (work) => gen(function* () {
4526
5165
  };
4527
5166
  const exitCode = sandbox === undefined ? yield* gen(function* () {
4528
5167
  const spawner = yield* ChildProcessSpawner;
4529
- const handle = yield* spawner.spawn(make11(input.bun ?? process.execPath, [file], {
4530
- 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)),
4531
5170
  stdout: "inherit",
4532
5171
  stderr: "inherit",
4533
5172
  extendEnv: false,
@@ -4679,6 +5318,7 @@ export {
4679
5318
  AgentRuntimePackageName,
4680
5319
  BundleRefused,
4681
5320
  CommandFailed2 as CommandFailed,
5321
+ CommandRunner2 as CommandRunner,
4682
5322
  ContainerError,
4683
5323
  EgressProxyImage,
4684
5324
  HERDR_SOCKET_PROTOCOL,
@@ -4715,6 +5355,7 @@ export {
4715
5355
  dockerContainerRunner,
4716
5356
  dockerMissing,
4717
5357
  executeHerdrStepAttempt,
5358
+ gitCheckouts,
4718
5359
  harnessAdapter,
4719
5360
  herdrAgentBackendLayer,
4720
5361
  herdrExecutionExtension2 as herdrExecutionExtension,
@@ -4745,6 +5386,7 @@ export {
4745
5386
  previewAgentEnvironmentPlan,
4746
5387
  probeSandboxHost,
4747
5388
  programProcessExecutor,
5389
+ readManifestSkillPaths,
4748
5390
  recordList,
4749
5391
  recordValue,
4750
5392
  refuseAgentRequest,
@@ -4780,6 +5422,7 @@ export {
4780
5422
  stringValue,
4781
5423
  superviseReplica,
4782
5424
  verificationExecutor,
5425
+ withSourceResolutionCache,
4783
5426
  writeAgentEnvironmentConfigFile,
4784
5427
  writeSrtSettings
4785
5428
  };