@supercorks/krnl 0.1.12 → 0.1.13

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/README.md CHANGED
@@ -81,10 +81,22 @@ projects with one accessible local folder. Git mappings default to a new worktre
81
81
  needed. Non-Git projects use their saved folder. Missing folders, multi-root projects, and disabled
82
82
  project hooks are explained beside that project in Kernel.
83
83
 
84
- Tested Codex capabilities: desktop **26.901.51231** with bundled CLI **0.153.4**. The companion prefers
84
+ New Work checkouts live in Codex's configured worktree directory (normally
85
+ `~/.codex/worktrees/<full-task-id>/<repository-name>`), so Desktop can group them under
86
+ the selected saved project. Each task still has its own branch and checkout. Existing
87
+ checkouts and their saved paths are preserved when the companion updates.
88
+
89
+ Tested Codex capabilities: desktop **26.908.40834** with bundled CLI **0.154.0-alpha.6.2**. The companion prefers
85
90
  `/Applications/ChatGPT.app/Contents/Resources/codex`, falling back to `codex` on PATH. The app-server
86
91
  capabilities are experimental, so other Codex versions require verification.
87
92
 
93
+ In Kernel, **Stop** beside **Open in Codex** interrupts the companion-owned first Work turn
94
+ and releases the same saved task to desktop. Kernel shows **Stopping…** until release is
95
+ confirmed, then **Stopped. Continue in Codex.** It stays open and does not send a new message,
96
+ mark the Kernel task complete, or discard completed work. If confirmation takes longer than
97
+ fifteen seconds, **Retry Stop** retains the same durable request. Desktop continuations, Drafts,
98
+ and linked tasks have no Stop control in Kernel.
99
+
88
100
  The companion releases its first-turn app-server after completion or a request for input. Continue
89
101
  in desktop after that release; an interrupted structured question may be asked again. Kernel
90
102
  observes Running/Done across later desktop turns through hooks. It does not answer questions or
@@ -206,6 +218,13 @@ Install the current CLI and run `krnl connect codex` with the current Kernel ser
206
218
  Hooks update every association and retain an event until all receipt writes succeed, including
207
219
  across restarts.
208
220
 
221
+ CLI 0.1.13 adds first-turn ownership reports and durable stop delivery. Deploy the matching
222
+ Kernel migration and web contract before updating this package. Startup enriches historical
223
+ receipts with unknown ownership; it never assumes that a Desktop turn belongs to the companion.
224
+ An accepted stop survives network failures. After restart, release is confirmed only when the
225
+ saved owner's exit or process-identity replacement can be proved; inaccessible process metadata
226
+ remains unknown. Wait for active first turns to finish before restarting the service to upgrade.
227
+
209
228
  ## Bundled third-party software
210
229
 
211
230
  This executable includes Zod under the following license:
@@ -2737,13 +2737,13 @@ function time(args) {
2737
2737
  return new RegExp(`^${timeSource(args)}$`);
2738
2738
  }
2739
2739
  function datetime(args) {
2740
- const time3 = timeSource({ precision: args.precision });
2740
+ const time4 = timeSource({ precision: args.precision });
2741
2741
  const opts = ["Z"];
2742
2742
  if (args.local)
2743
2743
  opts.push("");
2744
2744
  if (args.offset)
2745
2745
  opts.push(`([+-](?:[01]\\d|2[0-3]):[0-5]\\d)`);
2746
- const timeRegex = `${time3}(?:${opts.join("|")})`;
2746
+ const timeRegex = `${time4}(?:${opts.join("|")})`;
2747
2747
  return new RegExp(`^${dateSource}T(?:${timeRegex})$`);
2748
2748
  }
