@codeworksh/harness 0.3.0 → 0.4.0-canary.52.1.ge7df22a4

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";
@@ -42,6 +44,7 @@ var schema_exports$8 = /* @__PURE__ */ __exportAll({
42
44
  CompactionData: () => CompactionData,
43
45
  Config: () => Config$1,
44
46
  DEFAULT_TITLE: () => DEFAULT_TITLE,
47
+ Execution: () => Execution,
45
48
  ID: () => ID$1,
46
49
  IDFromDb: () => IDFromDb,
47
50
  InterruptReason: () => InterruptReason,
@@ -86,6 +89,12 @@ const Config$1 = Schema.Struct({
86
89
  * different facts, and inspecting the cause cannot tell them apart.
87
90
  */
88
91
  const InterruptReason = Schema.Literals(["user", "shutdown"]);
92
+ /**
93
+ * A run left unfinished: `running` while one is in flight, and still after the process died during
94
+ * it; `closed` once a shutdown stopped it. Either resumes when the server starts again; a run that
95
+ * ended any other way leaves none.
96
+ */
97
+ const Execution = Schema.Literals(["running", "closed"]);
89
98
  const JsonObject = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown));
90
99
  const MessageEnvelopeIdentity = Schema.fromJsonString(Schema.Struct({ messageId: Schema.String }));
91
100
  const CompactionData = Schema.fromJsonString(Schema.Struct({
@@ -134,10 +143,10 @@ var global_exports = /* @__PURE__ */ __exportAll({
134
143
  app: () => app,
135
144
  appConfigDir: () => appConfigDir,
136
145
  authFile: () => authFile,
137
- defaultLayer: () => defaultLayer$2,
146
+ defaultLayer: () => defaultLayer$4,
138
147
  homeConfig: () => homeConfig,
139
148
  layer: () => layer$27,
140
- layerWith: () => layerWith,
149
+ layerWith: () => layerWith$1,
141
150
  make: () => make$18,
142
151
  resolve: () => resolve$4
143
152
  });
@@ -184,8 +193,8 @@ const build = (input) => Effect.gen(function* () {
184
193
  return Service$17.of(paths);
185
194
  });
186
195
  const layer$27 = Layer.effect(Service$17, build({}));
187
- const defaultLayer$2 = layer$27;
188
- const layerWith = (input) => Layer.effect(Service$17, build(input));
196
+ const defaultLayer$4 = layer$27;
197
+ const layerWith$1 = (input) => Layer.effect(Service$17, build(input));
189
198
  //#endregion
190
199
  //#region src/db/migrations.ts
191
200
  const migrations = {
@@ -366,6 +375,14 @@ const migrations = {
366
375
  }),
367
376
  "202609290001_session_config": Effect.gen(function* () {
368
377
  yield* (yield* SqlClient.SqlClient)`ALTER TABLE session ADD COLUMN config TEXT`;
378
+ }),
379
+ "202610230001_session_input_cancel": Effect.gen(function* () {
380
+ yield* (yield* SqlClient.SqlClient)`ALTER TABLE session_input ADD COLUMN cancelled_seq INTEGER`;
381
+ }),
382
+ "202610240001_session_execution": Effect.gen(function* () {
383
+ const sql = yield* SqlClient.SqlClient;
384
+ yield* sql`ALTER TABLE session ADD COLUMN execution TEXT`;
385
+ yield* sql`ALTER TABLE session ADD COLUMN restarts INTEGER NOT NULL DEFAULT 0`;
369
386
  })
370
387
  };
371
388
  //#endregion
@@ -374,7 +391,7 @@ var db_exports = /* @__PURE__ */ __exportAll({
374
391
  Database: () => db_exports,
375
392
  SqlClient: () => SqlClient$1,
376
393
  SqlSchema: () => SqlSchema$1,
377
- defaultLayer: () => defaultLayer$1,
394
+ defaultLayer: () => defaultLayer$3,
378
395
  layer: () => layer$26,
379
396
  locationConfig: () => locationConfig,
380
397
  path: () => path$1,
@@ -408,7 +425,7 @@ const path$1 = Effect.fn("Database.path")(function* (data) {
408
425
  const directory = data ?? (yield* resolve$4()).data;
409
426
  return resolveDatabaseLocation(yield* locationConfig, directory);
410
427
  });
411
- const defaultLayer$1 = Layer.unwrap(path$1().pipe(Effect.map(layer$26)));
428
+ const defaultLayer$3 = Layer.unwrap(path$1().pipe(Effect.map(layer$26)));
412
429
  //#endregion
413
430
  //#region src/event/schema.ts
414
431
  var schema_exports$7 = /* @__PURE__ */ __exportAll({});
@@ -589,6 +606,8 @@ var SessionRow = class extends Model$1.Class("SessionRow")({
589
606
  tokensOutput: Model$1.GeneratedByDb(Schema.Int),
590
607
  tokensCacheRead: Model$1.GeneratedByDb(Schema.Int),
591
608
  tokensCacheWrite: Model$1.GeneratedByDb(Schema.Int),
609
+ execution: Model$1.FieldOption(Execution),
610
+ restarts: Model$1.GeneratedByDb(Schema.Int),
592
611
  ...Timestamps
593
612
  }) {};
594
613
  var SessionInputRow = class extends Model$1.Class("SessionInputRow")({
@@ -598,6 +617,7 @@ var SessionInputRow = class extends Model$1.Class("SessionInputRow")({
598
617
  delivery: Schema.Literals(inputDeliveries),
599
618
  admittedSeq: Schema.Int,
600
619
  promotedSeq: Model$1.FieldOption(Schema.Int),
620
+ cancelledSeq: Model$1.FieldOption(Schema.Int),
601
621
  createdAt: Model$1.DateTimeInsertFromNumber
602
622
  }) {};
603
623
  var SessionEntryRow = class extends Model$1.Class("SessionEntryRow")({
@@ -729,6 +749,8 @@ var list_exports = /* @__PURE__ */ __exportAll({
729
749
  LLMThinkingStart: () => LLMThinkingStart,
730
750
  PluginUpdated: () => PluginUpdated,
731
751
  PromptAdmitted: () => PromptAdmitted,
752
+ PromptCancelled: () => PromptCancelled,
753
+ PromptUpdated: () => PromptUpdated,
732
754
  Prompted: () => Prompted,
733
755
  PublicDefinitions: () => PublicDefinitions,
734
756
  RetryFinished: () => RetryFinished,
@@ -766,6 +788,29 @@ const Prompted = schema_exports$7.EventSchema.define({
766
788
  ...durableOptions,
767
789
  schema: PromptFields
768
790
  });
791
+ /** A pending prompt withdrawn before promotion; it never enters the conversation. */
792
+ const PromptCancelled = schema_exports$7.EventSchema.define({
793
+ type: "session.prompt.cancelled",
794
+ ...durableOptions,
795
+ schema: {
796
+ ...baseOptions,
797
+ messageId: ID
798
+ }
799
+ });
800
+ /**
801
+ * A pending prompt moved to the other lane or rewritten before promotion; it keeps its place. Only
802
+ * what changed is carried, so two updates of different fields both stand.
803
+ */
804
+ const PromptUpdated = schema_exports$7.EventSchema.define({
805
+ type: "session.prompt.updated",
806
+ ...durableOptions,
807
+ schema: {
808
+ ...baseOptions,
809
+ messageId: ID,
810
+ prompt: Prompt.pipe(schema_exports$9.optional),
811
+ delivery: Delivery.pipe(schema_exports$9.optional)
812
+ }
813
+ });
769
814
  const ConfigChanged = schema_exports$7.EventSchema.define({
770
815
  type: "session.config.changed",
771
816
  ...durableOptions,
@@ -1000,14 +1045,14 @@ const PluginUpdated = schema_exports$7.EventSchema.define({
1000
1045
  * Everything the kernel can publish. `PublicDefinitions` is the narrower set
1001
1046
  * that reaches clients -- adding an event here does not publish it.
1002
1047
  */
1003
- const Definitions = schema_exports$7.EventSchema.inventory(ExecutionStarted, ExecutionSucceeded, ExecutionFailed, ExecutionInterrupted, PromptAdmitted, Prompted, ConfigChanged, SessionForked, TurnStarted, TurnEnded, TurnAborted, RetryScheduled, RetryFinished, LLMStarted, LLMTextStart, LLMTextDelta, LLMTextEnd, LLMThinkingStart, LLMThinkingDelta, LLMThinkingEnd, LLMEnded, LLMFailed, ToolStarted, ToolProgress$2, ToolSettled, PluginUpdated);
1048
+ const Definitions = schema_exports$7.EventSchema.inventory(ExecutionStarted, ExecutionSucceeded, ExecutionFailed, ExecutionInterrupted, PromptAdmitted, Prompted, PromptCancelled, PromptUpdated, ConfigChanged, SessionForked, TurnStarted, TurnEnded, TurnAborted, RetryScheduled, RetryFinished, LLMStarted, LLMTextStart, LLMTextDelta, LLMTextEnd, LLMThinkingStart, LLMThinkingDelta, LLMThinkingEnd, LLMEnded, LLMFailed, ToolStarted, ToolProgress$2, ToolSettled, PluginUpdated);
1004
1049
  /**
1005
1050
  * The wire contract: what a client is allowed to receive. Identical to
1006
1051
  * `Definitions` today, and separate so that adding a kernel event is not by
1007
1052
  * itself a decision to publish it.
1008
1053
  */
1009
1054
  const PublicDefinitions = Definitions;
1010
- const DurableDefinitions = schema_exports$7.EventSchema.inventory(PromptAdmitted, Prompted, ConfigChanged, SessionForked, TurnEnded, LLMStarted, LLMEnded, LLMFailed, ToolSettled);
1055
+ const DurableDefinitions = schema_exports$7.EventSchema.inventory(PromptAdmitted, Prompted, PromptCancelled, PromptUpdated, ConfigChanged, SessionForked, TurnEnded, LLMStarted, LLMEnded, LLMFailed, ToolSettled);
1011
1056
  const Manifest$2 = {
1012
1057
  definitions: schema_exports$7.EventSchema.durable([...DurableDefinitions]),
1013
1058
  schema: Schema.Union([...DurableDefinitions], { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
@@ -1579,7 +1624,7 @@ var session_exports$1 = /* @__PURE__ */ __exportAll({
1579
1624
  SessionNotLinkedError: () => SessionNotLinkedError,
1580
1625
  SessionRow: () => SessionRow,
1581
1626
  ToolCallNotFoundError: () => ToolCallNotFoundError,
1582
- defaultLayer: () => defaultLayer,
1627
+ defaultLayer: () => defaultLayer$2,
1583
1628
  entryStates: () => entryStates,
1584
1629
  entryTypes: () => entryTypes,
1585
1630
  layer: () => layer$23,
@@ -1663,6 +1708,16 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
1663
1708
  SELECT p.* FROM space p JOIN session s ON s.space_id = p.id WHERE s.id = ${sessionId}
1664
1709
  `
1665
1710
  });
1711
+ const findProject = SqlSchema.findOneOption({
1712
+ Request: Schema.String,
1713
+ Result: ProjectRow,
1714
+ execute: (sessionId) => sql`
1715
+ SELECT p.* FROM project p
1716
+ JOIN space sp ON sp.project_id = p.id
1717
+ JOIN session s ON s.space_id = sp.id
1718
+ WHERE s.id = ${sessionId}
1719
+ `
1720
+ });
1666
1721
  const findSpaceById = SqlSchema.findOneOption({
1667
1722
  Request: Schema.String,
1668
1723
  Result: SpaceRow,
@@ -1795,6 +1850,7 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
1795
1850
  directory: input.directory,
1796
1851
  hostDir: Option.fromUndefinedOr(input.hostDir),
1797
1852
  config: Option.none(),
1853
+ execution: Option.none(),
1798
1854
  title: input.title,
1799
1855
  tag: Option.fromUndefinedOr(input.tag),
1800
1856
  metadata: Option.fromUndefinedOr(input.metadata),
@@ -1811,6 +1867,13 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
1811
1867
  const space = Effect.fn("Session.space")(function* (sessionId) {
1812
1868
  return Option.map(yield* findSpace(sessionId).pipe(Effect.orDie), fromRow$1);
1813
1869
  });
1870
+ const project = Effect.fn("Session.project")(function* (sessionId) {
1871
+ return Option.map(yield* findProject(sessionId).pipe(Effect.orDie), (row) => new schema_exports$6.ProjectSchema.Info({
1872
+ id: schema_exports$6.ProjectSchema.ID.make(row.id),
1873
+ name: row.name,
1874
+ status: row.status
1875
+ }));
1876
+ });
1814
1877
  const list = Effect.fn("Session.list")(function* (input) {
1815
1878
  return yield* (input === void 0 ? selectAll() : "spaceId" in input ? selectBySpace(input.spaceId) : selectByProject(input.projectId)).pipe(Effect.orDie);
1816
1879
  });
@@ -2141,6 +2204,7 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2141
2204
  directory: source.value.directory,
2142
2205
  hostDir: source.value.hostDir,
2143
2206
  config: source.value.config,
2207
+ execution: Option.none(),
2144
2208
  title: input.title ?? source.value.title,
2145
2209
  tag: input.tag === void 0 ? source.value.tag : Option.some(input.tag),
2146
2210
  metadata: source.value.metadata,
@@ -2250,6 +2314,41 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2250
2314
  WHERE id = ${sessionId}
2251
2315
  `;
2252
2316
  }, Effect.orDie);
2317
+ const markRunning = Effect.fn("Session.markRunning")(function* (sessionId) {
2318
+ yield* sql`UPDATE session SET execution = 'running' WHERE id = ${sessionId}`;
2319
+ }, Effect.orDie);
2320
+ const endExecution = Effect.fn("Session.endExecution")(function* (sessionId, end, stoppedAt) {
2321
+ if (end === "closed") {
2322
+ yield* sql`UPDATE session SET execution = 'closed' WHERE id = ${sessionId}`;
2323
+ return;
2324
+ }
2325
+ const after = end === "stopped" ? stoppedAt ?? Number.MAX_SAFE_INTEGER : -1;
2326
+ yield* sql`
2327
+ UPDATE session SET execution = NULL, restarts = 0
2328
+ WHERE id = ${sessionId} AND NOT EXISTS (
2329
+ SELECT 1 FROM session_input
2330
+ WHERE session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL
2331
+ AND admitted_seq > ${after}
2332
+ )
2333
+ `;
2334
+ }, Effect.orDie);
2335
+ const claimRestart = Effect.fn("Session.claimRestart")(function* (sessionId) {
2336
+ const rows = yield* sql`
2337
+ UPDATE session
2338
+ SET restarts = restarts + (CASE WHEN execution = 'running' THEN 1 ELSE 0 END), execution = 'running'
2339
+ WHERE id = ${sessionId} AND execution IS NOT NULL
2340
+ RETURNING restarts
2341
+ `;
2342
+ return Option.fromUndefinedOr(rows[0]?.restarts);
2343
+ }, Effect.orDie);
2344
+ const resetRestarts = Effect.fn("Session.resetRestarts")(function* (sessionId) {
2345
+ yield* sql`UPDATE session SET restarts = 0 WHERE id = ${sessionId} AND restarts > 0`;
2346
+ }, Effect.orDie);
2347
+ const unfinished = SqlSchema.findAll({
2348
+ Request: Schema.Void,
2349
+ Result: SessionRow,
2350
+ execute: () => sql`SELECT * FROM session WHERE execution IS NOT NULL ORDER BY updated_at`
2351
+ })(void 0).pipe(Effect.orDie, Effect.withSpan("Session.unfinished"));
2253
2352
  const relink = Effect.fn("Session.relink")(function* (input) {
2254
2353
  const reject = (reason) => new RelinkError({
2255
2354
  sessionId: input.sessionId,
@@ -2311,8 +2410,14 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2311
2410
  link,
2312
2411
  retitle,
2313
2412
  configure,
2413
+ markRunning,
2414
+ endExecution,
2415
+ claimRestart,
2416
+ resetRestarts,
2417
+ unfinished,
2314
2418
  get,
2315
2419
  space,
2420
+ project,
2316
2421
  list,
2317
2422
  entry,
2318
2423
  path,
@@ -2329,7 +2434,7 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2329
2434
  setLabel
2330
2435
  });
2331
2436
  }));
2332
- const defaultLayer = layer$23.pipe(Layer.provide(defaultLayer$1));
2437
+ const defaultLayer$2 = layer$23.pipe(Layer.provide(defaultLayer$3));
2333
2438
  //#endregion
2334
2439
  //#region src/context/errors.ts
2335
2440
  var ContextDecodeError = class extends Schema.TaggedError()("ContextDecodeError", {
@@ -4274,6 +4379,17 @@ const make$9 = () => {
4274
4379
  }) };
4275
4380
  };
4276
4381
  //#endregion
4382
+ //#region src/sandbox/sandbox.ts
4383
+ /**
4384
+ * Assemble a local backend into a sandbox with a **real host shell** — the
4385
+ * right choice whenever commands must invoke actual binaries (`git`, package
4386
+ * managers). For a sandbox whose shell should stay inside the VFS, use
4387
+ * `EnvBash.services` instead, which wires just-bash over the same tree.
4388
+ */
4389
+ const services = (backend, identity) => Layer.provideMerge(io_exports.SandboxIO.mount(identity), Layer.provideMerge(Layer.merge(layer$21, layer$18()), backend));
4390
+ /** Default sandbox: the real OS filesystem and processes, mounted at `cwd`. */
4391
+ const defaultLayer$1 = (cwd) => services(layer$20(), io_exports.SandboxIO.host(cwd));
4392
+ //#endregion
4277
4393
  //#region src/git/git.ts
4278
4394
  var AppProcessError = class extends Schema.TaggedError()("AppProcessError", {
4279
4395
  command: Schema.String,
@@ -4464,7 +4580,8 @@ const up = Effect.fn("SandboxFileSystem.up")(function* (fs, options) {
4464
4580
  const isDirectory$1 = Effect.fn("SandboxFileSystem.isDirectory")(function* (fs, path) {
4465
4581
  return yield* fs.stat(path).pipe(Effect.map((stat) => stat.isDirectory), Effect.orElseSucceed(() => false));
4466
4582
  });
4467
- Effect.fn("SandboxFileSystem.isFile")(function* (fs, path) {
4583
+ /** True when `path` exists and is a regular file; false when it is absent or unreadable. */
4584
+ const isFile = Effect.fn("SandboxFileSystem.isFile")(function* (fs, path) {
4468
4585
  return yield* fs.stat(path).pipe(Effect.map((stat) => stat.isFile), Effect.orElseSucceed(() => false));
4469
4586
  });
4470
4587
  /** Read a file, or `undefined` when it is missing or unreadable. */
@@ -4611,6 +4728,16 @@ const layer$13 = Layer.effect(Service$10, Effect.gen(function* () {
4611
4728
  }));
4612
4729
  //#endregion
4613
4730
  //#region src/project/project.ts
4731
+ var project_exports = /* @__PURE__ */ __exportAll({
4732
+ Project: () => project_exports,
4733
+ ProjectNotFoundError: () => ProjectNotFoundError,
4734
+ Service: () => Service$9,
4735
+ defaultLayer: () => defaultLayer,
4736
+ layer: () => layer$12,
4737
+ layerWith: () => layerWith,
4738
+ provisional: () => provisional,
4739
+ rename: () => rename
4740
+ });
4614
4741
  /** Deterministic, env-local id for a directory without a portable identity. Never written to a marker. */
4615
4742
  const provisional = (env, location) => schema_exports$6.ProjectSchema.ID.make(fast(JSON.stringify([
4616
4743
  "dir",
@@ -4664,7 +4791,7 @@ const layer$12 = Layer.effect(Service$9, Effect.gen(function* () {
4664
4791
  main: repo.gitDir === repo.store
4665
4792
  }]).toSorted((a, b) => Number(b.main) - Number(a.main));
4666
4793
  });
4667
- const resolveOrCreate = Effect.fn("Project.resolveOrCreate")(function* (cwd) {
4794
+ const resolveOrCreate = Effect.fn("Project.resolveOrCreate")(function* (cwd, options) {
4668
4795
  if (!(yield* fs.exists(cwd).pipe(Effect.orDie))) return yield* new DirectoryNotFoundError$1({
4669
4796
  directory: cwd,
4670
4797
  sandboxInstanceId: env
@@ -4678,7 +4805,7 @@ const layer$12 = Layer.effect(Service$9, Effect.gen(function* () {
4678
4805
  const ident = repo === void 0 ? void 0 : yield* repos.identity(repo);
4679
4806
  const worktree = repo?.directory ?? location;
4680
4807
  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);
4808
+ const name = options?.name ?? ident?.name ?? posix.basename(worktree);
4682
4809
  const vcs = repo === void 0 ? void 0 : {
4683
4810
  type: "git",
4684
4811
  store: repo.store
@@ -4750,6 +4877,32 @@ const layer$12 = Layer.effect(Service$9, Effect.gen(function* () {
4750
4877
  list
4751
4878
  });
4752
4879
  }));
4880
+ var ProjectNotFoundError = class extends Schema.TaggedError()("ProjectNotFoundError", { projectId: Schema.String }) {};
4881
+ /** Renames a project. Needs only the database, not a mount: a name is the same in every env. */
4882
+ const rename = Effect.fn("Project.rename")(function* (input) {
4883
+ const sql = yield* SqlClient.SqlClient;
4884
+ const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
4885
+ const row = yield* SqlSchema.findOneOption({
4886
+ Request: Schema.Struct({
4887
+ id: Schema.String,
4888
+ name: Schema.String
4889
+ }),
4890
+ Result: ProjectRow,
4891
+ execute: ({ id, name }) => sql`UPDATE project SET name = ${name}, updated_at = ${now} WHERE id = ${id} RETURNING *`
4892
+ })({
4893
+ id: input.projectId,
4894
+ name: input.name
4895
+ }).pipe(Effect.orDie);
4896
+ if (Option.isNone(row)) return yield* new ProjectNotFoundError({ projectId: input.projectId });
4897
+ return new schema_exports$6.ProjectSchema.Info({
4898
+ id: schema_exports$6.ProjectSchema.ID.make(row.value.id),
4899
+ name: row.value.name,
4900
+ status: row.value.status
4901
+ });
4902
+ });
4903
+ /** Takes an assembled sandbox — local or remote — not a local backend. */
4904
+ 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));
4905
+ const defaultLayer = (path) => layerWith(defaultLayer$1(path));
4753
4906
  //#endregion
4754
4907
  //#region src/location/location.ts
4755
4908
  var location_exports = /* @__PURE__ */ __exportAll({
@@ -4761,10 +4914,10 @@ var location_exports = /* @__PURE__ */ __exportAll({
4761
4914
  layer: () => layer$11,
4762
4915
  layerMounted: () => layerMounted
4763
4916
  });
4764
- const layer$11 = () => Layer.effect(Service, Effect.gen(function* () {
4917
+ const layer$11 = (options) => Layer.effect(Service, Effect.gen(function* () {
4765
4918
  const instance = yield* io_exports.SandboxIO.Current;
4766
4919
  const directory = schema_exports$9.AbsolutePath.make(instance.cwd);
4767
- const resolved = yield* (yield* Service$9).resolveOrCreate(directory);
4920
+ const resolved = yield* (yield* Service$9).resolveOrCreate(directory, options);
4768
4921
  return Service.of(new Info({
4769
4922
  directory: resolved.directory,
4770
4923
  space: resolved.space,
@@ -4776,7 +4929,7 @@ const layer$11 = () => Layer.effect(Service, Effect.gen(function* () {
4776
4929
  * folded in. What remains is exactly the mount and the database — for callers
4777
4930
  * that already hold both, such as the runner and `Session.create`.
4778
4931
  */
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))));
4932
+ 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
4933
  //#endregion
4781
4934
  //#region src/runner/run.ts
4782
4935
  var run_exports = /* @__PURE__ */ __exportAll({
@@ -4871,7 +5024,6 @@ var ProviderError = class extends Schema.TaggedError()("Runner.ProviderError", {
4871
5024
  var ModelCatalogError = class extends Schema.TaggedError()("Runner.ModelCatalogError", {
4872
5025
  path: Schema.String,
4873
5026
  reason: Schema.Literals([
4874
- "missing",
4875
5027
  "unreadable",
4876
5028
  "empty",
4877
5029
  "invalid"
@@ -4905,6 +5057,7 @@ var Service$6 = class extends Context.Service()("@codeworksh/harness/runner/exec
4905
5057
  Layer.succeed(Service$6, Service$6.of({
4906
5058
  active: Effect.succeed(/* @__PURE__ */ new Set()),
4907
5059
  resume: () => Effect.void,
5060
+ recover: () => Effect.void,
4908
5061
  wake: () => Effect.void,
4909
5062
  interrupt: () => Effect.succeed(false),
4910
5063
  awaitIdle: () => Effect.void
@@ -4918,7 +5071,8 @@ const Admitted = Schema.Struct({
4918
5071
  prompt: Prompt,
4919
5072
  delivery: Delivery,
4920
5073
  timeCreated: schema_exports$9.DateTimeUtcFromMillis,
4921
- promotedSeq: schema_exports$9.NonNegativeInt.pipe(schema_exports$9.optional)
5074
+ promotedSeq: schema_exports$9.NonNegativeInt.pipe(schema_exports$9.optional),
5075
+ cancelledSeq: schema_exports$9.NonNegativeInt.pipe(schema_exports$9.optional)
4922
5076
  }).annotate({ identifier: "SessionInput.Admitted" });
4923
5077
  //#endregion
4924
5078
  //#region src/session/input/input.ts
@@ -4939,7 +5093,8 @@ const fromRow = (row) => Admitted.make({
4939
5093
  prompt: row.prompt,
4940
5094
  delivery: row.delivery,
4941
5095
  timeCreated: row.createdAt,
4942
- ...Option.isNone(row.promotedSeq) ? {} : { promotedSeq: row.promotedSeq.value }
5096
+ ...Option.isNone(row.promotedSeq) ? {} : { promotedSeq: row.promotedSeq.value },
5097
+ ...Option.isNone(row.cancelledSeq) ? {} : { cancelledSeq: row.cancelledSeq.value }
4943
5098
  });
4944
5099
  /**
4945
5100
  * Whether a stored input carries the prompt the caller believes it admitted.
@@ -4963,6 +5118,38 @@ const make$8 = Effect.gen(function* () {
4963
5118
  INSERT INTO session_input ${sql.insert(row)}
4964
5119
  ON CONFLICT(id) DO NOTHING
4965
5120
  RETURNING id
5121
+ `
5122
+ });
5123
+ const updateRow = SqlSchema.findAll({
5124
+ Request: Schema.Struct({
5125
+ id: Schema.String,
5126
+ sessionId: Schema.String,
5127
+ prompt: Schema.NullOr(Schema.fromJsonString(Prompt)),
5128
+ delivery: Schema.NullOr(Delivery)
5129
+ }),
5130
+ Result: Schema.Struct({ id: Schema.String }),
5131
+ execute: ({ id, sessionId, prompt, delivery }) => sql`
5132
+ UPDATE session_input SET prompt = COALESCE(${prompt}, prompt), delivery = COALESCE(${delivery}, delivery)
5133
+ WHERE id = ${id} AND session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL
5134
+ RETURNING id
5135
+ `
5136
+ });
5137
+ const reviveRow = SqlSchema.findAll({
5138
+ Request: Schema.Struct({
5139
+ id: Schema.String,
5140
+ sessionId: Schema.String,
5141
+ prompt: Schema.fromJsonString(Prompt),
5142
+ delivery: Delivery,
5143
+ admittedSeq: Schema.Int,
5144
+ createdAt: schema_exports$9.DateTimeUtcFromMillis
5145
+ }),
5146
+ Result: Schema.Struct({ id: Schema.String }),
5147
+ execute: ({ id, sessionId, prompt, delivery, admittedSeq, createdAt }) => sql`
5148
+ UPDATE session_input
5149
+ SET prompt = ${prompt}, delivery = ${delivery}, admitted_seq = ${admittedSeq}, created_at = ${createdAt},
5150
+ cancelled_seq = NULL
5151
+ WHERE id = ${id} AND session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NOT NULL
5152
+ RETURNING id
4966
5153
  `
4967
5154
  });
4968
5155
  const promoteRow = SqlSchema.findAll({
@@ -4974,8 +5161,21 @@ const make$8 = Effect.gen(function* () {
4974
5161
  Result: SessionInputRow,
4975
5162
  execute: ({ id, sessionId, promotedSeq }) => sql`
4976
5163
  UPDATE session_input SET promoted_seq = ${promotedSeq}
4977
- WHERE id = ${id} AND session_id = ${sessionId} AND promoted_seq IS NULL
5164
+ WHERE id = ${id} AND session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL
4978
5165
  RETURNING *
5166
+ `
5167
+ });
5168
+ const cancelRow = SqlSchema.findAll({
5169
+ Request: Schema.Struct({
5170
+ id: Schema.String,
5171
+ sessionId: Schema.String,
5172
+ cancelledSeq: Schema.Int
5173
+ }),
5174
+ Result: Schema.Struct({ id: Schema.String }),
5175
+ execute: ({ id, sessionId, cancelledSeq }) => sql`
5176
+ UPDATE session_input SET cancelled_seq = ${cancelledSeq}
5177
+ WHERE id = ${id} AND session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL
5178
+ RETURNING id
4979
5179
  `
4980
5180
  });
4981
5181
  const selectEntry = SqlSchema.findAll({
@@ -4991,8 +5191,17 @@ const make$8 = Effect.gen(function* () {
4991
5191
  Result: Schema.Struct({ id: Schema.String }),
4992
5192
  execute: ({ sessionId, delivery }) => sql`
4993
5193
  SELECT id FROM session_input
4994
- WHERE session_id = ${sessionId} AND promoted_seq IS NULL AND delivery = ${delivery}
5194
+ WHERE session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL AND delivery = ${delivery}
4995
5195
  LIMIT 1
5196
+ `
5197
+ });
5198
+ const selectPending = SqlSchema.findAll({
5199
+ Request: Schema.String,
5200
+ Result: SessionInputRow,
5201
+ execute: (sessionId) => sql`
5202
+ SELECT * FROM session_input
5203
+ WHERE session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL
5204
+ ORDER BY admitted_seq
4996
5205
  `
4997
5206
  });
4998
5207
  const selectPendingSteers = SqlSchema.findAll({
@@ -5005,6 +5214,7 @@ const make$8 = Effect.gen(function* () {
5005
5214
  SELECT * FROM session_input
5006
5215
  WHERE session_id = ${sessionId}
5007
5216
  AND promoted_seq IS NULL
5217
+ AND cancelled_seq IS NULL
5008
5218
  AND delivery = 'steer'
5009
5219
  AND admitted_seq <= ${cutoff}
5010
5220
  ORDER BY admitted_seq
@@ -5015,7 +5225,7 @@ const make$8 = Effect.gen(function* () {
5015
5225
  Result: SessionInputRow,
5016
5226
  execute: (sessionId) => sql`
5017
5227
  SELECT * FROM session_input
5018
- WHERE session_id = ${sessionId} AND promoted_seq IS NULL AND delivery = 'followUp'
5228
+ WHERE session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL AND delivery = 'followUp'
5019
5229
  ORDER BY admitted_seq
5020
5230
  LIMIT 1
5021
5231
  `
@@ -5025,7 +5235,7 @@ const make$8 = Effect.gen(function* () {
5025
5235
  }, Effect.orDie);
5026
5236
  const admit = Effect.fn("SessionInput.admit")(function* (input) {
5027
5237
  const existing = yield* find(input.id);
5028
- if (Option.isSome(existing)) return existing.value;
5238
+ if (Option.isSome(existing) && existing.value.cancelledSeq === void 0) return existing.value;
5029
5239
  const timestamp = yield* DateTime.now;
5030
5240
  return yield* events.publish(PromptAdmitted, {
5031
5241
  messageId: input.id,
@@ -5040,10 +5250,7 @@ const make$8 = Effect.gen(function* () {
5040
5250
  prompt: input.prompt,
5041
5251
  delivery: input.delivery,
5042
5252
  timeCreated: timestamp
5043
- }))), Effect.catchDefect((defect) => find(input.id).pipe(Effect.flatMap(Option.match({
5044
- onNone: () => Effect.die(defect),
5045
- onSome: (stored) => Effect.succeed(stored)
5046
- })))));
5253
+ }))), Effect.catchDefect((defect) => find(input.id).pipe(Effect.flatMap((stored) => Option.isSome(stored) && stored.value.cancelledSeq === void 0 ? Effect.succeed(stored.value) : Effect.die(defect)))));
5047
5254
  });
5048
5255
  const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(function* (input) {
5049
5256
  if ((yield* selectEntry(input.id)).length > 0) return yield* Effect.die(new LifecycleConflict({ id: input.id }));
@@ -5054,7 +5261,16 @@ const make$8 = Effect.gen(function* () {
5054
5261
  delivery: input.delivery,
5055
5262
  admittedSeq: input.admittedSeq,
5056
5263
  promotedSeq: Option.none(),
5264
+ cancelledSeq: Option.none(),
5057
5265
  createdAt: VariantSchema.Override(input.timeCreated)
5266
+ })).length > 0) return;
5267
+ if ((yield* reviveRow({
5268
+ id: input.id,
5269
+ sessionId: input.sessionId,
5270
+ prompt: input.prompt,
5271
+ delivery: input.delivery,
5272
+ admittedSeq: input.admittedSeq,
5273
+ createdAt: input.timeCreated
5058
5274
  })).length === 0) return yield* Effect.die(new LifecycleConflict({ id: input.id }));
5059
5275
  }, Effect.orDie);
5060
5276
  const projectPrompted = Effect.fn("SessionInput.projectPrompted")(function* (input) {
@@ -5069,7 +5285,7 @@ const make$8 = Effect.gen(function* () {
5069
5285
  }
5070
5286
  const stored = yield* find(input.id);
5071
5287
  if (Option.isSome(stored)) {
5072
- if (!matchesProjection(stored.value, input) || stored.value.promotedSeq !== input.promotedSeq) return yield* Effect.die(new LifecycleConflict({ id: input.id }));
5288
+ if (!matchesProjection(stored.value, input) || stored.value.promotedSeq !== input.promotedSeq || stored.value.cancelledSeq !== void 0) return yield* Effect.die(new LifecycleConflict({ id: input.id }));
5073
5289
  return;
5074
5290
  }
5075
5291
  yield* insertRow({
@@ -5079,9 +5295,50 @@ const make$8 = Effect.gen(function* () {
5079
5295
  delivery: input.delivery,
5080
5296
  admittedSeq: input.promotedSeq,
5081
5297
  promotedSeq: Option.some(input.promotedSeq),
5298
+ cancelledSeq: Option.none(),
5082
5299
  createdAt: VariantSchema.Override(input.timeCreated)
5083
5300
  });
5084
5301
  }, Effect.orDie);
5302
+ const projectCancelled = Effect.fn("SessionInput.projectCancelled")(function* (input) {
5303
+ if ((yield* cancelRow(input)).length > 0) return;
5304
+ const stored = yield* find(input.id);
5305
+ if (Option.isSome(stored) && stored.value.cancelledSeq === input.cancelledSeq) return;
5306
+ return yield* Effect.die(new LifecycleConflict({ id: input.id }));
5307
+ }, Effect.orDie);
5308
+ const projectUpdated = Effect.fn("SessionInput.projectUpdated")(function* (input) {
5309
+ if ((yield* updateRow({
5310
+ id: input.id,
5311
+ sessionId: input.sessionId,
5312
+ prompt: input.prompt ?? null,
5313
+ delivery: input.delivery ?? null
5314
+ })).length > 0) return;
5315
+ return yield* Effect.die(new LifecycleConflict({ id: input.id }));
5316
+ }, Effect.orDie);
5317
+ const update = Effect.fn("SessionInput.update")(function* (sessionId, id, patch) {
5318
+ const stored = yield* find(id);
5319
+ if (Option.isNone(stored) || stored.value.sessionId !== sessionId || stored.value.promotedSeq !== void 0 || stored.value.cancelledSeq !== void 0) return Option.none();
5320
+ const prompt = patch.prompt === void 0 || Prompt.equivalence(patch.prompt, stored.value.prompt) ? void 0 : patch.prompt;
5321
+ const delivery = patch.delivery === void 0 || patch.delivery === stored.value.delivery ? void 0 : patch.delivery;
5322
+ if (prompt === void 0 && delivery === void 0) return stored;
5323
+ const timestamp = yield* DateTime.now;
5324
+ return yield* events.publish(PromptUpdated, {
5325
+ sessionId,
5326
+ messageId: id,
5327
+ timestamp,
5328
+ ...prompt === void 0 ? {} : { prompt },
5329
+ ...delivery === void 0 ? {} : { delivery }
5330
+ }).pipe(Effect.andThen(find(id)), Effect.catchDefect((defect) => isLifecycleConflict(defect) ? Effect.succeed(Option.none()) : Effect.die(defect)));
5331
+ });
5332
+ const cancel = Effect.fn("SessionInput.cancel")(function* (sessionId, id) {
5333
+ const stored = yield* find(id);
5334
+ if (Option.isNone(stored) || stored.value.sessionId !== sessionId || stored.value.promotedSeq !== void 0 || stored.value.cancelledSeq !== void 0) return false;
5335
+ const timestamp = yield* DateTime.now;
5336
+ return yield* events.publish(PromptCancelled, {
5337
+ sessionId,
5338
+ messageId: id,
5339
+ timestamp
5340
+ }).pipe(Effect.as(true), Effect.catchDefect((defect) => isLifecycleConflict(defect) ? Effect.succeed(false) : Effect.die(defect)));
5341
+ });
5085
5342
  const publishPrompted = Effect.fn("SessionInput.publishPrompted")(function* (sessionId, rows) {
5086
5343
  let promoted = 0;
5087
5344
  for (const row of rows) {
@@ -5092,7 +5349,7 @@ const make$8 = Effect.gen(function* () {
5092
5349
  messageId: id,
5093
5350
  prompt: row.prompt,
5094
5351
  delivery: row.delivery
5095
- }).pipe(Effect.as(true), Effect.catchDefect((defect) => isLifecycleConflict(defect) ? find(id).pipe(Effect.flatMap((stored) => Option.isSome(stored) && stored.value.promotedSeq !== void 0 ? Effect.succeed(false) : Effect.die(defect))) : Effect.die(defect)))) promoted += 1;
5352
+ }).pipe(Effect.as(true), Effect.catchDefect((defect) => isLifecycleConflict(defect) ? find(id).pipe(Effect.flatMap((stored) => Option.isSome(stored) && (stored.value.promotedSeq !== void 0 || stored.value.cancelledSeq !== void 0 || stored.value.admittedSeq !== row.admittedSeq || stored.value.delivery !== row.delivery || !Prompt.equivalence(stored.value.prompt, row.prompt)) ? Effect.succeed(false) : Effect.die(defect))) : Effect.die(defect)))) promoted += 1;
5096
5353
  }
5097
5354
  return promoted;
5098
5355
  });
@@ -5101,6 +5358,13 @@ const make$8 = Effect.gen(function* () {
5101
5358
  admit,
5102
5359
  projectAdmitted,
5103
5360
  projectPrompted,
5361
+ projectCancelled,
5362
+ projectUpdated,
5363
+ update,
5364
+ cancel,
5365
+ pending: Effect.fn("SessionInput.pending")(function* (sessionId) {
5366
+ return (yield* selectPending(sessionId)).map(fromRow);
5367
+ }, Effect.orDie),
5104
5368
  hasPending: Effect.fn("SessionInput.hasPending")(function* (sessionId, delivery) {
5105
5369
  return (yield* selectPendingOne({
5106
5370
  sessionId,
@@ -5126,6 +5390,7 @@ const make$8 = Effect.gen(function* () {
5126
5390
  var control_exports = /* @__PURE__ */ __exportAll({
5127
5391
  Control: () => control_exports,
5128
5392
  PromptConflictError: () => PromptConflictError,
5393
+ RESTART_LIMIT: () => 5,
5129
5394
  Service: () => Service$5,
5130
5395
  layer: () => layer$10
5131
5396
  });
@@ -5144,6 +5409,7 @@ const layer$10 = Layer.effect(Service$5, Effect.gen(function* () {
5144
5409
  const inputs = yield* make$8;
5145
5410
  const execution = yield* Service$6;
5146
5411
  const isLifecycleConflict = Schema.is(LifecycleConflict);
5412
+ const wake = (sessionId) => sessions.markRunning(sessionId).pipe(Effect.andThen(execution.wake(sessionId)));
5147
5413
  /**
5148
5414
  * Uninterruptible end to end: a client that disconnects mid-call must not
5149
5415
  * leave a prompt half-admitted. Everything durable happens inside `admit`,
@@ -5174,10 +5440,10 @@ const layer$10 = Layer.effect(Service$5, Effect.gen(function* () {
5174
5440
  });
5175
5441
  return admitted;
5176
5442
  });
5177
- const prompt = Effect.fn("Control.prompt")((input) => Effect.uninterruptible(admit(input).pipe(Effect.tap((admitted) => execution.wake(admitted.sessionId)))));
5443
+ const prompt = Effect.fn("Control.prompt")((input) => Effect.uninterruptible(admit(input).pipe(Effect.tap((admitted) => wake(admitted.sessionId)))));
5178
5444
  const run = Effect.fn("Control.run")((input) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
5179
5445
  const admitted = yield* admit(input);
5180
- yield* execution.wake(admitted.sessionId);
5446
+ yield* wake(admitted.sessionId);
5181
5447
  yield* restore(execution.awaitIdle(admitted.sessionId));
5182
5448
  return admitted;
5183
5449
  })));
@@ -5189,16 +5455,40 @@ const layer$10 = Layer.effect(Service$5, Effect.gen(function* () {
5189
5455
  const resume = Effect.fn("Control.resume")(function* (sessionId) {
5190
5456
  const session = yield* sessions.get(sessionId);
5191
5457
  if (Option.isNone(session)) return yield* new SessionNotFoundError({ sessionId });
5458
+ yield* sessions.markRunning(sessionId);
5192
5459
  yield* execution.resume(sessionId);
5193
5460
  });
5194
5461
  const interrupt = Effect.fn("Control.interrupt")((sessionId, options) => Effect.uninterruptible(execution.interrupt(sessionId, options?.reason ?? "user", options)));
5462
+ const update = Effect.fn("Control.update")((sessionId, id, patch) => Effect.uninterruptible(Effect.gen(function* () {
5463
+ const updated = yield* inputs.update(sessionId, id, patch);
5464
+ if (Option.isNone(updated)) return false;
5465
+ if (patch.delivery === "steer") yield* wake(sessionId);
5466
+ return true;
5467
+ })));
5468
+ const resumeUnfinished = Effect.gen(function* () {
5469
+ const running = yield* execution.active;
5470
+ const unfinished = (yield* sessions.unfinished).filter((row) => !running.has(row.id));
5471
+ yield* Effect.forEach(unfinished, Effect.fnUntraced(function* (row) {
5472
+ if ((yield* execution.active).has(row.id)) return;
5473
+ const restarts = yield* sessions.claimRestart(row.id);
5474
+ if (Option.isNone(restarts) || restarts.value >= 5) return;
5475
+ yield* execution.recover(row.id).pipe(Effect.tapCause((cause) => Effect.logWarning("Resuming an unfinished session failed", cause).pipe(Effect.annotateLogs({ sessionId: row.id }))), Effect.ignore);
5476
+ }), {
5477
+ concurrency: "unbounded",
5478
+ discard: true
5479
+ });
5480
+ }).pipe(Effect.withSpan("Control.resumeUnfinished"));
5195
5481
  return Service$5.of({
5196
5482
  prompt,
5197
5483
  run,
5198
5484
  wait,
5199
5485
  resume,
5200
5486
  interrupt,
5201
- active: execution.active
5487
+ active: execution.active,
5488
+ resumeUnfinished,
5489
+ pending: inputs.pending,
5490
+ cancel: inputs.cancel,
5491
+ update
5202
5492
  });
5203
5493
  }));
5204
5494
  //#endregion
@@ -5312,7 +5602,7 @@ var settings_exports = /* @__PURE__ */ __exportAll({
5312
5602
  layer: () => layer$8,
5313
5603
  load: () => load$3,
5314
5604
  modules: () => modules,
5315
- parse: () => parse$4,
5605
+ parse: () => parse$5,
5316
5606
  paths: () => paths,
5317
5607
  projectRoot: () => projectRoot,
5318
5608
  userConfigDir: () => userConfigDir
@@ -5401,7 +5691,7 @@ const decodeFormatter = SchemaIssue.makeFormatterStandardSchemaV1({ leafHook: (i
5401
5691
  const message = "annotations" in issue ? issue.annotations?.message : void 0;
5402
5692
  return typeof message === "string" ? message.replaceAll(/\s*\n\s*/g, " ") : "invalid value";
5403
5693
  } });
5404
- const parse$4 = Effect.fn("Settings.parse")(function* (path, source) {
5694
+ const parse$5 = Effect.fn("Settings.parse")(function* (path, source) {
5405
5695
  const errors = [];
5406
5696
  const json = parse(source, errors, {
5407
5697
  allowTrailingComma: true,
@@ -5449,7 +5739,7 @@ const attempt = (path) => fileSystem.readFileString(path).pipe(Effect.mapError((
5449
5739
  path,
5450
5740
  reason: "read",
5451
5741
  detail: error.reason._tag
5452
- })), Effect.flatMap((source) => parse$4(path, source)));
5742
+ })), Effect.flatMap((source) => parse$5(path, source)));
5453
5743
  /**
5454
5744
  * One read of every layer. Exported for the harness constructor, which needs the plugin
5455
5745
  * selection before any layer is built; everything else goes through `Service`.
@@ -5563,8 +5853,13 @@ const stale = Effect.fn("ModelCatalog.stale")(function* (path) {
5563
5853
  const ttl = yield* ttlConfig;
5564
5854
  return (yield* Clock.currentTimeMillis) - mtime.value >= Duration.toMillis(ttl);
5565
5855
  });
5856
+ /** OpenRouter's catalog is narrowed to what the host's key may use, when one is set. */
5857
+ const openRouterKey = () => getEnvApiKey({ env: ["OPENROUTER_API_KEY"] });
5566
5858
  const download$1 = (path) => Effect.tryPromise({
5567
- try: () => generateModels({ path }),
5859
+ try: () => downloadCatalog({
5860
+ path,
5861
+ openRouterKey: openRouterKey()
5862
+ }),
5568
5863
  catch: (cause) => new RefreshError({
5569
5864
  path,
5570
5865
  detail: cause instanceof Error ? cause.message : String(cause)
@@ -5585,8 +5880,7 @@ const refresh$1 = Effect.fn("ModelCatalog.refresh")(function* (home, options = {
5585
5880
  });
5586
5881
  /**
5587
5882
  * 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.
5883
+ * download leaves the old file in place; without one, aikit's bundled baseline is read.
5590
5884
  */
5591
5885
  const check$1 = Effect.fn("ModelCatalog.check")(function* ({ path, pinned }) {
5592
5886
  if (pinned || !(yield* stale(path))) return;
@@ -5622,7 +5916,7 @@ const loadError = (cause) => {
5622
5916
  if (typeof cause !== "object" || cause === null || !("name" in cause) || cause.name !== "ModelCatalogLoadError") return;
5623
5917
  if (!("data" in cause) || typeof cause.data !== "object" || cause.data === null) return void 0;
5624
5918
  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;
5919
+ 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
5920
  return new ModelCatalogError({
5627
5921
  path: data.path,
5628
5922
  reason: data.reason,
@@ -5755,6 +6049,22 @@ const defaultPromptPlugin = (0, plugin_exports$1.define)({
5755
6049
  })
5756
6050
  });
5757
6051
  //#endregion
6052
+ //#region src/plugin/builtin/prompt/walk.ts
6053
+ const stripBom$1 = (raw) => raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
6054
+ /** `directory` and every ancestor up to the sandbox root, closest first. */
6055
+ const ancestors = (directory) => {
6056
+ const chain = [];
6057
+ for (let current = directory;; current = posix.dirname(current)) {
6058
+ chain.push(current);
6059
+ if (posix.dirname(current) === current) return chain;
6060
+ }
6061
+ };
6062
+ /**
6063
+ * Which of `paths` exist as the requested kind (`entry` takes either). Absence stays silent; any other failure
6064
+ * (permissions, a remote blip) is logged so a dropped entry is not a mystery, then counts as absent.
6065
+ */
6066
+ 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]))));
6067
+ //#endregion
5758
6068
  //#region src/plugin/builtin/prompt/instruction.ts
5759
6069
  /** First readable match per directory wins. */
5760
6070
  const names = [
@@ -5766,22 +6076,9 @@ const names = [
5766
6076
  "CLAUDE.MD"
5767
6077
  ];
5768
6078
  const content = (raw) => {
5769
- const text = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
6079
+ const text = stripBom$1(raw);
5770
6080
  return text.trim().length === 0 ? void 0 : text;
5771
6081
  };
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
6082
  /** A file that stats but does not read falls through to the next candidate. */
5786
6083
  const first = Effect.fnUntraced(function* (fs, directory, files) {
5787
6084
  for (const name of names) {
@@ -5809,9 +6106,9 @@ const shadowing = (location) => {
5809
6106
  return location.space.location.startsWith(`${main}/`) ? main : void 0;
5810
6107
  };
5811
6108
  /** 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) {
6109
+ const discover$1 = Effect.fnUntraced(function* (fs, location) {
5813
6110
  const chain = ancestors(location.directory);
5814
- const files = yield* present$1(fs, chain.flatMap((directory) => names.map((name) => posix.join(directory, name))));
6111
+ const files = yield* present$1(fs, chain.flatMap((directory) => names.map((name) => posix.join(directory, name))), "file", "instruction file");
5815
6112
  const found = yield* Effect.forEach(chain, (directory) => first(fs, directory, files), { concurrency: "unbounded" });
5816
6113
  const main = shadowing(location);
5817
6114
  const worktree = main === void 0 ? void 0 : found[chain.indexOf(location.space.location)];
@@ -5827,7 +6124,7 @@ const instructionPlugin = (0, plugin_exports$1.define)({
5827
6124
  kind: "prompt",
5828
6125
  setup: Effect.fn("InstructionPlugin.setup")(function* (ctx) {
5829
6126
  const fs = yield* io_exports.SandboxIO.FileSystem;
5830
- const files = (yield* discover(fs, ctx.location)).filter((file) => file.content !== void 0);
6127
+ const files = (yield* discover$1(fs, ctx.location)).filter((file) => file.content !== void 0);
5831
6128
  if (files.length === 0) return;
5832
6129
  const prompt = ctx.plugin.prompt;
5833
6130
  prompt.sections.append(Section.ProjectContext, "Project-Specific Instructions & Guidelines:");
@@ -5835,6 +6132,236 @@ const instructionPlugin = (0, plugin_exports$1.define)({
5835
6132
  })
5836
6133
  });
5837
6134
  //#endregion
6135
+ //#region src/plugin/builtin/prompt/skill.ts
6136
+ /** Max name length per Agent Skills spec. */
6137
+ const MAX_NAME_LENGTH = 64;
6138
+ /** Max description length per Agent Skills spec. */
6139
+ const MAX_DESCRIPTION_LENGTH = 1024;
6140
+ const IGNORE_FILE_NAMES = [
6141
+ ".gitignore",
6142
+ ".ignore",
6143
+ ".fdignore"
6144
+ ];
6145
+ const normalizeNewlines = (value) => value.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
6146
+ const extractFrontmatter = (content) => {
6147
+ const normalized = normalizeNewlines(stripBom$1(content));
6148
+ if (!normalized.startsWith("---")) return void 0;
6149
+ const endIndex = normalized.indexOf("\n---", 3);
6150
+ if (endIndex === -1) return void 0;
6151
+ return normalized.slice(4, endIndex);
6152
+ };
6153
+ const parseFrontmatter = (raw) => {
6154
+ const yamlString = extractFrontmatter(raw);
6155
+ if (yamlString === void 0) return { frontmatter: {} };
6156
+ try {
6157
+ const parsed = parse$1(yamlString);
6158
+ return { frontmatter: typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {} };
6159
+ } catch (error) {
6160
+ return {
6161
+ frontmatter: {},
6162
+ error
6163
+ };
6164
+ }
6165
+ };
6166
+ const validateName = (name) => {
6167
+ const errors = [];
6168
+ if (name.length > MAX_NAME_LENGTH) errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
6169
+ if (!/^[a-z0-9-]+$/.test(name)) errors.push("name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)");
6170
+ if (name.startsWith("-") || name.endsWith("-")) errors.push("name must not start or end with a hyphen");
6171
+ if (name.includes("--")) errors.push("name must not contain consecutive hyphens");
6172
+ return errors;
6173
+ };
6174
+ const validateDescription = (description) => {
6175
+ const errors = [];
6176
+ if (typeof description !== "string" || description.trim() === "") errors.push("description is required");
6177
+ else if (description.length > MAX_DESCRIPTION_LENGTH) errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
6178
+ return errors;
6179
+ };
6180
+ const prefixIgnorePattern = (line, prefix) => {
6181
+ const trimmed = line.trim();
6182
+ if (!trimmed) return void 0;
6183
+ if (trimmed.startsWith("#") && !trimmed.startsWith("\\#")) return void 0;
6184
+ let pattern = line;
6185
+ let negated = false;
6186
+ if (pattern.startsWith("!")) {
6187
+ negated = true;
6188
+ pattern = pattern.slice(1);
6189
+ } else if (pattern.startsWith("\\!")) pattern = pattern.slice(1);
6190
+ if (pattern.startsWith("/")) pattern = pattern.slice(1);
6191
+ const prefixed = prefix ? `${prefix}${pattern}` : pattern;
6192
+ return negated ? `!${prefixed}` : prefixed;
6193
+ };
6194
+ const addIgnoreRules = Effect.fnUntraced(function* (fs, ig, dir, root, entryNames) {
6195
+ const relativeDir = posix.relative(root, dir);
6196
+ const prefix = relativeDir && relativeDir !== "." ? `${relativeDir}/` : "";
6197
+ const presentIgnoreFiles = IGNORE_FILE_NAMES.filter((name) => entryNames.has(name));
6198
+ if (presentIgnoreFiles.length === 0) return;
6199
+ const contents = yield* Effect.forEach(presentIgnoreFiles, (filename) => fs.readFile(posix.join(dir, filename)).pipe(Effect.option), { concurrency: "unbounded" });
6200
+ for (const content of contents) {
6201
+ if (Option.isNone(content)) continue;
6202
+ const patterns = content.value.split(/\r?\n/).map((line) => prefixIgnorePattern(line, prefix)).filter((line) => line !== void 0);
6203
+ if (patterns.length > 0) ig.add(patterns);
6204
+ }
6205
+ });
6206
+ const loadSkillFromFile = Effect.fnUntraced(function* (fs, filePath) {
6207
+ const isDeclaredSkill = posix.basename(filePath) === "SKILL.md";
6208
+ const raw = yield* fs.readFile(filePath).pipe(Effect.tapError((error) => Effect.logWarning(`could not read skill file ${filePath}`, error.cause)), Effect.option);
6209
+ if (Option.isNone(raw)) return void 0;
6210
+ const { frontmatter, error } = parseFrontmatter(raw.value);
6211
+ if (error !== void 0) {
6212
+ if (isDeclaredSkill) yield* Effect.logWarning(`could not parse skill file ${filePath}`, error);
6213
+ return;
6214
+ }
6215
+ const description = frontmatter.description;
6216
+ const hasDescription = typeof description === "string" && description.trim() !== "";
6217
+ if (!isDeclaredSkill && !hasDescription) return void 0;
6218
+ const skillDir = posix.dirname(filePath);
6219
+ const parentDirName = posix.basename(skillDir);
6220
+ for (const error of validateDescription(description)) yield* Effect.logWarning(`skill ${filePath}: ${error}`);
6221
+ const name = (typeof frontmatter.name === "string" ? frontmatter.name : void 0) || parentDirName;
6222
+ for (const error of validateName(name)) yield* Effect.logWarning(`skill ${filePath}: ${error}`);
6223
+ if (!hasDescription) return void 0;
6224
+ return {
6225
+ name,
6226
+ description,
6227
+ filePath,
6228
+ baseDir: skillDir,
6229
+ disableModelInvocation: frontmatter["disable-model-invocation"] === true
6230
+ };
6231
+ });
6232
+ const scanDir = (fs, dir, mode, root, ig, visited) => Effect.gen(function* () {
6233
+ const realDir = yield* realpath(fs, dir);
6234
+ if (visited.has(realDir)) return [];
6235
+ const nextVisited = /* @__PURE__ */ new Set([...visited, realDir]);
6236
+ 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);
6237
+ if (Option.isNone(listed)) return [];
6238
+ const entries = [...listed.value].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
6239
+ const entrySet = new Set(entries);
6240
+ yield* addIgnoreRules(fs, ig, dir, root, entrySet);
6241
+ if (entrySet.has("SKILL.md")) {
6242
+ const fullPath = posix.join(dir, "SKILL.md");
6243
+ const relPath = posix.relative(root, fullPath);
6244
+ if ((yield* isFile(fs, fullPath)) && !ig.ignores(relPath)) {
6245
+ const skill = yield* loadSkillFromFile(fs, fullPath);
6246
+ return skill === void 0 ? [] : [skill];
6247
+ }
6248
+ }
6249
+ const visible = entries.filter((name) => !name.startsWith(".") && name !== "node_modules");
6250
+ const statted = yield* Effect.forEach(visible, (name) => {
6251
+ const fullPath = posix.join(dir, name);
6252
+ return fs.stat(fullPath).pipe(Effect.map((stat) => ({
6253
+ name,
6254
+ fullPath,
6255
+ stat
6256
+ })), Effect.option);
6257
+ }, { concurrency: "unbounded" });
6258
+ const skills = [];
6259
+ for (const item of statted) {
6260
+ if (Option.isNone(item)) continue;
6261
+ const { name, fullPath, stat } = item.value;
6262
+ const relPath = posix.relative(root, fullPath);
6263
+ if (ig.ignores(stat.isDirectory ? `${relPath}/` : relPath)) continue;
6264
+ if (stat.isDirectory) {
6265
+ const nested = yield* scanDir(fs, fullPath, mode, root, ig, nextVisited);
6266
+ skills.push(...nested);
6267
+ continue;
6268
+ }
6269
+ if (stat.isFile && name.endsWith(".md") && (mode === "codework" && dir === root || mode === "agents" && dir !== root)) {
6270
+ const skill = yield* loadSkillFromFile(fs, fullPath);
6271
+ if (skill !== void 0) skills.push(skill);
6272
+ }
6273
+ }
6274
+ return skills;
6275
+ });
6276
+ /**
6277
+ * Load skills from a single directory in the sandbox.
6278
+ *
6279
+ * Discovery rules:
6280
+ * - If a directory contains `SKILL.md`, treat it as a skill root and do not recurse further.
6281
+ * - In `"codework"` mode, direct `.md` children in the root are also loaded; in `"agents"` mode,
6282
+ * `.md` files in subdirectories are loaded while root `.md` files are ignored.
6283
+ * - Recurse into subdirectories (skipping dotfiles, `node_modules`, and ignored paths).
6284
+ */
6285
+ const loadSkillsFromDir = (fs, dir, mode) => scanDir(fs, dir, mode, dir, ignore(), /* @__PURE__ */ new Set());
6286
+ /**
6287
+ * Discover skills inside the sandbox: `.codework/skills` at the cwd, then
6288
+ * `.agents/skills` from the cwd up to the git root (the closest ancestor holding `.git`), or up to
6289
+ * the sandbox root outside a repository. Earlier skills win name collisions.
6290
+ */
6291
+ const discover = Effect.fnUntraced(function* (fs, location) {
6292
+ const chain = ancestors(location.directory);
6293
+ const own = posix.join(location.directory, appConfigDir, "skills");
6294
+ const agents = chain.map((directory) => posix.join(directory, ".agents", "skills"));
6295
+ 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" });
6296
+ const root = chain.findIndex((directory) => gits.has(posix.join(directory, ".git")));
6297
+ const walked = root === -1 ? agents : agents.slice(0, root + 1);
6298
+ const sources = [{
6299
+ dir: own,
6300
+ mode: "codework"
6301
+ }, ...walked.map((dir) => ({
6302
+ dir,
6303
+ mode: "agents"
6304
+ }))].filter(({ dir }) => dirs.has(dir));
6305
+ const found = yield* Effect.forEach(sources, ({ dir, mode }) => loadSkillsFromDir(fs, dir, mode), { concurrency: "unbounded" });
6306
+ const skillMap = /* @__PURE__ */ new Map();
6307
+ const realPathSet = /* @__PURE__ */ new Set();
6308
+ for (const skill of found.flat()) {
6309
+ const realPath = yield* realpath(fs, skill.filePath);
6310
+ if (realPathSet.has(realPath)) continue;
6311
+ const existing = skillMap.get(skill.name);
6312
+ if (existing !== void 0) yield* Effect.logWarning(`skill ${skill.filePath}: name "${skill.name}" collision with ${existing.filePath}`);
6313
+ else {
6314
+ skillMap.set(skill.name, skill);
6315
+ realPathSet.add(realPath);
6316
+ }
6317
+ }
6318
+ return Array.from(skillMap.values());
6319
+ });
6320
+ const escapeXml = (str) => str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
6321
+ /**
6322
+ * Format skills for inclusion in `Section.Skills` per the Agent Skills specification.
6323
+ * Skills with `disableModelInvocation: true` are excluded from the prompt.
6324
+ */
6325
+ const formatSkillsForPrompt = (skills, fileReadTool = "read") => {
6326
+ const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
6327
+ if (visibleSkills.length === 0) return "";
6328
+ const lines = [
6329
+ "The following skills provide specialized instructions for specific tasks.",
6330
+ 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.",
6331
+ "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.",
6332
+ "",
6333
+ "<available_skills>"
6334
+ ];
6335
+ for (const skill of visibleSkills) {
6336
+ lines.push(" <skill>");
6337
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
6338
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
6339
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
6340
+ lines.push(" </skill>");
6341
+ }
6342
+ lines.push("</available_skills>");
6343
+ return lines.join("\n");
6344
+ };
6345
+ const readers = ["read", "bash"];
6346
+ const skillPlugin = (0, plugin_exports$1.define)({
6347
+ id: "codework.prompt.skill",
6348
+ kind: "prompt",
6349
+ setup: Effect.fn("SkillPlugin.setup")(function* (ctx) {
6350
+ const tools = ctx.plugin.tools;
6351
+ const visible = (name) => {
6352
+ const tool = tools.get(name);
6353
+ return tool !== void 0 && tool.promptSnippet !== void 0 && tool.promptSnippet.length > 0;
6354
+ };
6355
+ const fileReadTool = readers.find(visible) ?? (readers.some((name) => tools.has(name)) ? "indirect" : void 0);
6356
+ if (fileReadTool === void 0) return;
6357
+ const fs = yield* io_exports.SandboxIO.FileSystem;
6358
+ const skills = yield* discover(fs, ctx.location);
6359
+ const formatted = formatSkillsForPrompt(skills, fileReadTool);
6360
+ if (formatted.length === 0) return;
6361
+ ctx.plugin.prompt.sections.append(Section.Skills, formatted);
6362
+ })
6363
+ });
6364
+ //#endregion
5838
6365
  //#region src/tool/truncate.ts
5839
6366
  /**
5840
6367
  * Shared truncation utilities for tool outputs.
@@ -6146,8 +6673,10 @@ const windowBytes = (limits) => limits.maxBytes * 2;
6146
6673
  */
6147
6674
  const script = (command, file, limits, pipeline, report) => [
6148
6675
  `f=${(0, shell_exports.quote)(file)}`,
6676
+ `c=${(0, shell_exports.quote)(command)}`,
6677
+ "if command -v bash >/dev/null 2>&1; then s=bash; else s=sh; fi",
6149
6678
  "run() (",
6150
- command,
6679
+ " if [ \"$s\" = bash ]; then bash -c \"$c\"; else eval \"$c\"; fi",
6151
6680
  ")",
6152
6681
  `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
6682
  " echo \"$$ $(ps -o pgid= -p $$ 2>/dev/null)\" > \"$f.pid\"",
@@ -6164,18 +6693,19 @@ const script = (command, file, limits, pipeline, report) => [
6164
6693
  `printf '%s\\n' ${(0, shell_exports.quote)(unsaved(file))}`,
6165
6694
  "run 2>&1"
6166
6695
  ].join("\n");
6167
- const header = new RegExp(`^${HEADER} (\\d+) (\\d+) ([01])(?: (\\d+))?$`, "m");
6168
- const parse$3 = (match) => ({
6696
+ const header = new RegExp(`^${HEADER} (\\d+) (\\d+) ([01]) (bash|sh)(?: (\\d+))?$`, "m");
6697
+ const parse$4 = (match) => ({
6169
6698
  bytes: Number(match[1]),
6170
6699
  newlines: Number(match[2]),
6171
- kept: match[3] === "1"
6700
+ kept: match[3] === "1",
6701
+ shell: match[4] === "sh" ? "sh" : "bash"
6172
6702
  });
6173
6703
  /**
6174
6704
  * For a backend that streams: all output still flows back as it is produced, and
6175
6705
  * a copy lands in the file. The report rides on stderr, the wrapper's own channel
6176
6706
  * since the command's is folded into stdout.
6177
6707
  */
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`]);
6708
+ 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
6709
  /** Read {@link streaming}'s stderr: the report, if it got that far, and anything else the wrapper said. */
6180
6710
  const report = (stderr) => {
6181
6711
  const match = header.exec(stderr);
@@ -6184,7 +6714,7 @@ const report = (stderr) => {
6184
6714
  rest: stderr
6185
6715
  };
6186
6716
  return {
6187
- report: parse$3(match),
6717
+ report: parse$4(match),
6188
6718
  rest: stderr.slice(0, match.index) + stderr.slice(match.index + match[0].length).replace(/^\n/, "")
6189
6719
  };
6190
6720
  };
@@ -6197,7 +6727,7 @@ const buffered = (command, file, limits) => {
6197
6727
  const tail = `tail -n ${limits.maxLines} "$f" | tail -c ${windowBytes(limits)}`;
6198
6728
  return script(command, file, limits, (source) => `{ ${source} | cat; } >> "$f"`, [
6199
6729
  `w=$(${tail} | wc -c | awk '{ print $1 }')`,
6200
- `printf '${HEADER} %s %s %s %s\\n' "$b" "$n" "$keep" "$w"`,
6730
+ `printf '${HEADER} %s %s %s %s %s\\n' "$b" "$n" "$keep" "$s" "$w"`,
6201
6731
  tail
6202
6732
  ]);
6203
6733
  };
@@ -6205,11 +6735,11 @@ const buffered = (command, file, limits) => {
6205
6735
  const window = (stdout) => {
6206
6736
  const newline = stdout.indexOf("\n");
6207
6737
  const match = header.exec(newline === -1 ? stdout : stdout.slice(0, newline));
6208
- if (match === null || match[4] === void 0) return void 0;
6738
+ if (match === null || match[5] === void 0) return void 0;
6209
6739
  return {
6210
- ...parse$3(match),
6740
+ ...parse$4(match),
6211
6741
  text: stdout.slice(newline + 1),
6212
- textBytes: Number(match[4])
6742
+ textBytes: Number(match[5])
6213
6743
  };
6214
6744
  };
6215
6745
  /**
@@ -6357,7 +6887,9 @@ const BashSuccess = Schema.Struct({
6357
6887
  /** Non-zero exit — expected, model-visible. Carries the same shape as success. */
6358
6888
  var BashFailed = class extends Schema.TaggedError()("BashFailed", {
6359
6889
  ...outputFields,
6360
- exitCode: Schema.Finite
6890
+ exitCode: Schema.Finite,
6891
+ /** The shell that ran the command, when the sandbox reported it. */
6892
+ shell: Schema.optional(Schema.Literals(["bash", "sh"]))
6361
6893
  }) {};
6362
6894
  /** Deadline exceeded — expected, model-visible. Carries the partial output produced so far. */
6363
6895
  var BashTimedOut = class extends Schema.TaggedError()("BashTimedOut", {
@@ -6367,7 +6899,7 @@ var BashTimedOut = class extends Schema.TaggedError()("BashTimedOut", {
6367
6899
  const BashFailure = Schema.Union([BashFailed, BashTimedOut]);
6368
6900
  /** The reason for a failure, after any output: the model sees nothing else of `details`. */
6369
6901
  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}`;
6902
+ 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
6903
  return failure.output ? `${failure.output.replace(/\n$/, "")}\n\n${status}` : status;
6372
6904
  };
6373
6905
  const bashDef = tool_exports.define({
@@ -6421,6 +6953,7 @@ const present = (snap, lastLineBytes, saved) => {
6421
6953
  };
6422
6954
  };
6423
6955
  const utf8 = new TextEncoder();
6956
+ const shellOf = (report) => report !== void 0 ? { shell: report.shell } : {};
6424
6957
  /**
6425
6958
  * A command cut off by its deadline never reached the wrapper's own cleanup: kill
6426
6959
  * what is left of it and remove its side files, keeping the output file only when
@@ -6454,7 +6987,8 @@ const runBuffered = (shell, params, file) => Effect.gen(function* () {
6454
6987
  const presented = present(acc.snapshot(), acc.getLastLineBytes(), window$1?.kept === true ? file : void 0);
6455
6988
  if (result.exitCode !== 0) return yield* new BashFailed({
6456
6989
  exitCode: result.exitCode,
6457
- ...presented
6990
+ ...presented,
6991
+ ...shellOf(window$1)
6458
6992
  });
6459
6993
  return {
6460
6994
  exitCode: result.exitCode,
@@ -6512,7 +7046,8 @@ const runStreaming = (shell, stream, params, file) => Effect.gen(function* () {
6512
7046
  const code = yield* Ref.get(exitCode);
6513
7047
  if (code === null || code !== 0) return yield* new BashFailed({
6514
7048
  exitCode: code ?? -1,
6515
- ...presented
7049
+ ...presented,
7050
+ ...shellOf(report$1)
6516
7051
  });
6517
7052
  return {
6518
7053
  exitCode: code,
@@ -6895,7 +7430,7 @@ const editDef = tool_exports.define({
6895
7430
  })
6896
7431
  });
6897
7432
  const fail$4 = (message) => Effect.fail(new EditFailed({ message }));
6898
- /** Filesystem failures name the backend's error code, as Pi's do. */
7433
+ /** Filesystem failures name the backend's error code. */
6899
7434
  const accessFailure = (path) => (error) => {
6900
7435
  const code = error.cause?.code;
6901
7436
  return fail$4(`Could not edit file: ${path}. Error code: ${typeof code === "string" ? code : "unknown"}.`);
@@ -7282,7 +7817,7 @@ const pipeline = (argv, limit, errors) => [
7282
7817
  ].join("; ");
7283
7818
  const STATUS = /^\{"type":"rc","code":(\d+)\}$/;
7284
7819
  /** The pipeline's output: match events, ripgrep's status, and anything else, which is its stderr. */
7285
- const parse$2 = (stdout, stderr) => {
7820
+ const parse$3 = (stdout, stderr) => {
7286
7821
  const matches = [];
7287
7822
  const errors = [];
7288
7823
  let status;
@@ -7366,7 +7901,7 @@ const grepHandler = (params) => Effect.gen(function* () {
7366
7901
  if (params.glob) argv.push("--glob", params.glob);
7367
7902
  argv.push("-e", params.pattern, searchPath);
7368
7903
  const result = yield* shell.exec(pipeline(argv, limit, errors));
7369
- const parsed = parse$2(result.stdout, result.stderr);
7904
+ const parsed = parse$3(result.stdout, result.stderr);
7370
7905
  return {
7371
7906
  ...parsed,
7372
7907
  exitCode: parsed.status ?? result.exitCode
@@ -7931,7 +8466,8 @@ const builtins = Object.freeze([
7931
8466
  findPlugin,
7932
8467
  lsPlugin,
7933
8468
  defaultPromptPlugin,
7934
- instructionPlugin
8469
+ instructionPlugin,
8470
+ skillPlugin
7935
8471
  ]);
7936
8472
  //#endregion
7937
8473
  //#region src/plugin/error.ts
@@ -8371,7 +8907,7 @@ const isPath = (reference) => reference.startsWith("file:") || reference.startsW
8371
8907
  * - a **CLI argument** anchors to the process's startup directory, because that is where the
8372
8908
  * person typed it.
8373
8909
  */
8374
- const parse$1 = (reference, from) => Effect.gen(function* () {
8910
+ const parse$2 = (reference, from) => Effect.gen(function* () {
8375
8911
  const fail = (reason, message) => new SourceError({
8376
8912
  reason,
8377
8913
  reference,
@@ -9047,7 +9583,7 @@ const inspect = Effect.fn("PluginLoader.inspect")(function* (reference, options)
9047
9583
  reference,
9048
9584
  file: options.file
9049
9585
  };
9050
- const target = yield* parse$1(reference, options.hostDir);
9586
+ const target = yield* parse$2(reference, options.hostDir);
9051
9587
  const loaded = yield* load$2(target, origin, options);
9052
9588
  return {
9053
9589
  id: loaded.plugin.id,
@@ -9073,7 +9609,7 @@ const load$2 = Effect.fn("PluginLoader.load")(function* (target, origin, options
9073
9609
  * are one package, and `./plugins/x.ts` is the file it resolves to from the directory that
9074
9610
  * declared it. Pure -- nothing is installed, imported or read.
9075
9611
  */
9076
- const canonical = (reference, hostDir) => Effect.map(parse$1(reference, hostDir), canonical$1);
9612
+ const canonical = (reference, hostDir) => Effect.map(parse$2(reference, hostDir), canonical$1);
9077
9613
  //#endregion
9078
9614
  //#region src/plugin/catalog.ts
9079
9615
  /**
@@ -9135,7 +9671,7 @@ const load$1 = Effect.fn("PluginCatalog.load")(function* (references, options) {
9135
9671
  remember(plugin, []);
9136
9672
  continue;
9137
9673
  }
9138
- const target = yield* parse$1(reference, options.hostDir);
9674
+ const target = yield* parse$2(reference, options.hostDir);
9139
9675
  const key = target.kind === "local" ? target.path : target.spec;
9140
9676
  const already = seen.get(key);
9141
9677
  if (already !== void 0) {
@@ -9391,12 +9927,18 @@ const layer$6 = Layer.effect(Service$6, Effect.gen(function* () {
9391
9927
  const runner = yield* Service$7;
9392
9928
  const events = yield* Service$16;
9393
9929
  const reportLifecycle = (sessionId, publish) => publish.pipe(Effect.tapCause((cause) => Cause.hasInterruptsOnly(cause) ? Effect.void : Effect.logError("Failed to publish session execution lifecycle", cause).pipe(Effect.annotateLogs({ sessionId }))), Effect.ignore);
9930
+ const recovering = /* @__PURE__ */ new Set();
9931
+ const stoppedAt = /* @__PURE__ */ new Map();
9394
9932
  const coordinator = yield* make$7({
9395
- started: (sessionId) => reportLifecycle(sessionId, DateTime.now.pipe(Effect.andThen((timestamp) => events.publish(ExecutionStarted, {
9933
+ started: (sessionId) => reportLifecycle(sessionId, (recovering.has(sessionId) ? Effect.void : store.markRunning(sessionId)).pipe(Effect.andThen(DateTime.now), Effect.andThen((timestamp) => events.publish(ExecutionStarted, {
9396
9934
  sessionId,
9397
9935
  timestamp
9398
9936
  })))),
9399
9937
  settled: (sessionId, exit, reason) => reportLifecycle(sessionId, Effect.gen(function* () {
9938
+ const interrupted = Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause);
9939
+ const stop = stoppedAt.get(sessionId);
9940
+ stoppedAt.delete(sessionId);
9941
+ yield* store.endExecution(sessionId, !interrupted ? "finished" : reason === "user" ? "stopped" : "closed", stop);
9400
9942
  const timestamp = yield* DateTime.now;
9401
9943
  if (Exit.isSuccess(exit)) return yield* events.publish(ExecutionSucceeded, {
9402
9944
  sessionId,
@@ -9416,6 +9958,8 @@ const layer$6 = Layer.effect(Service$6, Effect.gen(function* () {
9416
9958
  drain: Effect.fnUntraced(function* (sessionId, force) {
9417
9959
  const session = yield* store.get(sessionId);
9418
9960
  if (Option.isNone(session)) return yield* new SessionNotFoundError({ sessionId });
9961
+ const recovered = recovering.delete(sessionId);
9962
+ if (recovered && Option.isNone(session.value.execution)) return;
9419
9963
  const space = yield* store.space(sessionId);
9420
9964
  if (Option.isNone(space)) return yield* new SessionLinkedSpaceNotFoundError({ sessionId });
9421
9965
  const instanceId = space.value.env;
@@ -9436,15 +9980,30 @@ const layer$6 = Layer.effect(Service$6, Effect.gen(function* () {
9436
9980
  })));
9437
9981
  return yield* runner.run({
9438
9982
  sessionId,
9439
- force
9983
+ force,
9984
+ recovered
9440
9985
  }).pipe(Effect.provideContext(Context.merge(locationContext, mountContext)));
9441
9986
  }).pipe(Effect.scoped, Effect.tapCause((cause) => Cause.hasDies(cause) ? Effect.logError("defect", cause).pipe(Effect.annotateLogs({ sessionId })) : Effect.void));
9442
9987
  })
9443
9988
  });
9989
+ const interrupt = (sessionId, reason, options) => Effect.gen(function* () {
9990
+ const recorded = reason === "user" && !stoppedAt.has(sessionId);
9991
+ if (recorded) stoppedAt.set(sessionId, yield* events.latestSequence(sessionId));
9992
+ const accepted = yield* coordinator.interrupt(sessionId, reason, options);
9993
+ if (recorded && !accepted) stoppedAt.delete(sessionId);
9994
+ return accepted;
9995
+ });
9996
+ const recover = (sessionId) => Effect.suspend(() => {
9997
+ recovering.add(sessionId);
9998
+ return coordinator.run(sessionId);
9999
+ }).pipe(Effect.ensuring(Effect.sync(() => {
10000
+ recovering.delete(sessionId);
10001
+ })));
9444
10002
  return Service$6.of({
9445
10003
  active: coordinator.active,
9446
- interrupt: coordinator.interrupt,
10004
+ interrupt,
9447
10005
  resume: coordinator.run,
10006
+ recover,
9448
10007
  wake: coordinator.wake,
9449
10008
  awaitIdle: coordinator.awaitIdle
9450
10009
  });
@@ -10970,12 +11529,13 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
10970
11529
  }));
10971
11530
  return interruptedCause;
10972
11531
  });
10973
- const runTurnAttempt = Effect.fn("Loop.runTurnAttempt", { level: "Info" })(function* (promotion, snapshot) {
11532
+ const runTurnAttempt = Effect.fn("Loop.runTurnAttempt", { level: "Info" })(function* (promotion, snapshot, continuing) {
10974
11533
  const sessionId = snapshot.sessionId;
10975
11534
  if (promotion !== void 0) {
10976
11535
  const cutoff = yield* events.latestSequence(sessionId);
10977
- if (promotion === "followUp") yield* inputs.promoteFollowUp(sessionId);
10978
- yield* inputs.promoteSteers(sessionId, cutoff);
11536
+ const followUp = promotion === "followUp" && (yield* inputs.promoteFollowUp(sessionId));
11537
+ const steers = yield* inputs.promoteSteers(sessionId, cutoff);
11538
+ if (!continuing && !followUp && steers === 0) return { needsContinuation: false };
10979
11539
  }
10980
11540
  const assembled = yield* context.assemble(sessionId);
10981
11541
  const leaf = assembled.lastAssistant?.entryId === assembled.leafEntryId ? assembled.lastAssistant.message : void 0;
@@ -11074,8 +11634,9 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
11074
11634
  });
11075
11635
  }));
11076
11636
  });
11077
- const runTurn = Effect.fn("Loop.runTurn", { level: "Info" })(function* (promotion, snapshot) {
11078
- return yield* runTurnAttempt(promotion, snapshot);
11637
+ /** `continuing`: earlier work (tool results, a forced resume) needs a reply even when nothing is promoted. */
11638
+ const runTurn = Effect.fn("Loop.runTurn", { level: "Info" })(function* (promotion, snapshot, continuing) {
11639
+ return yield* runTurnAttempt(promotion, snapshot, continuing);
11079
11640
  });
11080
11641
  const run = Effect.fn("Loop.run", { level: "Info" })(function* (input) {
11081
11642
  yield* healDanglingDraft(input.sessionId);
@@ -11087,17 +11648,24 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
11087
11648
  "gen_ai.agent.name": "codework",
11088
11649
  "gen_ai.conversation.id": input.sessionId
11089
11650
  });
11090
- let promotion = hasSteer ? "steer" : hasFollowUp ? "followUp" : void 0;
11651
+ const cutOff = input.recovered === true && !hasSteer && hasFollowUp && (yield* context.assemble(input.sessionId).pipe(Effect.map((assembled) => {
11652
+ const leaf = assembled.lastAssistant?.entryId === assembled.leafEntryId ? assembled.lastAssistant.message : void 0;
11653
+ return assembled.messages.length > 0 && leaf?.stopReason !== "stop" && leaf?.stopReason !== "length";
11654
+ })));
11655
+ let promotion = hasSteer ? "steer" : hasFollowUp && !cutOff ? "followUp" : void 0;
11091
11656
  const drain = Effect.gen(function* () {
11092
11657
  let shouldRun = true;
11658
+ let continuing = input.force;
11093
11659
  while (shouldRun) {
11094
11660
  const snapshot = yield* state.snapshot(input.sessionId);
11095
11661
  let needsContinuation = true;
11096
11662
  while (needsContinuation) {
11097
- const result = yield* runTurn(promotion, snapshot);
11663
+ const result = yield* runTurn(promotion, snapshot, continuing);
11098
11664
  promotion = "steer";
11665
+ continuing = result.needsContinuation;
11099
11666
  needsContinuation = result.needsContinuation || (yield* inputs.hasPending(input.sessionId, "steer"));
11100
11667
  }
11668
+ continuing = false;
11101
11669
  shouldRun = yield* inputs.hasPending(input.sessionId, "followUp");
11102
11670
  promotion = shouldRun ? "followUp" : void 0;
11103
11671
  }
@@ -11219,6 +11787,20 @@ const layer$2 = Layer.effectDiscard(Effect.gen(function* () {
11219
11787
  timeCreated: event.data.timestamp
11220
11788
  });
11221
11789
  }));
11790
+ yield* events.project(PromptCancelled, (event) => Effect.gen(function* () {
11791
+ if (event.durable === void 0) return yield* Effect.die("PromptCancelled is missing its aggregate sequence");
11792
+ yield* input.projectCancelled({
11793
+ cancelledSeq: event.durable.seq,
11794
+ id: event.data.messageId,
11795
+ sessionId: event.data.sessionId
11796
+ });
11797
+ }));
11798
+ yield* events.project(PromptUpdated, (event) => input.projectUpdated({
11799
+ id: event.data.messageId,
11800
+ sessionId: event.data.sessionId,
11801
+ ...event.data.prompt === void 0 ? {} : { prompt: event.data.prompt },
11802
+ ...event.data.delivery === void 0 ? {} : { delivery: event.data.delivery }
11803
+ }));
11222
11804
  yield* events.project(Prompted, (event) => Effect.gen(function* () {
11223
11805
  if (event.durable === void 0) return yield* Effect.die("Prompted is missing its aggregate sequence");
11224
11806
  yield* input.projectPrompted({
@@ -11304,7 +11886,7 @@ const layer$2 = Layer.effectDiscard(Effect.gen(function* () {
11304
11886
  id: event.data.messageId,
11305
11887
  sessionId: event.data.sessionId,
11306
11888
  state: "committed"
11307
- }));
11889
+ }).pipe(Effect.andThen(sessions.resetRestarts(event.data.sessionId))));
11308
11890
  }));
11309
11891
  //#endregion
11310
11892
  //#region src/session/live.ts
@@ -11318,7 +11900,7 @@ var harness_exports = /* @__PURE__ */ __exportAll({
11318
11900
  const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
11319
11901
  const hostCwd = options.hostCwd ?? process.cwd();
11320
11902
  const paths = yield* resolve$4(options.home === void 0 ? {} : { home: hostPath.resolve(hostCwd, expandTilde(options.home, hostPath)) });
11321
- const global = layerWith(paths);
11903
+ const global = layerWith$1(paths);
11322
11904
  const settingsOptions = options.userConfigDir === void 0 ? {} : { userConfigDir: userConfigDir(options.userConfigDir, hostCwd) };
11323
11905
  const config = yield* load$3({
11324
11906
  ...settingsOptions,
@@ -11337,7 +11919,7 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
11337
11919
  * `hostDir` led to; only the process view falls back to `hostCwd`.
11338
11920
  */
11339
11921
  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()));
11922
+ 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
11923
  const resolveOnly = {
11342
11924
  ...catalogOptions,
11343
11925
  install: required
@@ -11579,13 +12161,26 @@ const makeHandle = Effect.fn("Session.makeHandle")(function* (id) {
11579
12161
  const space = Option.getOrUndefined(yield* sessions.space(id));
11580
12162
  const sandbox = space === void 0 || space.env === instance_exports.SandboxInstance.ID.local ? void 0 : Option.getOrUndefined(yield* sandboxes.get(space.env));
11581
12163
  const hostDir = Option.getOrUndefined(row.hostDir);
12164
+ const project = Option.getOrUndefined(yield* sessions.project(id));
12165
+ const config = Option.getOrUndefined(row.config);
12166
+ const model = config?.model;
12167
+ const thinkingLevel = config?.thinkingLevel;
12168
+ const parked = Option.isSome(row.execution) && row.restarts >= 5 && !(yield* control.active).has(id);
11582
12169
  return {
11583
12170
  id,
11584
12171
  title: row.title,
11585
12172
  directory: row.directory,
11586
12173
  ...hostDir === void 0 ? {} : { hostDir },
11587
12174
  hasHostLink: hostDir !== void 0,
11588
- ...sandbox === void 0 ? {} : { sandbox }
12175
+ ...sandbox === void 0 ? {} : { sandbox },
12176
+ ...project === void 0 ? {} : { project: {
12177
+ id: project.id,
12178
+ name: project.name
12179
+ } },
12180
+ updatedAt: DateTime.toEpochMillis(row.updatedAt),
12181
+ ...model === void 0 ? {} : { model },
12182
+ ...thinkingLevel === void 0 ? {} : { thinkingLevel },
12183
+ parked
11589
12184
  };
11590
12185
  }).pipe(Effect.withSpan("Session.info"));
11591
12186
  return {
@@ -11613,7 +12208,7 @@ const create$1 = Effect.fn("Session.create")(function* (input = {}) {
11613
12208
  const sandboxes = yield* Controller;
11614
12209
  const id = ID$1.create();
11615
12210
  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 });
12211
+ 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
12212
  yield* sessions.create({
11618
12213
  id,
11619
12214
  spaceId: location.space.id,
@@ -11727,7 +12322,7 @@ var plugin_exports = /* @__PURE__ */ __exportAll({
11727
12322
  inspect: () => inspect,
11728
12323
  lendResolve: () => lendResolve,
11729
12324
  localName: () => localName,
11730
- parse: () => parse$1,
12325
+ parse: () => parse$2,
11731
12326
  probe: () => probe,
11732
12327
  resolve: () => resolve$2,
11733
12328
  resolveCached: () => required,
@@ -11740,6 +12335,6 @@ var DateTimeUtcFromMillis = schema_exports$9.DateTimeUtcFromMillis;
11740
12335
  var EventSchema = schema_exports$7.EventSchema;
11741
12336
  var SandboxError = errors_exports.SandboxError;
11742
12337
  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 };
12338
+ 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
12339
 
11745
12340
  //# sourceMappingURL=effect.mjs.map