@codeworksh/harness 0.0.1-dev.20260925163524 → 0.1.0-dev.20261007133219

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
@@ -3,32 +3,34 @@ import { a as io_exports, i as errors_exports, n as shell_exports, o as instance
3
3
  import "./daytona-BAurx6Gp.mjs";
4
4
  import "./vercel-C0v5nh20.mjs";
5
5
  import { createRequire, registerHooks } from "node:module";
6
- import { Cause, Clock, Config, Context, Crypto, DateTime, Deferred, Duration, Effect, Encoding, Exit, Fiber, FiberSet, FileSystem, Layer, LayerMap, Option, Path, Predicate, PubSub, Queue, Ref, Result, Schedule, Schema, SchemaIssue, Scope, Semaphore, Stream, String as String$1 } from "effect";
6
+ import { Cause, Clock, Config, Context, Crypto, DateTime, Deferred, Duration, Effect, Exit, Fiber, FiberSet, FileSystem, Layer, LayerMap, Option, Path, Predicate, PubSub, Queue, Random, Ref, Result, Schedule, Schema, SchemaIssue, Scope, Semaphore, Stream, String as String$1 } from "effect";
7
+ import { Message, Model, getEnvApiKey, llm, stream } from "@codeworksh/aikit";
7
8
  import { MessageID as ID, SessionID as ID$1 } from "@codeworksh/plugin/ids";
8
- import { Migrator, SqlClient, SqlClient as SqlClient$1, SqlError, SqlSchema, SqlSchema as SqlSchema$1 } from "effect/unstable/sql";
9
+ import { Migrator, SqlClient, SqlClient as SqlClient$1, SqlError, SqlSchema, SqlSchema as SqlSchema$1 } from "effect/sql";
9
10
  import { uuidv7 } from "uuidv7";
10
11
  import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node";
11
12
  import * as os from "node:os";
12
13
  import { homedir, tmpdir } from "node:os";
13
14
  import { NodeChildProcessSpawner, NodeCrypto, NodeFileSystem, NodePath } from "@effect/platform-node";
14
- import { Model, VariantSchema } from "effect/unstable/schema";
15
- import { Message, Model as Model$1, llm, stream } from "@codeworksh/aikit";
15
+ import { Model as Model$1, VariantSchema } from "effect/schema";
16
+ import { Hex } from "effect/encoding";
16
17
  import { MemoryProvider, RealFSProvider, SqliteProvider, create } from "@platformatic/vfs";
17
18
  import fs, { statSync } from "node:fs";
18
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
19
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
19
20
  import { Buffer as Buffer$1 } from "node:buffer";
20
21
  import { Bash } from "just-bash";
21
- import { DirectoryNotFoundError, DirectoryNotFoundError as DirectoryNotFoundError$1, Info, Info as Info$1, NotDirectoryError, NotDirectoryError as NotDirectoryError$1, Ref as Ref$2, Service, Service as Service$8 } from "@codeworksh/plugin/location";
22
+ 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";
22
23
  import { generateModels } from "@codeworksh/aikit/modelgen";
24
+ import { JsonGitHubCopilotAuthStorage, getGitHubCopilotApiKey } from "@codeworksh/aikit/oauth/github/copilot";
25
+ import { JsonOpenAICodexAuthStorage, getOpenAICodexApiKey } from "@codeworksh/aikit/oauth/openai/codex";
26
+ import { parse, printParseErrorCode } from "jsonc-parser";
27
+ import * as Section from "@codeworksh/plugin/plugin/section";
23
28
  import { fileURLToPath, pathToFileURL } from "node:url";
24
29
  import path from "node:path";
25
30
  import { Script, constants } from "node:vm";
26
31
  import npa from "npm-package-arg";
27
32
  import * as AikitFailure from "@codeworksh/aikit/failure";
28
- import { JsonGitHubCopilotAuthStorage, getGitHubCopilotApiKey } from "@codeworksh/aikit/oauth/github/copilot";
29
- import { JsonOpenAICodexAuthStorage, getOpenAICodexApiKey } from "@codeworksh/aikit/oauth/openai/codex";
30
33
  import "@codeworksh/plugin/tool/error";
31
- import { parse, printParseErrorCode } from "jsonc-parser";
32
34
  //#region src/schema.ts
33
35
  var schema_exports$9 = /* @__PURE__ */ __exportAll({});
34
36
  import * as import__codeworksh_plugin_schema from "@codeworksh/plugin/schema";
@@ -38,15 +40,46 @@ __reExport(schema_exports$9, import__codeworksh_plugin_schema);
38
40
  var schema_exports$8 = /* @__PURE__ */ __exportAll({
39
41
  AssistantEnvelopeUsage: () => AssistantEnvelopeUsage,
40
42
  CompactionData: () => CompactionData,
43
+ Config: () => Config$1,
44
+ DEFAULT_TITLE: () => DEFAULT_TITLE,
41
45
  ID: () => ID$1,
42
46
  IDFromDb: () => IDFromDb,
43
47
  InterruptReason: () => InterruptReason,
44
48
  JsonObject: () => JsonObject,
45
49
  MessageEnvelopeIdentity: () => MessageEnvelopeIdentity,
46
50
  SessionSchema: () => schema_exports$8,
47
- Usage: () => Usage
51
+ TITLE_MAX_LENGTH: () => 48,
52
+ ThinkingLevel: () => ThinkingLevel,
53
+ Usage: () => Usage,
54
+ titleFrom: () => titleFrom
48
55
  });
49
56
  const IDFromDb = Schema.String.pipe(Schema.brand("Session.ID"));
57
+ /** The title of a session created without one; its first prompt replaces it. */
58
+ const DEFAULT_TITLE = "Session";
59
+ /**
60
+ * A title from a prompt: its first non-empty line, whitespace collapsed, cut on a word boundary
61
+ * to {@link TITLE_MAX_LENGTH} characters including the ellipsis. `undefined` for a blank prompt.
62
+ */
63
+ const titleFrom = (text) => {
64
+ const line = text.split("\n").map((one) => one.replace(/\s+/g, " ").trim()).find((one) => one.length > 0);
65
+ if (line === void 0 || line.length <= 48) return line;
66
+ const cut = line.slice(0, 47);
67
+ const space = cut.lastIndexOf(" ");
68
+ return `${(space > 0 ? cut.slice(0, space) : cut).trimEnd()}…`;
69
+ };
70
+ const ThinkingLevel = Schema.Literals(Object.values(Model.ThinkingLevelEnum));
71
+ /**
72
+ * The model and thinking level chosen for a session, over its settings. Every key is optional:
73
+ * an absent one means the session follows its settings. `session.config.changed` carries only the
74
+ * keys it changes, and the `session.config` column holds them merged.
75
+ */
76
+ const Config$1 = Schema.Struct({
77
+ model: (0, schema_exports$9.optional)(Schema.Struct({
78
+ provider: Schema.String,
79
+ id: Schema.String
80
+ })),
81
+ thinkingLevel: (0, schema_exports$9.optional)(ThinkingLevel)
82
+ });
50
83
  /**
51
84
  * Why an execution stopped. Supplied by whoever asked for the interruption --
52
85
  * a stop the user asked for and a stop the process imposed on shutdown are
@@ -101,10 +134,10 @@ var global_exports = /* @__PURE__ */ __exportAll({
101
134
  app: () => app,
102
135
  appConfigDir: () => appConfigDir,
103
136
  authFile: () => authFile,
104
- defaultLayer: () => defaultLayer$4,
137
+ defaultLayer: () => defaultLayer$2,
105
138
  homeConfig: () => homeConfig,
106
139
  layer: () => layer$27,
107
- layerWith: () => layerWith$1,
140
+ layerWith: () => layerWith,
108
141
  make: () => make$18,
109
142
  resolve: () => resolve$3
110
143
  });
@@ -150,8 +183,8 @@ const build = (input) => Effect.gen(function* () {
150
183
  return Service$17.of(paths);
151
184
  });
152
185
  const layer$27 = Layer.effect(Service$17, build({}));
153
- const defaultLayer$4 = layer$27;
154
- const layerWith$1 = (input) => Layer.effect(Service$17, build(input));
186
+ const defaultLayer$2 = layer$27;
187
+ const layerWith = (input) => Layer.effect(Service$17, build(input));
155
188
  //#endregion
156
189
  //#region src/db/migrations.ts
157
190
  const migrations = {
@@ -329,6 +362,9 @@ const migrations = {
329
362
  }),
330
363
  "202609190001_session_host_dir": Effect.gen(function* () {
331
364
  yield* (yield* SqlClient.SqlClient)`ALTER TABLE session ADD COLUMN host_dir TEXT`;
365
+ }),
366
+ "202609290001_session_config": Effect.gen(function* () {
367
+ yield* (yield* SqlClient.SqlClient)`ALTER TABLE session ADD COLUMN config TEXT`;
332
368
  })
333
369
  };
334
370
  //#endregion
@@ -337,7 +373,7 @@ var db_exports = /* @__PURE__ */ __exportAll({
337
373
  Database: () => db_exports,
338
374
  SqlClient: () => SqlClient$1,
339
375
  SqlSchema: () => SqlSchema$1,
340
- defaultLayer: () => defaultLayer$3,
376
+ defaultLayer: () => defaultLayer$1,
341
377
  layer: () => layer$26,
342
378
  locationConfig: () => locationConfig,
343
379
  path: () => path$1,
@@ -371,7 +407,7 @@ const path$1 = Effect.fn("Database.path")(function* (data) {
371
407
  const directory = data ?? (yield* resolve$3()).data;
372
408
  return resolveDatabaseLocation(yield* locationConfig, directory);
373
409
  });
374
- const defaultLayer$3 = Layer.unwrap(path$1().pipe(Effect.map(layer$26)));
410
+ const defaultLayer$1 = Layer.unwrap(path$1().pipe(Effect.map(layer$26)));
375
411
  //#endregion
376
412
  //#region src/event/schema.ts
377
413
  var schema_exports$7 = /* @__PURE__ */ __exportAll({});
@@ -386,13 +422,54 @@ __reExport(schema_exports$6, import__codeworksh_plugin_project);
386
422
  //#region src/session/prompt/schema.ts
387
423
  var schema_exports = /* @__PURE__ */ __exportAll({
388
424
  Delivery: () => Delivery,
425
+ IMAGE_MAX_BASE64: () => IMAGE_MAX_BASE64,
426
+ IMAGE_TYPES: () => IMAGE_TYPES,
427
+ ImagePart: () => ImagePart,
428
+ InvalidPromptError: () => InvalidPromptError,
429
+ Part: () => Part,
389
430
  Prompt: () => Prompt,
390
- PromptSchema: () => schema_exports
431
+ PromptSchema: () => schema_exports,
432
+ TextPart: () => TextPart,
433
+ imageProblem: () => imageProblem
434
+ });
435
+ const IMAGE_TYPES = [
436
+ "image/png",
437
+ "image/jpeg",
438
+ "image/gif",
439
+ "image/webp"
440
+ ];
441
+ const IMAGE_MAX_BASE64 = 4718592;
442
+ const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
443
+ const imageProblem = (image) => {
444
+ if (!IMAGE_TYPES.includes(image.mimeType)) return `Unsupported image type ${image.mimeType}; use ${IMAGE_TYPES.join(", ")}`;
445
+ if (image.data.length > 4718592) return `Image exceeds the ${IMAGE_MAX_BASE64 / 1024 / 1024} MB limit of base64`;
446
+ if (image.data.length === 0 || image.data.length % 4 !== 0 || !BASE64.test(image.data)) return "Image data is not base64";
447
+ };
448
+ const TextPart = Schema.Struct({
449
+ type: Schema.Literal("text"),
450
+ text: Schema.String
391
451
  });
392
- const Prompt = Schema.Struct({ text: Schema.String }).annotate({ identifier: "Prompt" }).pipe((0, schema_exports$9.withStatics)((schema) => ({
452
+ /** aikit's image content: base64 `data` and its `mimeType`. */
453
+ const ImagePart = Schema.Struct({
454
+ type: Schema.Literal("image"),
455
+ data: Schema.String,
456
+ mimeType: Schema.String
457
+ }).check(Schema.makeFilter(imageProblem));
458
+ const Part = Schema.Union([TextPart, ImagePart]);
459
+ /** A user prompt: aikit user-message parts, in the order they were written. */
460
+ const Prompt = Schema.Struct({ parts: Schema.Array(Part).check(Schema.isMinLength(1)) }).annotate({ identifier: "Prompt" }).pipe((0, schema_exports$9.withStatics)((schema) => ({
393
461
  equivalence: Schema.toEquivalence(schema),
394
- fromUserMessage: (input) => schema.make({ text: input.text })
462
+ fromText: (text) => schema.make({ parts: [{
463
+ type: "text",
464
+ text
465
+ }] }),
466
+ text: (prompt) => prompt.parts.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n\n")
395
467
  })));
468
+ var InvalidPromptError = class extends Schema.TaggedError()("InvalidPromptError", { reason: Schema.String }) {
469
+ get message() {
470
+ return this.reason;
471
+ }
472
+ };
396
473
  const Delivery = Schema.Literals(["steer", "followUp"]);
397
474
  //#endregion
398
475
  //#region src/space/schema.ts
@@ -402,8 +479,8 @@ __reExport(schema_exports$5, import__codeworksh_plugin_space);
402
479
  //#endregion
403
480
  //#region src/db/schema.sql.ts
404
481
  const Timestamps = {
405
- createdAt: Model.DateTimeInsertFromNumber,
406
- updatedAt: Model.DateTimeUpdateFromNumber
482
+ createdAt: Model$1.DateTimeInsertFromNumber,
483
+ updatedAt: Model$1.DateTimeUpdateFromNumber
407
484
  };
408
485
  const Metadata = Schema.Record(Schema.String, Schema.String);
409
486
  const entryTypes = [
@@ -441,19 +518,19 @@ const toolStatuses = [
441
518
  ];
442
519
  const inputDeliveries = ["steer", "followUp"];
443
520
  const EventData = Schema.Record(Schema.String, Schema.Unknown);
444
- Model.Class("EventSequenceRow")({
521
+ Model$1.Class("EventSequenceRow")({
445
522
  aggregateId: Schema.String,
446
523
  seq: Schema.Natural,
447
- ownerId: Model.FieldOption(Schema.String)
524
+ ownerId: Model$1.FieldOption(Schema.String)
448
525
  });
449
- var EventRow = class extends Model.Class("EventRow")({
526
+ var EventRow = class extends Model$1.Class("EventRow")({
450
527
  id: schema_exports$7.EventSchema.ID,
451
528
  aggregateId: Schema.String,
452
529
  seq: Schema.Natural,
453
530
  type: Schema.String,
454
- data: Model.JsonFromString(EventData)
531
+ data: Model$1.JsonFromString(EventData)
455
532
  }) {};
456
- var ProjectRow = class extends Model.Class("ProjectRow")({
533
+ var ProjectRow = class extends Model$1.Class("ProjectRow")({
457
534
  id: Schema.String,
458
535
  name: Schema.String,
459
536
  status: schema_exports$6.ProjectSchema.Status,
@@ -467,81 +544,82 @@ var ProjectRow = class extends Model.Class("ProjectRow")({
467
544
  * The host has no row: `local` is reserved and `NULL` is its storage form, so
468
545
  * this table only ever holds namespaces something had to provision.
469
546
  */
470
- var SandboxInstanceRow = class extends Model.Class("SandboxInstanceRow")({
547
+ var SandboxInstanceRow = class extends Model$1.Class("SandboxInstanceRow")({
471
548
  id: instance_exports.SandboxInstance.ID,
472
549
  driver: Schema.String,
473
550
  kind: instance_exports.SandboxInstance.Kind,
474
- providerResourceId: Model.FieldOption(Schema.String),
475
- runtimeConfig: Model.FieldOption(Schema.String),
551
+ providerResourceId: Model$1.FieldOption(Schema.String),
552
+ runtimeConfig: Model$1.FieldOption(Schema.String),
476
553
  ownership: instance_exports.SandboxInstance.Ownership,
477
554
  status: instance_exports.SandboxInstance.Status,
478
- providerStatus: Model.FieldOption(Schema.String),
479
- stateObservedAt: Model.DateTimeFromNumberWithNow,
480
- metadata: Model.FieldOption(Model.JsonFromString(Metadata)),
481
- lastError: Model.FieldOption(Model.JsonFromString(instance_exports.SandboxInstance.PersistedError)),
482
- lastMountedAt: Model.FieldOption(Schema.DateTimeUtcFromMillis),
483
- lastUnmountedAt: Model.FieldOption(Schema.DateTimeUtcFromMillis),
484
- lastUsedAt: Model.FieldOption(Schema.DateTimeUtcFromMillis),
485
- removedAt: Model.FieldOption(Schema.DateTimeUtcFromMillis),
555
+ providerStatus: Model$1.FieldOption(Schema.String),
556
+ stateObservedAt: Model$1.DateTimeFromNumberWithNow,
557
+ metadata: Model$1.FieldOption(Model$1.JsonFromString(Metadata)),
558
+ lastError: Model$1.FieldOption(Model$1.JsonFromString(instance_exports.SandboxInstance.PersistedError)),
559
+ lastMountedAt: Model$1.FieldOption(Schema.DateTimeUtcFromMillis),
560
+ lastUnmountedAt: Model$1.FieldOption(Schema.DateTimeUtcFromMillis),
561
+ lastUsedAt: Model$1.FieldOption(Schema.DateTimeUtcFromMillis),
562
+ removedAt: Model$1.FieldOption(Schema.DateTimeUtcFromMillis),
486
563
  ...Timestamps
487
564
  }) {};
488
- var SpaceRow = class extends Model.Class("SpaceRow")({
565
+ var SpaceRow = class extends Model$1.Class("SpaceRow")({
489
566
  id: Schema.String,
490
567
  projectId: Schema.String,
491
568
  location: schema_exports$9.AbsolutePath,
492
569
  kind: schema_exports$5.SpaceSchema.Kind,
493
- env: Model.FieldOption(instance_exports.SandboxInstance.ID),
570
+ env: Model$1.FieldOption(instance_exports.SandboxInstance.ID),
494
571
  status: schema_exports$5.SpaceSchema.Status,
495
572
  ...Timestamps
496
573
  }) {};
497
- var SessionRow = class extends Model.Class("SessionRow")({
574
+ var SessionRow = class extends Model$1.Class("SessionRow")({
498
575
  id: IDFromDb,
499
576
  spaceId: Schema.String,
500
- parentId: Model.FieldOption(IDFromDb),
577
+ parentId: Model$1.FieldOption(IDFromDb),
501
578
  slug: Schema.String,
502
579
  directory: schema_exports$9.AbsolutePath,
503
- hostDir: Model.FieldOption(schema_exports$9.AbsolutePath),
580
+ hostDir: Model$1.FieldOption(schema_exports$9.AbsolutePath),
581
+ config: Model$1.FieldOption(Model$1.JsonFromString(Config$1)),
504
582
  title: Schema.String,
505
- tag: Model.FieldOption(Schema.String),
506
- metadata: Model.FieldOption(Model.JsonFromString(Metadata)),
507
- leafEntryId: Model.FieldOption(Schema.String),
508
- cost: Model.GeneratedByDb(Schema.Finite),
509
- tokensInput: Model.GeneratedByDb(Schema.Int),
510
- tokensOutput: Model.GeneratedByDb(Schema.Int),
511
- tokensCacheRead: Model.GeneratedByDb(Schema.Int),
512
- tokensCacheWrite: Model.GeneratedByDb(Schema.Int),
583
+ tag: Model$1.FieldOption(Schema.String),
584
+ metadata: Model$1.FieldOption(Model$1.JsonFromString(Metadata)),
585
+ leafEntryId: Model$1.FieldOption(Schema.String),
586
+ cost: Model$1.GeneratedByDb(Schema.Finite),
587
+ tokensInput: Model$1.GeneratedByDb(Schema.Int),
588
+ tokensOutput: Model$1.GeneratedByDb(Schema.Int),
589
+ tokensCacheRead: Model$1.GeneratedByDb(Schema.Int),
590
+ tokensCacheWrite: Model$1.GeneratedByDb(Schema.Int),
513
591
  ...Timestamps
514
592
  }) {};
515
- var SessionInputRow = class extends Model.Class("SessionInputRow")({
593
+ var SessionInputRow = class extends Model$1.Class("SessionInputRow")({
516
594
  id: Schema.String,
517
595
  sessionId: IDFromDb,
518
- prompt: Model.JsonFromString(Prompt),
596
+ prompt: Model$1.JsonFromString(Prompt),
519
597
  delivery: Schema.Literals(inputDeliveries),
520
598
  admittedSeq: Schema.Int,
521
- promotedSeq: Model.FieldOption(Schema.Int),
522
- createdAt: Model.DateTimeInsertFromNumber
599
+ promotedSeq: Model$1.FieldOption(Schema.Int),
600
+ createdAt: Model$1.DateTimeInsertFromNumber
523
601
  }) {};
524
- var SessionEntryRow = class extends Model.Class("SessionEntryRow")({
602
+ var SessionEntryRow = class extends Model$1.Class("SessionEntryRow")({
525
603
  id: Schema.String,
526
604
  sessionId: IDFromDb,
527
- parentId: Model.FieldOption(Schema.String),
605
+ parentId: Model$1.FieldOption(Schema.String),
528
606
  seq: schema_exports$9.NonNegativeInt,
529
607
  type: Schema.Literals(entryTypes),
530
608
  state: Schema.Literals(entryStates),
531
609
  data: Schema.String,
532
- label: Model.FieldOption(Schema.String),
533
- metadata: Model.FieldOption(Model.JsonFromString(Metadata)),
610
+ label: Model$1.FieldOption(Schema.String),
611
+ metadata: Model$1.FieldOption(Model$1.JsonFromString(Metadata)),
534
612
  ...Timestamps
535
613
  }) {};
536
- var SessionEntryPartRow = class extends Model.Class("SessionEntryPartRow")({
614
+ var SessionEntryPartRow = class extends Model$1.Class("SessionEntryPartRow")({
537
615
  id: Schema.String,
538
616
  entryId: Schema.String,
539
617
  sessionId: IDFromDb,
540
618
  partIndex: Schema.Int,
541
619
  type: Schema.Literals(partTypes),
542
- status: Model.FieldOption(Schema.Literals(toolStatuses)),
543
- callId: Model.FieldOption(Schema.String),
544
- toolName: Model.FieldOption(Schema.String),
620
+ status: Model$1.FieldOption(Schema.Literals(toolStatuses)),
621
+ callId: Model$1.FieldOption(Schema.String),
622
+ toolName: Model$1.FieldOption(Schema.String),
545
623
  data: Schema.String,
546
624
  ...Timestamps
547
625
  }) {};
@@ -631,6 +709,7 @@ var schema_exports$4 = /* @__PURE__ */ __exportAll({
631
709
  //#endregion
632
710
  //#region src/event/list.ts
633
711
  var list_exports = /* @__PURE__ */ __exportAll({
712
+ ConfigChanged: () => ConfigChanged,
634
713
  Definitions: () => Definitions,
635
714
  DurableDefinitions: () => DurableDefinitions,
636
715
  EventList: () => list_exports,
@@ -651,6 +730,8 @@ var list_exports = /* @__PURE__ */ __exportAll({
651
730
  PromptAdmitted: () => PromptAdmitted,
652
731
  Prompted: () => Prompted,
653
732
  PublicDefinitions: () => PublicDefinitions,
733
+ RetryFinished: () => RetryFinished,
734
+ RetryScheduled: () => RetryScheduled,
654
735
  SessionForked: () => SessionForked,
655
736
  ToolProgress: () => ToolProgress$2,
656
737
  ToolSettled: () => ToolSettled,
@@ -684,6 +765,14 @@ const Prompted = schema_exports$7.EventSchema.define({
684
765
  ...durableOptions,
685
766
  schema: PromptFields
686
767
  });
768
+ const ConfigChanged = schema_exports$7.EventSchema.define({
769
+ type: "session.config.changed",
770
+ ...durableOptions,
771
+ schema: {
772
+ ...baseOptions,
773
+ ...Config$1.fields
774
+ }
775
+ });
687
776
  const LLMFields = {
688
777
  ...baseOptions,
689
778
  messageId: ID
@@ -713,6 +802,30 @@ const TurnAborted = schema_exports$7.EventSchema.define({
713
802
  cause: TurnAbortCause
714
803
  }
715
804
  });
805
+ /**
806
+ * A failed provider request will be repeated after `delayMs`. `attempt` counts retries from 1, so
807
+ * the request that failed was attempt `attempt - 1`; the failed draft is already settled.
808
+ */
809
+ const RetryScheduled = schema_exports$7.EventSchema.define({
810
+ type: "session.retry.scheduled",
811
+ schema: {
812
+ ...baseOptions,
813
+ attempt: schema_exports$9.NonNegativeInt,
814
+ maxRetries: schema_exports$9.NonNegativeInt,
815
+ delayMs: schema_exports$9.NonNegativeInt,
816
+ message: Schema.String
817
+ }
818
+ });
819
+ /** Once per turn that retried: whether a retry succeeded, after how many. */
820
+ const RetryFinished = schema_exports$7.EventSchema.define({
821
+ type: "session.retry.finished",
822
+ schema: {
823
+ ...baseOptions,
824
+ attempt: schema_exports$9.NonNegativeInt,
825
+ success: Schema.Boolean,
826
+ message: (0, schema_exports$9.optional)(Schema.String)
827
+ }
828
+ });
716
829
  /** Durable insertion of the request's draft assistant placeholder. */
717
830
  const LLMStarted = schema_exports$7.EventSchema.define({
718
831
  type: "session.llm.started",
@@ -787,7 +900,10 @@ const ToolStarted = schema_exports$7.EventSchema.define({
787
900
  schema: {
788
901
  ...LLMFields,
789
902
  callID: Schema.String,
790
- name: Schema.String
903
+ name: Schema.String,
904
+ /** The tool's display label, when its definition declares one. */
905
+ label: (0, schema_exports$9.optional)(Schema.String),
906
+ arguments: Schema.Record(Schema.String, Schema.Json)
791
907
  }
792
908
  });
793
909
  const ToolProgress$2 = schema_exports$7.EventSchema.define({
@@ -883,14 +999,14 @@ const PluginUpdated = schema_exports$7.EventSchema.define({
883
999
  * Everything the kernel can publish. `PublicDefinitions` is the narrower set
884
1000
  * that reaches clients -- adding an event here does not publish it.
885
1001
  */
886
- const Definitions = schema_exports$7.EventSchema.inventory(ExecutionStarted, ExecutionSucceeded, ExecutionFailed, ExecutionInterrupted, PromptAdmitted, Prompted, SessionForked, TurnStarted, TurnEnded, TurnAborted, LLMStarted, LLMTextStart, LLMTextDelta, LLMTextEnd, LLMThinkingStart, LLMThinkingDelta, LLMThinkingEnd, LLMEnded, LLMFailed, ToolStarted, ToolProgress$2, ToolSettled, PluginUpdated);
1002
+ 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);
887
1003
  /**
888
1004
  * The wire contract: what a client is allowed to receive. Identical to
889
1005
  * `Definitions` today, and separate so that adding a kernel event is not by
890
1006
  * itself a decision to publish it.
891
1007
  */
892
1008
  const PublicDefinitions = Definitions;
893
- const DurableDefinitions = schema_exports$7.EventSchema.inventory(PromptAdmitted, Prompted, SessionForked, TurnEnded, LLMStarted, LLMEnded, LLMFailed, ToolSettled);
1009
+ const DurableDefinitions = schema_exports$7.EventSchema.inventory(PromptAdmitted, Prompted, ConfigChanged, SessionForked, TurnEnded, LLMStarted, LLMEnded, LLMFailed, ToolSettled);
894
1010
  const Manifest$2 = {
895
1011
  definitions: schema_exports$7.EventSchema.durable([...DurableDefinitions]),
896
1012
  schema: Schema.Union([...DurableDefinitions], { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
@@ -1235,7 +1351,7 @@ const layer$25 = Layer.effect(Service$16, Effect.gen(function* () {
1235
1351
  const encoder = new TextEncoder();
1236
1352
  function fast(input) {
1237
1353
  const data = typeof input === "string" ? encoder.encode(input) : input;
1238
- return Encoding.encodeHex(Effect.runSync(crypto.digest("SHA-1", data)));
1354
+ return Hex.encode(Effect.runSync(crypto.digest("SHA-1", data)));
1239
1355
  }
1240
1356
  //#endregion
1241
1357
  //#region src/space/space.ts
@@ -1377,6 +1493,77 @@ const layer$24 = Layer.effect(Service$15, Effect.gen(function* () {
1377
1493
  });
1378
1494
  }));
1379
1495
  //#endregion
1496
+ //#region src/util/slug.ts
1497
+ const PREFIXES = [
1498
+ "binary",
1499
+ "protocol",
1500
+ "astromech",
1501
+ "thermal",
1502
+ "ionic",
1503
+ "encrypted",
1504
+ "holonet",
1505
+ "hyperspace",
1506
+ "magnetic",
1507
+ "logic",
1508
+ "plasma",
1509
+ "vector",
1510
+ "optical",
1511
+ "quantum",
1512
+ "mechanical",
1513
+ "servo",
1514
+ "fusion",
1515
+ "flux",
1516
+ "sychro",
1517
+ "static"
1518
+ ];
1519
+ const HARDWARE = [
1520
+ "core",
1521
+ "unit",
1522
+ "node",
1523
+ "array",
1524
+ "link",
1525
+ "processor",
1526
+ "module",
1527
+ "uplink",
1528
+ "interface",
1529
+ "splicer",
1530
+ "matrix",
1531
+ "buffer",
1532
+ "relay",
1533
+ "circuit",
1534
+ "sensor",
1535
+ "driver",
1536
+ "bridge",
1537
+ "manifold",
1538
+ "oscillator",
1539
+ "terminal"
1540
+ ];
1541
+ const SECTORS = [
1542
+ "kuat",
1543
+ "corellia",
1544
+ "coruscant",
1545
+ "bespin",
1546
+ "kamino",
1547
+ "mustafar",
1548
+ "fondor",
1549
+ "hosnian",
1550
+ "lothal",
1551
+ "scarif",
1552
+ "sub-level",
1553
+ "deep-space",
1554
+ "outer-rim",
1555
+ "mid-rim",
1556
+ "sector-7"
1557
+ ];
1558
+ const CONNECTORS = [
1559
+ "at",
1560
+ "of",
1561
+ "in"
1562
+ ];
1563
+ const create$3 = Effect.fnUntraced(function* (id) {
1564
+ return `${yield* Random.choice(PREFIXES)}-${yield* Random.choice(HARDWARE)}-${yield* Random.choice(CONNECTORS)}-${yield* Random.choice(SECTORS)}-${id.slice(-8)}`;
1565
+ });
1566
+ //#endregion
1380
1567
  //#region src/session/session.ts
1381
1568
  var session_exports$1 = /* @__PURE__ */ __exportAll({
1382
1569
  EntryNotFoundError: () => EntryNotFoundError,
@@ -1391,7 +1578,7 @@ var session_exports$1 = /* @__PURE__ */ __exportAll({
1391
1578
  SessionNotLinkedError: () => SessionNotLinkedError,
1392
1579
  SessionRow: () => SessionRow,
1393
1580
  ToolCallNotFoundError: () => ToolCallNotFoundError,
1394
- defaultLayer: () => defaultLayer$2,
1581
+ defaultLayer: () => defaultLayer,
1395
1582
  entryStates: () => entryStates,
1396
1583
  entryTypes: () => entryTypes,
1397
1584
  layer: () => layer$23,
@@ -1603,9 +1790,10 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
1603
1790
  id,
1604
1791
  spaceId: input.spaceId,
1605
1792
  parentId: Option.fromUndefinedOr(input.parentId),
1606
- slug: input.slug,
1793
+ slug: yield* create$3(id),
1607
1794
  directory: input.directory,
1608
1795
  hostDir: Option.fromUndefinedOr(input.hostDir),
1796
+ config: Option.none(),
1609
1797
  title: input.title,
1610
1798
  tag: Option.fromUndefinedOr(input.tag),
1611
1799
  metadata: Option.fromUndefinedOr(input.metadata),
@@ -1948,9 +2136,10 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
1948
2136
  id: newSessionId,
1949
2137
  spaceId: source.value.spaceId,
1950
2138
  parentId: Option.some(source.value.id),
1951
- slug: input.slug,
2139
+ slug: yield* create$3(newSessionId),
1952
2140
  directory: source.value.directory,
1953
2141
  hostDir: source.value.hostDir,
2142
+ config: source.value.config,
1954
2143
  title: input.title ?? source.value.title,
1955
2144
  tag: input.tag === void 0 ? source.value.tag : Option.some(input.tag),
1956
2145
  metadata: source.value.metadata,
@@ -2045,6 +2234,21 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2045
2234
  if (Option.isNone(updated)) return yield* Effect.die(/* @__PURE__ */ new Error("session link did not persist"));
2046
2235
  return updated.value;
2047
2236
  });
2237
+ const retitle = Effect.fn("Session.retitle")(function* (sessionId, title) {
2238
+ return (yield* sql`
2239
+ UPDATE session SET title = ${title}
2240
+ WHERE id = ${sessionId} AND title = ${DEFAULT_TITLE}
2241
+ RETURNING id
2242
+ `.pipe(Effect.orDie)).length > 0;
2243
+ });
2244
+ const encodeConfig = Schema.encodeEffect(Schema.fromJsonString(Config$1));
2245
+ const configure = Effect.fn("Session.configure")(function* (sessionId, config) {
2246
+ const patch = yield* encodeConfig(config);
2247
+ yield* sql`
2248
+ UPDATE session SET config = json_patch(COALESCE(config, '{}'), ${patch})
2249
+ WHERE id = ${sessionId}
2250
+ `;
2251
+ }, Effect.orDie);
2048
2252
  const relink = Effect.fn("Session.relink")(function* (input) {
2049
2253
  const reject = (reason) => new RelinkError({
2050
2254
  sessionId: input.sessionId,
@@ -2104,6 +2308,8 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2104
2308
  return Service$14.of({
2105
2309
  create,
2106
2310
  link,
2311
+ retitle,
2312
+ configure,
2107
2313
  get,
2108
2314
  space,
2109
2315
  list,
@@ -2122,7 +2328,7 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2122
2328
  setLabel
2123
2329
  });
2124
2330
  }));
2125
- const defaultLayer$2 = layer$23.pipe(Layer.provide(defaultLayer$3));
2331
+ const defaultLayer = layer$23.pipe(Layer.provide(defaultLayer$1));
2126
2332
  //#endregion
2127
2333
  //#region src/context/errors.ts
2128
2334
  var ContextDecodeError = class extends Schema.TaggedError()("ContextDecodeError", {
@@ -3857,17 +4063,6 @@ const make$9 = () => {
3857
4063
  }) };
3858
4064
  };
3859
4065
  //#endregion
3860
- //#region src/sandbox/sandbox.ts
3861
- /**
3862
- * Assemble a local backend into a sandbox with a **real host shell** — the
3863
- * right choice whenever commands must invoke actual binaries (`git`, package
3864
- * managers). For a sandbox whose shell should stay inside the VFS, use
3865
- * `EnvBash.services` instead, which wires just-bash over the same tree.
3866
- */
3867
- const services = (backend, identity) => Layer.provideMerge(io_exports.SandboxIO.mount(identity), Layer.provideMerge(Layer.merge(layer$21, layer$18()), backend));
3868
- /** Default sandbox: the real OS filesystem and processes, mounted at `cwd`. */
3869
- const defaultLayer$1 = (cwd) => services(layer$20(), io_exports.SandboxIO.host(cwd));
3870
- //#endregion
3871
4066
  //#region src/git/git.ts
3872
4067
  var AppProcessError = class extends Schema.TaggedError()("AppProcessError", {
3873
4068
  command: Schema.String,
@@ -4351,16 +4546,13 @@ var location_exports = /* @__PURE__ */ __exportAll({
4351
4546
  Info: () => Info$1,
4352
4547
  Location: () => location_exports,
4353
4548
  NotDirectoryError: () => NotDirectoryError,
4354
- Ref: () => Ref$2,
4355
4549
  Service: () => Service$8,
4356
- defaultLayer: () => defaultLayer,
4357
4550
  layer: () => layer$11,
4358
- layerMounted: () => layerMounted,
4359
- layerWith: () => layerWith
4551
+ layerMounted: () => layerMounted
4360
4552
  });
4361
- const layer$11 = (ref = {}) => Layer.effect(Service, Effect.gen(function* () {
4553
+ const layer$11 = () => Layer.effect(Service, Effect.gen(function* () {
4362
4554
  const instance = yield* io_exports.SandboxIO.Current;
4363
- const directory = schema_exports$9.AbsolutePath.make(ref.directory ?? instance.cwd);
4555
+ const directory = schema_exports$9.AbsolutePath.make(instance.cwd);
4364
4556
  const resolved = yield* (yield* Service$9).resolveOrCreate(directory);
4365
4557
  return Service.of(new Info({
4366
4558
  directory: resolved.directory,
@@ -4373,9 +4565,7 @@ const layer$11 = (ref = {}) => Layer.effect(Service, Effect.gen(function* () {
4373
4565
  * folded in. What remains is exactly the mount and the database — for callers
4374
4566
  * that already hold both, such as the runner and `Session.create`.
4375
4567
  */
4376
- const layerMounted = (ref = {}) => layer$11(ref).pipe(Layer.provide(layer$12.pipe(Layer.provide(Layer.mergeAll(layer$14, layer$13, layer$24)), Layer.provide(layer$15))));
4377
- const layerWith = (ref, sandbox) => layerMounted(ref).pipe(Layer.provide(sandbox), Layer.provide(defaultLayer$3));
4378
- const defaultLayer = (ref, path) => layerWith(ref, defaultLayer$1(path));
4568
+ const layerMounted = () => layer$11().pipe(Layer.provide(layer$12.pipe(Layer.provide(Layer.mergeAll(layer$14, layer$13, layer$24)), Layer.provide(layer$15))));
4379
4569
  //#endregion
4380
4570
  //#region src/runner/run.ts
4381
4571
  var run_exports = /* @__PURE__ */ __exportAll({
@@ -4870,91 +5060,340 @@ const layer$9 = (definitions = Definitions) => Layer.sync(Service$4, () => {
4870
5060
  });
4871
5061
  });
4872
5062
  //#endregion
4873
- //#region src/model/catalog.ts
4874
- var catalog_exports = /* @__PURE__ */ __exportAll({
4875
- ModelCatalog: () => catalog_exports,
4876
- RefreshError: () => RefreshError,
4877
- filename: () => filename,
5063
+ //#region src/settings/schema.ts
5064
+ var schema_exports$3 = /* @__PURE__ */ __exportAll({});
5065
+ import * as import__codeworksh_plugin_settings from "@codeworksh/plugin/settings";
5066
+ __reExport(schema_exports$3, import__codeworksh_plugin_settings);
5067
+ //#endregion
5068
+ //#region src/settings/merge.ts
5069
+ /**
5070
+ * A plain string-keyed record: what a settings patch merges, and what a plugin's `options` block
5071
+ * has to be. A Map, a typed array or a class instance is an object to `typeof` and not one of
5072
+ * these — `Object.freeze` alone does not tell them apart, and it throws on some of them.
5073
+ */
5074
+ const isRecord$1 = (value) => Predicate.isObject(value) && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
5075
+ const copy = (value) => {
5076
+ if (Array.isArray(value)) return value.map(copy);
5077
+ return isRecord$1(value) ? merge({}, value) : value;
5078
+ };
5079
+ /** Null/undefined properties are absent, objects merge, and arrays replace. Never mutates inputs. */
5080
+ function merge(base, ...patches) {
5081
+ const result = /* @__PURE__ */ new Map();
5082
+ for (const patch of [base, ...patches]) {
5083
+ if (patch === void 0) continue;
5084
+ for (const [key, value] of Object.entries(patch)) {
5085
+ if (value === null || value === void 0) continue;
5086
+ const previous = result.get(key);
5087
+ result.set(key, isRecord$1(previous) && isRecord$1(value) ? merge(previous, value) : copy(value));
5088
+ }
5089
+ }
5090
+ return Object.fromEntries(result);
5091
+ }
5092
+ /** Normalize nullable properties before schema validation, without changing array positions. */
5093
+ const normalize$1 = copy;
5094
+ //#endregion
5095
+ //#region src/settings/settings.ts
5096
+ var settings_exports = /* @__PURE__ */ __exportAll({
5097
+ Service: () => Service$3,
5098
+ Settings: () => settings_exports,
5099
+ SettingsError: () => SettingsError,
5100
+ defaults: () => schema_exports$3.defaults,
4878
5101
  layer: () => layer$8,
4879
- loadError: () => loadError,
4880
- models: () => models,
4881
- providers: () => providers,
4882
- refresh: () => refresh$1,
4883
- sync: () => sync,
4884
- ttlConfig: () => ttlConfig
5102
+ load: () => load$3,
5103
+ modules: () => modules,
5104
+ parse: () => parse$2,
5105
+ paths: () => paths,
5106
+ projectRoot: () => projectRoot,
5107
+ userConfigDir: () => userConfigDir
4885
5108
  });
4886
- const filename = "models.gen.json";
4887
- /** How old the catalog may get before a check downloads a new one. */
4888
- const ttlConfig = Config.Duration("CODEWORK_MODELS_TTL").pipe(Config.withDefault(Duration.minutes(15)));
4889
- /** How often a server looks at the file. A look is a `stat`; only a stale file costs a download. */
4890
- const checkEvery = Duration.minutes(1);
4891
- var RefreshError = class extends Schema.TaggedError()("ModelCatalog.RefreshError", {
5109
+ /**
5110
+ * An app-level `--user-config-dir` as the absolute directory it names: `~` is the user's home, and
5111
+ * a relative spelling is relative to `hostCwd` -- never to a session's `hostDir`, which is not
5112
+ * where the flag was typed.
5113
+ */
5114
+ const userConfigDir = (raw, hostCwd) => hostPath.resolve(hostCwd, expandTilde(raw, hostPath));
5115
+ /** The settings files a directory may hold, in the order it is searched. */
5116
+ const settingsIn = (directory) => [hostPath.join(directory, "settings.jsonc"), hostPath.join(directory, "settings.json")];
5117
+ /** A directory and every ancestor above it, nearest first. */
5118
+ const ancestors = (from) => {
5119
+ const chain = [];
5120
+ for (let directory = from;; directory = hostPath.dirname(directory)) {
5121
+ chain.push(directory);
5122
+ if (hostPath.dirname(directory) === directory) return chain;
5123
+ }
5124
+ };
5125
+ const isDirectory = (path) => fileSystem.stat(path).pipe(Effect.map((info) => info.type === "Directory"), Effect.orElseSucceed(() => false));
5126
+ /**
5127
+ * The project a directory is in: the nearest ancestor holding a `.codework` **directory**.
5128
+ *
5129
+ * This is git's rule. `~/workspace/app/.codework` makes `~/workspace/app` a project, and
5130
+ * `~/workspace/app/packages/fubar` is inside it. Nearest wins outright -- the walk stops at the
5131
+ * first marker, so an inner project is never merged with an outer one.
5132
+ *
5133
+ * The marker is the directory, not a file in it. An empty `.codework/` is still a project root,
5134
+ * which is the point: creating the directory is how a person says "a project begins here", and it
5135
+ * has to mean that before any settings exist. It also makes the question one `stat` per ancestor
5136
+ * whose answer does not change as files come and go inside.
5137
+ *
5138
+ * `<home>` is skipped. `~/.codework` is the user layer, not a project; without this, running
5139
+ * anywhere under `$HOME` would find it as one and read the user layer twice -- and since plugin
5140
+ * entries accumulate across layers, every user plugin would load twice.
5141
+ */
5142
+ const projectRoot = Effect.fn("Settings.projectRoot")(function* (from, home) {
5143
+ const userConfig = hostPath.resolve(home);
5144
+ for (const directory of ancestors(hostPath.resolve(from))) {
5145
+ const marker = hostPath.join(directory, appConfigDir);
5146
+ if (hostPath.resolve(marker) === userConfig) continue;
5147
+ if (yield* isDirectory(marker)) return directory;
5148
+ }
5149
+ });
5150
+ /**
5151
+ * Ordered layers, each a group of candidates where the first file that exists is selected -- a
5152
+ * group never contributes more than one file.
5153
+ *
5154
+ * `.jsonc` is the canonical spelling: these files are written by hand and read as JSONC, so the
5155
+ * extension says what the file is and an editor validates it as such. `.json` remains a fallback
5156
+ * everywhere, tried second, because the parser reads either and a file already named that must
5157
+ * keep working.
5158
+ *
5159
+ * `root` is an already-discovered project root ({@link projectRoot}), not a directory to search
5160
+ * from: the walk is a `stat` per ancestor and belongs with the other I/O. `undefined` contributes
5161
+ * no project layer at all, which is what a session with no host directory gets.
5162
+ */
5163
+ function paths(input) {
5164
+ return [settingsIn(input.userConfigDir ?? input.home), input.root === void 0 ? [] : settingsIn(hostPath.join(input.root, appConfigDir))];
5165
+ }
5166
+ var SettingsError = class extends Schema.TaggedError()("SettingsError", {
4892
5167
  path: Schema.String,
5168
+ reason: Schema.Literals([
5169
+ "read",
5170
+ "parse",
5171
+ "decode"
5172
+ ]),
4893
5173
  detail: Schema.String
4894
5174
  }) {
4895
5175
  get message() {
4896
- return `failed to refresh the model catalog at ${this.path}: ${this.detail}`;
5176
+ return `${this.path}: ${this.reason}: ${this.detail}`;
4897
5177
  }
4898
5178
  };
4899
- /** Point aikit at `<home>/models.gen.json`, which `CODEWORK_MODELS_FILE` overrides. */
4900
- const use = (home) => {
4901
- const own = hostPath.resolve(home, filename);
4902
- Model$1.configureCatalog(own);
4903
- const path = Model$1.catalogPath();
4904
- return {
4905
- path,
4906
- pinned: path !== own
4907
- };
5179
+ /** `line:column`, 1-based, for a parser offset into the source. */
5180
+ const at = (source, offset) => {
5181
+ const before = source.slice(0, offset).split("\n");
5182
+ return `${before.length}:${(before.at(-1)?.length ?? 0) + 1}`;
4908
5183
  };
4909
- const modified = (path) => fileSystem.stat(path).pipe(Effect.map((info) => Option.map(info.mtime, (date) => date.getTime())), Effect.orElseSucceed(() => Option.none()));
4910
- const stale = Effect.fn("ModelCatalog.stale")(function* (path) {
4911
- const mtime = yield* modified(path);
4912
- if (Option.isNone(mtime)) return true;
4913
- const ttl = yield* ttlConfig;
4914
- return (yield* Clock.currentTimeMillis) - mtime.value >= Duration.toMillis(ttl);
4915
- });
4916
- const download$1 = (path) => Effect.tryPromise({
4917
- try: () => generateModels({ path }),
4918
- catch: (cause) => new RefreshError({
4919
- path,
4920
- detail: cause instanceof Error ? cause.message : String(cause)
4921
- })
4922
- });
4923
- /** Download a new catalog for the home when it is missing, older than the TTL, or `force` asks. */
4924
- const refresh$1 = Effect.fn("ModelCatalog.refresh")(function* (home, options = {}) {
4925
- const { path } = use(home);
4926
- if (options.force !== true && !(yield* stale(path))) return {
4927
- path,
4928
- refreshed: false
4929
- };
4930
- yield* download$1(path);
4931
- return {
4932
- path,
4933
- refreshed: true
4934
- };
4935
- });
4936
5184
  /**
4937
- * The automatic check: refresh a stale catalog unless it is pinned, and never fail. A failed
4938
- * download leaves the old file in place; a missing one surfaces later as a
4939
- * `Runner.ModelCatalogError` from whatever needed it.
5185
+ * Decode failures name the field, and explain it only with a message a schema check wrote. Effect's
5186
+ * defaults quote the rejected value, and in a settings file that value can be a secret, so any
5187
+ * other failure reads "invalid value".
4940
5188
  */
4941
- const check$1 = Effect.fn("ModelCatalog.check")(function* ({ path, pinned }) {
4942
- if (pinned || !(yield* stale(path))) return;
4943
- yield* download$1(path);
4944
- });
4945
- const quietly = (effect) => effect.pipe(Effect.asVoid, Effect.catch((error) => Effect.logWarning(`model catalog: ${error.message}`)));
4946
- /** Check the home's catalog once, for a command that runs and exits; returns the file aikit reads. */
4947
- const sync = Effect.fn("ModelCatalog.sync")(function* (home) {
4948
- const target = use(home);
4949
- yield* quietly(check$1(target));
4950
- return target.path;
5189
+ const decodeFormatter = SchemaIssue.makeFormatterStandardSchemaV1({ leafHook: (issue) => {
5190
+ const message = "annotations" in issue ? issue.annotations?.message : void 0;
5191
+ return typeof message === "string" ? message.replaceAll(/\s*\n\s*/g, " ") : "invalid value";
5192
+ } });
5193
+ const parse$2 = Effect.fn("Settings.parse")(function* (path, source) {
5194
+ const errors = [];
5195
+ const json = parse(source, errors, {
5196
+ allowTrailingComma: true,
5197
+ disallowComments: false
5198
+ });
5199
+ const first = errors[0];
5200
+ if (first !== void 0) return yield* new SettingsError({
5201
+ path,
5202
+ reason: "parse",
5203
+ detail: `${printParseErrorCode(first.error)} at ${at(source, first.offset)}`
5204
+ });
5205
+ const document = normalize$1(json);
5206
+ if (Predicate.isObject(document) && Predicate.isObject(json) && "plugins" in json) Object.assign(document, { plugins: json.plugins });
5207
+ return yield* Schema.decodeUnknownEffect(schema_exports$3.Patch)(document).pipe(Effect.mapError((error) => new SettingsError({
5208
+ path,
5209
+ reason: "decode",
5210
+ detail: decodeFormatter(error.issue).issues.map((issue) => `${issue.path?.map((part) => typeof part === "object" ? String(part.key) : part).join(".") ?? "settings"}: ${issue.message}`).join("; ")
5211
+ })));
4951
5212
  });
5213
+ var Service$3 = class extends Context.Service()("@codeworksh/harness/settings/settings/Service") {};
4952
5214
  /**
4953
- * Check at boot, and with `watch`, keep checking for the life of the layer. A file whose mtime
4954
- * moved -- refreshed here, by `codework models refresh`, or by hand -- is reloaded, so the
4955
- * process never serves a catalog older than what is on disk.
5215
+ * Anchor relative plugin entries to the file that declared them.
5216
+ *
5217
+ * A reference is otherwise resolved against the host startup directory, which is right for
5218
+ * a project file sitting in it and meaningless for `~/.codework/settings.json`, where
5219
+ * `./plugins/x.ts` would name a different file in every project the process is started in.
5220
+ * `file:` URLs, package specs and package names are left exactly as written, and a `plugin`
5221
+ * key is an ID rather than a location. A configuration entry's `package` is anchored like a
5222
+ * module entry, so a relative path names the same module in both spellings.
4956
5223
  */
4957
- const layer$8 = (options) => Layer.effectDiscard(Effect.gen(function* () {
5224
+ const anchored = (entry, file) => {
5225
+ const directory = hostPath.dirname(file);
5226
+ const resolve = (reference) => reference.startsWith("./") || reference.startsWith("../") ? hostPath.resolve(directory, reference) : reference;
5227
+ if (typeof entry === "string") return resolve(entry);
5228
+ return "package" in entry ? {
5229
+ ...entry,
5230
+ package: resolve(entry.package)
5231
+ } : entry;
5232
+ };
5233
+ const anchor$1 = (patch, file) => patch.plugins === void 0 ? patch : {
5234
+ ...patch,
5235
+ plugins: patch.plugins.map((entry) => anchored(entry, file))
5236
+ };
5237
+ const attempt = (path) => fileSystem.readFileString(path).pipe(Effect.mapError((error) => new SettingsError({
5238
+ path,
5239
+ reason: "read",
5240
+ detail: error.reason._tag
5241
+ })), Effect.flatMap((source) => parse$2(path, source)));
5242
+ /**
5243
+ * One read of every layer. Exported for the harness constructor, which needs the plugin
5244
+ * selection before any layer is built; everything else goes through `Service`.
5245
+ */
5246
+ const load$3 = Effect.fn("Settings.load")(function* (options) {
5247
+ const root = options.hostDir === void 0 ? void 0 : yield* projectRoot(options.hostDir, options.home);
5248
+ const files = paths({
5249
+ home: options.home,
5250
+ root,
5251
+ ...options.userConfigDir === void 0 ? {} : { userConfigDir: options.userConfigDir }
5252
+ });
5253
+ let settings = merge(schema_exports$3.defaults);
5254
+ let declared = schema_exports$3.defaults.declared;
5255
+ let models = schema_exports$3.defaults.models;
5256
+ for (const group of files) for (const path of group) {
5257
+ const result = yield* Effect.result(attempt(path));
5258
+ if (Result.isSuccess(result)) {
5259
+ const { models: entries, ...patch } = anchor$1(result.success, path);
5260
+ settings = merge(settings, patch);
5261
+ if (entries !== void 0) models = [...models, ...entries.map((entry) => ({
5262
+ entry,
5263
+ file: path
5264
+ }))];
5265
+ if (result.success.plugins !== void 0) declared = [...declared, ...result.success.plugins.map((written) => ({
5266
+ written,
5267
+ entry: anchored(written, path),
5268
+ file: path
5269
+ }))];
5270
+ break;
5271
+ }
5272
+ const error = result.failure;
5273
+ if (error.reason === "read" && error.detail === "NotFound") continue;
5274
+ return yield* error;
5275
+ }
5276
+ return {
5277
+ ...settings,
5278
+ plugins: declared.map((one) => one.entry),
5279
+ declared,
5280
+ models
5281
+ };
5282
+ });
5283
+ /**
5284
+ * Each module reference once, owned by the **last** file to declare it (D6).
5285
+ *
5286
+ * The owner is not bookkeeping: its `.npmrc` chain keys the store artifact. The CLI that installs
5287
+ * a reference and the runtime that loads it must pick the same owner, or `plugin update` refreshes
5288
+ * an artifact the runtime never reads. Only a string entry loads a module, so only a string entry
5289
+ * owns one -- a `{ package }` patch in another file configures it and must not move its registry.
5290
+ * A reference keeps the position of its first declaration.
5291
+ */
5292
+ const modules = (declared) => {
5293
+ const owners = /* @__PURE__ */ new Map();
5294
+ for (const one of declared) if (typeof one.entry === "string") owners.set(one.entry, one);
5295
+ return owners;
5296
+ };
5297
+ const layer$8 = (options = {}) => Layer.effect(Service$3, Effect.gen(function* () {
5298
+ const global = yield* Service$17;
5299
+ return Service$3.of({ load: (hostDir) => load$3({
5300
+ ...options,
5301
+ home: global.home,
5302
+ hostDir
5303
+ }) });
5304
+ }));
5305
+ //#endregion
5306
+ //#region src/model/catalog.ts
5307
+ var catalog_exports = /* @__PURE__ */ __exportAll({
5308
+ Credentials: () => Credentials,
5309
+ ModelCatalog: () => catalog_exports,
5310
+ RefreshError: () => RefreshError,
5311
+ available: () => available,
5312
+ effective: () => effective,
5313
+ entry: () => entry,
5314
+ filename: () => filename,
5315
+ fromSettings: () => fromSettings,
5316
+ layer: () => layer$7,
5317
+ loadError: () => loadError,
5318
+ models: () => models,
5319
+ oauthStorage: () => oauthStorage,
5320
+ providers: () => providers,
5321
+ refresh: () => refresh$1,
5322
+ sync: () => sync,
5323
+ ttlConfig: () => ttlConfig
5324
+ });
5325
+ const filename = "models.gen.json";
5326
+ /** How old the catalog may get before a check downloads a new one. */
5327
+ const ttlConfig = Config.Duration("CODEWORK_MODELS_TTL").pipe(Config.withDefault(Duration.minutes(15)));
5328
+ /** How often a server looks at the file. A look is a `stat`; only a stale file costs a download. */
5329
+ const checkEvery = Duration.minutes(1);
5330
+ var RefreshError = class extends Schema.TaggedError()("ModelCatalog.RefreshError", {
5331
+ path: Schema.String,
5332
+ detail: Schema.String
5333
+ }) {
5334
+ get message() {
5335
+ return `failed to refresh the model catalog at ${this.path}: ${this.detail}`;
5336
+ }
5337
+ };
5338
+ /** Point aikit at `<home>/models.gen.json`, which `CODEWORK_MODELS_FILE` overrides. */
5339
+ const use = (home) => {
5340
+ const own = hostPath.resolve(home, filename);
5341
+ Model.configureCatalog(own);
5342
+ const path = Model.catalogPath();
5343
+ return {
5344
+ path,
5345
+ pinned: path !== own
5346
+ };
5347
+ };
5348
+ const modified = (path) => fileSystem.stat(path).pipe(Effect.map((info) => Option.map(info.mtime, (date) => date.getTime())), Effect.orElseSucceed(() => Option.none()));
5349
+ const stale = Effect.fn("ModelCatalog.stale")(function* (path) {
5350
+ const mtime = yield* modified(path);
5351
+ if (Option.isNone(mtime)) return true;
5352
+ const ttl = yield* ttlConfig;
5353
+ return (yield* Clock.currentTimeMillis) - mtime.value >= Duration.toMillis(ttl);
5354
+ });
5355
+ const download$1 = (path) => Effect.tryPromise({
5356
+ try: () => generateModels({ path }),
5357
+ catch: (cause) => new RefreshError({
5358
+ path,
5359
+ detail: cause instanceof Error ? cause.message : String(cause)
5360
+ })
5361
+ });
5362
+ /** Download a new catalog for the home when it is missing, older than the TTL, or `force` asks. */
5363
+ const refresh$1 = Effect.fn("ModelCatalog.refresh")(function* (home, options = {}) {
5364
+ const { path } = use(home);
5365
+ if (options.force !== true && !(yield* stale(path))) return {
5366
+ path,
5367
+ refreshed: false
5368
+ };
5369
+ yield* download$1(path);
5370
+ return {
5371
+ path,
5372
+ refreshed: true
5373
+ };
5374
+ });
5375
+ /**
5376
+ * The automatic check: refresh a stale catalog unless it is pinned, and never fail. A failed
5377
+ * download leaves the old file in place; a missing one surfaces later as a
5378
+ * `Runner.ModelCatalogError` from whatever needed it.
5379
+ */
5380
+ const check$1 = Effect.fn("ModelCatalog.check")(function* ({ path, pinned }) {
5381
+ if (pinned || !(yield* stale(path))) return;
5382
+ yield* download$1(path);
5383
+ });
5384
+ const quietly = (effect) => effect.pipe(Effect.asVoid, Effect.catch((error) => Effect.logWarning(`model catalog: ${error.message}`)));
5385
+ /** Check the home's catalog once, for a command that runs and exits; returns the file aikit reads. */
5386
+ const sync = Effect.fn("ModelCatalog.sync")(function* (home) {
5387
+ const target = use(home);
5388
+ yield* quietly(check$1(target));
5389
+ return target.path;
5390
+ });
5391
+ /**
5392
+ * Check at boot, and with `watch`, keep checking for the life of the layer. A file whose mtime
5393
+ * moved -- refreshed here, by `codework models refresh`, or by hand -- is reloaded, so the
5394
+ * process never serves a catalog older than what is on disk.
5395
+ */
5396
+ const layer$7 = (options) => Layer.effectDiscard(Effect.gen(function* () {
4958
5397
  const target = use(options.home);
4959
5398
  yield* quietly(check$1(target));
4960
5399
  if (!options.watch) return;
@@ -4963,7 +5402,7 @@ const layer$8 = (options) => Layer.effectDiscard(Effect.gen(function* () {
4963
5402
  const tick = Effect.gen(function* () {
4964
5403
  yield* quietly(check$1(target));
4965
5404
  const mtime = yield* current;
4966
- if (mtime !== (yield* Ref.getAndSet(seen, mtime))) Model$1.reloadCatalog();
5405
+ if (mtime !== (yield* Ref.getAndSet(seen, mtime))) Model.reloadCatalog();
4967
5406
  });
4968
5407
  yield* Effect.forkScoped(tick.pipe(Effect.repeat(Schedule.spaced(checkEvery))));
4969
5408
  }));
@@ -4980,79 +5419,112 @@ const loadError = (cause) => {
4980
5419
  });
4981
5420
  };
4982
5421
  const catalogError = (cause) => loadError(cause) ?? new ModelCatalogError({
4983
- path: Model$1.catalogPath(),
5422
+ path: Model.catalogPath(),
4984
5423
  reason: "unreadable",
4985
5424
  detail: cause instanceof Error ? cause.message : "failed to load the model catalog"
4986
5425
  });
4987
5426
  const models = Effect.tryPromise({
4988
- try: () => Model$1.getBuiltInModels(),
5427
+ try: () => Model.getBuiltInModels(),
4989
5428
  catch: catalogError
4990
5429
  });
4991
5430
  const providers = Effect.tryPromise({
4992
- try: () => Model$1.getProviders(),
5431
+ try: () => Model.getProviders(),
4993
5432
  catch: catalogError
4994
5433
  });
4995
- //#endregion
4996
- //#region src/plugin/plugin.ts
4997
- var plugin_exports$1 = /* @__PURE__ */ __exportAll({});
4998
- import * as import__codeworksh_plugin_plugin from "@codeworksh/plugin/plugin";
4999
- __reExport(plugin_exports$1, import__codeworksh_plugin_plugin);
5000
- //#endregion
5001
- //#region src/plugin/builtin/prompt/default.ts
5002
- /** The default coding-agent foundation, used unless `promptCustom` replaces it. */
5003
- const foundation = `You are an expert coding assistant operating inside codework, a coding agent harness.`;
5004
5434
  /**
5005
- * Guidelines that hold regardless of which tools are registered.
5006
- *
5007
- * Kept short on purpose. Every line here is spent on every request, so a line
5008
- * earns its place only if a model measurably behaves worse without it.
5435
+ * Where this process reads OAuth logins: `<home>/aikit/auth.json` when a home was chosen,
5436
+ * otherwise `undefined`, which lets aikit resolve it (`CODEWORK_CREDENTIALS`, then the default home).
5437
+ * One value, so the model calls and {@link available} can never read different files.
5009
5438
  */
5010
- const standingGuidelines = [
5011
- "Be concise. Report what you did and what you found, not what you are about to do.",
5012
- "Quote exact paths and command output rather than paraphrasing them.",
5013
- "If a command fails, read the error before retrying."
5014
- ];
5015
- /** Collapse whitespace so two spellings of one guideline dedupe against each other. */
5016
- const normalize$1 = (value) => value.trim().replace(/\s+/g, " ");
5439
+ var Credentials = class extends Context.Service()("@codeworksh/harness/model/catalog/Credentials") {};
5440
+ /** The storage options an aikit OAuth store takes for {@link Credentials.authFile}. */
5441
+ const oauthStorage = (authFile) => authFile === void 0 ? {} : { path: authFile };
5442
+ /** Whether an OAuth login is stored for the protocol. Reads the file; never refreshes a token. */
5443
+ const loggedIn = (protocol, authFile) => {
5444
+ const storage = oauthStorage(authFile);
5445
+ const store = protocol === "openai-codex" ? new JsonOpenAICodexAuthStorage(storage) : protocol === "github-copilot" ? new JsonGitHubCopilotAuthStorage(storage) : void 0;
5446
+ if (store === void 0) return Effect.succeed(false);
5447
+ return Effect.tryPromise(() => store.get()).pipe(Effect.map((stored) => stored !== void 0), Effect.orElseSucceed(() => false));
5448
+ };
5017
5449
  /**
5018
- * Tool-contributed guidelines followed by the standing ones, normalized and
5019
- * deduplicated with first occurrence winning.
5020
- *
5021
- * Tool order is bucket order, which is the same order the index renders in and the
5022
- * same order the provider receives definitions in -- one ordering, not three.
5450
+ * A settings entry as the catalog reads it. Settings entries are read exactly like generated ones;
5451
+ * the one difference is `provider.key`, written `"${NAME}"`, which becomes the variable name aikit
5452
+ * looks up when it calls the model.
5023
5453
  */
5024
- const guidelines = (tools) => {
5025
- const seen = /* @__PURE__ */ new Set();
5026
- const collected = [];
5027
- for (const line of [...tools.flatMap((tool) => tool.promptGuidelines ?? []), ...standingGuidelines]) {
5028
- const normalized = normalize$1(line);
5029
- if (normalized.length === 0 || seen.has(normalized)) continue;
5030
- seen.add(normalized);
5031
- collected.push(normalized);
5454
+ const fromSettings = (entry) => entry.provider.key === void 0 ? entry : {
5455
+ ...entry,
5456
+ provider: {
5457
+ ...entry.provider,
5458
+ key: (0, schema_exports$3.keyName)(entry.provider.key)
5032
5459
  }
5033
- return collected;
5034
5460
  };
5461
+ /** The settings entry for a model, if one declares it: the last declaration wins. */
5462
+ const entry = (declared, provider, id) => {
5463
+ const found = declared.findLast((one) => one.entry.provider.id === provider && one.entry.id === id);
5464
+ return found === void 0 ? void 0 : fromSettings(found.entry);
5465
+ };
5466
+ /**
5467
+ * The generated catalog with settings entries appended in layer order: a new provider or model is
5468
+ * added, and an entry for an existing one replaces it, as a later entry in a generated file would.
5469
+ */
5470
+ const effective = Effect.fn("ModelCatalog.effective")(function* (declared) {
5471
+ const catalog = { ...yield* models };
5472
+ for (const { entry: one } of declared) catalog[one.provider.id] = {
5473
+ ...catalog[one.provider.id],
5474
+ [one.id]: fromSettings(one)
5475
+ };
5476
+ return catalog;
5477
+ });
5478
+ const oauthProtocols = /* @__PURE__ */ new Set(["openai-codex", "github-copilot"]);
5035
5479
  /**
5036
- * The rendered tool index.
5480
+ * The effective catalog's providers this process has credentials for, optionally only those
5481
+ * credentialed one way. `hostDir` selects the project whose settings entries apply; without it,
5482
+ * only the user's.
5037
5483
  *
5038
- * `(none)` rather than an omitted section: a model told it has no tools behaves
5039
- * better than one left to infer it from silence.
5484
+ * `apiKey`: the provider's `key` or `env` names are set in the host environment (they include the
5485
+ * OAuth providers' env tokens). `oauth`: a login is stored in the {@link Credentials} file. `none`:
5486
+ * the provider names no key and isn't an OAuth provider. A provider with a key and a login reports
5487
+ * `apiKey`, the one a model call uses first.
5488
+ *
5489
+ * Presence, not validity: nothing is sent over the network, so a revoked key still counts. A
5490
+ * client showing a model picker should still add the session's current model.
5040
5491
  */
5041
- const toolIndex = (tools) => {
5042
- const listed = tools.filter((tool) => tool.promptSnippet !== void 0 && tool.promptSnippet.length > 0);
5043
- if (listed.length === 0) return "(none)";
5044
- return listed.map((tool) => `- ${tool.name}: ${tool.promptSnippet}`).join("\n");
5045
- };
5046
- const assemble = (input) => {
5047
- const sections = [input.promptCustom ?? foundation];
5048
- sections.push(`Available tools:\n${toolIndex(input.tools)}`);
5049
- const lines = guidelines(input.tools);
5050
- if (lines.length > 0) sections.push(`Guidelines:\n${lines.map((line) => `- ${line}`).join("\n")}`);
5051
- const append = input.promptSystemAppend?.trim();
5052
- if (append !== void 0 && append.length > 0) sections.push(append);
5053
- sections.push(`Current working directory: ${input.directory}`);
5054
- return sections.join("\n\n");
5055
- };
5492
+ const available = Effect.fn("ModelCatalog.available")(function* (filter = {}) {
5493
+ const { authFile } = yield* Credentials;
5494
+ const settings = yield* Service$3;
5495
+ const catalog = yield* effective((yield* settings.load(filter.hostDir)).models);
5496
+ const usable = [];
5497
+ for (const [id, entries] of Object.entries(catalog)) {
5498
+ const first = entries === void 0 ? void 0 : Object.values(entries)[0];
5499
+ if (entries === void 0 || first === void 0) continue;
5500
+ const keyless = first.provider.key === void 0 && first.provider.env.length === 0 && !oauthProtocols.has(first.protocol);
5501
+ const credential = getEnvApiKey(first.provider) !== void 0 ? "apiKey" : (yield* loggedIn(first.protocol, authFile)) ? "oauth" : keyless ? "none" : void 0;
5502
+ if (credential === void 0 || filter.credential !== void 0 && filter.credential !== credential) continue;
5503
+ usable.push({
5504
+ id,
5505
+ credential,
5506
+ models: entries
5507
+ });
5508
+ }
5509
+ return usable;
5510
+ });
5511
+ //#endregion
5512
+ //#region src/plugin/plugin.ts
5513
+ var plugin_exports$1 = /* @__PURE__ */ __exportAll({});
5514
+ import * as import__codeworksh_plugin_plugin from "@codeworksh/plugin/plugin";
5515
+ __reExport(plugin_exports$1, import__codeworksh_plugin_plugin);
5516
+ //#endregion
5517
+ //#region src/plugin/builtin/prompt/default.ts
5518
+ /** The default prompt foundation, used unless `promptCustom` replaces it. */
5519
+ const foundation = "You are an expert coding assistant operating inside codework, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
5520
+ /** Every line here is spent on every request; keep it short. */
5521
+ const standingRules = ["Be concise in your responses", "Show file paths clearly when working with files"];
5522
+ /** Bash stands in for file tools only until dedicated ones are registered. */
5523
+ const toolRules = (names) => names.has("bash") && ![
5524
+ "grep",
5525
+ "find",
5526
+ "ls"
5527
+ ].some((name) => names.has(name)) ? ["Use bash for file operations like ls, rg, find"] : [];
5056
5528
  /**
5057
5529
  * A caller slot, awaited lazily. A throw or rejection fails the snapshot, which the
5058
5530
  * host attributes to this plugin's id.
@@ -5063,13 +5535,81 @@ const defaultPromptPlugin = (0, plugin_exports$1.define)({
5063
5535
  kind: "prompt",
5064
5536
  setup: Effect.fn("DefaultPromptPlugin.setup")(function* (ctx) {
5065
5537
  const custom = yield* slot(ctx, ctx.config.promptCustom);
5066
- const append = yield* slot(ctx, ctx.config.promptSystemAppend);
5067
- ctx.plugin.prompt.set(assemble({
5068
- tools: ctx.plugin.tools.list(),
5069
- directory: ctx.location.directory,
5070
- ...custom === void 0 ? {} : { promptCustom: custom },
5071
- ...append === void 0 ? {} : { promptSystemAppend: append }
5072
- }));
5538
+ const append = (yield* slot(ctx, ctx.config.promptSystemAppend))?.trim();
5539
+ const prompt = ctx.plugin.prompt;
5540
+ prompt.foundation.set(custom ?? foundation);
5541
+ const names = new Set(ctx.plugin.tools.list().map((tool) => tool.name));
5542
+ for (const rule of [...toolRules(names), ...standingRules]) prompt.sections.append(Section.Rules, rule);
5543
+ if (append !== void 0 && append.length > 0) prompt.sections.append(Section.Addendum, append);
5544
+ })
5545
+ });
5546
+ //#endregion
5547
+ //#region src/plugin/builtin/prompt/instruction.ts
5548
+ /** First readable match per directory wins. */
5549
+ const names = [
5550
+ "AGENTS.override.md",
5551
+ "CLAUDE.override.md",
5552
+ "AGENTS.md",
5553
+ "AGENTS.MD",
5554
+ "CLAUDE.md",
5555
+ "CLAUDE.MD"
5556
+ ];
5557
+ const content = (raw) => {
5558
+ const text = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
5559
+ return text.trim().length === 0 ? void 0 : text;
5560
+ };
5561
+ const first = Effect.fnUntraced(function* (fs, directory) {
5562
+ for (const name of names) {
5563
+ const path = posix.join(directory, name);
5564
+ if (!(yield* fs.stat(path).pipe(Effect.map((stat) => stat.isFile), Effect.orElseSucceed(() => false)))) continue;
5565
+ const raw = yield* fs.readFile(path).pipe(Effect.tapError((cause) => Effect.logWarning(`could not read instruction file ${path}`, cause)), Effect.option);
5566
+ if (Option.isSome(raw)) return {
5567
+ name,
5568
+ path,
5569
+ content: content(raw.value)
5570
+ };
5571
+ }
5572
+ });
5573
+ /** Only the path is escaped; file content goes in verbatim. */
5574
+ const attribute = (value) => value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;");
5575
+ /**
5576
+ * The main checkout's directory when the cwd is in a linked worktree nested inside it (e.g.
5577
+ * `repo/.claude/worktrees/x`), so its same-named file is not applied twice. Sibling worktrees,
5578
+ * bare layouts and submodules are never shadowed.
5579
+ */
5580
+ const shadowing = (location) => {
5581
+ const store = location.project.vcs?.store;
5582
+ if (location.space.kind !== "linked" || store === void 0 || posix.basename(store) !== ".git") return void 0;
5583
+ const main = posix.dirname(store);
5584
+ return location.space.location.startsWith(`${main}/`) ? main : void 0;
5585
+ };
5586
+ const instructionPlugin = (0, plugin_exports$1.define)({
5587
+ id: "codework.prompt.instruction",
5588
+ kind: "prompt",
5589
+ setup: Effect.fn("InstructionPlugin.setup")(function* (ctx) {
5590
+ const fs = yield* io_exports.SandboxIO.FileSystem;
5591
+ const probed = /* @__PURE__ */ new Map();
5592
+ const at = Effect.fnUntraced(function* (directory) {
5593
+ if (!probed.has(directory)) probed.set(directory, yield* first(fs, directory));
5594
+ return probed.get(directory);
5595
+ });
5596
+ const walked = [];
5597
+ const main = shadowing(ctx.location);
5598
+ const worktree = main === void 0 ? void 0 : yield* at(ctx.location.space.location);
5599
+ let current = ctx.location.directory;
5600
+ while (true) {
5601
+ const here = yield* at(current);
5602
+ const shadowed = current === main && worktree !== void 0 && here?.name === worktree.name;
5603
+ if (here !== void 0 && !shadowed) walked.push(here);
5604
+ const parent = posix.dirname(current);
5605
+ if (parent === current) break;
5606
+ current = parent;
5607
+ }
5608
+ const files = walked.reverse().filter((file) => file.content !== void 0);
5609
+ if (files.length === 0) return;
5610
+ const prompt = ctx.plugin.prompt;
5611
+ prompt.sections.append(Section.ProjectContext, "Project-Specific Instructions & Guidelines:");
5612
+ for (const file of files) prompt.sections.append(Section.ProjectContext, `<project_instructions path="${attribute(file.path)}">\n${file.content}\n</project_instructions>`);
5073
5613
  })
5074
5614
  });
5075
5615
  //#endregion
@@ -5301,7 +5841,7 @@ var Accumulator = class {
5301
5841
  ensureTempFile() {
5302
5842
  if (this.tempFile !== void 0) return Effect.void;
5303
5843
  return Effect.gen({ self: this }, function* () {
5304
- const suffix = Encoding.encodeHex(yield* crypto.randomBytes(8));
5844
+ const suffix = Hex.encode(yield* crypto.randomBytes(8));
5305
5845
  this.tempFilePath = defaultTempFilePath(this.tempFilePrefix, suffix);
5306
5846
  this.tempFile = yield* fileSystem.open(this.tempFilePath, { flag: "w" });
5307
5847
  for (const chunk of this.rawChunks) yield* this.tempFile.writeAll(chunk);
@@ -5480,7 +6020,7 @@ const footer = (t, fullOutputPath, lastLineBytes) => {
5480
6020
  };
5481
6021
  /** Write full output to a host temp file (best-effort; undefined on failure). */
5482
6022
  const spillToTempFile = (content) => Effect.gen(function* () {
5483
- const suffix = Encoding.encodeHex(yield* crypto.randomBytes(6));
6023
+ const suffix = Hex.encode(yield* crypto.randomBytes(6));
5484
6024
  const path = posix.join(tmpdir(), `codework-bash-${suffix}.log`);
5485
6025
  yield* fileSystem.writeFileString(path, content);
5486
6026
  return path;
@@ -5583,36 +6123,13 @@ const bashPlugin = (0, plugin_exports$1.define)({
5583
6123
  //#region src/plugin/builtin.ts
5584
6124
  /**
5585
6125
  * Every plugin the harness ships, and the selection a caller who passes no `plugins` gets. Tool
5586
- * contributors come first so the Prompt plugin that indexes them runs after they registered.
5587
- */
5588
- const builtins = Object.freeze([bashPlugin, defaultPromptPlugin]);
5589
- //#endregion
5590
- //#region src/settings/merge.ts
5591
- /**
5592
- * A plain string-keyed record: what a settings patch merges, and what a plugin's `options` block
5593
- * has to be. A Map, a typed array or a class instance is an object to `typeof` and not one of
5594
- * these — `Object.freeze` alone does not tell them apart, and it throws on some of them.
6126
+ * contributors come first; within the prompt domain, declaration order is run order.
5595
6127
  */
5596
- const isRecord$1 = (value) => Predicate.isObject(value) && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
5597
- const copy = (value) => {
5598
- if (Array.isArray(value)) return value.map(copy);
5599
- return isRecord$1(value) ? merge({}, value) : value;
5600
- };
5601
- /** Null/undefined properties are absent, objects merge, and arrays replace. Never mutates inputs. */
5602
- function merge(base, ...patches) {
5603
- const result = /* @__PURE__ */ new Map();
5604
- for (const patch of [base, ...patches]) {
5605
- if (patch === void 0) continue;
5606
- for (const [key, value] of Object.entries(patch)) {
5607
- if (value === null || value === void 0) continue;
5608
- const previous = result.get(key);
5609
- result.set(key, isRecord$1(previous) && isRecord$1(value) ? merge(previous, value) : copy(value));
5610
- }
5611
- }
5612
- return Object.fromEntries(result);
5613
- }
5614
- /** Normalize nullable properties before schema validation, without changing array positions. */
5615
- const normalize = copy;
6128
+ const builtins = Object.freeze([
6129
+ bashPlugin,
6130
+ defaultPromptPlugin,
6131
+ instructionPlugin
6132
+ ]);
5616
6133
  //#endregion
5617
6134
  //#region src/util/module.ts
5618
6135
  /*!
@@ -6169,7 +6686,7 @@ const isPath = (reference) => reference.startsWith("file:") || reference.startsW
6169
6686
  * - a **CLI argument** anchors to the process's startup directory, because that is where the
6170
6687
  * person typed it.
6171
6688
  */
6172
- const parse$2 = (reference, from) => Effect.gen(function* () {
6689
+ const parse$1 = (reference, from) => Effect.gen(function* () {
6173
6690
  const fail = (reason, message) => new SourceError({
6174
6691
  reason,
6175
6692
  reference,
@@ -6386,7 +6903,7 @@ const slugOf = (target) => target.kind === "git" ? target.slug : canonical$1(tar
6386
6903
  */
6387
6904
  const digest = Effect.fn("PluginStore.digest")(function* (target, cache, from) {
6388
6905
  const source = target.kind === "registry" ? `${yield* registry(target, cache, from)}\0${target.spec}` : target.spec;
6389
- return Encoding.encodeHex(yield* crypto.digest("SHA-256", new TextEncoder().encode(source)));
6906
+ return Hex.encode(yield* crypto.digest("SHA-256", new TextEncoder().encode(source)));
6390
6907
  });
6391
6908
  const entryDir = Effect.fn("PluginStore.entryDir")(function* (target, cache, from) {
6392
6909
  const key = yield* digest(target, cache, from);
@@ -6790,7 +7307,7 @@ const localUrl = Effect.fn("PluginLoader.localUrl")(function* (location, origin)
6790
7307
  */
6791
7308
  const install = Effect.fn("PluginLoader.install")(function* (target, cache, origin, options) {
6792
7309
  const added = yield* add(target, cache, {
6793
- from: anchor$1(origin.file, options.hostDir),
7310
+ from: anchor(origin.file, options.hostDir),
6794
7311
  validate: (fetched) => imported(url(fetched.entrypoint), origin, options.import)
6795
7312
  });
6796
7313
  return {
@@ -6806,13 +7323,13 @@ const install = Effect.fn("PluginLoader.install")(function* (target, cache, orig
6806
7323
  * package boundary discovered upward from the chain's start -- the root itself -- so anchoring at
6807
7324
  * the file's own directory would miss it. Absent a declaring file the host context stands in.
6808
7325
  */
6809
- const anchor$1 = (file, fallback) => {
7326
+ const anchor = (file, fallback) => {
6810
7327
  if (file === void 0) return fallback;
6811
7328
  const directory = hostPath.dirname(file);
6812
7329
  return hostPath.basename(directory) === ".codework" ? hostPath.dirname(directory) : directory;
6813
7330
  };
6814
7331
  /** The default installer, or the one the caller injected. */
6815
- const installer = (target, origin, options) => options.install === void 0 ? install(target, options.cache, origin, options) : options.install(target, options.cache, anchor$1(origin.file, options.hostDir));
7332
+ const installer = (target, origin, options) => options.install === void 0 ? install(target, options.cache, origin, options) : options.install(target, options.cache, anchor(origin.file, options.hostDir));
6816
7333
  /** Import a module and confirm it default-exports a plugin. */
6817
7334
  const imported = Effect.fn("PluginLoader.imported")(function* (url, origin, load) {
6818
7335
  const module = yield* Effect.tryPromise({
@@ -6844,15 +7361,15 @@ const inspect = Effect.fn("PluginLoader.inspect")(function* (reference, options)
6844
7361
  reference,
6845
7362
  file: options.file
6846
7363
  };
6847
- const target = yield* parse$2(reference, options.hostDir);
6848
- const loaded = yield* load$3(target, origin, options);
7364
+ const target = yield* parse$1(reference, options.hostDir);
7365
+ const loaded = yield* load$2(target, origin, options);
6849
7366
  return {
6850
7367
  id: loaded.plugin.id,
6851
7368
  ...loaded.version === void 0 ? {} : { version: loaded.version },
6852
7369
  ...loaded.name === void 0 ? {} : { name: loaded.name }
6853
7370
  };
6854
7371
  });
6855
- const load$3 = Effect.fn("PluginLoader.load")(function* (target, origin, options) {
7372
+ const load$2 = Effect.fn("PluginLoader.load")(function* (target, origin, options) {
6856
7373
  const installed = yield* target.kind === "local" ? localUrl(target.path, origin).pipe(Effect.mapError((cause) => sourceFailure(origin, cause))) : installer(target, origin, options).pipe(Effect.mapError((cause) => origin.file === void 0 ? cause : declaredIn(cause, origin.file)));
6857
7374
  const address = target.kind === "local" && options.reload !== void 0 && options.reload > 0 ? `${installed.url}?reload=${options.reload}` : installed.url;
6858
7375
  return {
@@ -6870,7 +7387,7 @@ const load$3 = Effect.fn("PluginLoader.load")(function* (target, origin, options
6870
7387
  * are one package, and `./plugins/x.ts` is the file it resolves to from the directory that
6871
7388
  * declared it. Pure -- nothing is installed, imported or read.
6872
7389
  */
6873
- const canonical = (reference, hostDir) => Effect.map(parse$2(reference, hostDir), canonical$1);
7390
+ const canonical = (reference, hostDir) => Effect.map(parse$1(reference, hostDir), canonical$1);
6874
7391
  //#endregion
6875
7392
  //#region src/plugin/catalog.ts
6876
7393
  /**
@@ -6897,7 +7414,7 @@ const isPatch = (reference) => Predicate.isObject(reference) && !Predicate.isFun
6897
7414
  *
6898
7415
  * Loading is keyed by what a reference resolves to, so naming one module twice imports it once.
6899
7416
  */
6900
- const load$2 = Effect.fn("PluginCatalog.load")(function* (references, options) {
7417
+ const load$1 = Effect.fn("PluginCatalog.load")(function* (references, options) {
6901
7418
  const plugins = /* @__PURE__ */ new Map();
6902
7419
  const aliases = /* @__PURE__ */ new Map();
6903
7420
  const versions = /* @__PURE__ */ new Map();
@@ -6932,14 +7449,14 @@ const load$2 = Effect.fn("PluginCatalog.load")(function* (references, options) {
6932
7449
  remember(plugin, []);
6933
7450
  continue;
6934
7451
  }
6935
- const target = yield* parse$2(reference, options.hostDir);
7452
+ const target = yield* parse$1(reference, options.hostDir);
6936
7453
  const key = target.kind === "local" ? target.path : target.spec;
6937
7454
  const already = seen.get(key);
6938
7455
  if (already !== void 0) {
6939
7456
  aliases.set(reference, already);
6940
7457
  continue;
6941
7458
  }
6942
- const definition = yield* load$3(target, origin, options);
7459
+ const definition = yield* load$2(target, origin, options);
6943
7460
  yield* note(definition.plugin, definition.version === void 0 ? key : `${key}@${definition.version}`);
6944
7461
  const names = target.kind === "local" ? [target.path, ...definition.name === void 0 ? [] : [definition.name]] : [canonical$1(target), target.spec];
6945
7462
  remember(definition.plugin, [reference, ...names], {
@@ -7061,12 +7578,12 @@ const follow = Effect.fn("PluginCatalog.follow")(function* (references, pool, op
7061
7578
  for (const reference of references) {
7062
7579
  if (typeof reference !== "string") continue;
7063
7580
  const owner = options.declared?.get(reference);
7064
- const from = anchor$1(owner, options.hostDir);
7581
+ const from = anchor(owner, options.hostDir);
7065
7582
  const id = pool.aliases.get(reference);
7066
7583
  if (id !== void 0) {
7067
7584
  const origin = pool.origins.get(id);
7068
7585
  if (owner === void 0 || origin?.reference !== reference) continue;
7069
- if (anchor$1(origin.file, options.hostDir) === from) continue;
7586
+ if (anchor(origin.file, options.hostDir) === from) continue;
7070
7587
  }
7071
7588
  if (Option.isSome(yield* filed(reference, from))) {
7072
7589
  moved = true;
@@ -7075,7 +7592,7 @@ const follow = Effect.fn("PluginCatalog.follow")(function* (references, pool, op
7075
7592
  }
7076
7593
  if (!moved) for (const origin of pool.origins.values()) {
7077
7594
  if (origin.generation === void 0) continue;
7078
- const current = yield* filed(origin.reference, anchor$1(origin.file, options.hostDir));
7595
+ const current = yield* filed(origin.reference, anchor(origin.file, options.hostDir));
7079
7596
  if (Option.isSome(current) && current.value.generation !== void 0 && current.value.generation > origin.generation) {
7080
7597
  moved = true;
7081
7598
  break;
@@ -7084,7 +7601,7 @@ const follow = Effect.fn("PluginCatalog.follow")(function* (references, pool, op
7084
7601
  if (!moved) return Option.none();
7085
7602
  const retained = [];
7086
7603
  for (const origin of pool.origins.values()) {
7087
- if (references.includes(origin.reference) || Option.isSome(yield* filed(origin.reference, anchor$1(origin.file, options.hostDir)))) {
7604
+ if (references.includes(origin.reference) || Option.isSome(yield* filed(origin.reference, anchor(origin.file, options.hostDir)))) {
7088
7605
  retained.push(origin);
7089
7606
  continue;
7090
7607
  }
@@ -7094,13 +7611,13 @@ const follow = Effect.fn("PluginCatalog.follow")(function* (references, pool, op
7094
7611
  const known = /* @__PURE__ */ new Map();
7095
7612
  for (const origin of retained) if (origin.file !== void 0) known.set(origin.reference, origin.file);
7096
7613
  for (const [reference, file] of options.declared ?? /* @__PURE__ */ new Map()) known.set(reference, file);
7097
- return Option.some(yield* load$2(accumulated, {
7614
+ return Option.some(yield* load$1(accumulated, {
7098
7615
  ...options,
7099
7616
  declared: known
7100
7617
  }));
7101
7618
  });
7102
7619
  Effect.fn("PluginCatalog.prepare")(function* (references, options) {
7103
- const pool = yield* load$2(references, options);
7620
+ const pool = yield* load$1(references, options);
7104
7621
  return (yield* select(references, pool)).selection;
7105
7622
  });
7106
7623
  //#endregion
@@ -7178,7 +7695,7 @@ const make$7 = (options) => Effect.gen(function* () {
7178
7695
  });
7179
7696
  //#endregion
7180
7697
  //#region src/runner/execute.ts
7181
- const layer$7 = Layer.effect(Service$6, Effect.gen(function* () {
7698
+ const layer$6 = Layer.effect(Service$6, Effect.gen(function* () {
7182
7699
  const store = yield* Service$14;
7183
7700
  const sandbox = yield* Controller;
7184
7701
  const sql = yield* SqlClient.SqlClient;
@@ -7244,11 +7761,6 @@ const layer$7 = Layer.effect(Service$6, Effect.gen(function* () {
7244
7761
  });
7245
7762
  }));
7246
7763
  //#endregion
7247
- //#region src/settings/schema.ts
7248
- var schema_exports$3 = /* @__PURE__ */ __exportAll({});
7249
- import * as import__codeworksh_plugin_settings from "@codeworksh/plugin/settings";
7250
- __reExport(schema_exports$3, import__codeworksh_plugin_settings);
7251
- //#endregion
7252
7764
  //#region src/settings/resolve.ts
7253
7765
  const patternMatches = (pattern, id) => new RegExp(`^${pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(id);
7254
7766
  function collect(settings, provider, id) {
@@ -7287,11 +7799,12 @@ function resolveRequest(block, model) {
7287
7799
  "toolExecution",
7288
7800
  "contextWindow",
7289
7801
  "protocol",
7802
+ "apiKey",
7290
7803
  "extras"
7291
7804
  ].includes(key)));
7292
7805
  return {
7293
7806
  ...resolveOptions(block),
7294
- ...Object.keys(bag).length === 0 ? {} : { providerOptions: { [Model$1.optionsKey(model)]: bag } }
7807
+ ...Object.keys(bag).length === 0 ? {} : { providerOptions: { [Model.optionsKey(model)]: bag } }
7295
7808
  };
7296
7809
  }
7297
7810
  /**
@@ -7525,15 +8038,27 @@ const messageFailure = (message) => {
7525
8038
  };
7526
8039
  /** Resolve once before exchange setup; execution reuses this exact instance. */
7527
8040
  const resolve$1 = Effect.fn("LLM.resolve")(function* (input) {
7528
- const model = yield* Effect.tryPromise({
7529
- try: () => llm(input.provider, input.model, resolveOverrides(input.settings ?? {})),
8041
+ const overrides = resolveOverrides(input.settings ?? {});
8042
+ const declared = entry(input.models ?? [], input.provider, input.model);
8043
+ const model = declared !== void 0 ? {
8044
+ ...declared,
8045
+ ...overrides
8046
+ } : yield* Effect.tryPromise({
8047
+ try: () => llm(input.provider, input.model, overrides),
7530
8048
  catch: (cause) => loadError(cause) ?? providerErrorFromUnknown(input, cause)
7531
8049
  });
7532
8050
  if (model === void 0) return yield* new ModelNotFoundError({
7533
8051
  provider: input.provider,
7534
8052
  model: input.model
7535
8053
  });
7536
- return model;
8054
+ const apiKey = input.settings?.apiKey;
8055
+ return apiKey === void 0 ? model : {
8056
+ ...model,
8057
+ provider: {
8058
+ ...model.provider,
8059
+ key: (0, schema_exports$3.keyName)(apiKey)
8060
+ }
8061
+ };
7537
8062
  });
7538
8063
  /**
7539
8064
  * Fill in what an OAuth provider needs and the model catalog cannot carry: the
@@ -7542,7 +8067,7 @@ const resolve$1 = Effect.fn("LLM.resolve")(function* (input) {
7542
8067
  * with no stored login resolves to `{}` so aikit reports the missing key.
7543
8068
  */
7544
8069
  const oauthCredentials = async (protocol, options) => {
7545
- const storage = options.authFile === void 0 ? {} : { path: options.authFile };
8070
+ const storage = oauthStorage(options.authFile);
7546
8071
  if (protocol === "openai-codex") {
7547
8072
  const apiKey = await getOpenAICodexApiKey({ storage: new JsonOpenAICodexAuthStorage(storage) });
7548
8073
  return apiKey === void 0 ? {} : { apiKey };
@@ -7915,20 +8440,109 @@ const make$3 = (tools) => {
7915
8440
  Context.Service()("@codeworksh/harness/tool/registry/ToolRegistry");
7916
8441
  const make$2 = () => {
7917
8442
  let open = true;
7918
- let value;
8443
+ let foundation;
8444
+ const sections = /* @__PURE__ */ new Map();
8445
+ const writable = () => {
8446
+ if (!open) throw new Error("prompt registry is closed");
8447
+ };
8448
+ const slot = (section) => {
8449
+ writable();
8450
+ const reason = Section.invalid(section.name);
8451
+ if (reason !== void 0) throw new Error(reason);
8452
+ if (!Section.isFormat(section.format)) throw new Error(`prompt section ${section.name} has an unknown format: ${JSON.stringify(section.format)}`);
8453
+ const builtin = Section.builtins.find((candidate) => candidate.name === section.name);
8454
+ if (builtin !== void 0 && builtin.format !== section.format) throw new Error(`prompt section ${section.name} is built in as "${builtin.format}"`);
8455
+ const existing = sections.get(section.name);
8456
+ if (existing === void 0) {
8457
+ const opened = {
8458
+ format: section.format,
8459
+ entries: []
8460
+ };
8461
+ sections.set(section.name, opened);
8462
+ return opened;
8463
+ }
8464
+ if (existing.format !== section.format) throw new Error(`prompt section ${section.name} is "${existing.format}"; a write as "${section.format}" cannot merge into it`);
8465
+ return existing;
8466
+ };
7919
8467
  return {
7920
8468
  registry: Object.freeze({
7921
- set: (prompt) => {
7922
- if (!open) throw new Error("prompt registry is closed");
7923
- value = prompt;
7924
- },
7925
- get: () => value
8469
+ foundation: Object.freeze({
8470
+ set: (text) => {
8471
+ writable();
8472
+ foundation = text;
8473
+ },
8474
+ get: () => foundation
8475
+ }),
8476
+ sections: Object.freeze({
8477
+ append: (section, entry) => {
8478
+ slot(section).entries.push(entry);
8479
+ },
8480
+ set: (section, entries) => {
8481
+ slot(section).entries = [...entries];
8482
+ },
8483
+ remove: (section) => {
8484
+ writable();
8485
+ sections.delete(section.name);
8486
+ },
8487
+ get: (section) => {
8488
+ const found = sections.get(section.name);
8489
+ return found === void 0 ? void 0 : Object.freeze([...found.entries]);
8490
+ }
8491
+ })
7926
8492
  }),
7927
8493
  close: () => {
7928
8494
  open = false;
7929
8495
  },
7930
- value: () => value
8496
+ snapshot: () => ({
8497
+ foundation,
8498
+ sections: new Map(Array.from(sections, ([name, { format, entries }]) => [name, {
8499
+ format,
8500
+ entries: [...entries]
8501
+ }]))
8502
+ })
8503
+ };
8504
+ };
8505
+ //#endregion
8506
+ //#region src/plugin/prompt/render.ts
8507
+ const tag = (name, body) => `<${name}>\n${body}\n</${name}>`;
8508
+ /** Two spellings of one list entry dedupe against each other. */
8509
+ const normalize = (value) => value.trim().replace(/\s+/g, " ");
8510
+ const body = ({ format, entries }) => {
8511
+ if (format === "text") {
8512
+ const kept = entries.filter((entry) => entry.trim().length > 0);
8513
+ return kept.length === 0 ? void 0 : kept.join("\n\n");
8514
+ }
8515
+ const seen = /* @__PURE__ */ new Set();
8516
+ for (const entry of entries) {
8517
+ const normalized = normalize(entry);
8518
+ if (normalized.length > 0) seen.add(normalized);
8519
+ }
8520
+ return seen.size === 0 ? void 0 : Array.from(seen, (line) => `- ${line}`).join("\n");
8521
+ };
8522
+ /** `(none)` rather than no section: a model told it has no tools behaves better than one left guessing. */
8523
+ const toolIndex = (tools) => {
8524
+ const listed = tools.filter((tool) => tool.promptSnippet !== void 0 && tool.promptSnippet.length > 0);
8525
+ if (listed.length === 0) return "(none)";
8526
+ return listed.map((tool) => `- ${tool.name}: ${tool.promptSnippet}`).join("\n");
8527
+ };
8528
+ const render = (input) => {
8529
+ const written = new Map(input.prompt.sections);
8530
+ const guidelines = input.tools.flatMap((tool) => tool.promptGuidelines ?? []);
8531
+ written.set(Section.Rules.name, {
8532
+ format: Section.Rules.format,
8533
+ entries: [...guidelines, ...written.get(Section.Rules.name)?.entries ?? []]
8534
+ });
8535
+ const parts = [input.prompt.foundation ?? "You are an expert coding assistant operating inside codework, a coding agent harness.", tag("tools", toolIndex(input.tools))];
8536
+ const push = (name) => {
8537
+ const section = written.get(name);
8538
+ const rendered = section === void 0 ? void 0 : body(section);
8539
+ if (rendered !== void 0) parts.push(tag(name, rendered));
8540
+ written.delete(name);
7931
8541
  };
8542
+ for (const section of Section.builtins) push(section.name);
8543
+ parts.push(tag("cwd", input.directory));
8544
+ for (const name of Array.from(written.keys())) push(name);
8545
+ return parts.join("\n\n");
7932
8546
  };
7933
8547
  //#endregion
7934
8548
  //#region src/plugin/tool/registry.ts
@@ -8020,11 +8634,16 @@ const make = () => {
8020
8634
  prompt: prompt.registry
8021
8635
  }),
8022
8636
  close,
8023
- freeze: () => {
8637
+ freeze: (directory) => {
8024
8638
  close();
8639
+ const resolved = make$3(tools.entries()).resolve();
8025
8640
  return {
8026
- tools: make$3(tools.entries()).resolve(),
8027
- systemPrompt: prompt.value() ?? "You are an AI agent for CodeWork. CodeWork is the best agent harness for code & work."
8641
+ tools: resolved,
8642
+ systemPrompt: render({
8643
+ prompt: prompt.snapshot(),
8644
+ tools: resolved.defs,
8645
+ directory
8646
+ })
8028
8647
  };
8029
8648
  }
8030
8649
  };
@@ -8069,7 +8688,7 @@ const run = Effect.fn("PluginHost.run")(function* (selection, input) {
8069
8688
  }));
8070
8689
  }));
8071
8690
  return yield* Effect.try({
8072
- try: buckets.freeze,
8691
+ try: () => buckets.freeze(input.location.directory),
8073
8692
  catch: (cause) => new SetupError({
8074
8693
  message: `plugin snapshot freeze failed: ${cause instanceof Error ? cause.message : String(cause)}`,
8075
8694
  cause
@@ -8079,8 +8698,8 @@ const run = Effect.fn("PluginHost.run")(function* (selection, input) {
8079
8698
  });
8080
8699
  //#endregion
8081
8700
  //#region src/session/runtime.ts
8082
- var Service$3 = class extends Context.Service()("@codeworksh/harness/session/runtime/Service") {};
8083
- const layer$6 = Layer.effect(Service$3, Effect.gen(function* () {
8701
+ var Service$2 = class extends Context.Service()("@codeworksh/harness/session/runtime/Service") {};
8702
+ const layer$5 = Layer.effect(Service$2, Effect.gen(function* () {
8084
8703
  const bindings = yield* Ref.make(/* @__PURE__ */ new Map());
8085
8704
  const get = (sessionId) => Ref.get(bindings).pipe(Effect.map((entries) => Option.fromUndefinedOr(entries.get(sessionId))));
8086
8705
  const set = (sessionId, value) => Ref.update(bindings, (entries) => {
@@ -8101,7 +8720,7 @@ const layer$6 = Layer.effect(Service$3, Effect.gen(function* () {
8101
8720
  next.delete(sessionId);
8102
8721
  return next;
8103
8722
  });
8104
- return Service$3.of({
8723
+ return Service$2.of({
8105
8724
  get,
8106
8725
  set,
8107
8726
  update,
@@ -8109,202 +8728,6 @@ const layer$6 = Layer.effect(Service$3, Effect.gen(function* () {
8109
8728
  });
8110
8729
  }));
8111
8730
  //#endregion
8112
- //#region src/settings/settings.ts
8113
- var settings_exports = /* @__PURE__ */ __exportAll({
8114
- Service: () => Service$2,
8115
- Settings: () => settings_exports,
8116
- SettingsError: () => SettingsError,
8117
- defaults: () => schema_exports$3.defaults,
8118
- layer: () => layer$5,
8119
- load: () => load$1,
8120
- modules: () => modules,
8121
- parse: () => parse$1,
8122
- paths: () => paths,
8123
- projectRoot: () => projectRoot,
8124
- userConfigDir: () => userConfigDir
8125
- });
8126
- /**
8127
- * An app-level `--user-config-dir` as the absolute directory it names: `~` is the user's home, and
8128
- * a relative spelling is relative to `hostCwd` -- never to a session's `hostDir`, which is not
8129
- * where the flag was typed.
8130
- */
8131
- const userConfigDir = (raw, hostCwd) => hostPath.resolve(hostCwd, expandTilde(raw, hostPath));
8132
- /** The settings files a directory may hold, in the order it is searched. */
8133
- const settingsIn = (directory) => [hostPath.join(directory, "settings.jsonc"), hostPath.join(directory, "settings.json")];
8134
- /** A directory and every ancestor above it, nearest first. */
8135
- const ancestors = (from) => {
8136
- const chain = [];
8137
- for (let directory = from;; directory = hostPath.dirname(directory)) {
8138
- chain.push(directory);
8139
- if (hostPath.dirname(directory) === directory) return chain;
8140
- }
8141
- };
8142
- const isDirectory = (path) => fileSystem.stat(path).pipe(Effect.map((info) => info.type === "Directory"), Effect.orElseSucceed(() => false));
8143
- /**
8144
- * The project a directory is in: the nearest ancestor holding a `.codework` **directory**.
8145
- *
8146
- * This is git's rule. `~/workspace/app/.codework` makes `~/workspace/app` a project, and
8147
- * `~/workspace/app/packages/fubar` is inside it. Nearest wins outright -- the walk stops at the
8148
- * first marker, so an inner project is never merged with an outer one.
8149
- *
8150
- * The marker is the directory, not a file in it. An empty `.codework/` is still a project root,
8151
- * which is the point: creating the directory is how a person says "a project begins here", and it
8152
- * has to mean that before any settings exist. It also makes the question one `stat` per ancestor
8153
- * whose answer does not change as files come and go inside.
8154
- *
8155
- * `<home>` is skipped. `~/.codework` is the user layer, not a project; without this, running
8156
- * anywhere under `$HOME` would find it as one and read the user layer twice -- and since plugin
8157
- * entries accumulate across layers, every user plugin would load twice.
8158
- */
8159
- const projectRoot = Effect.fn("Settings.projectRoot")(function* (from, home) {
8160
- const userConfig = hostPath.resolve(home);
8161
- for (const directory of ancestors(hostPath.resolve(from))) {
8162
- const marker = hostPath.join(directory, appConfigDir);
8163
- if (hostPath.resolve(marker) === userConfig) continue;
8164
- if (yield* isDirectory(marker)) return directory;
8165
- }
8166
- });
8167
- /**
8168
- * Ordered layers, each a group of candidates where the first file that exists is selected -- a
8169
- * group never contributes more than one file.
8170
- *
8171
- * `.jsonc` is the canonical spelling: these files are written by hand and read as JSONC, so the
8172
- * extension says what the file is and an editor validates it as such. `.json` remains a fallback
8173
- * everywhere, tried second, because the parser reads either and a file already named that must
8174
- * keep working.
8175
- *
8176
- * `root` is an already-discovered project root ({@link projectRoot}), not a directory to search
8177
- * from: the walk is a `stat` per ancestor and belongs with the other I/O. `undefined` contributes
8178
- * no project layer at all, which is what a session with no host directory gets.
8179
- */
8180
- function paths(input) {
8181
- return [settingsIn(input.userConfigDir ?? input.home), input.root === void 0 ? [] : settingsIn(hostPath.join(input.root, appConfigDir))];
8182
- }
8183
- var SettingsError = class extends Schema.TaggedError()("SettingsError", {
8184
- path: Schema.String,
8185
- reason: Schema.Literals([
8186
- "read",
8187
- "parse",
8188
- "decode"
8189
- ]),
8190
- detail: Schema.String
8191
- }) {
8192
- get message() {
8193
- return `${this.path}: ${this.reason}: ${this.detail}`;
8194
- }
8195
- };
8196
- /** `line:column`, 1-based, for a parser offset into the source. */
8197
- const at = (source, offset) => {
8198
- const before = source.slice(0, offset).split("\n");
8199
- return `${before.length}:${(before.at(-1)?.length ?? 0) + 1}`;
8200
- };
8201
- const parse$1 = Effect.fn("Settings.parse")(function* (path, source) {
8202
- const errors = [];
8203
- const json = parse(source, errors, {
8204
- allowTrailingComma: true,
8205
- disallowComments: false
8206
- });
8207
- const first = errors[0];
8208
- if (first !== void 0) return yield* new SettingsError({
8209
- path,
8210
- reason: "parse",
8211
- detail: `${printParseErrorCode(first.error)} at ${at(source, first.offset)}`
8212
- });
8213
- const document = normalize(json);
8214
- if (Predicate.isObject(document) && Predicate.isObject(json) && "plugins" in json) Object.assign(document, { plugins: json.plugins });
8215
- return yield* Schema.decodeUnknownEffect(schema_exports$3.Patch)(document).pipe(Effect.mapError((error) => new SettingsError({
8216
- path,
8217
- reason: "decode",
8218
- detail: SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues.map((issue) => `${issue.path?.map((part) => typeof part === "object" ? String(part.key) : part).join(".") ?? "settings"}: invalid value`).join("; ")
8219
- })));
8220
- });
8221
- var Service$2 = class extends Context.Service()("@codeworksh/harness/settings/settings/Service") {};
8222
- /**
8223
- * Anchor relative plugin entries to the file that declared them.
8224
- *
8225
- * A reference is otherwise resolved against the host startup directory, which is right for
8226
- * a project file sitting in it and meaningless for `~/.codework/settings.json`, where
8227
- * `./plugins/x.ts` would name a different file in every project the process is started in.
8228
- * `file:` URLs, package specs and package names are left exactly as written, and a `plugin`
8229
- * key is an ID rather than a location. A configuration entry's `package` is anchored like a
8230
- * module entry, so a relative path names the same module in both spellings.
8231
- */
8232
- const anchored = (entry, file) => {
8233
- const directory = hostPath.dirname(file);
8234
- const resolve = (reference) => reference.startsWith("./") || reference.startsWith("../") ? hostPath.resolve(directory, reference) : reference;
8235
- if (typeof entry === "string") return resolve(entry);
8236
- return "package" in entry ? {
8237
- ...entry,
8238
- package: resolve(entry.package)
8239
- } : entry;
8240
- };
8241
- const anchor = (patch, file) => patch.plugins === void 0 ? patch : {
8242
- ...patch,
8243
- plugins: patch.plugins.map((entry) => anchored(entry, file))
8244
- };
8245
- const attempt = (path) => fileSystem.readFileString(path).pipe(Effect.mapError((error) => new SettingsError({
8246
- path,
8247
- reason: "read",
8248
- detail: error.reason._tag
8249
- })), Effect.flatMap((source) => parse$1(path, source)));
8250
- /**
8251
- * One read of every layer. Exported for the harness constructor, which needs the plugin
8252
- * selection before any layer is built; everything else goes through `Service`.
8253
- */
8254
- const load$1 = Effect.fn("Settings.load")(function* (options) {
8255
- const root = options.hostDir === void 0 ? void 0 : yield* projectRoot(options.hostDir, options.home);
8256
- const files = paths({
8257
- home: options.home,
8258
- root,
8259
- ...options.userConfigDir === void 0 ? {} : { userConfigDir: options.userConfigDir }
8260
- });
8261
- let settings = merge(schema_exports$3.defaults);
8262
- let declared = schema_exports$3.defaults.declared;
8263
- for (const group of files) for (const path of group) {
8264
- const result = yield* Effect.result(attempt(path));
8265
- if (Result.isSuccess(result)) {
8266
- const patch = anchor(result.success, path);
8267
- settings = merge(settings, patch);
8268
- if (result.success.plugins !== void 0) declared = [...declared, ...result.success.plugins.map((written) => ({
8269
- written,
8270
- entry: anchored(written, path),
8271
- file: path
8272
- }))];
8273
- break;
8274
- }
8275
- const error = result.failure;
8276
- if (error.reason === "read" && error.detail === "NotFound") continue;
8277
- return yield* error;
8278
- }
8279
- return {
8280
- ...settings,
8281
- plugins: declared.map((one) => one.entry),
8282
- declared
8283
- };
8284
- });
8285
- /**
8286
- * Each module reference once, owned by the **last** file to declare it (D6).
8287
- *
8288
- * The owner is not bookkeeping: its `.npmrc` chain keys the store artifact. The CLI that installs
8289
- * a reference and the runtime that loads it must pick the same owner, or `plugin update` refreshes
8290
- * an artifact the runtime never reads. Only a string entry loads a module, so only a string entry
8291
- * owns one -- a `{ package }` patch in another file configures it and must not move its registry.
8292
- * A reference keeps the position of its first declaration.
8293
- */
8294
- const modules = (declared) => {
8295
- const owners = /* @__PURE__ */ new Map();
8296
- for (const one of declared) if (typeof one.entry === "string") owners.set(one.entry, one);
8297
- return owners;
8298
- };
8299
- const layer$5 = (options = {}) => Layer.effect(Service$2, Effect.gen(function* () {
8300
- const global = yield* Service$17;
8301
- return Service$2.of({ load: (hostDir) => load$1({
8302
- ...options,
8303
- home: global.home,
8304
- hostDir
8305
- }) });
8306
- }));
8307
- //#endregion
8308
8731
  //#region src/state/state.ts
8309
8732
  var state_exports = /* @__PURE__ */ __exportAll({
8310
8733
  Service: () => Service$1,
@@ -8354,9 +8777,9 @@ const resolver = (input) => typeof input === "string" ? () => input : input;
8354
8777
  */
8355
8778
  const layer$4 = (options, pool, references, follow, rebuild, viewOf = () => Effect.undefined) => {
8356
8779
  return Layer.effect(Service$1, Effect.gen(function* () {
8357
- const runtime = yield* Service$3;
8780
+ const runtime = yield* Service$2;
8358
8781
  const sessions = yield* Service$14;
8359
- const settings = yield* Service$2;
8782
+ const settings = yield* Service$3;
8360
8783
  const eventRegistry = yield* Service$4;
8361
8784
  const eventService = yield* Service$16;
8362
8785
  const events = makeEvents(eventService);
@@ -8409,6 +8832,26 @@ const layer$4 = (options, pool, references, follow, rebuild, viewOf = () => Effe
8409
8832
  ..."reference" in cause && typeof cause.reference === "string" ? { reference: cause.reference } : {},
8410
8833
  ...sessionId === void 0 ? {} : { sessionId }
8411
8834
  });
8835
+ /** Settings and composed configuration for a session's next exchange. */
8836
+ const configure = Effect.fnUntraced(function* (sessionId) {
8837
+ const session = Option.getOrUndefined(yield* sessions.get(sessionId));
8838
+ const hostDir = session === void 0 ? void 0 : Option.getOrUndefined(session.hostDir);
8839
+ const chosen = session === void 0 ? {} : Option.getOrElse(session.config, () => ({}));
8840
+ const sessionOptions = {
8841
+ ...chosen.model === void 0 ? {} : {
8842
+ provider: chosen.model.provider,
8843
+ model: chosen.model.id
8844
+ },
8845
+ ...chosen.thinkingLevel === void 0 ? {} : { thinkingLevel: chosen.thinkingLevel },
8846
+ ...Option.getOrElse(yield* runtime.get(sessionId), () => ({}))
8847
+ };
8848
+ const loadedSettings = yield* settings.load(hostDir);
8849
+ return {
8850
+ hostDir,
8851
+ loadedSettings,
8852
+ configured: compose(loadedSettings, options, sessionOptions)
8853
+ };
8854
+ });
8412
8855
  return Service$1.of({
8413
8856
  reload: Effect.gen(function* () {
8414
8857
  const failures = /* @__PURE__ */ new Map();
@@ -8427,12 +8870,16 @@ const layer$4 = (options, pool, references, follow, rebuild, viewOf = () => Effe
8427
8870
  failure: first
8428
8871
  };
8429
8872
  }),
8873
+ configuration: Effect.fn("State.configuration")(function* (sessionId) {
8874
+ const { configured } = yield* configure(sessionId);
8875
+ return {
8876
+ provider: configured.provider,
8877
+ model: configured.model,
8878
+ thinkingLevel: configured.thinkingLevel
8879
+ };
8880
+ }),
8430
8881
  snapshot: Effect.fn("State.snapshot")(function* (sessionId) {
8431
- const sessionOptions = Option.getOrElse(yield* runtime.get(sessionId), () => ({}));
8432
- const session = yield* sessions.get(sessionId);
8433
- const hostDir = Option.isNone(session) ? void 0 : Option.getOrUndefined(session.value.hostDir);
8434
- const loadedSettings = yield* settings.load(hostDir);
8435
- const configured = compose(loadedSettings, options, sessionOptions);
8882
+ const { hostDir, loadedSettings, configured } = yield* configure(sessionId);
8436
8883
  const { promptCustom, promptSystemAppend, provider: _provider, model: _model, thinkingLevel: _thinkingLevel, toolExecution: _toolExecution, ...rest } = configured.runtime;
8437
8884
  const { provider, model, thinkingLevel, toolExecution } = configured;
8438
8885
  const request = merge(resolveOptions(configured.block), rest);
@@ -8441,7 +8888,8 @@ const layer$4 = (options, pool, references, follow, rebuild, viewOf = () => Effe
8441
8888
  const resolvedModel = yield* resolve$1({
8442
8889
  provider,
8443
8890
  model,
8444
- settings: configured.block
8891
+ settings: configured.block,
8892
+ models: loadedSettings.models
8445
8893
  });
8446
8894
  const refs = references(loadedSettings);
8447
8895
  const failed = (cause) => new SnapshotError({
@@ -8460,7 +8908,15 @@ const layer$4 = (options, pool, references, follow, rebuild, viewOf = () => Effe
8460
8908
  const chosen = yield* select(refs, loaded).pipe(Effect.mapError(failed));
8461
8909
  for (const reference of chosen.missing) {
8462
8910
  const where = loadedSettings.declared.find((one) => typeof one.entry === "string" ? one.entry === reference : "package" in one.entry && one.entry.package === reference);
8463
- yield* Effect.logWarning(`plugin ${reference} is configured but not loaded — run \`codework plugin install\`${where === void 0 ? "" : ` (declared in ${where.file})`}`);
8911
+ const error = `plugin ${reference} is configured but not loaded — run \`codework plugin install\`${where === void 0 ? "" : ` (declared in ${where.file})`}`;
8912
+ yield* Effect.logWarning(error);
8913
+ yield* eventService.publish(PluginUpdated, {
8914
+ status: "failed",
8915
+ reference,
8916
+ error,
8917
+ ...where === void 0 ? {} : { file: where.file },
8918
+ sessionId
8919
+ });
8464
8920
  }
8465
8921
  return {
8466
8922
  sessionId,
@@ -8488,13 +8944,34 @@ const layer$4 = (options, pool, references, follow, rebuild, viewOf = () => Effe
8488
8944
  thinkingLevel,
8489
8945
  request,
8490
8946
  settings: configured.block,
8491
- toolExecution
8947
+ toolExecution,
8948
+ retry: loadedSettings.retry
8492
8949
  };
8493
8950
  })
8494
8951
  });
8495
8952
  }));
8496
8953
  };
8497
8954
  //#endregion
8955
+ //#region src/runner/retry.ts
8956
+ /**
8957
+ * Milliseconds to wait before retry `attempt` (1-based), or `undefined` to give up.
8958
+ *
8959
+ * Only a provider failure the provider itself calls transient is retried. A quota error is
8960
+ * excluded even though it often arrives as a retryable 429: waiting does not refill a quota.
8961
+ * Context overflow is a non-retryable 400 and is left to compaction.
8962
+ *
8963
+ * A server-sent `retry-after` raises the delay, never lowers it. One longer than `maxDelayMs`
8964
+ * gives up rather than retrying early into the same rejection.
8965
+ */
8966
+ const delay = (policy, attempt, error) => {
8967
+ if (attempt > policy.maxRetries) return void 0;
8968
+ if (!Schema.is(ProviderError)(error) || !error.isRetryable) return void 0;
8969
+ if (error.reason._tag === "Runner.ProviderQuotaError") return void 0;
8970
+ const backoff = Math.min(policy.baseDelayMs * 2 ** (attempt - 1), policy.maxDelayMs);
8971
+ const requested = error.reason.retryAfter === void 0 ? 0 : Duration.toMillis(error.reason.retryAfter);
8972
+ return requested > policy.maxDelayMs ? void 0 : Math.max(backoff, Math.ceil(requested));
8973
+ };
8974
+ //#endregion
8498
8975
  //#region src/runner/loop.ts
8499
8976
  const terminalResult = (text) => ({
8500
8977
  content: [{
@@ -8614,7 +9091,9 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
8614
9091
  sessionId: snapshot.sessionId,
8615
9092
  messageId,
8616
9093
  callID: call.callID,
8617
- name: call.name
9094
+ name: call.name,
9095
+ label: snapshot.tools.defs.find((def) => def.name === call.name)?.label,
9096
+ arguments: call.arguments
8618
9097
  });
8619
9098
  const handled = yield* snapshot.tools.handle(call, {
8620
9099
  sessionId: snapshot.sessionId,
@@ -8661,11 +9140,14 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
8661
9140
  if (leaf !== void 0 && (leaf.stopReason === "stop" || leaf.stopReason === "length")) return { needsContinuation: false };
8662
9141
  let publisher;
8663
9142
  let committed = false;
8664
- return yield* Effect.gen(function* () {
8665
- yield* events.publish(TurnStarted, {
8666
- timestamp: yield* DateTime.now,
8667
- sessionId
8668
- });
9143
+ let retries = 0;
9144
+ /** Abort the latest attempt's draft, unless its own terminal event already did. */
9145
+ const settleDraft = (cause) => Effect.gen(function* () {
9146
+ if (publisher?.startedMessageId === void 0) return;
9147
+ const stored = yield* sessions.entry(publisher.startedMessageId);
9148
+ if (Option.isSome(stored) && stored.value.entry.state === "draft") yield* publishFailure(stored.value, cause);
9149
+ });
9150
+ const attempt = Effect.gen(function* () {
8669
9151
  publisher = yield* make$6({ sessionId }).pipe(Effect.provideService(Service$16, events));
8670
9152
  const terminal = yield* requestLLM({
8671
9153
  sessionId,
@@ -8689,6 +9171,43 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
8689
9171
  model: snapshot.model
8690
9172
  }, messageFailure(terminal.message));
8691
9173
  }
9174
+ return terminal;
9175
+ });
9176
+ const request = () => attempt.pipe(Effect.catch((error) => {
9177
+ const delayMs = delay(snapshot.retry, retries + 1, error);
9178
+ if (delayMs === void 0) return Effect.fail(error);
9179
+ return Effect.gen(function* () {
9180
+ retries += 1;
9181
+ yield* Effect.uninterruptible(settleDraft({
9182
+ _tag: "error",
9183
+ message: error.message
9184
+ }));
9185
+ yield* events.publish(RetryScheduled, {
9186
+ timestamp: yield* DateTime.now,
9187
+ sessionId,
9188
+ attempt: retries,
9189
+ maxRetries: snapshot.retry.maxRetries,
9190
+ delayMs,
9191
+ message: error.message
9192
+ });
9193
+ yield* Effect.sleep(Duration.millis(delayMs));
9194
+ return yield* request();
9195
+ });
9196
+ }));
9197
+ return yield* Effect.gen(function* () {
9198
+ yield* events.publish(TurnStarted, {
9199
+ timestamp: yield* DateTime.now,
9200
+ sessionId
9201
+ });
9202
+ const terminal = yield* request().pipe(Effect.onExit((exit) => retries === 0 ? Effect.void : Effect.gen(function* () {
9203
+ yield* events.publish(RetryFinished, {
9204
+ timestamp: yield* DateTime.now,
9205
+ sessionId,
9206
+ attempt: retries,
9207
+ success: Exit.isSuccess(exit),
9208
+ ...Exit.isSuccess(exit) ? {} : { message: Cause.hasInterrupts(exit.cause) ? "interrupted" : errorMessage(exit.cause) }
9209
+ });
9210
+ })));
8692
9211
  const interrupted = yield* settleTools(snapshot, terminal.message, terminal.reason, () => {
8693
9212
  committed = true;
8694
9213
  });
@@ -8697,24 +9216,20 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
8697
9216
  needsContinuation: terminal.reason === "toolUse",
8698
9217
  messageId: terminal.message.messageId
8699
9218
  };
8700
- }).pipe(Effect.catchCause((cause) => {
8701
- if (committed) return Effect.failCause(cause);
9219
+ }).pipe(Effect.onError((cause) => {
9220
+ if (committed) return Effect.void;
8702
9221
  const turnCause = Cause.hasInterrupts(cause) ? { _tag: "interrupted" } : {
8703
9222
  _tag: "error",
8704
9223
  message: errorMessage(cause)
8705
9224
  };
8706
- const record = Effect.gen(function* () {
8707
- if (publisher?.startedMessageId !== void 0) {
8708
- const stored = yield* sessions.entry(publisher.startedMessageId);
8709
- if (Option.isSome(stored) && stored.value.entry.state === "draft") yield* publishFailure(stored.value, turnCause);
8710
- }
9225
+ return Effect.gen(function* () {
9226
+ yield* settleDraft(turnCause);
8711
9227
  yield* events.publish(TurnAborted, {
8712
9228
  timestamp: yield* DateTime.now,
8713
9229
  sessionId,
8714
9230
  cause: turnCause
8715
9231
  });
8716
9232
  });
8717
- return Effect.uninterruptible(record).pipe(Effect.andThen(Effect.failCause(cause)));
8718
9233
  }));
8719
9234
  });
8720
9235
  const runTurn = Effect.fn("Loop.runTurn")(function* (promotion, snapshot) {
@@ -8868,10 +9383,7 @@ const layer$2 = Layer.effectDiscard(Effect.gen(function* () {
8868
9383
  messageId: event.data.messageId,
8869
9384
  role: "user",
8870
9385
  time: { created: DateTime.toEpochMillis(event.data.timestamp) },
8871
- parts: [{
8872
- type: "text",
8873
- text: event.data.prompt.text
8874
- }]
9386
+ parts: [...event.data.prompt.parts]
8875
9387
  })).pipe(Effect.orDie);
8876
9388
  yield* sessions.append({
8877
9389
  id: event.data.messageId,
@@ -8881,7 +9393,13 @@ const layer$2 = Layer.effectDiscard(Effect.gen(function* () {
8881
9393
  ...encoded,
8882
9394
  ...event.metadata === void 0 ? {} : { metadata: event.metadata }
8883
9395
  }).pipe(Effect.orDie);
9396
+ const title = titleFrom(Prompt.text(event.data.prompt));
9397
+ if (title !== void 0) yield* sessions.retitle(event.data.sessionId, title);
8884
9398
  }));
9399
+ yield* events.project(ConfigChanged, (event) => {
9400
+ const { sessionId, timestamp: _timestamp, ...config } = event.data;
9401
+ return sessions.configure(sessionId, config);
9402
+ });
8885
9403
  yield* events.project(LLMStarted, (event) => Effect.gen(function* () {
8886
9404
  if (event.durable === void 0) return yield* Effect.die("LLMStarted is missing its aggregate sequence");
8887
9405
  const encoded = yield* encodeAssistant({
@@ -8950,9 +9468,9 @@ var harness_exports = /* @__PURE__ */ __exportAll({
8950
9468
  const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
8951
9469
  const hostCwd = options.hostCwd ?? process.cwd();
8952
9470
  const paths = yield* resolve$3(options.home === void 0 ? {} : { home: hostPath.resolve(hostCwd, expandTilde(options.home, hostPath)) });
8953
- const global = layerWith$1(paths);
9471
+ const global = layerWith(paths);
8954
9472
  const settingsOptions = options.userConfigDir === void 0 ? {} : { userConfigDir: userConfigDir(options.userConfigDir, hostCwd) };
8955
- const config = yield* load$1({
9473
+ const config = yield* load$3({
8956
9474
  ...settingsOptions,
8957
9475
  home: paths.home
8958
9476
  });
@@ -8969,12 +9487,12 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
8969
9487
  * `hostDir` led to; only the process view falls back to `hostCwd`.
8970
9488
  */
8971
9489
  const hostDirOf = (root) => root ?? hostCwd;
8972
- 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()));
9490
+ 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()));
8973
9491
  const resolveOnly = {
8974
9492
  ...catalogOptions,
8975
9493
  install: required
8976
9494
  };
8977
- const pool = yield* Ref.make(yield* load$2(references(config), {
9495
+ const pool = yield* Ref.make(yield* load$1(references(config), {
8978
9496
  ...resolveOnly,
8979
9497
  declared: declaredIn(config)
8980
9498
  }));
@@ -8988,7 +9506,7 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
8988
9506
  let reloads = 0;
8989
9507
  const rebuild = (root, loaded) => Effect.gen(function* () {
8990
9508
  reloads += 1;
8991
- const current = yield* load$1({
9509
+ const current = yield* load$3({
8992
9510
  ...settingsOptions,
8993
9511
  home: paths.home,
8994
9512
  ...root === void 0 ? {} : { hostDir: root }
@@ -8996,7 +9514,7 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
8996
9514
  const retained = [];
8997
9515
  const configured = references(current);
8998
9516
  for (const origin of loaded.origins.values()) {
8999
- if (configured.includes(origin.reference) || Option.isSome(yield* filed(origin.reference, anchor$1(origin.file, hostDirOf(root))))) {
9517
+ if (configured.includes(origin.reference) || Option.isSome(yield* filed(origin.reference, anchor(origin.file, hostDirOf(root))))) {
9000
9518
  retained.push(origin);
9001
9519
  continue;
9002
9520
  }
@@ -9006,7 +9524,7 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
9006
9524
  const known = /* @__PURE__ */ new Map();
9007
9525
  for (const origin of retained) if (origin.file !== void 0) known.set(origin.reference, origin.file);
9008
9526
  for (const [reference, file] of declaredIn(current)) known.set(reference, file);
9009
- return yield* load$2(accumulated, {
9527
+ return yield* load$1(accumulated, {
9010
9528
  ...resolveOnly,
9011
9529
  hostDir: hostDirOf(root),
9012
9530
  declared: known,
@@ -9021,10 +9539,10 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
9021
9539
  const authFile$1 = authFile(options.home === void 0 ? void 0 : paths.home);
9022
9540
  const open = options.llm ?? openWith(authFile$1 === void 0 ? {} : { authFile: authFile$1 });
9023
9541
  const loop = layer$3({ request: make$5(open) });
9024
- return layer$10.pipe(Layer.provideMerge(layer$7.pipe(Layer.provide(loop))), Layer.provideMerge(layer$4({}, pool, references, followStore, rebuild, viewOf)), Layer.provideMerge(layer$5(settingsOptions)), Layer.provideMerge(layer$6), Layer.provideMerge(sandboxes), Layer.provideMerge(layer$22), Layer.provideMerge(layer$1), Layer.provideMerge(layer$25), Layer.provideMerge(layer$9(definitions)), Layer.provideMerge(database), Layer.provideMerge(global), Layer.provideMerge(layer$8({
9542
+ return layer$10.pipe(Layer.provideMerge(layer$6.pipe(Layer.provide(loop))), Layer.provideMerge(layer$4({}, pool, references, followStore, rebuild, viewOf)), Layer.provideMerge(layer$8(settingsOptions)), Layer.provideMerge(layer$5), Layer.provideMerge(sandboxes), Layer.provideMerge(layer$22), Layer.provideMerge(layer$1), Layer.provideMerge(layer$25), Layer.provideMerge(layer$9(definitions)), Layer.provideMerge(database), Layer.provideMerge(global), Layer.provideMerge(layer$7({
9025
9543
  home: paths.home,
9026
9544
  watch: options.watchModels ?? false
9027
- })));
9545
+ })), Layer.provideMerge(Layer.succeed(Credentials, { authFile: authFile$1 })));
9028
9546
  }));
9029
9547
  //#endregion
9030
9548
  //#region src/effect/sandbox.ts
@@ -9151,30 +9669,54 @@ var session_exports = /* @__PURE__ */ __exportAll({
9151
9669
  SessionMessageSchema: () => schema_exports$4,
9152
9670
  SessionSchema: () => schema_exports$8,
9153
9671
  attach: () => attach,
9672
+ configuration: () => configuration,
9154
9673
  create: () => create$1,
9155
9674
  get: () => get,
9156
9675
  link: () => link,
9157
9676
  relink: () => relink
9158
9677
  });
9678
+ /** What stays in this process. The model and thinking level are the session's {@link chosen} config. */
9159
9679
  const runtimeBindings = (input) => ({
9160
9680
  ...input.model?.options,
9161
- ...input.model === void 0 ? {} : {
9162
- provider: input.model.provider,
9163
- model: input.model.id
9164
- },
9165
- ...input.thinkingLevel === void 0 ? {} : { thinkingLevel: input.thinkingLevel },
9166
9681
  ...input.tools?.execution === void 0 ? {} : { toolExecution: input.tools.execution },
9167
9682
  ...input.systemPrompt?.custom === void 0 ? {} : { promptCustom: input.systemPrompt.custom },
9168
9683
  ...input.systemPrompt?.append === void 0 ? {} : { promptSystemAppend: input.systemPrompt.append }
9169
9684
  });
9170
- const promptInput = (input) => {
9685
+ /** The keys of the session's durable config this input names. */
9686
+ const chosen = (input) => ({
9687
+ ...input.model === void 0 ? {} : { model: {
9688
+ provider: input.model.provider,
9689
+ id: input.model.id
9690
+ } },
9691
+ ...input.thinkingLevel === void 0 ? {} : { thinkingLevel: input.thinkingLevel }
9692
+ });
9693
+ /** Records a config choice; the projector merges it into the session row in the same commit. */
9694
+ const choose = Effect.fn("Session.choose")(function* (sessionId, input) {
9695
+ const config = chosen(input);
9696
+ if (Object.keys(config).length === 0) return;
9697
+ yield* (yield* Service$16).publish(ConfigChanged, {
9698
+ sessionId,
9699
+ timestamp: yield* DateTime.now,
9700
+ ...config
9701
+ });
9702
+ });
9703
+ const promptInput = Effect.fnUntraced(function* (input) {
9171
9704
  const value = typeof input === "string" ? { text: input } : input;
9705
+ const parts = "parts" in value ? value.parts : [{
9706
+ type: "text",
9707
+ text: value.text
9708
+ }];
9709
+ if (parts.length === 0) return yield* new InvalidPromptError({ reason: "A prompt needs at least one part" });
9710
+ for (const part of parts) {
9711
+ const problem = part.type === "image" ? imageProblem(part) : void 0;
9712
+ if (problem !== void 0) return yield* new InvalidPromptError({ reason: problem });
9713
+ }
9172
9714
  return {
9173
- prompt: Prompt.make({ text: value.text }),
9715
+ prompt: Prompt.make({ parts }),
9174
9716
  ...value.delivery === void 0 ? {} : { delivery: value.delivery },
9175
9717
  ...value.id === void 0 ? {} : { id: value.id }
9176
9718
  };
9177
- };
9719
+ });
9178
9720
  const makeHandle = Effect.fn("Session.makeHandle")(function* (id) {
9179
9721
  const sessions = yield* Service$14;
9180
9722
  const control = yield* Service$5;
@@ -9200,14 +9742,14 @@ const makeHandle = Effect.fn("Session.makeHandle")(function* (id) {
9200
9742
  id,
9201
9743
  active: control.active.pipe(Effect.map((active) => active.has(id))),
9202
9744
  info,
9203
- prompt: (input) => control.prompt({
9745
+ prompt: (input) => promptInput(input).pipe(Effect.flatMap((prompt) => control.prompt({
9204
9746
  sessionId: id,
9205
- ...promptInput(input)
9206
- }),
9207
- run: (input) => control.run({
9747
+ ...prompt
9748
+ }))),
9749
+ run: (input) => promptInput(input).pipe(Effect.flatMap((prompt) => control.run({
9208
9750
  sessionId: id,
9209
- ...promptInput(input)
9210
- }),
9751
+ ...prompt
9752
+ }))),
9211
9753
  wait: () => control.wait(id),
9212
9754
  resume: () => control.resume(id),
9213
9755
  interrupt: () => control.interrupt(id, { awaitSettlement: true }),
@@ -9217,7 +9759,7 @@ const makeHandle = Effect.fn("Session.makeHandle")(function* (id) {
9217
9759
  });
9218
9760
  const create$1 = Effect.fn("Session.create")(function* (input = {}) {
9219
9761
  const sessions = yield* Service$14;
9220
- const runtime = yield* Service$3;
9762
+ const runtime = yield* Service$2;
9221
9763
  const sandboxes = yield* Controller;
9222
9764
  const id = ID$1.create();
9223
9765
  const sandboxId = input.sandbox?.id ?? instance_exports.SandboxInstance.ID.local;
@@ -9226,11 +9768,11 @@ const create$1 = Effect.fn("Session.create")(function* (input = {}) {
9226
9768
  id,
9227
9769
  spaceId: location.space.id,
9228
9770
  directory: location.directory,
9229
- slug: id,
9230
9771
  title: input.title ?? "Session",
9231
9772
  ...input.hostDir === void 0 ? {} : { hostDir: declaredHostDir(input.hostDir) }
9232
9773
  });
9233
9774
  yield* runtime.set(id, runtimeBindings(input));
9775
+ yield* choose(id, input);
9234
9776
  return yield* makeHandle(id);
9235
9777
  });
9236
9778
  /**
@@ -9296,9 +9838,20 @@ const get = Effect.fn("Session.get")(function* (sessionId) {
9296
9838
  const attach = Effect.fn("Session.attach")(function* (input) {
9297
9839
  const found = yield* get(input.sessionId);
9298
9840
  if (Option.isNone(found)) return yield* new SessionNotFoundError({ sessionId: input.sessionId });
9299
- yield* (yield* Service$3).update(input.sessionId, runtimeBindings(input));
9841
+ yield* (yield* Service$2).update(input.sessionId, runtimeBindings(input));
9842
+ yield* choose(input.sessionId, input);
9300
9843
  return found.value;
9301
9844
  });
9845
+ /**
9846
+ * The model and thinking level the session's next exchange runs with. A choice made through
9847
+ * {@link create} or {@link attach} is saved with the session and outlives the process; without
9848
+ * one, the session follows its settings.
9849
+ */
9850
+ const configuration = Effect.fn("Session.configuration")(function* (sessionId) {
9851
+ const sessions = yield* Service$14;
9852
+ if (Option.isNone(yield* sessions.get(sessionId))) return yield* new SessionNotFoundError({ sessionId });
9853
+ return yield* Service$1.use((state) => state.configuration(sessionId));
9854
+ });
9302
9855
  //#endregion
9303
9856
  //#region src/plugin/prompt/schema.ts
9304
9857
  var schema_exports$1 = /* @__PURE__ */ __exportAll({});
@@ -9310,11 +9863,12 @@ var plugin_exports = /* @__PURE__ */ __exportAll({
9310
9863
  InstallError: () => InstallError,
9311
9864
  LoadError: () => LoadError,
9312
9865
  Prompt: () => schema_exports$1,
9866
+ Section: () => Section,
9313
9867
  SetupError: () => SetupError,
9314
9868
  SourceError: () => SourceError,
9315
9869
  StoreError: () => StoreError,
9316
9870
  Tool: () => schema_exports$2,
9317
- anchor: () => anchor$1,
9871
+ anchor: () => anchor,
9318
9872
  canonical: () => canonical,
9319
9873
  check: () => check,
9320
9874
  define: () => plugin_exports$1.define,
@@ -9322,7 +9876,7 @@ var plugin_exports = /* @__PURE__ */ __exportAll({
9322
9876
  identity: () => digest,
9323
9877
  inspect: () => inspect,
9324
9878
  localName: () => localName,
9325
- parse: () => parse$2,
9879
+ parse: () => parse$1,
9326
9880
  probe: () => probe,
9327
9881
  resolve: () => resolve$2,
9328
9882
  resolveCached: () => required,