@mp-lb/berth 0.1.1 → 0.1.3

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.
Files changed (2) hide show
  1. package/dist/berth.js +362 -228
  2. package/package.json +1 -1
package/dist/berth.js CHANGED
@@ -5995,7 +5995,7 @@ var require_lib = __commonJS({
5995
5995
  });
5996
5996
 
5997
5997
  // package.json
5998
- var version = "0.1.1";
5998
+ var version = "0.1.3";
5999
5999
 
6000
6000
  // src/runAuthCommand.ts
6001
6001
  import { spawn } from "node:child_process";
@@ -22749,9 +22749,6 @@ var runDeliveryCommand = async (args2) => {
22749
22749
  throw new Error("Use berth delivery start, complete, record or schema.");
22750
22750
  };
22751
22751
 
22752
- // src/runInstanceCommand.ts
22753
- import { hostname as hostname4 } from "node:os";
22754
-
22755
22752
  // ../../packages/core/src/work/schemas/slugSchema.ts
22756
22753
  var slugSchema = external_exports.string().trim().min(1).max(80).regex(
22757
22754
  /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
@@ -22968,8 +22965,7 @@ var prepareInstance = async (value) => {
22968
22965
 
22969
22966
  // src/readTaskLaunchContext.ts
22970
22967
  import { realpath as realpath4 } from "node:fs/promises";
22971
- import { homedir as homedir5, hostname as hostname3 } from "node:os";
22972
- import { basename, dirname as dirname3, join as join5 } from "node:path";
22968
+ import { hostname as hostname3 } from "node:os";
22973
22969
 
22974
22970
  // ../../packages/core/src/work/inspectPreparedInstance.ts
22975
22971
  import { execFile as execFile3 } from "node:child_process";
@@ -23066,6 +23062,66 @@ var inspectPreparedInstance = async (value) => {
23066
23062
  return { directory, repositories };
23067
23063
  };
23068
23064
 
23065
+ // ../../packages/core/src/work/machineIdentity.ts
23066
+ import { randomUUID } from "node:crypto";
23067
+ import {
23068
+ existsSync as existsSync2,
23069
+ linkSync,
23070
+ mkdirSync,
23071
+ readFileSync,
23072
+ unlinkSync,
23073
+ writeFileSync
23074
+ } from "node:fs";
23075
+ import { homedir as homedir5 } from "node:os";
23076
+ import { join as join5 } from "node:path";
23077
+ var identitySchema = external_exports.object({ id: external_exports.uuid() });
23078
+ var machineIdentity = (home = homedir5(), legacyFile) => {
23079
+ const folder = join5(home, ".berth");
23080
+ const path2 = join5(folder, "identity.json");
23081
+ const read2 = () => identitySchema.parse(JSON.parse(readFileSync(path2, "utf8")));
23082
+ if (existsSync2(path2)) return read2();
23083
+ const appData = process.platform === "darwin" ? join5(home, "Library", "Application Support") : process.platform === "win32" ? process.env.APPDATA : join5(home, ".config");
23084
+ const current = appData && join5(appData, "Berth");
23085
+ const legacy = appData && join5(appData, "Factory");
23086
+ const desktopFolder = current && legacy && !existsSync2(current) && existsSync2(legacy) ? legacy : current;
23087
+ const oldFile = legacyFile ?? (desktopFolder ? join5(desktopFolder, "machine-id") : void 0);
23088
+ let id = randomUUID();
23089
+ if (oldFile && existsSync2(oldFile))
23090
+ id = external_exports.uuid().parse(readFileSync(oldFile, "utf8").trim());
23091
+ mkdirSync(folder, { recursive: true, mode: 448 });
23092
+ const temporary = join5(folder, `.identity-${randomUUID()}`);
23093
+ try {
23094
+ writeFileSync(temporary, `${JSON.stringify({ id }, null, 2)}
23095
+ `, {
23096
+ mode: 384,
23097
+ flag: "wx"
23098
+ });
23099
+ try {
23100
+ linkSync(temporary, path2);
23101
+ } catch (error51) {
23102
+ if (error51.code !== "EEXIST") throw error51;
23103
+ }
23104
+ } finally {
23105
+ if (existsSync2(temporary)) unlinkSync(temporary);
23106
+ }
23107
+ return read2();
23108
+ };
23109
+
23110
+ // src/resolveSessionMachine.ts
23111
+ var resolveSessionMachine = (config2) => {
23112
+ const { id } = machineIdentity();
23113
+ if (process.env.BERTH_MACHINE_ID && process.env.BERTH_MACHINE_ID !== id)
23114
+ throw new Error(
23115
+ `BERTH_MACHINE_ID does not match this machine's identity (${id}).`
23116
+ );
23117
+ const machine = config2.workMachines.find((item) => item.id === id);
23118
+ if (!machine)
23119
+ throw new Error(
23120
+ `This machine (${id}) is not configured. Configure it in Berth desktop work machine settings.`
23121
+ );
23122
+ return machine;
23123
+ };
23124
+
23069
23125
  // src/readTaskLaunchContext.ts
23070
23126
  var readTaskLaunchContext = async (value) => {
23071
23127
  const taskUuid = external_exports.uuid().parse(value);
@@ -23080,29 +23136,8 @@ var readTaskLaunchContext = async (value) => {
23080
23136
  throw new Error("Task project is unavailable.");
23081
23137
  const config2 = await client.config.read.query();
23082
23138
  const osHostname = hostname3();
23083
- const configuredId = process.env.BERTH_MACHINE_ID;
23084
23139
  const currentCwd = await realpath4(process.cwd()).catch(() => null);
23085
- const cwdMatches = [];
23086
- if (!configuredId && currentCwd) {
23087
- for (const item of config2.workMachines) {
23088
- const folder = item.instancesFolder === "~" ? homedir5() : item.instancesFolder.startsWith("~/") ? join5(homedir5(), item.instancesFolder.slice(2)) : item.instancesFolder;
23089
- const root = await realpath4(folder).catch(() => null);
23090
- if (!root || dirname3(currentCwd) !== root) continue;
23091
- const registered = await listInstances(item.instancesFolder);
23092
- if (registered.some(
23093
- (instance) => instance.taskId === taskUuid && instance.slug === basename(currentCwd)
23094
- ))
23095
- cwdMatches.push(item);
23096
- }
23097
- }
23098
- const matches = config2.workMachines.filter(
23099
- (item) => configuredId ? item.id === configuredId : item.name === osHostname
23100
- );
23101
- const machine = configuredId ? matches.length === 1 ? matches[0] : void 0 : cwdMatches.length === 1 ? cwdMatches[0] : cwdMatches.length > 1 ? void 0 : matches.length === 1 ? matches[0] : config2.workMachines.length === 1 ? config2.workMachines[0] : void 0;
23102
- if (!machine)
23103
- throw new Error(
23104
- "Work machine is ambiguous. Set BERTH_MACHINE_ID to a configured machine UUID."
23105
- );
23140
+ const machine = resolveSessionMachine(config2);
23106
23141
  const all = await listInstances(machine.instancesFolder);
23107
23142
  const linked = all.filter((instance) => instance.taskId === taskUuid);
23108
23143
  if (linked.some((instance) => instance.projectId !== task.projectId))
@@ -23205,14 +23240,7 @@ var runInstanceCommand = async (args2) => {
23205
23240
  `Task ${taskSlug} has no immutable ID; migrate it before linking an instance.`
23206
23241
  );
23207
23242
  const config2 = await client.config.read.query();
23208
- const matches = config2.workMachines.filter(
23209
- (machine2) => machine2.id === process.env.BERTH_MACHINE_ID || !process.env.BERTH_MACHINE_ID && machine2.name === hostname4()
23210
- );
23211
- const machine = matches.length === 1 ? matches[0] : !process.env.BERTH_MACHINE_ID && config2.workMachines.length === 1 ? config2.workMachines[0] : void 0;
23212
- if (!machine)
23213
- throw new Error(
23214
- "Configure a work machine for this hostname, or set BERTH_MACHINE_ID to select one."
23215
- );
23243
+ const machine = resolveSessionMachine(config2);
23216
23244
  return prepareInstance({
23217
23245
  projectId: project.id,
23218
23246
  slug,
@@ -23223,6 +23251,36 @@ var runInstanceCommand = async (args2) => {
23223
23251
  });
23224
23252
  };
23225
23253
 
23254
+ // ../../packages/core/src/work/schemas/messageEventSchema.ts
23255
+ var messageEventSchema = external_exports.strictObject({
23256
+ at: external_exports.iso.datetime(),
23257
+ actor: external_exports.string().trim().min(1).nullable(),
23258
+ action: external_exports.enum([
23259
+ "assigned",
23260
+ "commented",
23261
+ "closed",
23262
+ "reopened",
23263
+ "requested",
23264
+ "approved",
23265
+ "rejected",
23266
+ "deferred",
23267
+ "restored"
23268
+ ]),
23269
+ assignee: external_exports.string().trim().min(1).nullable().optional(),
23270
+ request: external_exports.strictObject({
23271
+ summary: external_exports.string(),
23272
+ evidence: external_exports.string(),
23273
+ deliveryAttemptId: external_exports.string().nullable()
23274
+ }).optional(),
23275
+ note: external_exports.string().trim().max(8e3).optional()
23276
+ });
23277
+
23278
+ // ../../packages/core/src/work/schemas/messageAppendSchema.ts
23279
+ var messageAppendSchema = messageEventSchema.omit({ at: true, request: true }).extend({
23280
+ id: external_exports.string().min(1),
23281
+ expectedLength: external_exports.number().int().nonnegative()
23282
+ });
23283
+
23226
23284
  // ../../packages/core/src/work/schemas/messageTypeSchema.ts
23227
23285
  var messageTypeSchema = external_exports.string().trim().min(1).max(100).regex(/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/);
23228
23286
 
@@ -23247,6 +23305,7 @@ var messageListSchema = external_exports.strictObject({
23247
23305
  // ../../packages/core/src/work/schemas/messageCreateSchema.ts
23248
23306
  var messageCreateSchema = external_exports.strictObject({
23249
23307
  recipient: messageRecipientSchema,
23308
+ sender: external_exports.string().trim().min(1).nullable().default(null),
23250
23309
  type: messageTypeSchema.default("message"),
23251
23310
  priority: external_exports.number().int().default(0),
23252
23311
  projectId: slugSchema.nullable(),
@@ -23255,14 +23314,149 @@ var messageCreateSchema = external_exports.strictObject({
23255
23314
  body: external_exports.string().trim().min(1).max(8e3)
23256
23315
  });
23257
23316
 
23317
+ // ../../packages/core/src/work/schemas/workerSchema.ts
23318
+ var workerSchema = external_exports.strictObject({
23319
+ threadId: external_exports.string().trim().min(1).max(200).describe("Stable worker thread ID."),
23320
+ host: external_exports.string().trim().min(1).max(200).describe("Host identifier for the worker thread, such as local.")
23321
+ }).describe(
23322
+ "Structured worker destination; an identifier is not authentication or credential approval."
23323
+ );
23324
+
23325
+ // ../../packages/core/src/work/schemas/blockerHandoffReceiptSchema.ts
23326
+ var blockerHandoffReceiptSchema = external_exports.strictObject({
23327
+ updateCount: external_exports.number().int().positive().describe(
23328
+ "Blocker update-log length identifying the original unblocked cycle."
23329
+ ),
23330
+ consumer: slugSchema.describe("Controller that reserved this cycle."),
23331
+ worker: workerSchema,
23332
+ reservedAt: external_exports.iso.datetime().describe("Time the reservation was saved."),
23333
+ delivery: external_exports.strictObject({
23334
+ receiptId: external_exports.string().trim().min(1).max(200).describe("External admitted turn or delivery receipt ID."),
23335
+ confirmedAt: external_exports.iso.datetime().describe("Time delivery was recorded."),
23336
+ sourceUpdateId: external_exports.uuid().optional().describe(
23337
+ "Existing informational update used to import a historical receipt."
23338
+ )
23339
+ }).nullable().describe(
23340
+ "Null means delivery is uncertain or pending; reconcile before sending. A populated delivery must not be resent."
23341
+ )
23342
+ }).describe(
23343
+ "Durable handoff receipt keyed by blocker update cycle; retained when reblocked."
23344
+ );
23345
+
23346
+ // ../../packages/core/src/work/schemas/blockerEventSchema.ts
23347
+ var blockerEventSchema = external_exports.strictObject({
23348
+ state: external_exports.enum(["blocked", "unblocked"]).describe(
23349
+ "blocked is recorded by controller; unblocked is recorded by user."
23350
+ ),
23351
+ notes: external_exports.string().max(4e3).describe("Feedback or resolution notes for this change. Markdown.")
23352
+ }).describe(
23353
+ "An append-only update in the blocker\u2019s embedded log, oldest first."
23354
+ );
23355
+
23258
23356
  // ../../packages/core/src/work/schemas/messageStatusSchema.ts
23259
23357
  var messageStatusSchema = external_exports.enum(["active", "later", "unblocked", "archived"]).describe(
23260
23358
  "Message lifecycle: active, later, unblocked or archived. Berth applies no type-specific transitions."
23261
23359
  );
23262
23360
 
23361
+ // ../../packages/core/src/work/schemas/blockerStatusSchema.ts
23362
+ var blockerStatusSchema = messageStatusSchema.describe(
23363
+ "active: user queue; later: deferred, still blocking; unblocked: resolved by the user, awaiting controller acknowledgment (whether claimed or not); archived: acknowledged. Use queue commands to transition."
23364
+ );
23365
+
23366
+ // ../../packages/core/src/work/schemas/blockerBaseSchema.ts
23367
+ var blockerBaseSchema = external_exports.strictObject({
23368
+ id: external_exports.uuid().describe(
23369
+ "Immutable UUID generated by the server when this item is created."
23370
+ ),
23371
+ taskSlug: taskSlugSchema.describe(
23372
+ "The existing task this blocker belongs to."
23373
+ ),
23374
+ status: blockerStatusSchema,
23375
+ updates: external_exports.array(blockerEventSchema).min(1).describe(
23376
+ "Embedded state change log, oldest first. Appended only by state-transition commands."
23377
+ ),
23378
+ handoffs: external_exports.array(blockerHandoffReceiptSchema).default([]).describe(
23379
+ "Durable diagnostics handoffs, one per unblocked update cycle."
23380
+ ),
23381
+ createdAt: external_exports.iso.datetime().nullable().describe(
23382
+ "Creation time in UTC, or null for imported records whose time is unknown."
23383
+ ),
23384
+ completedAt: external_exports.iso.datetime().nullable().describe(
23385
+ "Time the user marked unblocked; null before unblocking or for legacy completions."
23386
+ ),
23387
+ acknowledgedAt: external_exports.iso.datetime().nullable().describe(
23388
+ "Time the consumer acknowledged the item, or null before acknowledgment."
23389
+ ),
23390
+ dismissedAt: external_exports.iso.datetime().nullable().optional().describe(
23391
+ "Time the user hid this pending-review cycle from the web inbox; controller acknowledgment is independent."
23392
+ ),
23393
+ consumer: slugSchema.nullable().describe(
23394
+ "Controller consumer owning this acknowledgment queue item. Set atomically by pull; null before pulling."
23395
+ ),
23396
+ outcome: external_exports.string().max(4e3).describe(
23397
+ "User completion note or legacy resolution; may be empty. Markdown."
23398
+ ),
23399
+ selectedOptionIndex: external_exports.number().int().min(-1).nullable().describe(
23400
+ "Zero-based chosen decision option, or -1 for None of the above. Null for actions or decisions without a recorded choice."
23401
+ )
23402
+ }).describe(
23403
+ "Server-managed identity, queue state and completion receipt for a blocker."
23404
+ );
23405
+
23406
+ // ../../packages/core/src/work/schemas/prioritySchema.ts
23407
+ var prioritySchema = external_exports.enum(["high", "medium", "low", "unset"]).describe("Priority: high, medium, low, or unset when not assigned.");
23408
+
23409
+ // ../../packages/core/src/work/schemas/decisionOptionSchema.ts
23410
+ var decisionOptionSchema = external_exports.strictObject({
23411
+ title: external_exports.string().trim().min(1).max(120).describe("Short name for this choice."),
23412
+ body: external_exports.string().trim().min(1).max(4e3).describe(
23413
+ "Explain this option, its consequences and tradeoffs. Markdown."
23414
+ ),
23415
+ priority: prioritySchema.describe(
23416
+ "Preference for this option: high, medium, low or unset. High is the strongest recommendation."
23417
+ )
23418
+ }).describe("One selectable option in a decision.");
23419
+
23420
+ // ../../packages/core/src/work/schemas/decisionSchema.ts
23421
+ var decisionSchema = external_exports.strictObject({
23422
+ background: external_exports.string().trim().min(1).max(4e3).describe("Context the user needs to understand the decision. Markdown."),
23423
+ problem: external_exports.string().trim().min(1).max(4e3).describe("The problem or question that needs a decision. Markdown."),
23424
+ options: external_exports.array(decisionOptionSchema).max(12).describe(
23425
+ "Predefined choices presented to the user. The UI always adds None of the above; do not include it in this array. New decisions require at least one option; legacy decisions may have none. Completion records the chosen index or -1 for None of the above."
23426
+ )
23427
+ }).describe("Decision content: background, problem, and prioritized options.");
23428
+
23429
+ // ../../packages/core/src/work/schemas/actionSchema.ts
23430
+ var actionSchema = external_exports.strictObject({
23431
+ background: external_exports.string().trim().min(1).max(4e3).describe("Context explaining why this action is needed. Markdown."),
23432
+ instructions: external_exports.string().trim().min(1).max(6e3).describe(
23433
+ "Concrete instructions for the user to carry out before marking done. Markdown."
23434
+ )
23435
+ }).describe("Action content: background and instructions.");
23436
+
23437
+ // ../../packages/core/src/work/schemas/blockerSchema.ts
23438
+ var blockerSchema = external_exports.discriminatedUnion("type", [
23439
+ blockerBaseSchema.extend({
23440
+ ...decisionSchema.shape,
23441
+ type: external_exports.literal("decision").describe(
23442
+ "A one-time decision. Once submitted its choice is final; create a new decision for a new question."
23443
+ )
23444
+ }),
23445
+ blockerBaseSchema.extend({
23446
+ ...actionSchema.shape,
23447
+ type: external_exports.literal("action").describe(
23448
+ "An action requiring instructions to be carried out. It may be returned with feedback and actioned again."
23449
+ )
23450
+ })
23451
+ ]).describe(
23452
+ "A typed blocker and its queue lifecycle. Completed content is immutable until archived so consumers receive stable results."
23453
+ );
23454
+
23263
23455
  // ../../packages/core/src/work/schemas/messageSchema.ts
23264
23456
  var messageSchema = external_exports.strictObject({
23265
23457
  id: external_exports.string(),
23458
+ sender: external_exports.string().min(1).nullable().optional(),
23459
+ timeline: external_exports.array(messageEventSchema).optional(),
23266
23460
  type: messageTypeSchema,
23267
23461
  recipient: messageRecipientSchema,
23268
23462
  priority: external_exports.number().int(),
@@ -23274,7 +23468,8 @@ var messageSchema = external_exports.strictObject({
23274
23468
  status: messageStatusSchema,
23275
23469
  claimedAt: external_exports.iso.datetime().nullable(),
23276
23470
  consumer: external_exports.string().nullable(),
23277
- sourceId: external_exports.string()
23471
+ sourceId: external_exports.string(),
23472
+ blocker: blockerSchema.optional()
23278
23473
  });
23279
23474
 
23280
23475
  // ../../packages/core/src/work/schemas/messageUpdateSchema.ts
@@ -23295,28 +23490,33 @@ var runMessageCommand = async (args2) => {
23295
23490
  if (extra.length) throw new Error("Quote the JSON payload as one argument.");
23296
23491
  if (action === "schema") {
23297
23492
  const schemas = {
23493
+ append: messageAppendSchema,
23298
23494
  create: messageCreateSchema,
23299
23495
  update: messageUpdateSchema,
23300
23496
  list: messageListSchema
23301
23497
  };
23302
23498
  const key = input ?? "create";
23303
- if (key !== "create" && key !== "update" && key !== "list")
23304
- throw new Error("Choose create, update or list schema.");
23499
+ if (key !== "append" && key !== "create" && key !== "update" && key !== "list")
23500
+ throw new Error("Choose create, update, append or list schema.");
23305
23501
  return external_exports.toJSONSchema(schemas[key], { io: "input" });
23306
23502
  }
23307
23503
  const api = createWorkClient().message;
23308
23504
  if (action === "types" && input === void 0) return api.types.query();
23505
+ if (action === "read" && typeof input === "string")
23506
+ return api.read.query(input);
23309
23507
  const payload = input === void 0 ? void 0 : JSON.parse(input);
23310
23508
  if (action === "list")
23311
23509
  return api.list.query(messageListSchema.optional().parse(payload));
23312
23510
  if (action === "read" && typeof input === "string")
23313
23511
  return api.read.query(input);
23512
+ if (action === "append")
23513
+ return api.append.mutate(messageAppendSchema.parse(payload));
23314
23514
  if (action === "create")
23315
23515
  return api.create.mutate(messageCreateSchema.parse(payload));
23316
23516
  if (action === "update")
23317
23517
  return api.update.mutate(messageUpdateSchema.parse(payload));
23318
23518
  throw new Error(
23319
- "Messages support types, create, update, read, list and schema."
23519
+ "Messages support types, create, update, append, read, list and schema."
23320
23520
  );
23321
23521
  };
23322
23522
 
@@ -23463,9 +23663,6 @@ var projectReleaseTargetSchema = external_exports.strictObject({
23463
23663
  "One independently released part of a project: its icon, name, and URL."
23464
23664
  );
23465
23665
 
23466
- // ../../packages/core/src/work/schemas/prioritySchema.ts
23467
- var prioritySchema = external_exports.enum(["high", "medium", "low", "unset"]).describe("Priority: high, medium, low, or unset when not assigned.");
23468
-
23469
23666
  // ../../packages/core/src/work/schemas/projectRepoSchema.ts
23470
23667
  var projectRepoSchema = external_exports.strictObject({
23471
23668
  url: linkSchema.refine((url2) => url2.length > 0, "Enter a repo URL."),
@@ -23691,14 +23888,6 @@ var taskSignOffSchema = external_exports.strictObject({
23691
23888
  "User sign-off for a task. Set only through the sign-off endpoint, never through task CRUD."
23692
23889
  );
23693
23890
 
23694
- // ../../packages/core/src/work/schemas/workerSchema.ts
23695
- var workerSchema = external_exports.strictObject({
23696
- threadId: external_exports.string().trim().min(1).max(200).describe("Stable worker thread ID."),
23697
- host: external_exports.string().trim().min(1).max(200).describe("Host identifier for the worker thread, such as local.")
23698
- }).describe(
23699
- "Structured worker destination; an identifier is not authentication or credential approval."
23700
- );
23701
-
23702
23891
  // ../../packages/core/src/work/schemas/terminalWorkerSchema.ts
23703
23892
  var terminalWorkerSchema = external_exports.strictObject({
23704
23893
  sessionId: external_exports.uuid().describe("Berth terminal session UUID."),
@@ -23913,6 +24102,22 @@ var runPromptCommand = async (args2) => {
23913
24102
  );
23914
24103
  };
23915
24104
 
24105
+ // ../../packages/core/src/work/data/getSessionRecoveryProblem.ts
24106
+ var getSessionRecoveryProblem = (session, task) => {
24107
+ if (!task || session.taskId !== task.uuid || task.status !== "working" || task.archivedAt)
24108
+ return "Task is not in the working column.";
24109
+ if (task.workerRun?.completedAt)
24110
+ return "This worker has already completed its work.";
24111
+ const owner = task.ownership?.worker;
24112
+ if (owner && (!("sessionId" in owner) || owner.sessionId !== session.id || owner.machineId !== session.machineId))
24113
+ return "Task is assigned to another worker.";
24114
+ if (task.workerId && task.workerId !== session.workerSessionId)
24115
+ return "Task is assigned to another worker.";
24116
+ if (session.endedAt || !session.startedAt)
24117
+ return "Session is not an unexpectedly exited session.";
24118
+ return null;
24119
+ };
24120
+
23916
24121
  // ../../packages/core/src/work/data/renderAgentPrompt.ts
23917
24122
  var import_handlebars = __toESM(require_lib(), 1);
23918
24123
 
@@ -23932,7 +24137,7 @@ var renderAgentPrompt = (prompts, key, variables) => import_handlebars.default.c
23932
24137
  })(variables);
23933
24138
 
23934
24139
  // src/runSessionCommand.ts
23935
- import { setTimeout as delay2 } from "node:timers/promises";
24140
+ import { setTimeout as delay3 } from "node:timers/promises";
23936
24141
  import { realpathSync } from "node:fs";
23937
24142
  import { homedir as homedir6 } from "node:os";
23938
24143
  import { join as join6 } from "node:path";
@@ -23990,56 +24195,8 @@ var findWorkerType = (types, id) => {
23990
24195
  return type;
23991
24196
  };
23992
24197
 
23993
- // ../../packages/core/src/work/data/resolveDefaultWorkerTypeId.ts
23994
- var resolveDefaultWorkerTypeId = (types, current) => types.some((type) => type.id === current) ? current : types[0]?.id ?? null;
23995
-
23996
- // src/prepareTaskWorkerInstance.ts
23997
- var prepareTaskWorkerInstance = async (taskId, instanceSlug) => {
23998
- external_exports.uuid().parse(taskId);
23999
- const config2 = await createWorkClient().config.read.query();
24000
- const workerTypeId = resolveDefaultWorkerTypeId(
24001
- config2.workerTypes ?? [],
24002
- config2.defaultWorkerTypeId
24003
- );
24004
- if (!workerTypeId) throw new Error("Configure a default worker type first.");
24005
- findWorkerType(config2.workerTypes ?? [], workerTypeId);
24006
- const context = await readTaskLaunchContext(taskId);
24007
- if (!["draft", "queue"].includes(context.task.status))
24008
- throw new Error(
24009
- "Only an unstarted Draft or Queue task can start a worker."
24010
- );
24011
- if (context.task.blocked || context.task.ownership?.worker || context.task.workerId)
24012
- throw new Error("Task is blocked or already assigned to a worker.");
24013
- if (context.state === "missing_instance") {
24014
- if (!context.project) throw new Error("Task has no project to prepare.");
24015
- await runInstanceCommand([
24016
- "prepare",
24017
- "--project",
24018
- context.project.id,
24019
- "--task",
24020
- context.task.slug,
24021
- ...instanceSlug ? ["--slug", instanceSlug] : []
24022
- ]);
24023
- } else if (context.state !== "ready") {
24024
- throw new Error(`Task instance is ${context.state}; cannot start worker.`);
24025
- }
24026
- return workerTypeId;
24027
- };
24028
-
24029
- // src/resolveSessionMachine.ts
24030
- import { hostname as hostname5 } from "node:os";
24031
- var resolveSessionMachine = (config2) => {
24032
- const configured = process.env.BERTH_MACHINE_ID;
24033
- const matches = config2.workMachines.filter(
24034
- (item) => configured ? item.id === configured : item.name === hostname5()
24035
- );
24036
- const machine = matches.length === 1 ? matches[0] : !configured && config2.workMachines.length === 1 ? config2.workMachines[0] : void 0;
24037
- if (!machine)
24038
- throw new Error(
24039
- "Work machine is ambiguous. Set BERTH_MACHINE_ID to a configured machine UUID."
24040
- );
24041
- return machine;
24042
- };
24198
+ // src/recoverTerminalSession.ts
24199
+ import { setTimeout as delay2 } from "node:timers/promises";
24043
24200
 
24044
24201
  // src/tmuxSession.ts
24045
24202
  import { spawnSync } from "node:child_process";
@@ -24122,6 +24279,79 @@ var attachTmuxSession = (name) => {
24122
24279
  throw new Error("Could not attach to the tmux session.");
24123
24280
  };
24124
24281
 
24282
+ // src/recoverTerminalSession.ts
24283
+ var recoverTerminalSession = async (session, worker) => {
24284
+ if (session.endedAt || !session.startedAt)
24285
+ throw new Error("Session is not an unexpectedly exited session.");
24286
+ if (tmuxSessionExists(session.terminalSessionId))
24287
+ throw new Error("Session is already running.");
24288
+ if (session.workerTypeId !== "shell" && (!worker?.resumeCommand || !session.workerSessionId))
24289
+ throw new Error(
24290
+ "Worker has no configured resume command or conversation ID."
24291
+ );
24292
+ if (worker && !/{{\s*prompt\s*}}/.test(worker.resumeCommand))
24293
+ throw new Error("Resume command must include {{prompt}} to continue work.");
24294
+ if (worker && !/{{\s*workerSessionId\s*}}/.test(worker.resumeCommand))
24295
+ throw new Error(
24296
+ "Resume command must include {{workerSessionId}} to resume the original conversation."
24297
+ );
24298
+ const command = worker ? renderWorkerCommand(worker.resumeCommand, {
24299
+ prompt: `Your terminal was interrupted. Review task ${session.taskId ?? ""} and your prior progress, then continue working. Respect any existing blockers and do not repeat completed actions.`,
24300
+ sessionId: session.id,
24301
+ workerSessionId: session.workerSessionId ?? "",
24302
+ taskId: session.taskId ?? "",
24303
+ directory: session.directory,
24304
+ terminalSessionId: session.terminalSessionId
24305
+ }) : null;
24306
+ startTmuxSession(
24307
+ session.terminalSessionId,
24308
+ session.directory,
24309
+ command,
24310
+ session.id,
24311
+ session.machineId
24312
+ );
24313
+ await delay2(250);
24314
+ if (!tmuxSessionExists(session.terminalSessionId))
24315
+ throw new Error("Worker exited immediately after recovery.");
24316
+ return { id: session.id, recovered: true };
24317
+ };
24318
+
24319
+ // ../../packages/core/src/work/data/resolveDefaultWorkerTypeId.ts
24320
+ var resolveDefaultWorkerTypeId = (types, current) => types.some((type) => type.id === current) ? current : types[0]?.id ?? null;
24321
+
24322
+ // src/prepareTaskWorkerInstance.ts
24323
+ var prepareTaskWorkerInstance = async (taskId, instanceSlug) => {
24324
+ external_exports.uuid().parse(taskId);
24325
+ const config2 = await createWorkClient().config.read.query();
24326
+ const workerTypeId = resolveDefaultWorkerTypeId(
24327
+ config2.workerTypes ?? [],
24328
+ config2.defaultWorkerTypeId
24329
+ );
24330
+ if (!workerTypeId) throw new Error("Configure a default worker type first.");
24331
+ findWorkerType(config2.workerTypes ?? [], workerTypeId);
24332
+ const context = await readTaskLaunchContext(taskId);
24333
+ if (!["draft", "queue"].includes(context.task.status))
24334
+ throw new Error(
24335
+ "Only an unstarted Draft or Queue task can start a worker."
24336
+ );
24337
+ if (context.task.blocked || context.task.ownership?.worker || context.task.workerId)
24338
+ throw new Error("Task is blocked or already assigned to a worker.");
24339
+ if (context.state === "missing_instance") {
24340
+ if (!context.project) throw new Error("Task has no project to prepare.");
24341
+ await runInstanceCommand([
24342
+ "prepare",
24343
+ "--project",
24344
+ context.project.id,
24345
+ "--task",
24346
+ context.task.slug,
24347
+ ...instanceSlug ? ["--slug", instanceSlug] : []
24348
+ ]);
24349
+ } else if (context.state !== "ready") {
24350
+ throw new Error(`Task instance is ${context.state}; cannot start worker.`);
24351
+ }
24352
+ return workerTypeId;
24353
+ };
24354
+
24125
24355
  // src/runSessionCommand.ts
24126
24356
  var runSessionCommand = async (args2) => {
24127
24357
  const [action, value, agentValue, instanceSlug, ...extra] = args2;
@@ -24147,6 +24377,27 @@ var runSessionCommand = async (args2) => {
24147
24377
  live: session.machineId === machine.id ? tmuxSessionExists(session.terminalSessionId) : null
24148
24378
  };
24149
24379
  }
24380
+ if (action === "recover" && value && !agentValue) {
24381
+ const session = await client.terminalSession.read.query(
24382
+ external_exports.uuid().parse(value)
24383
+ );
24384
+ const config2 = await client.config.read.query();
24385
+ const machine = resolveSessionMachine(config2);
24386
+ if (session.machineId !== machine.id)
24387
+ throw new Error("Session belongs to another work machine.");
24388
+ if (!session.taskId)
24389
+ throw new Error("Only task sessions can be recovered.");
24390
+ const tasks = await client.task.list.query({ status: "working" });
24391
+ const problem = getSessionRecoveryProblem(
24392
+ session,
24393
+ tasks.find((task) => task.uuid === session.taskId)
24394
+ );
24395
+ if (problem) throw new Error(problem);
24396
+ return recoverTerminalSession(
24397
+ session,
24398
+ session.workerTypeId === "shell" ? null : findWorkerType(config2.workerTypes ?? [], session.workerTypeId)
24399
+ );
24400
+ }
24150
24401
  if (action === "attach" && value && !agentValue) {
24151
24402
  const session = await client.terminalSession.read.query(
24152
24403
  external_exports.uuid().parse(value)
@@ -24218,7 +24469,7 @@ var runSessionCommand = async (args2) => {
24218
24469
  session.id,
24219
24470
  machine.id
24220
24471
  );
24221
- await delay2(250);
24472
+ await delay3(250);
24222
24473
  if (!tmuxSessionExists(session.terminalSessionId))
24223
24474
  throw new Error("Agent process exited immediately after launch.");
24224
24475
  return client.terminalSession.update.mutate({
@@ -24324,7 +24575,7 @@ var runSessionCommand = async (args2) => {
24324
24575
  session.id,
24325
24576
  context.machine.id
24326
24577
  );
24327
- await delay2(250);
24578
+ await delay3(250);
24328
24579
  if (!tmuxSessionExists(session.terminalSessionId))
24329
24580
  throw new Error("Agent process exited immediately after launch.");
24330
24581
  } catch (error51) {
@@ -24343,7 +24594,7 @@ var runSessionCommand = async (args2) => {
24343
24594
  });
24344
24595
  }
24345
24596
  throw new Error(
24346
- "Use berth session list|read|status|create|start|launch|attach|kill|delete|register|schema. Run berth --help."
24597
+ "Use berth session list|read|status|create|start|launch|recover|attach|kill|delete|register|schema. Run berth --help."
24347
24598
  );
24348
24599
  };
24349
24600
 
@@ -24437,6 +24688,9 @@ var runRoleCommand = async (args2) => {
24437
24688
  // ../../packages/core/src/work/schemas/signOffRequestSchema.ts
24438
24689
  var signOffRequestSchema = external_exports.strictObject({
24439
24690
  slug: taskSlugSchema,
24691
+ sender: external_exports.string().trim().min(1).optional().describe(
24692
+ "Requesting role within the project; defaults to delivery-owner."
24693
+ ),
24440
24694
  deliveryAttemptId: external_exports.uuid().optional().describe(
24441
24695
  "Latest recorded delivery attempt; omitted selects latest. Legacy issued requests remain valid without an attempt."
24442
24696
  ),
@@ -24448,128 +24702,6 @@ var signOffRequestSchema = external_exports.strictObject({
24448
24702
  )
24449
24703
  });
24450
24704
 
24451
- // ../../packages/core/src/work/schemas/blockerHandoffReceiptSchema.ts
24452
- var blockerHandoffReceiptSchema = external_exports.strictObject({
24453
- updateCount: external_exports.number().int().positive().describe(
24454
- "Blocker update-log length identifying the original unblocked cycle."
24455
- ),
24456
- consumer: slugSchema.describe("Controller that reserved this cycle."),
24457
- worker: workerSchema,
24458
- reservedAt: external_exports.iso.datetime().describe("Time the reservation was saved."),
24459
- delivery: external_exports.strictObject({
24460
- receiptId: external_exports.string().trim().min(1).max(200).describe("External admitted turn or delivery receipt ID."),
24461
- confirmedAt: external_exports.iso.datetime().describe("Time delivery was recorded."),
24462
- sourceUpdateId: external_exports.uuid().optional().describe(
24463
- "Existing informational update used to import a historical receipt."
24464
- )
24465
- }).nullable().describe(
24466
- "Null means delivery is uncertain or pending; reconcile before sending. A populated delivery must not be resent."
24467
- )
24468
- }).describe(
24469
- "Durable handoff receipt keyed by blocker update cycle; retained when reblocked."
24470
- );
24471
-
24472
- // ../../packages/core/src/work/schemas/blockerEventSchema.ts
24473
- var blockerEventSchema = external_exports.strictObject({
24474
- state: external_exports.enum(["blocked", "unblocked"]).describe(
24475
- "blocked is recorded by controller; unblocked is recorded by user."
24476
- ),
24477
- notes: external_exports.string().max(4e3).describe("Feedback or resolution notes for this change. Markdown.")
24478
- }).describe(
24479
- "An append-only update in the blocker\u2019s embedded log, oldest first."
24480
- );
24481
-
24482
- // ../../packages/core/src/work/schemas/blockerStatusSchema.ts
24483
- var blockerStatusSchema = messageStatusSchema.describe(
24484
- "active: user queue; later: deferred, still blocking; unblocked: resolved by the user, awaiting controller acknowledgment (whether claimed or not); archived: acknowledged. Use queue commands to transition."
24485
- );
24486
-
24487
- // ../../packages/core/src/work/schemas/blockerBaseSchema.ts
24488
- var blockerBaseSchema = external_exports.strictObject({
24489
- id: external_exports.uuid().describe(
24490
- "Immutable UUID generated by the server when this item is created."
24491
- ),
24492
- taskSlug: taskSlugSchema.describe(
24493
- "The existing task this blocker belongs to."
24494
- ),
24495
- status: blockerStatusSchema,
24496
- updates: external_exports.array(blockerEventSchema).min(1).describe(
24497
- "Embedded state change log, oldest first. Appended only by state-transition commands."
24498
- ),
24499
- handoffs: external_exports.array(blockerHandoffReceiptSchema).default([]).describe(
24500
- "Durable diagnostics handoffs, one per unblocked update cycle."
24501
- ),
24502
- createdAt: external_exports.iso.datetime().nullable().describe(
24503
- "Creation time in UTC, or null for imported records whose time is unknown."
24504
- ),
24505
- completedAt: external_exports.iso.datetime().nullable().describe(
24506
- "Time the user marked unblocked; null before unblocking or for legacy completions."
24507
- ),
24508
- acknowledgedAt: external_exports.iso.datetime().nullable().describe(
24509
- "Time the consumer acknowledged the item, or null before acknowledgment."
24510
- ),
24511
- dismissedAt: external_exports.iso.datetime().nullable().optional().describe(
24512
- "Time the user hid this pending-review cycle from the web inbox; controller acknowledgment is independent."
24513
- ),
24514
- consumer: slugSchema.nullable().describe(
24515
- "Controller consumer owning this acknowledgment queue item. Set atomically by pull; null before pulling."
24516
- ),
24517
- outcome: external_exports.string().max(4e3).describe(
24518
- "User completion note or legacy resolution; may be empty. Markdown."
24519
- ),
24520
- selectedOptionIndex: external_exports.number().int().min(-1).nullable().describe(
24521
- "Zero-based chosen decision option, or -1 for None of the above. Null for actions or decisions without a recorded choice."
24522
- )
24523
- }).describe(
24524
- "Server-managed identity, queue state and completion receipt for a blocker."
24525
- );
24526
-
24527
- // ../../packages/core/src/work/schemas/decisionOptionSchema.ts
24528
- var decisionOptionSchema = external_exports.strictObject({
24529
- title: external_exports.string().trim().min(1).max(120).describe("Short name for this choice."),
24530
- body: external_exports.string().trim().min(1).max(4e3).describe(
24531
- "Explain this option, its consequences and tradeoffs. Markdown."
24532
- ),
24533
- priority: prioritySchema.describe(
24534
- "Preference for this option: high, medium, low or unset. High is the strongest recommendation."
24535
- )
24536
- }).describe("One selectable option in a decision.");
24537
-
24538
- // ../../packages/core/src/work/schemas/decisionSchema.ts
24539
- var decisionSchema = external_exports.strictObject({
24540
- background: external_exports.string().trim().min(1).max(4e3).describe("Context the user needs to understand the decision. Markdown."),
24541
- problem: external_exports.string().trim().min(1).max(4e3).describe("The problem or question that needs a decision. Markdown."),
24542
- options: external_exports.array(decisionOptionSchema).max(12).describe(
24543
- "Predefined choices presented to the user. The UI always adds None of the above; do not include it in this array. New decisions require at least one option; legacy decisions may have none. Completion records the chosen index or -1 for None of the above."
24544
- )
24545
- }).describe("Decision content: background, problem, and prioritized options.");
24546
-
24547
- // ../../packages/core/src/work/schemas/actionSchema.ts
24548
- var actionSchema = external_exports.strictObject({
24549
- background: external_exports.string().trim().min(1).max(4e3).describe("Context explaining why this action is needed. Markdown."),
24550
- instructions: external_exports.string().trim().min(1).max(6e3).describe(
24551
- "Concrete instructions for the user to carry out before marking done. Markdown."
24552
- )
24553
- }).describe("Action content: background and instructions.");
24554
-
24555
- // ../../packages/core/src/work/schemas/blockerSchema.ts
24556
- var blockerSchema = external_exports.discriminatedUnion("type", [
24557
- blockerBaseSchema.extend({
24558
- ...decisionSchema.shape,
24559
- type: external_exports.literal("decision").describe(
24560
- "A one-time decision. Once submitted its choice is final; create a new decision for a new question."
24561
- )
24562
- }),
24563
- blockerBaseSchema.extend({
24564
- ...actionSchema.shape,
24565
- type: external_exports.literal("action").describe(
24566
- "An action requiring instructions to be carried out. It may be returned with feedback and actioned again."
24567
- )
24568
- })
24569
- ]).describe(
24570
- "A typed blocker and its queue lifecycle. Completed content is immutable until archived so consumers receive stable results."
24571
- );
24572
-
24573
24705
  // ../../packages/core/src/work/schemas/blockerPollSchema.ts
24574
24706
  var blockerPollSchema = external_exports.strictObject({
24575
24707
  id: external_exports.uuid(),
@@ -25209,6 +25341,7 @@ berth session status <session-uuid>
25209
25341
  berth session create [shell|worker-type-id] [directory]
25210
25342
  berth session start <task-uuid> [instance-slug]
25211
25343
  berth session launch <task-uuid> <worker-type-id> [instance-slug]
25344
+ berth session recover <session-uuid>
25212
25345
  berth session attach <session-uuid>
25213
25346
  berth session kill <session-uuid>
25214
25347
  berth session delete <session-uuid>
@@ -25235,7 +25368,8 @@ berth message create '{"recipient":{"name":"delivery-owner","projectId":"bond"},
25235
25368
  berth message update '{"id":"<message-id>","recipient":{"name":"delivery-owner","projectId":"bond"}}'
25236
25369
  berth message read <message-id>
25237
25370
  berth message list '{"recipient":{"name":"delivery-owner","projectId":"bond"}}'
25238
- berth message schema [create|update|list]
25371
+ berth message append '{"id":"<message-id>","expectedLength":1,"actor":"delivery-owner","action":"closed"}'
25372
+ berth message schema [create|update|append|list]
25239
25373
  Messages are stored records. Recipient, type, priority and status are independent.
25240
25374
  List includes stored messages; deleting one never deletes its task or blocker.
25241
25375
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mp-lb/berth",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {