@codeworksh/harness 0.3.0 → 0.4.0-dev.20261020131429

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/effect.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as __reExport, t as __exportAll } from "./rolldown-runtime-B4FMCO8f.mjs";
2
2
  import { a as io_exports, i as errors_exports, n as shell_exports, o as instance_exports, r as filesystem_exports, s as posix } from "./resource-DFZh0e-s.mjs";
3
3
  import { a as decode, c as importModule, d as share, l as lendResolve, n as dimensionNote, o as load$5, r as resize, s as load$4, u as resolveModule } from "./resize-D34zcVQl.mjs";
4
- import "./daytona-D4sNwzSr.mjs";
4
+ import "./daytona-BqRoRhe7.mjs";
5
5
  import "./vercel-3GouQSJI.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { Cause, Clock, Config, Context, Crypto, Data, DateTime, Deferred, Duration, Effect, Exit, Fiber, FiberSet, FileSystem, Layer, LayerMap, Metric, Option, Path, Predicate, PubSub, Queue, Random, Ref, Result, Schedule, Schema, SchemaIssue, Scope, Semaphore, Stream, String as String$1 } from "effect";
@@ -21,11 +21,13 @@ import { ChildProcess, ChildProcessSpawner } from "effect/process";
21
21
  import { Buffer as Buffer$1 } from "node:buffer";
22
22
  import { fileURLToPath, pathToFileURL } from "node:url";
23
23
  import { DirectoryNotFoundError, DirectoryNotFoundError as DirectoryNotFoundError$1, Info, Info as Info$1, NotDirectoryError, NotDirectoryError as NotDirectoryError$1, Service, Service as Service$8 } from "@codeworksh/plugin/location";
24
- import { generateModels } from "@codeworksh/aikit/modelgen";
24
+ import { downloadCatalog } from "@codeworksh/aikit/modelgen";
25
25
  import { JsonGitHubCopilotAuthStorage, getGitHubCopilotApiKey } from "@codeworksh/aikit/oauth/github/copilot";
26
26
  import { JsonOpenAICodexAuthStorage, getOpenAICodexApiKey } from "@codeworksh/aikit/oauth/openai/codex";
27
27
  import { parse, printParseErrorCode } from "jsonc-parser";
28
28
  import * as Section from "@codeworksh/plugin/plugin/section";
29
+ import ignore from "ignore";
30
+ import { parse as parse$1 } from "yaml";
29
31
  import * as Diff from "diff";
30
32
  import { FetchHttpClient, HttpClient } from "effect/http";
31
33
  import npa from "npm-package-arg";
@@ -134,10 +136,10 @@ var global_exports = /* @__PURE__ */ __exportAll({
134
136
  app: () => app,
135
137
  appConfigDir: () => appConfigDir,
136
138
  authFile: () => authFile,
137
- defaultLayer: () => defaultLayer$2,
139
+ defaultLayer: () => defaultLayer$4,
138
140
  homeConfig: () => homeConfig,
139
141
  layer: () => layer$27,
140
- layerWith: () => layerWith,
142
+ layerWith: () => layerWith$1,
141
143
  make: () => make$18,
142
144
  resolve: () => resolve$4
143
145
  });
@@ -184,8 +186,8 @@ const build = (input) => Effect.gen(function* () {
184
186
  return Service$17.of(paths);
185
187
  });
186
188
  const layer$27 = Layer.effect(Service$17, build({}));
187
- const defaultLayer$2 = layer$27;
188
- const layerWith = (input) => Layer.effect(Service$17, build(input));
189
+ const defaultLayer$4 = layer$27;
190
+ const layerWith$1 = (input) => Layer.effect(Service$17, build(input));
189
191
  //#endregion
190
192
  //#region src/db/migrations.ts