2749
2749
  var string = (params) => {
@@ -15702,6 +15702,16 @@ async function processIdentity(pid) {
15702
15702
  async function observedProcessAlive(pid, identity2) {
15703
15703
  return Boolean(identity2 && await processIdentity(pid) === identity2);
15704
15704
  }
15705
+ async function ownedProcessReleased(pid, identity2) {
15706
+ if (!pid || !identity2) return false;
15707
+ try {
15708
+ process.kill(pid, 0);
15709
+ } catch (error51) {
15710
+ return error51.code === "ESRCH";
15711
+ }
15712
+ const current = await processIdentity(pid);
15713
+ return Boolean(current && current !== identity2);
15714
+ }
15705
15715
  var service = "Kernel Codex companion";
15706
15716
  async function readCredential(id, keychainService = service) {
15707
15717
  const { stdout } = await execute("/usr/bin/security", [
@@ -15954,7 +15964,7 @@ async function captureHook(raw) {
15954
15964
  import { randomUUID as randomUUID3 } from "node:crypto";
15955
15965
  import { readdir as readdir2, unlink, rename as rename2, mkdir as mkdir3 } from "node:fs/promises";
15956
15966
  import { join as join4 } from "node:path";
15957
- import { homedir as homedir3 } from "node:os";
15967
+ import { homedir as homedir4 } from "node:os";
15958
15968
  import { setTimeout as delay } from "node:timers/promises";
15959
15969
 
15960
15970
  // ../domain/src/api-keys.ts
@@ -20193,6 +20203,12 @@ var updateTaskSchema = taskFieldsSchema.partial().extend({
20193
20203
  var createTaskCommentSchema = external_exports.object({
20194
20204
  body: external_exports.string().trim().min(1).max(1e4)
20195
20205
  }).strict();
20206
+ var updateTaskCommentSchema = createTaskCommentSchema.extend({
20207
+ version: external_exports.number().int().positive()
20208
+ });
20209
+ var deleteTaskCommentSchema = external_exports.object({
20210
+ version: external_exports.number().int().positive()
20211
+ }).strict();
20196
20212
  var bulkUpdateTasksSchema = external_exports.object({
20197
20213
  tasks: external_exports.array(
20198
20214
  external_exports.object({
@@ -20682,33 +20698,39 @@ var pageFields = {
20682
20698
  cursor: external_exports.string().max(2048).optional(),
20683
20699
  limit: external_exports.coerce.number().int().min(1).max(100).default(25)
20684
20700
  };
20701
+ function dashboardSelections(option) {
20702
+ return external_exports.union([
20703
+ external_exports.array(option),
20704
+ external_exports.string().transform((value) => value.trim() ? value.split(",").map((v2) => v2.trim()) : []).pipe(external_exports.array(option))
20705
+ ]).optional().transform((values) => [...new Set(values ?? [])].sort());
20706
+ }
20685
20707
  var dashboardTaskQuerySchema = external_exports.object({
20686
20708
  ...pageFields,
20687
20709
  search: external_exports.string().trim().max(160).default(""),
20688
- project: external_exports.string().uuid().or(external_exports.literal("none")).optional(),
20689
- status: external_exports.enum(["blocked", "backlog", "ready", "active", "done"]).optional(),
20710
+ project: dashboardSelections(external_exports.string().uuid().or(external_exports.literal("none"))),
20711
+ status: dashboardSelections(external_exports.enum(["blocked", "backlog", "ready", "active", "done"])),
20690
20712
  direction: external_exports.enum(["asc", "desc"]).default("asc")
20691
20713
  }).strict();
20692
20714
  var dashboardInvoiceQuerySchema = external_exports.object({
20693
20715
  ...pageFields,
20694
20716
  search: external_exports.string().trim().max(160).default(""),
20695
- payment: external_exports.enum(["unpaid", "partially_paid", "paid"]).optional(),
20696
- currency: billingCurrencySchema.optional(),
20717
+ payment: dashboardSelections(external_exports.enum(["unpaid", "partially_paid", "paid"])),
20718
+ currency: dashboardSelections(billingCurrencySchema),
20697
20719
  sort: external_exports.enum(["number", "issueDate", "dueDate", "totalMinor"]).default("issueDate"),
20698
20720
  direction: external_exports.enum(["asc", "desc"]).default("desc")
20699
- }).strict().refine((q2) => q2.sort !== "totalMinor" || Boolean(q2.currency), {
20700
- message: "Choose a currency before sorting totals",
20721
+ }).strict().refine((q2) => q2.sort !== "totalMinor" || q2.currency.length === 1, {
20722
+ message: "Choose exactly one currency before sorting totals",
20701
20723
  path: ["currency"]
20702
20724
  });
20703
20725
  var dashboardReportQuerySchema = external_exports.object({
20704
20726
  ...pageFields,
20705
20727
  from: billingDateSchema.optional(),
20706
20728
  to: billingDateSchema.optional(),
20707
- project: external_exports.string().uuid().or(external_exports.literal("none")).optional(),
20708
- task: external_exports.string().uuid().or(external_exports.literal("none")).optional(),
20709
- billable: external_exports.enum(["billable", "non_billable"]).optional(),
20710
- state: external_exports.enum(["unbilled", "billed"]).optional(),
20711
- currency: billingCurrencySchema.optional(),
20729
+ project: dashboardSelections(external_exports.string().uuid().or(external_exports.literal("none"))),
20730
+ task: dashboardSelections(external_exports.string().uuid().or(external_exports.literal("none"))),
20731
+ billable: dashboardSelections(external_exports.enum(["billable", "non_billable"])),
20732
+ state: dashboardSelections(external_exports.enum(["unbilled", "billed"])),
20733
+ currency: dashboardSelections(billingCurrencySchema),
20712
20734
  groupBy: external_exports.enum(["project", "task", "billability", "state"]).default("project"),
20713
20735
  sort: external_exports.enum([
20714
20736
  "workDate",
@@ -21443,6 +21465,46 @@ var quickSchedulingAvailabilityQuerySchema = external_exports.object({
21443
21465
  }
21444
21466
  });
21445
21467
 
21468
+ // ../domain/src/scheduling-link-customization.ts
21469
+ var date5 = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use YYYY-MM-DD").refine((value) => {
21470
+ try {
21471
+ return qi.PlainDate.from(value).toString() === value;
21472
+ } catch {
21473
+ return false;
21474
+ }
21475
+ }, "Use a real calendar date");
21476
+ var time3 = external_exports.string().regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/, "Use a time from 00:00 to 23:59");
21477
+ var schedulingLinkCustomizationSchema = external_exports.object({
21478
+ durationMinutes: external_exports.coerce.number().int().min(5).max(480).optional(),
21479
+ fromDate: date5.optional(),
21480
+ toDate: date5.optional(),
21481
+ after: time3.optional(),
21482
+ before: time3.optional(),
21483
+ timezone: external_exports.string().refine(isValidTimezone, "Choose a valid timezone").optional()
21484
+ }).superRefine((value, context) => {
21485
+ if ((value.fromDate || value.toDate || value.after || value.before) && !value.timezone) {
21486
+ context.addIssue({
21487
+ code: "custom",
21488
+ path: ["timezone"],
21489
+ message: "Choose a timezone for the date and time limits"
21490
+ });
21491
+ }
21492
+ if (value.fromDate && value.toDate && value.toDate < value.fromDate) {
21493
+ context.addIssue({
21494
+ code: "custom",
21495
+ path: ["toDate"],
21496
+ message: "Through date must be on or after From date"
21497
+ });
21498
+ }
21499
+ if (value.after && value.before && value.before <= value.after) {
21500
+ context.addIssue({
21501
+ code: "custom",
21502
+ path: ["before"],
21503
+ message: "Finish by must be later than Start at or after; overnight windows are not supported"
21504
+ });
21505
+ }
21506
+ });
21507
+
21446
21508
  // ../domain/src/scheduling-links.ts
21447
21509
  var normalizedUniqueDurations = external_exports.array(external_exports.number().int().min(5).max(480)).min(1).max(8).transform((durations) => [...new Set(durations)].sort((left, right) => left - right));
21448
21510
  var schedulingLinkSlugSchema = external_exports.string().trim().toLowerCase().min(3).max(80).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use lowercase letters, numbers, and single hyphens");
@@ -21486,7 +21548,7 @@ var schedulingAvailabilityQuerySchema = external_exports.object({
21486
21548
  durationMinutes: external_exports.coerce.number().int().min(5).max(480),
21487
21549
  from: external_exports.coerce.date(),
21488
21550
  to: external_exports.coerce.date()
21489
- }).refine((value) => value.to > value.from, {
21551
+ }).and(schedulingLinkCustomizationSchema).refine((value) => value.to > value.from, {
21490
21552
  message: "Availability must end after it starts",
21491
21553
  path: ["to"]
21492
21554
  }).refine((value) => value.to.getTime() - value.from.getTime() <= 32 * 864e5, {
@@ -22004,6 +22066,7 @@ var launchCodexTaskSchema = external_exports.strictObject({
22004
22066
  taskId: external_exports.uuid(),
22005
22067
  taskVersion: external_exports.number().int().positive(),
22006
22068
  mode: codexModeSchema,
22069
+ additionalInstructions: external_exports.string().max(CODEX_LAUNCH_MAX_LENGTH).optional(),
22007
22070
  model: codexModelIdSchema.optional(),
22008
22071
  reasoningEffort: codexReasoningEffortSchema.optional(),
22009
22072
  checkout: codexCheckoutSchema.optional(),
@@ -22075,7 +22138,27 @@ var cancelCodexOperationSchema = external_exports.strictObject({
22075
22138
  taskId: external_exports.uuid(),
22076
22139
  kind: external_exports.enum(["launch", "lookup"])
22077
22140
  });
22141
+ var stopCodexTurnSchema = external_exports.strictObject({ taskId: external_exports.uuid(), launchId: external_exports.uuid() });
22142
+ var codexFirstTurnReportSchema = external_exports.strictObject({
22143
+ owner: external_exports.enum(["unknown", "companion", "released"]),
22144
+ turnId: external_exports.string().min(1).max(200).nullable(),
22145
+ stopped: external_exports.boolean()
22146
+ }).refine(
22147
+ (value) => (value.owner !== "companion" || value.turnId !== null) && (!value.stopped || value.owner === "released" && value.turnId !== null),
22148
+ "Invalid first-turn ownership"
22149
+ );
22150
+ var codexStopRequestsSchema = external_exports.strictObject({
22151
+ requests: external_exports.array(
22152
+ external_exports.strictObject({
22153
+ launchId: external_exports.uuid(),
22154
+ threadId: external_exports.uuid(),
22155
+ turnId: external_exports.string().min(1).max(200),
22156
+ requestedAt: external_exports.iso.datetime()
22157
+ })
22158
+ ).max(200)
22159
+ });
22078
22160
  var codexStatusReportSchema = external_exports.strictObject({
22161
+ firstTurn: codexFirstTurnReportSchema.optional(),
22079
22162
  title: external_exports.string().trim().min(1).max(300).optional(),
22080
22163
  observationAgeMs: external_exports.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
22081
22164
  launchId: external_exports.uuid(),
@@ -22267,6 +22350,29 @@ var settingsHealthSnapshotSchema = external_exports.object({
22267
22350
  })
22268
22351
  });
22269
22352
 
22353
+ // ../domain/src/work-notes.ts
22354
+ var createTaskWorkNoteSchema = external_exports.object({
22355
+ body: external_exports.string().trim().min(1).max(5e4),
22356
+ codexHandoffId: external_exports.string().uuid().nullable().optional()
22357
+ }).strict();
22358
+ var updateTaskWorkNoteSchema = external_exports.object({
22359
+ body: external_exports.string().trim().min(1).max(5e4),
22360
+ version: external_exports.number().int().positive()
22361
+ }).strict();
22362
+ var deleteTaskWorkNoteSchema = external_exports.object({ version: external_exports.number().int().positive() }).strict();
22363
+ var workNoteSourceSchema = external_exports.enum(["task", "time-entry", "draft-entry"]);
22364
+ var workNoteReferenceSchema = external_exports.string().regex(
22365
+ /^(task|time-entry|draft-entry):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
22366
+ );
22367
+ var taskWorkNotesQuerySchema = external_exports.object({
22368
+ limit: external_exports.coerce.number().int().min(1).max(100).default(25),
22369
+ cursor: external_exports.string().max(2e3).optional(),
22370
+ codexHandoffId: external_exports.string().uuid().optional()
22371
+ });
22372
+ var workNoteHistoryQuerySchema = taskWorkNotesQuerySchema.extend({
22373
+ note: workNoteReferenceSchema.optional()
22374
+ });
22375
+
22270
22376
  // src/protocol.ts
22271
22377
  import { spawn as spawn2 } from "node:child_process";
22272
22378
  import { existsSync } from "node:fs";
@@ -22824,7 +22930,8 @@ async function linkedReceipt(server, watch, pairing, catalog) {
22824
22930
 
22825
22931
  // src/projects.ts
22826
22932
  import { mkdir as mkdir2, realpath as realpath3, stat as stat2 } from "node:fs/promises";
22827
- import { join as join3, isAbsolute as isAbsolute2 } from "node:path";
22933
+ import { homedir as homedir3 } from "node:os";
22934
+ import { basename, join as join3, isAbsolute as isAbsolute2 } from "node:path";
22828
22935
  async function discoverProjects(server) {
22829
22936
  const signal = AbortSignal.timeout(25e3);
22830
22937
  const projects = [];
@@ -22903,11 +23010,20 @@ function authorizedDestination(pairing, launch, catalog) {
22903
23010
  throw new DestinationAuthorizationError("The saved project folder must be absolute");
22904
23011
  return current;
22905
23012
  }
22906
- async function prepareCheckout(project, launch, directory) {
23013
+ async function prepareCheckout(project, launch, server) {
22907
23014
  if (launch.checkout === "folder") return project.root;
22908
23015
  if (!project.isGit || !project.defaultBranch) throw new Error("A Git default branch is required");
22909
- const cwd = join3(directory, "worktrees", launch.id);
22910
- await mkdir2(join3(directory, "worktrees"), { recursive: true, mode: 448 });
23016
+ const config2 = await server.call("config/read", { includeLayers: false }, 5e3);
23017
+ const configuredRoot = config2.config.desktop?.["git-worktree-root"];
23018
+ if (configuredRoot != null && typeof configuredRoot !== "string")
23019
+ throw new Error("The Codex worktree directory is invalid");
23020
+ const worktreesRoot = configuredRoot?.trim() || join3(process.env.CODEX_HOME || join3(homedir3(), ".codex"), "worktrees");
23021
+ if (!isAbsolute2(worktreesRoot)) throw new Error("The Codex worktree directory must be absolute");
23022
+ const container = join3(worktreesRoot, launch.id.replaceAll("-", ""));
23023
+ const name = basename(project.root);
23024
+ const checkoutName = /^\d{6}-\d{4}-[0-9a-f]{8}$/i.test(name) ? `project-${name}` : name;
23025
+ const cwd = join3(container, checkoutName);
23026
+ await mkdir2(container, { recursive: true, mode: 448 });
22911
23027
  await execute("git", [
22912
23028
  "-C",
22913
23029
  project.root,
@@ -22940,7 +23056,8 @@ function initialReceipt(launch) {
22940
23056
  state: "running",
22941
23057
  waiting: false,
22942
23058
  turnId: null,
22943
- message: null
23059
+ message: null,
23060
+ firstTurn: { owner: "unknown", turnId: null, stopped: false }
22944
23061
  }
22945
23062
  };
22946
23063
  }
@@ -23071,6 +23188,23 @@ async function runCompanion(machineId, entrypoint2, signal) {
23071
23188
  continue;
23072
23189
  }
23073
23190
  delete receipt.observedAt;
23191
+ if (!receipt.status.firstTurn) {
23192
+ receipt.status.firstTurn = {
23193
+ owner: "unknown",
23194
+ turnId: receipt.turnId ?? null,
23195
+ stopped: false
23196
+ };
23197
+ receipt.status.sequence++;
23198
+ }
23199
+ if (receipt.status.firstTurn?.owner === "companion") {
23200
+ const released = await ownedProcessReleased(receipt.ownerPid, receipt.ownerIdentity);
23201
+ receipt.status.firstTurn = {
23202
+ ...receipt.status.firstTurn,
23203
+ owner: released ? "released" : "unknown",
23204
+ stopped: released && Boolean(receipt.stopRequestedAt)
23205
+ };
23206
+ receipt.status.sequence++;
23207
+ }
23074
23208
  if (receipt.phase === "opening-draft") {
23075
23209
  receipt.phase = "drafted";
23076
23210
  receipt.draftOpenUncertain = true;
@@ -23164,24 +23298,32 @@ async function runCompanion(machineId, entrypoint2, signal) {
23164
23298
  let journalHealthy = true;
23165
23299
  let clockAnchor;
23166
23300
  const kernelNow = () => clockAnchor ? clockAnchor.server + Math.max(0, performance.now() - clockAnchor.monotonic) : Date.now();
23167
- const release = async (receipt, waiting, message) => {
23301
+ const release = async (receipt, waiting, message, stopRequestedAt) => {
23168
23302
  const owner = owners.get(receipt.launch.id);
23303
+ if (stopRequestedAt) receipt.stopRequestedAt = stopRequestedAt;
23169
23304
  receipt.status = {
23170
23305
  ...receipt.status,
23171
23306
  sequence: receipt.status.sequence + 1,
23172
23307
  state: "running",
23173
23308
  waiting,
23174
23309
  turnId: receipt.turnId ?? null,
23175
- message
23310
+ message: stopRequestedAt ? "Stopping the first turn." : message
23176
23311
  };
23177
23312
  receipt.handoffWaiting = waiting;
23178
23313
  observe(receipt);
23179
23314
  await save(receipt);
23180
- if (waiting && owner && receipt.turnId)
23181
- await owner.call("turn/interrupt", { threadId: receipt.threadId, turnId: receipt.turnId }).catch(() => void 0);
23315
+ if ((waiting || stopRequestedAt) && owner && receipt.turnId)
23316
+ await owner.call("turn/interrupt", { threadId: receipt.threadId, turnId: receipt.turnId }, 5e3).catch(() => void 0);
23182
23317
  await owner?.stop();
23183
23318
  owners.delete(receipt.launch.id);
23184
23319
  receipt.phase = "released";
23320
+ receipt.status.sequence++;
23321
+ receipt.status.firstTurn = {
23322
+ owner: "released",
23323
+ turnId: receipt.turnId ?? null,
23324
+ stopped: Boolean(receipt.stopRequestedAt)
23325
+ };
23326
+ receipt.status.message = message;
23185
23327
  if (!waiting) receipt.status.state = "done";
23186
23328
  await save(receipt);
23187
23329
  };
@@ -23220,7 +23362,7 @@ async function runCompanion(machineId, entrypoint2, signal) {
23220
23362
  await save(receipt);
23221
23363
  return;
23222
23364
  }
23223
- receipt.cwd = await prepareCheckout(project, launch, directory);
23365
+ receipt.cwd = await prepareCheckout(project, launch, observer);
23224
23366
  await save(receipt);
23225
23367
  if (!canBegin(receipt, kernelNow()))
23226
23368
  throw new Error("The launch expired during checkout preparation");
@@ -23281,6 +23423,8 @@ async function runCompanion(machineId, entrypoint2, signal) {
23281
23423
  observe(receipt);
23282
23424
  receipt.turnId = response.turn.id;
23283
23425
  receipt.status.turnId = response.turn.id;
23426
+ receipt.status.firstTurn = { owner: "companion", turnId: response.turn.id, stopped: false };
23427
+ receipt.status.sequence++;
23284
23428
  await save(receipt);
23285
23429
  } catch (error51) {
23286
23430
  if (launch.mode === "draft") {
@@ -23309,6 +23453,11 @@ async function runCompanion(machineId, entrypoint2, signal) {
23309
23453
  }
23310
23454
  owners.delete(launch.id);
23311
23455
  if (receipt.threadId) {
23456
+ receipt.status.firstTurn = {
23457
+ owner: "released",
23458
+ turnId: receipt.turnId ?? null,
23459
+ stopped: false
23460
+ };
23312
23461
  receipt.phase = "released";
23313
23462
  receipt.status = {
23314
23463
  ...receipt.status,
@@ -23355,7 +23504,7 @@ async function runCompanion(machineId, entrypoint2, signal) {
23355
23504
  if (!observer) await connectObserver();
23356
23505
  pairingSchema.parse(await readJson(join4(directory, "pairing.json")));
23357
23506
  const discovered = await discoverProjects(catalogServer);
23358
- const ready = await hooksReady(catalogServer, [homedir3()], hookCommand(entrypoint2));
23507
+ const ready = await hooksReady(catalogServer, [homedir4()], hookCommand(entrypoint2));
23359
23508
  const projects = await mapConcurrent(discovered, 4, async (project) => {
23360
23509
  if (project.issue) return project;
23361
23510
  try {
@@ -23492,6 +23641,7 @@ async function runCompanion(machineId, entrypoint2, signal) {
23492
23641
  if (turn) {
23493
23642
  receipt.turnId = turn.id;
23494
23643
  receipt.status.turnId = turn.id;
23644
+ receipt.status.firstTurn = { owner: "companion", turnId: turn.id, stopped: false };
23495
23645
  observe(receipt);
23496
23646
  await save(receipt);
23497
23647
  }
@@ -23509,7 +23659,13 @@ async function runCompanion(machineId, entrypoint2, signal) {
23509
23659
  turn?.status === "failed" ? "Codex ended this turn with an error. Open the task for details." : null
23510
23660
  );
23511
23661
  } else if (event.method === "kernel/closed" && receipt.threadId) {
23662
+ owners.delete(receipt.launch.id);
23512
23663
  receipt.phase = "released";
23664
+ receipt.status.firstTurn = {
23665
+ owner: "released",
23666
+ turnId: receipt.turnId ?? null,
23667
+ stopped: Boolean(receipt.stopRequestedAt)
23668
+ };
23513
23669
  receipt.status = {
23514
23670
  ...receipt.status,
23515
23671
  sequence: receipt.status.sequence + 1,
@@ -23602,6 +23758,13 @@ async function runCompanion(machineId, entrypoint2, signal) {
23602
23758
  await save(receipt);
23603
23759
  return;
23604
23760
  }
23761
+ if (receipt.status.firstTurn?.owner === "unknown" && receipt.ownerIdentity && await ownedProcessReleased(receipt.ownerPid, receipt.ownerIdentity)) {
23762
+ receipt.status.firstTurn = {
23763
+ ...receipt.status.firstTurn,
23764
+ owner: "released",
23765
+ stopped: Boolean(receipt.stopRequestedAt)
23766
+ };
23767
+ }
23605
23768
  const { data } = await observer.call(
23606
23769
  "thread/turns/list",
23607
23770
  { threadId: receipt.threadId, limit: 1, itemsView: "notLoaded" },
@@ -23617,7 +23780,15 @@ async function runCompanion(machineId, entrypoint2, signal) {
23617
23780
  state: "done",
23618
23781
  waiting: false,
23619
23782
  turnId: turn.id,
23620
- message: turn.status === "failed" ? "Codex ended this turn with an error. Open the task for details." : interrupted ? "Stopped in Codex." : null
23783
+ message: turn.status === "failed" ? "Codex ended this turn with an error. Open the task for details." : interrupted ? receipt.status.firstTurn?.stopped && turn.id === receipt.turnId ? "Stopped. Continue in Codex." : "Stopped in Codex." : null
23784
+ };
23785
+ observe(receipt);
23786
+ } else if (receipt.status.firstTurn?.stopped && (!turn || turn.id === receipt.turnId) && (!receipt.status.turnId || receipt.status.turnId === receipt.turnId)) {
23787
+ receipt.status = {
23788
+ ...receipt.status,
23789
+ state: "done",
23790
+ waiting: false,
23791
+ message: "Stopped. Continue in Codex."
23621
23792
  };
23622
23793
  observe(receipt);
23623
23794
  } else if ((receipt.handoffWaiting || receipt.hookAt && alive) && receipt.status.state === "running") {
@@ -23643,6 +23814,20 @@ async function runCompanion(machineId, entrypoint2, signal) {
23643
23814
  })
23644
23815
  );
23645
23816
  };
23817
+ const stops = async () => {
23818
+ if (!clockAnchor) return;
23819
+ const { requests } = codexStopRequestsSchema.parse(await post("stops", {}));
23820
+ await mapConcurrent(
23821
+ requests,
23822
+ 4,
23823
+ (request) => writers.run(request.launchId, async () => {
23824
+ const receipt = receipts.get(request.launchId);
23825
+ if (!receipt || receipt.phase === "released" || !owners.has(request.launchId) || receipt.threadId !== request.threadId || receipt.turnId !== request.turnId || receipt.status.firstTurn?.owner !== "companion" || !("mode" in receipt.launch) || receipt.launch.mode !== "work")
23826
+ return;
23827
+ await release(receipt, false, "Stopped. Continue in Codex.", request.requestedAt);
23828
+ })
23829
+ );
23830
+ };
23646
23831
  let deliveryCursor = 0;
23647
23832
  const deliver = async () => {
23648
23833
  if (!clockAnchor) return;
@@ -23837,6 +24022,7 @@ async function runCompanion(machineId, entrypoint2, signal) {
23837
24022
  schedule("reconciliation", 2e3, reconcile);
23838
24023
  schedule("observation", 2e3, inspect);
23839
24024
  schedule("delivery", 0, deliver);
24025
+ schedule("stops", 2e3, stops);
23840
24026
  schedule("lookups", 2e3, lookups);
23841
24027
  schedule("launches", 2e3, launches);
23842
24028
  schedule("diagnostics", 2e3, diagnostics);
@@ -24048,14 +24234,14 @@ ${url2.href}
24048
24234
  // src/onboarding.ts
24049
24235
  import { randomBytes as randomBytes2, randomUUID as randomUUID6 } from "node:crypto";
24050
24236
  import { rm as rm2 } from "node:fs/promises";
24051
- import { homedir as homedir5, hostname as hostname4 } from "node:os";
24237
+ import { homedir as homedir6, hostname as hostname4 } from "node:os";
24052
24238
  import { join as join7 } from "node:path";
24053
24239
  import { setTimeout as delay2 } from "node:timers/promises";
24054
24240
  import { createInterface } from "node:readline/promises";
24055
24241
 
24056
24242
  // src/setup.ts
24057
24243
  import { chmod as chmod2, copyFile, mkdir as mkdir4, rename as rename3, rm } from "node:fs/promises";
24058
- import { homedir as homedir4 } from "node:os";
24244
+ import { homedir as homedir5 } from "node:os";
24059
24245
  import { dirname as dirname2, join as join6, resolve as resolve2 } from "node:path";
24060
24246
  import { randomUUID as randomUUID5 } from "node:crypto";
24061
24247
  var installedEntrypoint = join6(dataRoot, "bin/kernel-companion.mjs");
@@ -24072,7 +24258,7 @@ async function installBundle(entrypoint2) {
24072
24258
  }
24073
24259
  }
24074
24260
  const hooksPath = await installHooks(installedEntrypoint);
24075
- const server = new AppServer(homedir4());
24261
+ const server = new AppServer(homedir5());
24076
24262
  try {
24077
24263
  await server.initialize();
24078
24264
  await server.call("config/value/write", {
@@ -24093,7 +24279,7 @@ function serviceDefinition(id, executable = process.execPath, path = process.env
24093
24279
  const args = [executable, installedEntrypoint, "run", id].map((value) => `<string>${xml(value)}</string>`).join("");
24094
24280
  return `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>work.krnl.companion.${id}</string><key>ProgramArguments</key><array>${args}</array><key>WorkingDirectory</key><string>${xml(pairingDirectory(id))}</string><key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(path)}</string>${codexHome ? `<key>CODEX_HOME</key><string>${xml(codexHome)}</string>` : ""}</dict><key>StandardOutPath</key><string>${xml(join6(pairingDirectory(id), "service.log"))}</string><key>StandardErrorPath</key><string>${xml(join6(pairingDirectory(id), "service.log"))}</string><key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict><key>ThrottleInterval</key><integer>30</integer></dict></plist>`;
24095
24281
  }
24096
- var plistPath = (id) => join6(homedir4(), "Library/LaunchAgents", `work.krnl.companion.${external_exports.uuid().parse(id)}.plist`);
24282
+ var plistPath = (id) => join6(homedir5(), "Library/LaunchAgents", `work.krnl.companion.${external_exports.uuid().parse(id)}.plist`);
24097
24283
  async function stopService(id, remove = false) {
24098
24284
  external_exports.uuid().parse(id);
24099
24285
  await execute("launchctl", [
@@ -24137,7 +24323,7 @@ async function consent(account) {
24137
24323
  }
24138
24324
  async function connectCodex(entrypoint2) {
24139
24325
  const profile = await signedInProfile();
24140
- const server = new AppServer(homedir5());
24326
+ const server = new AppServer(homedir6());
24141
24327
  try {
24142
24328
  await server.initialize();
24143
24329
  await discoverProjects(server);
@@ -24211,11 +24397,11 @@ async function connectCodex(entrypoint2) {
24211
24397
  await unlock();
24212
24398
  }
24213
24399
  await installService(id);
24214
- const check2 = new AppServer(homedir5());
24400
+ const check2 = new AppServer(homedir6());
24215
24401
  let ready = false;
24216
24402
  try {
24217
24403
  await check2.initialize();
24218
- ready = await hooksReady(check2, [homedir5()], hookCommand(installedEntrypoint));
24404
+ ready = await hooksReady(check2, [homedir6()], hookCommand(installedEntrypoint));
24219
24405
  } finally {
24220
24406
  await check2.stop();
24221
24407
  }
@@ -24352,7 +24538,7 @@ async function main() {
24352
24538
  }
24353
24539
  if (command === "--version") {
24354
24540
  process.stdout.write(
24355
- `${false ? "development" : "0.1.12"}
24541
+ `${false ? "development" : "0.1.13"}
24356
24542
  `
24357
24543
  );
24358
24544
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supercorks/krnl",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Connect Kernel to Codex on your Mac",
5
5
  "type": "module",
6
6
  "bin": {