191
193
  const migrations = {
@@ -374,7 +376,7 @@ var db_exports = /* @__PURE__ */ __exportAll({
374
376
  Database: () => db_exports,
375
377
  SqlClient: () => SqlClient$1,
376
378
  SqlSchema: () => SqlSchema$1,
377
- defaultLayer: () => defaultLayer$1,
379
+ defaultLayer: () => defaultLayer$3,
378
380
  layer: () => layer$26,
379
381
  locationConfig: () => locationConfig,
380
382
  path: () => path$1,
@@ -408,7 +410,7 @@ const path$1 = Effect.fn("Database.path")(function* (data) {
408
410
  const directory = data ?? (yield* resolve$4()).data;
409
411
  return resolveDatabaseLocation(yield* locationConfig, directory);
410
412
  });
411
- const defaultLayer$1 = Layer.unwrap(path$1().pipe(Effect.map(layer$26)));
413
+ const defaultLayer$3 = Layer.unwrap(path$1().pipe(Effect.map(layer$26)));
412
414
  //#endregion
413
415
  //#region src/event/schema.ts
414
416
  var schema_exports$7 = /* @__PURE__ */ __exportAll({});
@@ -1579,7 +1581,7 @@ var session_exports$1 = /* @__PURE__ */ __exportAll({
1579
1581
  SessionNotLinkedError: () => SessionNotLinkedError,
1580
1582
  SessionRow: () => SessionRow,
1581
1583
  ToolCallNotFoundError: () => ToolCallNotFoundError,
1582
- defaultLayer: () => defaultLayer,
1584
+ defaultLayer: () => defaultLayer$2,
1583
1585
  entryStates: () => entryStates,
1584
1586
  entryTypes: () => entryTypes,
1585
1587
  layer: () => layer$23,
@@ -1663,6 +1665,16 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
1663
1665
  SELECT p.* FROM space p JOIN session s ON s.space_id = p.id WHERE s.id = ${sessionId}
1664
1666
  `
1665
1667
  });
1668
+ const findProject = SqlSchema.findOneOption({
1669
+ Request: Schema.String,
1670
+ Result: ProjectRow,
1671
+ execute: (sessionId) => sql`
1672
+ SELECT p.* FROM project p
1673
+ JOIN space sp ON sp.project_id = p.id
1674
+ JOIN session s ON s.space_id = sp.id
1675
+ WHERE s.id = ${sessionId}
1676
+ `
1677
+ });
1666
1678
  const findSpaceById = SqlSchema.findOneOption({
1667
1679
  Request: Schema.String,
1668
1680
  Result: SpaceRow,
@@ -1811,6 +1823,13 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
1811
1823
  const space = Effect.fn("Session.space")(function* (sessionId) {
1812
1824
  return Option.map(yield* findSpace(sessionId).pipe(Effect.orDie), fromRow$1);
1813
1825
  });
1826
+ const project = Effect.fn("Session.project")(function* (sessionId) {
1827
+ return Option.map(yield* findProject(sessionId).pipe(Effect.orDie), (row) => new schema_exports$6.ProjectSchema.Info({
1828
+ id: schema_exports$6.ProjectSchema.ID.make(row.id),
1829
+ name: row.name,
1830
+ status: row.status
1831
+ }));
1832
+ });
1814
1833
  const list = Effect.fn("Session.list")(function* (input) {
1815
1834
  return yield* (input === void 0 ? selectAll() : "spaceId" in input ? selectBySpace(input.spaceId) : selectByProject(input.projectId)).pipe(Effect.orDie);
1816
1835
  });
@@ -2313,6 +2332,7 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2313
2332
  configure,
2314
2333
  get,
2315
2334
  space,
2335
+ project,
2316
2336
  list,
2317
2337
  entry,
2318
2338
  path,
@@ -2329,7 +2349,7 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2329
2349
  setLabel
2330
2350
  });
2331
2351
  }));
2332
- const defaultLayer = layer$23.pipe(Layer.provide(defaultLayer$1));
2352
+ const defaultLayer$2 = layer$23.pipe(Layer.provide(defaultLayer$3));
2333
2353
  //#endregion
2334
2354
  //#region src/context/errors.ts
2335
2355
  var ContextDecodeError = class extends Schema.TaggedError()("ContextDecodeError", {
@@ -4274,6 +4294,17 @@ const make$9 = () => {
4274
4294
  }) };
4275
4295
  };
4276
4296
  //#endregion
4297
+ //#region src/sandbox/sandbox.ts
4298
+ /**
4299
+ * Assemble a local backend into a sandbox with a **real host shell** — the
4300
+ * right choice whenever commands must invoke actual binaries (`git`, package
4301
+ * managers). For a sandbox whose shell should stay inside the VFS, use
4302
+ * `EnvBash.services` instead, which wires just-bash over the same tree.
4303
+ */
4304
+ const services = (backend, identity) => Layer.provideMerge(io_exports.SandboxIO.mount(identity), Layer.provideMerge(Layer.merge(layer$21, layer$18()), backend));
4305
+ /** Default sandbox: the real OS filesystem and processes, mounted at `cwd`. */
4306
+ const defaultLayer$1 = (cwd) => services(layer$20(), io_exports.SandboxIO.host(cwd));
4307
+ //#endregion
4277
4308
  //#region src/git/git.ts
4278
4309
  var AppProcessError = class extends Schema.TaggedError()("AppProcessError", {
4279
4310
  command: Schema.String,
@@ -4464,7 +4495,8 @@ const up = Effect.fn("SandboxFileSystem.up")(function* (fs, options) {
4464
4495
  const isDirectory$1 = Effect.fn("SandboxFileSystem.isDirectory")(function* (fs, path) {
4465
4496
  return yield* fs.stat(path).pipe(Effect.map((stat) => stat.isDirectory), Effect.orElseSucceed(() => false));
4466
4497
  });
4467
- Effect.fn("SandboxFileSystem.isFile")(function* (fs, path) {
4498
+ /** True when `path` exists and is a regular file; false when it is absent or unreadable. */
4499
+ const isFile = Effect.fn("SandboxFileSystem.isFile")(function* (fs, path) {
4468
4500
  return yield* fs.stat(path).pipe(Effect.map((stat) => stat.isFile), Effect.orElseSucceed(() => false));
4469
4501
  });
4470
4502
  /** Read a file, or `undefined` when it is missing or unreadable. */
@@ -4611,6 +4643,16 @@ const layer$13 = Layer.effect(Service$10, Effect.gen(function* () {
4611
4643
  }));
4612
4644
  //#endregion
4613
4645
  //#region src/project/project.ts
4646
+ var project_exports = /* @__PURE__ */ __exportAll({
4647
+ Project: () => project_exports,
4648
+ ProjectNotFoundError: () => ProjectNotFoundError,
4649
+ Service: () => Service$9,
4650
+ defaultLayer: () => defaultLayer,
4651
+ layer: () => layer$12,
4652
+ layerWith: () => layerWith,
4653
+ provisional: () => provisional,
4654
+ rename: () => rename
4655
+ });
4614
4656
  /** Deterministic, env-local id for a directory without a portable identity. Never written to a marker. */
4615
4657
  const provisional = (env, location) => schema_exports$6.ProjectSchema.ID.make(fast(JSON.stringify([
4616
4658
  "dir",
@@ -4664,7 +4706,7 @@ const layer$12 = Layer.effect(Service$9, Effect.gen(function* () {
4664
4706
  main: repo.gitDir === repo.store
4665
4707
  }]).toSorted((a, b) => Number(b.main) - Number(a.main));
4666
4708
  });
4667
- const resolveOrCreate = Effect.fn("Project.resolveOrCreate")(function* (cwd) {
4709
+ const resolveOrCreate = Effect.fn("Project.resolveOrCreate")(function* (cwd, options) {
4668
4710
  if (!(yield* fs.exists(cwd).pipe(Effect.orDie))) return yield* new DirectoryNotFoundError$1({
4669
4711
  directory: cwd,
4670
4712
  sandboxInstanceId: env
@@ -4678,7 +4720,7 @@ const layer$12 = Layer.effect(Service$9, Effect.gen(function* () {
4678
4720
  const ident = repo === void 0 ? void 0 : yield* repos.identity(repo);
4679
4721
  const worktree = repo?.directory ?? location;
4680
4722
  const projectId = ident?.id === void 0 ? provisional(env, worktree) : schema_exports$6.ProjectSchema.ID.make(ident.id);
4681
- const name = ident?.name ?? posix.basename(worktree);
4723
+ const name = options?.name ?? ident?.name ?? posix.basename(worktree);
4682
4724
  const vcs = repo === void 0 ? void 0 : {
4683
4725
  type: "git",
4684
4726
  store: repo.store
@@ -4750,6 +4792,32 @@ const layer$12 = Layer.effect(Service$9, Effect.gen(function* () {
4750
4792
  list
4751
4793
  });
4752
4794
  }));
4795
+ var ProjectNotFoundError = class extends Schema.TaggedError()("ProjectNotFoundError", { projectId: Schema.String }) {};
4796
+ /** Renames a project. Needs only the database, not a mount: a name is the same in every env. */
4797
+ const rename = Effect.fn("Project.rename")(function* (input) {
4798
+ const sql = yield* SqlClient.SqlClient;
4799
+ const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
4800
+ const row = yield* SqlSchema.findOneOption({
4801
+ Request: Schema.Struct({
4802
+ id: Schema.String,
4803
+ name: Schema.String
4804
+ }),
4805
+ Result: ProjectRow,
4806
+ execute: ({ id, name }) => sql`UPDATE project SET name = ${name}, updated_at = ${now} WHERE id = ${id} RETURNING *`
4807
+ })({
4808
+ id: input.projectId,
4809
+ name: input.name
4810
+ }).pipe(Effect.orDie);
4811
+ if (Option.isNone(row)) return yield* new ProjectNotFoundError({ projectId: input.projectId });
4812
+ return new schema_exports$6.ProjectSchema.Info({
4813
+ id: schema_exports$6.ProjectSchema.ID.make(row.value.id),
4814
+ name: row.value.name,
4815
+ status: row.value.status
4816
+ });
4817
+ });
4818
+ /** Takes an assembled sandbox — local or remote — not a local backend. */
4819
+ const layerWith = (sandbox) => layer$12.pipe(Layer.provide(Layer.mergeAll(layer$14, layer$13, layer$24)), Layer.provide(layer$15), Layer.provide(sandbox), Layer.provide(defaultLayer$3));
4820
+ const defaultLayer = (path) => layerWith(defaultLayer$1(path));
4753
4821
  //#endregion
4754
4822
  //#region src/location/location.ts
4755
4823
  var location_exports = /* @__PURE__ */ __exportAll({
@@ -4761,10 +4829,10 @@ var location_exports = /* @__PURE__ */ __exportAll({
4761
4829
  layer: () => layer$11,
4762
4830
  layerMounted: () => layerMounted
4763
4831
  });
4764
- const layer$11 = () => Layer.effect(Service, Effect.gen(function* () {
4832
+ const layer$11 = (options) => Layer.effect(Service, Effect.gen(function* () {
4765
4833
  const instance = yield* io_exports.SandboxIO.Current;
4766
4834
  const directory = schema_exports$9.AbsolutePath.make(instance.cwd);
4767
- const resolved = yield* (yield* Service$9).resolveOrCreate(directory);
4835
+ const resolved = yield* (yield* Service$9).resolveOrCreate(directory, options);
4768
4836
  return Service.of(new Info({
4769
4837
  directory: resolved.directory,
4770
4838
  space: resolved.space,
@@ -4776,7 +4844,7 @@ const layer$11 = () => Layer.effect(Service, Effect.gen(function* () {
4776
4844
  * folded in. What remains is exactly the mount and the database — for callers
4777
4845
  * that already hold both, such as the runner and `Session.create`.
4778
4846
  */
4779
- const layerMounted = () => layer$11().pipe(Layer.provide(layer$12.pipe(Layer.provide(Layer.mergeAll(layer$14, layer$13, layer$24)), Layer.provide(layer$15))));
4847
+ const layerMounted = (options) => layer$11(options).pipe(Layer.provide(layer$12.pipe(Layer.provide(Layer.mergeAll(layer$14, layer$13, layer$24)), Layer.provide(layer$15))));
4780
4848
  //#endregion
4781
4849
  //#region src/runner/run.ts
4782
4850
  var run_exports = /* @__PURE__ */ __exportAll({
@@ -4871,7 +4939,6 @@ var ProviderError = class extends Schema.TaggedError()("Runner.ProviderError", {
4871
4939
  var ModelCatalogError = class extends Schema.TaggedError()("Runner.ModelCatalogError", {
4872
4940
  path: Schema.String,
4873
4941
  reason: Schema.Literals([
4874
- "missing",
4875
4942
  "unreadable",
4876
4943
  "empty",
4877
4944
  "invalid"
@@ -5312,7 +5379,7 @@ var settings_exports = /* @__PURE__ */ __exportAll({
5312
5379
  layer: () => layer$8,
5313
5380
  load: () => load$3,
5314
5381
  modules: () => modules,
5315
- parse: () => parse$4,
5382
+ parse: () => parse$5,
5316
5383
  paths: () => paths,
5317
5384
  projectRoot: () => projectRoot,
5318
5385
  userConfigDir: () => userConfigDir
@@ -5401,7 +5468,7 @@ const decodeFormatter = SchemaIssue.makeFormatterStandardSchemaV1({ leafHook: (i
5401
5468
  const message = "annotations" in issue ? issue.annotations?.message : void 0;
5402
5469
  return typeof message === "string" ? message.replaceAll(/\s*\n\s*/g, " ") : "invalid value";
5403
5470
  } });
5404
- const parse$4 = Effect.fn("Settings.parse")(function* (path, source) {
5471
+ const parse$5 = Effect.fn("Settings.parse")(function* (path, source) {
5405
5472
  const errors = [];
5406
5473
  const json = parse(source, errors, {
5407
5474
  allowTrailingComma: true,
@@ -5449,7 +5516,7 @@ const attempt = (path) => fileSystem.readFileString(path).pipe(Effect.mapError((
5449
5516
  path,
5450
5517
  reason: "read",
5451
5518
  detail: error.reason._tag
5452
- })), Effect.flatMap((source) => parse$4(path, source)));
5519
+ })), Effect.flatMap((source) => parse$5(path, source)));
5453
5520
  /**
5454
5521
  * One read of every layer. Exported for the harness constructor, which needs the plugin
5455
5522
  * selection before any layer is built; everything else goes through `Service`.
@@ -5563,8 +5630,13 @@ const stale = Effect.fn("ModelCatalog.stale")(function* (path) {
5563
5630
  const ttl = yield* ttlConfig;
5564
5631
  return (yield* Clock.currentTimeMillis) - mtime.value >= Duration.toMillis(ttl);
5565
5632
  });
5633
+ /** OpenRouter's catalog is narrowed to what the host's key may use, when one is set. */
5634
+ const openRouterKey = () => getEnvApiKey({ env: ["OPENROUTER_API_KEY"] });
5566
5635
  const download$1 = (path) => Effect.tryPromise({
5567
- try: () => generateModels({ path }),
5636
+ try: () => downloadCatalog({
5637
+ path,
5638
+ openRouterKey: openRouterKey()
5639
+ }),
5568
5640
  catch: (cause) => new RefreshError({
5569
5641
  path,
5570
5642
  detail: cause instanceof Error ? cause.message : String(cause)
@@ -5585,8 +5657,7 @@ const refresh$1 = Effect.fn("ModelCatalog.refresh")(function* (home, options = {
5585
5657
  });
5586
5658
  /**
5587
5659
  * The automatic check: refresh a stale catalog unless it is pinned, and never fail. A failed
5588
- * download leaves the old file in place; a missing one surfaces later as a
5589
- * `Runner.ModelCatalogError` from whatever needed it.
5660
+ * download leaves the old file in place; without one, aikit's bundled baseline is read.
5590
5661
  */
5591
5662
  const check$1 = Effect.fn("ModelCatalog.check")(function* ({ path, pinned }) {
5592
5663
  if (pinned || !(yield* stale(path))) return;
@@ -5622,7 +5693,7 @@ const loadError = (cause) => {
5622
5693
  if (typeof cause !== "object" || cause === null || !("name" in cause) || cause.name !== "ModelCatalogLoadError") return;
5623
5694
  if (!("data" in cause) || typeof cause.data !== "object" || cause.data === null) return void 0;
5624
5695
  const data = cause.data;
5625
- if (!("path" in data) || typeof data.path !== "string" || !("message" in data) || typeof data.message !== "string" || !("reason" in data) || data.reason !== "missing" && data.reason !== "unreadable" && data.reason !== "empty" && data.reason !== "invalid") return;
5696
+ if (!("path" in data) || typeof data.path !== "string" || !("message" in data) || typeof data.message !== "string" || !("reason" in data) || data.reason !== "unreadable" && data.reason !== "empty" && data.reason !== "invalid") return;
5626
5697
  return new ModelCatalogError({
5627
5698
  path: data.path,
5628
5699
  reason: data.reason,
@@ -5755,6 +5826,22 @@ const defaultPromptPlugin = (0, plugin_exports$1.define)({
5755
5826
  })
5756
5827
  });
5757
5828
  //#endregion
5829
+ //#region src/plugin/builtin/prompt/walk.ts
5830
+ const stripBom$1 = (raw) => raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
5831
+ /** `directory` and every ancestor up to the sandbox root, closest first. */
5832
+ const ancestors = (directory) => {
5833
+ const chain = [];
5834
+ for (let current = directory;; current = posix.dirname(current)) {
5835
+ chain.push(current);
5836
+ if (posix.dirname(current) === current) return chain;
5837
+ }
5838
+ };
5839
+ /**
5840
+ * Which of `paths` exist as the requested kind (`entry` takes either). Absence stays silent; any other failure
5841
+ * (permissions, a remote blip) is logged so a dropped entry is not a mystery, then counts as absent.
5842
+ */
5843
+ const present$1 = (fs, paths, kind, label) => Effect.forEach(paths, (path) => fs.exists(path).pipe(Effect.filterOrElse((exists) => !exists || kind === "entry", () => fs.stat(path).pipe(Effect.map((stat) => kind === "file" ? stat.isFile : stat.isDirectory))), Effect.catch((error) => filesystem_exports.SandboxFileSystem.isNotFoundError(error.cause) ? Effect.succeed(false) : Effect.logWarning(`could not stat ${label} ${path}`, error.cause).pipe(Effect.as(false)))), { concurrency: "unbounded" }).pipe(Effect.map((hits) => new Set(paths.filter((_, index) => hits[index]))));
5844
+ //#endregion
5758
5845
  //#region src/plugin/builtin/prompt/instruction.ts
5759
5846
  /** First readable match per directory wins. */
5760
5847
  const names = [
@@ -5766,22 +5853,9 @@ const names = [
5766
5853
  "CLAUDE.MD"
5767
5854
  ];
5768
5855
  const content = (raw) => {
5769
- const text = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
5856
+ const text = stripBom$1(raw);
5770
5857
  return text.trim().length === 0 ? void 0 : text;
5771
5858
  };
5772
- /** `directory` and every ancestor up to the sandbox root, closest first. */
5773
- const ancestors = (directory) => {
5774
- const chain = [];
5775
- for (let current = directory;; current = posix.dirname(current)) {
5776
- chain.push(current);
5777
- if (posix.dirname(current) === current) return chain;
5778
- }
5779
- };
5780
- /**
5781
- * Which of `paths` are files. Absence is the expected answer and stays silent; any other failure
5782
- * (permissions, a remote blip) is logged so a dropped file is not a mystery, then counts as absent.
5783
- */
5784
- const present$1 = (fs, paths) => Effect.forEach(paths, (path) => fs.exists(path).pipe(Effect.flatMap((exists) => exists ? fs.stat(path).pipe(Effect.map((stat) => stat.isFile)) : Effect.succeed(false)), Effect.catch((error) => filesystem_exports.SandboxFileSystem.isNotFoundError(error.cause) ? Effect.succeed(false) : Effect.logWarning(`could not stat instruction file ${path}`, error.cause).pipe(Effect.as(false)))), { concurrency: "unbounded" }).pipe(Effect.map((hits) => new Set(paths.filter((_, index) => hits[index]))));
5785
5859
  /** A file that stats but does not read falls through to the next candidate. */
5786
5860
  const first = Effect.fnUntraced(function* (fs, directory, files) {
5787
5861
  for (const name of names) {
@@ -5809,9 +5883,9 @@ const shadowing = (location) => {
5809
5883
  return location.space.location.startsWith(`${main}/`) ? main : void 0;
5810
5884
  };
5811
5885
  /** One file per directory from the sandbox root down to the cwd, closest last so it has the final word. */
5812
- const discover = Effect.fnUntraced(function* (fs, location) {
5886
+ const discover$1 = Effect.fnUntraced(function* (fs, location) {
5813
5887
  const chain = ancestors(location.directory);
5814
- const files = yield* present$1(fs, chain.flatMap((directory) => names.map((name) => posix.join(directory, name))));
5888
+ const files = yield* present$1(fs, chain.flatMap((directory) => names.map((name) => posix.join(directory, name))), "file", "instruction file");
5815
5889
  const found = yield* Effect.forEach(chain, (directory) => first(fs, directory, files), { concurrency: "unbounded" });
5816
5890
  const main = shadowing(location);
5817
5891
  const worktree = main === void 0 ? void 0 : found[chain.indexOf(location.space.location)];
@@ -5827,7 +5901,7 @@ const instructionPlugin = (0, plugin_exports$1.define)({
5827
5901
  kind: "prompt",
5828
5902
  setup: Effect.fn("InstructionPlugin.setup")(function* (ctx) {
5829
5903
  const fs = yield* io_exports.SandboxIO.FileSystem;
5830
- const files = (yield* discover(fs, ctx.location)).filter((file) => file.content !== void 0);
5904
+ const files = (yield* discover$1(fs, ctx.location)).filter((file) => file.content !== void 0);
5831
5905
  if (files.length === 0) return;
5832
5906
  const prompt = ctx.plugin.prompt;
5833
5907
  prompt.sections.append(Section.ProjectContext, "Project-Specific Instructions & Guidelines:");
@@ -5835,6 +5909,236 @@ const instructionPlugin = (0, plugin_exports$1.define)({
5835
5909
  })
5836
5910
  });
5837
5911
  //#endregion
5912
+ //#region src/plugin/builtin/prompt/skill.ts
5913
+ /** Max name length per Agent Skills spec. */
5914
+ const MAX_NAME_LENGTH = 64;
5915
+ /** Max description length per Agent Skills spec. */
5916
+ const MAX_DESCRIPTION_LENGTH = 1024;
5917
+ const IGNORE_FILE_NAMES = [
5918
+ ".gitignore",
5919
+ ".ignore",
5920
+ ".fdignore"
5921
+ ];
5922
+ const normalizeNewlines = (value) => value.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
5923
+ const extractFrontmatter = (content) => {
5924
+ const normalized = normalizeNewlines(stripBom$1(content));
5925
+ if (!normalized.startsWith("---")) return void 0;
5926
+ const endIndex = normalized.indexOf("\n---", 3);
5927
+ if (endIndex === -1) return void 0;
5928
+ return normalized.slice(4, endIndex);
5929
+ };
5930
+ const parseFrontmatter = (raw) => {
5931
+ const yamlString = extractFrontmatter(raw);
5932
+ if (yamlString === void 0) return { frontmatter: {} };
5933
+ try {
5934
+ const parsed = parse$1(yamlString);
5935
+ return { frontmatter: typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {} };
5936
+ } catch (error) {
5937
+ return {
5938
+ frontmatter: {},
5939
+ error
5940
+ };
5941
+ }
5942
+ };
5943
+ const validateName = (name) => {
5944
+ const errors = [];
5945
+ if (name.length > MAX_NAME_LENGTH) errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
5946
+ if (!/^[a-z0-9-]+$/.test(name)) errors.push("name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)");
5947
+ if (name.startsWith("-") || name.endsWith("-")) errors.push("name must not start or end with a hyphen");
5948
+ if (name.includes("--")) errors.push("name must not contain consecutive hyphens");
5949
+ return errors;
5950
+ };
5951
+ const validateDescription = (description) => {
5952
+ const errors = [];
5953
+ if (typeof description !== "string" || description.trim() === "") errors.push("description is required");
5954
+ else if (description.length > MAX_DESCRIPTION_LENGTH) errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
5955
+ return errors;
5956
+ };
5957
+ const prefixIgnorePattern = (line, prefix) => {
5958
+ const trimmed = line.trim();
5959
+ if (!trimmed) return void 0;
5960
+ if (trimmed.startsWith("#") && !trimmed.startsWith("\\#")) return void 0;
5961
+ let pattern = line;
5962
+ let negated = false;
5963
+ if (pattern.startsWith("!")) {
5964
+ negated = true;
5965
+ pattern = pattern.slice(1);
5966
+ } else if (pattern.startsWith("\\!")) pattern = pattern.slice(1);
5967
+ if (pattern.startsWith("/")) pattern = pattern.slice(1);
5968
+ const prefixed = prefix ? `${prefix}${pattern}` : pattern;
5969
+ return negated ? `!${prefixed}` : prefixed;
5970
+ };
5971
+ const addIgnoreRules = Effect.fnUntraced(function* (fs, ig, dir, root, entryNames) {
5972
+ const relativeDir = posix.relative(root, dir);
5973
+ const prefix = relativeDir && relativeDir !== "." ? `${relativeDir}/` : "";
5974
+ const presentIgnoreFiles = IGNORE_FILE_NAMES.filter((name) => entryNames.has(name));
5975
+ if (presentIgnoreFiles.length === 0) return;
5976
+ const contents = yield* Effect.forEach(presentIgnoreFiles, (filename) => fs.readFile(posix.join(dir, filename)).pipe(Effect.option), { concurrency: "unbounded" });
5977
+ for (const content of contents) {
5978
+ if (Option.isNone(content)) continue;
5979
+ const patterns = content.value.split(/\r?\n/).map((line) => prefixIgnorePattern(line, prefix)).filter((line) => line !== void 0);
5980
+ if (patterns.length > 0) ig.add(patterns);
5981
+ }
5982
+ });
5983
+ const loadSkillFromFile = Effect.fnUntraced(function* (fs, filePath) {
5984
+ const isDeclaredSkill = posix.basename(filePath) === "SKILL.md";
5985
+ const raw = yield* fs.readFile(filePath).pipe(Effect.tapError((error) => Effect.logWarning(`could not read skill file ${filePath}`, error.cause)), Effect.option);
5986
+ if (Option.isNone(raw)) return void 0;
5987
+ const { frontmatter, error } = parseFrontmatter(raw.value);
5988
+ if (error !== void 0) {
5989
+ if (isDeclaredSkill) yield* Effect.logWarning(`could not parse skill file ${filePath}`, error);
5990
+ return;
5991
+ }
5992
+ const description = frontmatter.description;
5993
+ const hasDescription = typeof description === "string" && description.trim() !== "";
5994
+ if (!isDeclaredSkill && !hasDescription) return void 0;
5995
+ const skillDir = posix.dirname(filePath);
5996
+ const parentDirName = posix.basename(skillDir);
5997
+ for (const error of validateDescription(description)) yield* Effect.logWarning(`skill ${filePath}: ${error}`);
5998
+ const name = (typeof frontmatter.name === "string" ? frontmatter.name : void 0) || parentDirName;
5999
+ for (const error of validateName(name)) yield* Effect.logWarning(`skill ${filePath}: ${error}`);
6000
+ if (!hasDescription) return void 0;
6001
+ return {
6002
+ name,
6003
+ description,
6004
+ filePath,
6005
+ baseDir: skillDir,
6006
+ disableModelInvocation: frontmatter["disable-model-invocation"] === true
6007
+ };
6008
+ });
6009
+ const scanDir = (fs, dir, mode, root, ig, visited) => Effect.gen(function* () {
6010
+ const realDir = yield* realpath(fs, dir);
6011
+ if (visited.has(realDir)) return [];
6012
+ const nextVisited = /* @__PURE__ */ new Set([...visited, realDir]);
6013
+ const listed = yield* fs.readdir(dir).pipe(Effect.tapError((error) => filesystem_exports.SandboxFileSystem.isNotFoundError(error.cause) ? Effect.void : Effect.logWarning(`could not read skill directory ${dir}`, error.cause)), Effect.option);
6014
+ if (Option.isNone(listed)) return [];
6015
+ const entries = [...listed.value].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
6016
+ const entrySet = new Set(entries);
6017
+ yield* addIgnoreRules(fs, ig, dir, root, entrySet);
6018
+ if (entrySet.has("SKILL.md")) {
6019
+ const fullPath = posix.join(dir, "SKILL.md");
6020
+ const relPath = posix.relative(root, fullPath);
6021
+ if ((yield* isFile(fs, fullPath)) && !ig.ignores(relPath)) {
6022
+ const skill = yield* loadSkillFromFile(fs, fullPath);
6023
+ return skill === void 0 ? [] : [skill];
6024
+ }
6025
+ }
6026
+ const visible = entries.filter((name) => !name.startsWith(".") && name !== "node_modules");
6027
+ const statted = yield* Effect.forEach(visible, (name) => {
6028
+ const fullPath = posix.join(dir, name);
6029
+ return fs.stat(fullPath).pipe(Effect.map((stat) => ({
6030
+ name,
6031
+ fullPath,
6032
+ stat
6033
+ })), Effect.option);
6034
+ }, { concurrency: "unbounded" });
6035
+ const skills = [];
6036
+ for (const item of statted) {
6037
+ if (Option.isNone(item)) continue;
6038
+ const { name, fullPath, stat } = item.value;
6039
+ const relPath = posix.relative(root, fullPath);
6040
+ if (ig.ignores(stat.isDirectory ? `${relPath}/` : relPath)) continue;
6041
+ if (stat.isDirectory) {
6042
+ const nested = yield* scanDir(fs, fullPath, mode, root, ig, nextVisited);
6043
+ skills.push(...nested);
6044
+ continue;
6045
+ }
6046
+ if (stat.isFile && name.endsWith(".md") && (mode === "codework" && dir === root || mode === "agents" && dir !== root)) {
6047
+ const skill = yield* loadSkillFromFile(fs, fullPath);
6048
+ if (skill !== void 0) skills.push(skill);
6049
+ }
6050
+ }
6051
+ return skills;
6052
+ });
6053
+ /**
6054
+ * Load skills from a single directory in the sandbox.
6055
+ *
6056
+ * Discovery rules:
6057
+ * - If a directory contains `SKILL.md`, treat it as a skill root and do not recurse further.
6058
+ * - In `"codework"` mode, direct `.md` children in the root are also loaded; in `"agents"` mode,
6059
+ * `.md` files in subdirectories are loaded while root `.md` files are ignored.
6060
+ * - Recurse into subdirectories (skipping dotfiles, `node_modules`, and ignored paths).
6061
+ */
6062
+ const loadSkillsFromDir = (fs, dir, mode) => scanDir(fs, dir, mode, dir, ignore(), /* @__PURE__ */ new Set());
6063
+ /**
6064
+ * Discover skills inside the sandbox: `.codework/skills` at the cwd, then
6065
+ * `.agents/skills` from the cwd up to the git root (the closest ancestor holding `.git`), or up to
6066
+ * the sandbox root outside a repository. Earlier skills win name collisions.
6067
+ */
6068
+ const discover = Effect.fnUntraced(function* (fs, location) {
6069
+ const chain = ancestors(location.directory);
6070
+ const own = posix.join(location.directory, appConfigDir, "skills");
6071
+ const agents = chain.map((directory) => posix.join(directory, ".agents", "skills"));
6072
+ const [dirs, gits] = yield* Effect.all([present$1(fs, [own, ...agents], "directory", "skill directory"), present$1(fs, chain.map((directory) => posix.join(directory, ".git")), "entry", "git marker")], { concurrency: "unbounded" });
6073
+ const root = chain.findIndex((directory) => gits.has(posix.join(directory, ".git")));
6074
+ const walked = root === -1 ? agents : agents.slice(0, root + 1);
6075
+ const sources = [{
6076
+ dir: own,
6077
+ mode: "codework"
6078
+ }, ...walked.map((dir) => ({
6079
+ dir,
6080
+ mode: "agents"
6081
+ }))].filter(({ dir }) => dirs.has(dir));
6082
+ const found = yield* Effect.forEach(sources, ({ dir, mode }) => loadSkillsFromDir(fs, dir, mode), { concurrency: "unbounded" });
6083
+ const skillMap = /* @__PURE__ */ new Map();
6084
+ const realPathSet = /* @__PURE__ */ new Set();
6085
+ for (const skill of found.flat()) {
6086
+ const realPath = yield* realpath(fs, skill.filePath);
6087
+ if (realPathSet.has(realPath)) continue;
6088
+ const existing = skillMap.get(skill.name);
6089
+ if (existing !== void 0) yield* Effect.logWarning(`skill ${skill.filePath}: name "${skill.name}" collision with ${existing.filePath}`);
6090
+ else {
6091
+ skillMap.set(skill.name, skill);
6092
+ realPathSet.add(realPath);
6093
+ }
6094
+ }
6095
+ return Array.from(skillMap.values());
6096
+ });
6097
+ const escapeXml = (str) => str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
6098
+ /**
6099
+ * Format skills for inclusion in `Section.Skills` per the Agent Skills specification.
6100
+ * Skills with `disableModelInvocation: true` are excluded from the prompt.
6101
+ */
6102
+ const formatSkillsForPrompt = (skills, fileReadTool = "read") => {
6103
+ const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
6104
+ if (visibleSkills.length === 0) return "";
6105
+ const lines = [
6106
+ "The following skills provide specialized instructions for specific tasks.",
6107
+ fileReadTool === "read" ? "Use the read tool to load a skill's file when the task matches its description." : fileReadTool === "bash" ? "Use bash to load a skill's file when the task matches its description." : "Load a skill's file when the task matches its description.",
6108
+ "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
6109
+ "",
6110
+ "<available_skills>"
6111
+ ];
6112
+ for (const skill of visibleSkills) {
6113
+ lines.push(" <skill>");
6114
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
6115
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
6116
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
6117
+ lines.push(" </skill>");
6118
+ }
6119
+ lines.push("</available_skills>");
6120
+ return lines.join("\n");
6121
+ };
6122
+ const readers = ["read", "bash"];
6123
+ const skillPlugin = (0, plugin_exports$1.define)({
6124
+ id: "codework.prompt.skill",
6125
+ kind: "prompt",
6126
+ setup: Effect.fn("SkillPlugin.setup")(function* (ctx) {
6127
+ const tools = ctx.plugin.tools;
6128
+ const visible = (name) => {
6129
+ const tool = tools.get(name);
6130
+ return tool !== void 0 && tool.promptSnippet !== void 0 && tool.promptSnippet.length > 0;
6131
+ };
6132
+ const fileReadTool = readers.find(visible) ?? (readers.some((name) => tools.has(name)) ? "indirect" : void 0);
6133
+ if (fileReadTool === void 0) return;
6134
+ const fs = yield* io_exports.SandboxIO.FileSystem;
6135
+ const skills = yield* discover(fs, ctx.location);
6136
+ const formatted = formatSkillsForPrompt(skills, fileReadTool);
6137
+ if (formatted.length === 0) return;
6138
+ ctx.plugin.prompt.sections.append(Section.Skills, formatted);
6139
+ })
6140
+ });
6141
+ //#endregion
5838
6142
  //#region src/tool/truncate.ts
5839
6143
  /**
5840
6144
  * Shared truncation utilities for tool outputs.
@@ -6146,8 +6450,10 @@ const windowBytes = (limits) => limits.maxBytes * 2;
6146
6450
  */
6147
6451
  const script = (command, file, limits, pipeline, report) => [
6148
6452
  `f=${(0, shell_exports.quote)(file)}`,
6453
+ `c=${(0, shell_exports.quote)(command)}`,
6454
+ "if command -v bash >/dev/null 2>&1; then s=bash; else s=sh; fi",
6149
6455
  "run() (",
6150
- command,
6456
+ " if [ \"$s\" = bash ]; then bash -c \"$c\"; else eval \"$c\"; fi",
6151
6457
  ")",
6152
6458
  `if mkdir -p ${(0, shell_exports.quote)(posix.dirname(file))} 2>/dev/null && (umask 077 2>/dev/null; set -C && : > "$f" && : > "$f.rc" && : > "$f.pid") 2>/dev/null; then`,
6153
6459
  " echo \"$$ $(ps -o pgid= -p $$ 2>/dev/null)\" > \"$f.pid\"",
@@ -6164,18 +6470,19 @@ const script = (command, file, limits, pipeline, report) => [
6164
6470
  `printf '%s\\n' ${(0, shell_exports.quote)(unsaved(file))}`,
6165
6471
  "run 2>&1"
6166
6472
  ].join("\n");
6167
- const header = new RegExp(`^${HEADER} (\\d+) (\\d+) ([01])(?: (\\d+))?$`, "m");
6168
- const parse$3 = (match) => ({
6473
+ const header = new RegExp(`^${HEADER} (\\d+) (\\d+) ([01]) (bash|sh)(?: (\\d+))?$`, "m");
6474
+ const parse$4 = (match) => ({
6169
6475
  bytes: Number(match[1]),
6170
6476
  newlines: Number(match[2]),
6171
- kept: match[3] === "1"
6477
+ kept: match[3] === "1",
6478
+ shell: match[4] === "sh" ? "sh" : "bash"
6172
6479
  });
6173
6480
  /**
6174
6481
  * For a backend that streams: all output still flows back as it is produced, and
6175
6482
  * a copy lands in the file. The report rides on stderr, the wrapper's own channel
6176
6483
  * since the command's is folded into stdout.
6177
6484
  */
6178
- const streaming = (command, file, limits) => script(command, file, limits, (source) => `${source} | tee -a "$f"`, [`printf '${HEADER} %s %s %s\\n' "$b" "$n" "$keep" >&2`]);
6485
+ const streaming = (command, file, limits) => script(command, file, limits, (source) => `${source} | tee -a "$f"`, [`printf '${HEADER} %s %s %s %s\\n' "$b" "$n" "$keep" "$s" >&2`]);
6179
6486
  /** Read {@link streaming}'s stderr: the report, if it got that far, and anything else the wrapper said. */
6180
6487
  const report = (stderr) => {
6181
6488
  const match = header.exec(stderr);
@@ -6184,7 +6491,7 @@ const report = (stderr) => {
6184
6491
  rest: stderr
6185
6492
  };
6186
6493
  return {
6187
- report: parse$3(match),
6494
+ report: parse$4(match),
6188
6495
  rest: stderr.slice(0, match.index) + stderr.slice(match.index + match[0].length).replace(/^\n/, "")
6189
6496
  };
6190
6497
  };
@@ -6197,7 +6504,7 @@ const buffered = (command, file, limits) => {
6197
6504
  const tail = `tail -n ${limits.maxLines} "$f" | tail -c ${windowBytes(limits)}`;
6198
6505
  return script(command, file, limits, (source) => `{ ${source} | cat; } >> "$f"`, [
6199
6506
  `w=$(${tail} | wc -c | awk '{ print $1 }')`,
6200
- `printf '${HEADER} %s %s %s %s\\n' "$b" "$n" "$keep" "$w"`,
6507
+ `printf '${HEADER} %s %s %s %s %s\\n' "$b" "$n" "$keep" "$s" "$w"`,
6201
6508
  tail
6202
6509
  ]);
6203
6510
  };
@@ -6205,11 +6512,11 @@ const buffered = (command, file, limits) => {
6205
6512
  const window = (stdout) => {
6206
6513
  const newline = stdout.indexOf("\n");
6207
6514
  const match = header.exec(newline === -1 ? stdout : stdout.slice(0, newline));
6208
- if (match === null || match[4] === void 0) return void 0;
6515
+ if (match === null || match[5] === void 0) return void 0;
6209
6516
  return {
6210
- ...parse$3(match),
6517
+ ...parse$4(match),
6211
6518
  text: stdout.slice(newline + 1),
6212
- textBytes: Number(match[4])
6519
+ textBytes: Number(match[5])
6213
6520
  };
6214
6521
  };
6215
6522
  /**
@@ -6357,7 +6664,9 @@ const BashSuccess = Schema.Struct({
6357
6664
  /** Non-zero exit — expected, model-visible. Carries the same shape as success. */
6358
6665
  var BashFailed = class extends Schema.TaggedError()("BashFailed", {
6359
6666
  ...outputFields,
6360
- exitCode: Schema.Finite
6667
+ exitCode: Schema.Finite,
6668
+ /** The shell that ran the command, when the sandbox reported it. */
6669
+ shell: Schema.optional(Schema.Literals(["bash", "sh"]))
6361
6670
  }) {};
6362
6671
  /** Deadline exceeded — expected, model-visible. Carries the partial output produced so far. */
6363
6672
  var BashTimedOut = class extends Schema.TaggedError()("BashTimedOut", {
@@ -6367,7 +6676,7 @@ var BashTimedOut = class extends Schema.TaggedError()("BashTimedOut", {
6367
6676
  const BashFailure = Schema.Union([BashFailed, BashTimedOut]);
6368
6677
  /** The reason for a failure, after any output: the model sees nothing else of `details`. */
6369
6678
  const withStatus = (failure) => {
6370
- const status = failure._tag === "BashTimedOut" ? `Command timed out after ${failure.timeoutSeconds} seconds` : failure.exitCode === -1 ? "Command terminated without an exit code" : `Command exited with code ${failure.exitCode}`;
6679
+ const status = failure._tag === "BashTimedOut" ? `Command timed out after ${failure.timeoutSeconds} seconds` : failure.exitCode === -1 ? "Command terminated without an exit code" : `Command exited with code ${failure.exitCode}${failure.shell === "sh" ? " (ran under sh: bash is not on PATH in this sandbox)" : ""}`;
6371
6680
  return failure.output ? `${failure.output.replace(/\n$/, "")}\n\n${status}` : status;
6372
6681
  };
6373
6682
  const bashDef = tool_exports.define({
@@ -6421,6 +6730,7 @@ const present = (snap, lastLineBytes, saved) => {
6421
6730
  };
6422
6731
  };
6423
6732
  const utf8 = new TextEncoder();
6733
+ const shellOf = (report) => report !== void 0 ? { shell: report.shell } : {};
6424
6734
  /**
6425
6735
  * A command cut off by its deadline never reached the wrapper's own cleanup: kill
6426
6736
  * what is left of it and remove its side files, keeping the output file only when
@@ -6454,7 +6764,8 @@ const runBuffered = (shell, params, file) => Effect.gen(function* () {
6454
6764
  const presented = present(acc.snapshot(), acc.getLastLineBytes(), window$1?.kept === true ? file : void 0);
6455
6765
  if (result.exitCode !== 0) return yield* new BashFailed({
6456
6766
  exitCode: result.exitCode,
6457
- ...presented
6767
+ ...presented,
6768
+ ...shellOf(window$1)
6458
6769
  });
6459
6770
  return {
6460
6771
  exitCode: result.exitCode,
@@ -6512,7 +6823,8 @@ const runStreaming = (shell, stream, params, file) => Effect.gen(function* () {
6512
6823
  const code = yield* Ref.get(exitCode);
6513
6824
  if (code === null || code !== 0) return yield* new BashFailed({
6514
6825
  exitCode: code ?? -1,
6515
- ...presented
6826
+ ...presented,
6827
+ ...shellOf(report$1)
6516
6828
  });
6517
6829
  return {
6518
6830
  exitCode: code,
@@ -7282,7 +7594,7 @@ const pipeline = (argv, limit, errors) => [
7282
7594
  ].join("; ");
7283
7595
  const STATUS = /^\{"type":"rc","code":(\d+)\}$/;
7284
7596
  /** The pipeline's output: match events, ripgrep's status, and anything else, which is its stderr. */
7285
- const parse$2 = (stdout, stderr) => {
7597
+ const parse$3 = (stdout, stderr) => {
7286
7598
  const matches = [];
7287
7599
  const errors = [];
7288
7600
  let status;
@@ -7366,7 +7678,7 @@ const grepHandler = (params) => Effect.gen(function* () {
7366
7678
  if (params.glob) argv.push("--glob", params.glob);
7367
7679
  argv.push("-e", params.pattern, searchPath);
7368
7680
  const result = yield* shell.exec(pipeline(argv, limit, errors));
7369
- const parsed = parse$2(result.stdout, result.stderr);
7681
+ const parsed = parse$3(result.stdout, result.stderr);
7370
7682
  return {
7371
7683
  ...parsed,
7372
7684
  exitCode: parsed.status ?? result.exitCode
@@ -7931,7 +8243,8 @@ const builtins = Object.freeze([
7931
8243
  findPlugin,
7932
8244
  lsPlugin,
7933
8245
  defaultPromptPlugin,
7934
- instructionPlugin
8246
+ instructionPlugin,
8247
+ skillPlugin
7935
8248
  ]);
7936
8249
  //#endregion
7937
8250
  //#region src/plugin/error.ts
@@ -8371,7 +8684,7 @@ const isPath = (reference) => reference.startsWith("file:") || reference.startsW
8371
8684
  * - a **CLI argument** anchors to the process's startup directory, because that is where the
8372
8685
  * person typed it.
8373
8686
  */
8374
- const parse$1 = (reference, from) => Effect.gen(function* () {
8687
+ const parse$2 = (reference, from) => Effect.gen(function* () {
8375
8688
  const fail = (reason, message) => new SourceError({
8376
8689
  reason,
8377
8690
  reference,
@@ -9047,7 +9360,7 @@ const inspect = Effect.fn("PluginLoader.inspect")(function* (reference, options)
9047
9360
  reference,
9048
9361
  file: options.file
9049
9362
  };
9050
- const target = yield* parse$1(reference, options.hostDir);
9363
+ const target = yield* parse$2(reference, options.hostDir);
9051
9364
  const loaded = yield* load$2(target, origin, options);
9052
9365
  return {
9053
9366
  id: loaded.plugin.id,
@@ -9073,7 +9386,7 @@ const load$2 = Effect.fn("PluginLoader.load")(function* (target, origin, options
9073
9386
  * are one package, and `./plugins/x.ts` is the file it resolves to from the directory that
9074
9387
  * declared it. Pure -- nothing is installed, imported or read.
9075
9388
  */
9076
- const canonical = (reference, hostDir) => Effect.map(parse$1(reference, hostDir), canonical$1);
9389
+ const canonical = (reference, hostDir) => Effect.map(parse$2(reference, hostDir), canonical$1);
9077
9390
  //#endregion
9078
9391
  //#region src/plugin/catalog.ts
9079
9392
  /**
@@ -9135,7 +9448,7 @@ const load$1 = Effect.fn("PluginCatalog.load")(function* (references, options) {
9135
9448
  remember(plugin, []);
9136
9449
  continue;
9137
9450
  }
9138
- const target = yield* parse$1(reference, options.hostDir);
9451
+ const target = yield* parse$2(reference, options.hostDir);
9139
9452
  const key = target.kind === "local" ? target.path : target.spec;
9140
9453
  const already = seen.get(key);
9141
9454
  if (already !== void 0) {
@@ -11318,7 +11631,7 @@ var harness_exports = /* @__PURE__ */ __exportAll({
11318
11631
  const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
11319
11632
  const hostCwd = options.hostCwd ?? process.cwd();
11320
11633
  const paths = yield* resolve$4(options.home === void 0 ? {} : { home: hostPath.resolve(hostCwd, expandTilde(options.home, hostPath)) });
11321
- const global = layerWith(paths);
11634
+ const global = layerWith$1(paths);
11322
11635
  const settingsOptions = options.userConfigDir === void 0 ? {} : { userConfigDir: userConfigDir(options.userConfigDir, hostCwd) };
11323
11636
  const config = yield* load$3({
11324
11637
  ...settingsOptions,
@@ -11337,7 +11650,7 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
11337
11650
  * `hostDir` led to; only the process view falls back to `hostCwd`.
11338
11651
  */
11339
11652
  const hostDirOf = (root) => root ?? hostCwd;
11340
- const filed = (reference, from) => parse$1(reference, from).pipe(Effect.flatMap((target) => target.kind === "local" ? fileSystem.exists(target.path).pipe(Effect.map((there) => there ? Option.some({}) : Option.none())) : resolve$2(target, paths.cache, from).pipe(Effect.map((entry) => Option.fromUndefinedOr(entry)))), Effect.orElseSucceed(() => Option.none()));
11653
+ const filed = (reference, from) => parse$2(reference, from).pipe(Effect.flatMap((target) => target.kind === "local" ? fileSystem.exists(target.path).pipe(Effect.map((there) => there ? Option.some({}) : Option.none())) : resolve$2(target, paths.cache, from).pipe(Effect.map((entry) => Option.fromUndefinedOr(entry)))), Effect.orElseSucceed(() => Option.none()));
11341
11654
  const resolveOnly = {
11342
11655
  ...catalogOptions,
11343
11656
  install: required
@@ -11579,13 +11892,24 @@ const makeHandle = Effect.fn("Session.makeHandle")(function* (id) {
11579
11892
  const space = Option.getOrUndefined(yield* sessions.space(id));
11580
11893
  const sandbox = space === void 0 || space.env === instance_exports.SandboxInstance.ID.local ? void 0 : Option.getOrUndefined(yield* sandboxes.get(space.env));
11581
11894
  const hostDir = Option.getOrUndefined(row.hostDir);
11895
+ const project = Option.getOrUndefined(yield* sessions.project(id));
11896
+ const config = Option.getOrUndefined(row.config);
11897
+ const model = config?.model;
11898
+ const thinkingLevel = config?.thinkingLevel;
11582
11899
  return {
11583
11900
  id,
11584
11901
  title: row.title,
11585
11902
  directory: row.directory,
11586
11903
  ...hostDir === void 0 ? {} : { hostDir },
11587
11904
  hasHostLink: hostDir !== void 0,
11588
- ...sandbox === void 0 ? {} : { sandbox }
11905
+ ...sandbox === void 0 ? {} : { sandbox },
11906
+ ...project === void 0 ? {} : { project: {
11907
+ id: project.id,
11908
+ name: project.name
11909
+ } },
11910
+ updatedAt: DateTime.toEpochMillis(row.updatedAt),
11911
+ ...model === void 0 ? {} : { model },
11912
+ ...thinkingLevel === void 0 ? {} : { thinkingLevel }
11589
11913
  };
11590
11914
  }).pipe(Effect.withSpan("Session.info"));
11591
11915
  return {
@@ -11613,7 +11937,7 @@ const create$1 = Effect.fn("Session.create")(function* (input = {}) {
11613
11937
  const sandboxes = yield* Controller;
11614
11938
  const id = ID$1.create();
11615
11939
  const sandboxId = input.sandbox?.id ?? instance_exports.SandboxInstance.ID.local;
11616
- const location = yield* sandboxes.withMount(sandboxId, Effect.provide(Service$8.use(Effect.succeed), layerMounted()), input.directory === void 0 ? void 0 : { cwd: input.directory });
11940
+ const location = yield* sandboxes.withMount(sandboxId, Effect.provide(Service$8.use(Effect.succeed), layerMounted(input.project === void 0 ? void 0 : { name: input.project.name })), input.directory === void 0 ? void 0 : { cwd: input.directory });
11617
11941
  yield* sessions.create({
11618
11942
  id,
11619
11943
  spaceId: location.space.id,
@@ -11727,7 +12051,7 @@ var plugin_exports = /* @__PURE__ */ __exportAll({
11727
12051
  inspect: () => inspect,
11728
12052
  lendResolve: () => lendResolve,
11729
12053
  localName: () => localName,
11730
- parse: () => parse$1,
12054
+ parse: () => parse$2,
11731
12055
  probe: () => probe,
11732
12056
  resolve: () => resolve$2,
11733
12057
  resolveCached: () => required,
@@ -11740,6 +12064,6 @@ var DateTimeUtcFromMillis = schema_exports$9.DateTimeUtcFromMillis;
11740
12064
  var EventSchema = schema_exports$7.EventSchema;
11741
12065
  var SandboxError = errors_exports.SandboxError;
11742
12066
  var optional = schema_exports$9.optional;
11743
- export { control_exports as Control, db_exports as Database, DateTimeUtcFromMillis, event_exports as Event, list_exports as EventList, registry_exports as EventRegistry, EventSchema, global_exports as Global, harness_exports as Harness, location_exports as Location, catalog_exports as ModelCatalog, plugin_exports as Plugin, schema_exports as PromptSchema, run_exports as Runner, sandbox_exports as Sandbox, SandboxError, session_exports as Session, session_exports$1 as SessionStore, settings_exports as Settings, state_exports as State, tool_exports as Tool, optional };
12067
+ export { control_exports as Control, db_exports as Database, DateTimeUtcFromMillis, event_exports as Event, list_exports as EventList, registry_exports as EventRegistry, EventSchema, global_exports as Global, harness_exports as Harness, location_exports as Location, catalog_exports as ModelCatalog, plugin_exports as Plugin, project_exports as Project, schema_exports as PromptSchema, run_exports as Runner, sandbox_exports as Sandbox, SandboxError, session_exports as Session, session_exports$1 as SessionStore, settings_exports as Settings, state_exports as State, tool_exports as Tool, optional };
11744
12068
 
11745
12069
  //# sourceMappingURL=effect.mjs.map