@codetreker/daynight 0.21.13 → 0.24.0

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 (102) hide show
  1. package/README.md +364 -287
  2. package/config.example.yml +69 -0
  3. package/dist/api/client.d.ts +2 -2
  4. package/dist/api/client.js +10 -7
  5. package/dist/api/client.js.map +1 -1
  6. package/dist/api/types.d.ts +1 -8
  7. package/dist/cli/prompts.d.ts +1 -0
  8. package/dist/commands/auth/credentials.js +1 -1
  9. package/dist/commands/auth/credentials.js.map +1 -1
  10. package/dist/commands/review/config.d.ts +2 -1
  11. package/dist/commands/review/config.js +22 -39
  12. package/dist/commands/review/config.js.map +1 -1
  13. package/dist/commands/review/configuration-guide.js +7 -6
  14. package/dist/commands/review/configuration-guide.js.map +1 -1
  15. package/dist/commands/review/definition.js +1 -1
  16. package/dist/commands/review/definition.js.map +1 -1
  17. package/dist/commands/review/repositories.js +19 -19
  18. package/dist/commands/review/repositories.js.map +1 -1
  19. package/dist/commands/skill/skills/using-daynight/references/execute-task-in-managed.md +6 -5
  20. package/dist/commands/skill/skills/using-daynight/references/handbook.md +1 -1
  21. package/dist/commands/skill/skills/using-daynight/references/handoff.md +1 -1
  22. package/dist/commands/worker/attach.d.ts +3 -0
  23. package/dist/commands/worker/attach.js +53 -0
  24. package/dist/commands/worker/attach.js.map +1 -0
  25. package/dist/commands/worker/cleanup.d.ts +8 -2
  26. package/dist/commands/worker/cleanup.js +44 -160
  27. package/dist/commands/worker/cleanup.js.map +1 -1
  28. package/dist/commands/worker/config.d.ts +13 -3
  29. package/dist/commands/worker/config.js +35 -13
  30. package/dist/commands/worker/config.js.map +1 -1
  31. package/dist/commands/worker/configuration-guide.d.ts +19 -0
  32. package/dist/commands/worker/configuration-guide.js +86 -0
  33. package/dist/commands/worker/configuration-guide.js.map +1 -0
  34. package/dist/commands/worker/control.d.ts +11 -18
  35. package/dist/commands/worker/control.js +75 -132
  36. package/dist/commands/worker/control.js.map +1 -1
  37. package/dist/commands/worker/coordinator.d.ts +10 -8
  38. package/dist/commands/worker/coordinator.js +311 -473
  39. package/dist/commands/worker/coordinator.js.map +1 -1
  40. package/dist/commands/worker/definition.js +34 -18
  41. package/dist/commands/worker/definition.js.map +1 -1
  42. package/dist/commands/worker/diagnostics.js +2 -3
  43. package/dist/commands/worker/diagnostics.js.map +1 -1
  44. package/dist/commands/worker/execution.d.ts +35 -10
  45. package/dist/commands/worker/execution.js +92 -283
  46. package/dist/commands/worker/execution.js.map +1 -1
  47. package/dist/commands/worker/herdr/agents.d.ts +12 -91
  48. package/dist/commands/worker/herdr/agents.js +111 -358
  49. package/dist/commands/worker/herdr/agents.js.map +1 -1
  50. package/dist/commands/worker/herdr/instance.d.ts +1 -0
  51. package/dist/commands/worker/herdr/instance.js +30 -5
  52. package/dist/commands/worker/herdr/instance.js.map +1 -1
  53. package/dist/commands/worker/herdr/preflight.d.ts +21 -9
  54. package/dist/commands/worker/herdr/preflight.js +118 -124
  55. package/dist/commands/worker/herdr/preflight.js.map +1 -1
  56. package/dist/commands/worker/herdr.d.ts +7 -63
  57. package/dist/commands/worker/herdr.js.map +1 -1
  58. package/dist/commands/worker/identity.js +2 -2
  59. package/dist/commands/worker/identity.js.map +1 -1
  60. package/dist/commands/worker/index.d.ts +8 -0
  61. package/dist/commands/worker/index.js +21 -14
  62. package/dist/commands/worker/index.js.map +1 -1
  63. package/dist/commands/worker/prompts.d.ts +10 -3
  64. package/dist/commands/worker/prompts.js +21 -32
  65. package/dist/commands/worker/prompts.js.map +1 -1
  66. package/dist/commands/worker/remote.d.ts +2 -3
  67. package/dist/commands/worker/remote.js +1 -6
  68. package/dist/commands/worker/remote.js.map +1 -1
  69. package/dist/commands/worker/repositories.d.ts +40 -0
  70. package/dist/commands/worker/repositories.js +236 -0
  71. package/dist/commands/worker/repositories.js.map +1 -0
  72. package/dist/commands/worker/runtime.d.ts +1 -0
  73. package/dist/commands/worker/runtime.js +135 -212
  74. package/dist/commands/worker/runtime.js.map +1 -1
  75. package/dist/commands/worker/setup-environment.d.ts +31 -0
  76. package/dist/commands/worker/setup-environment.js +146 -0
  77. package/dist/commands/worker/setup-environment.js.map +1 -0
  78. package/dist/commands/worker/setup.d.ts +11 -0
  79. package/dist/commands/worker/setup.js +113 -0
  80. package/dist/commands/worker/setup.js.map +1 -0
  81. package/dist/commands/worker/store.d.ts +12 -114
  82. package/dist/commands/worker/store.js +92 -817
  83. package/dist/commands/worker/store.js.map +1 -1
  84. package/dist/commands/worker/types.d.ts +10 -114
  85. package/dist/commands/worker/worktrees.d.ts +9 -9
  86. package/dist/commands/worker/worktrees.js +77 -37
  87. package/dist/commands/worker/worktrees.js.map +1 -1
  88. package/dist/config/document.d.ts +0 -1
  89. package/dist/config/document.js +1 -22
  90. package/dist/config/document.js.map +1 -1
  91. package/dist/config/errors.d.ts +2 -2
  92. package/dist/config/errors.js.map +1 -1
  93. package/dist/config/files.d.ts +7 -1
  94. package/dist/config/files.js +4 -1
  95. package/dist/config/files.js.map +1 -1
  96. package/dist/config/ownership.d.ts +12 -0
  97. package/dist/config/ownership.js +39 -0
  98. package/dist/config/ownership.js.map +1 -0
  99. package/dist/repository/location.d.ts +7 -0
  100. package/dist/repository/location.js +51 -0
  101. package/dist/repository/location.js.map +1 -0
  102. package/package.json +4 -3
@@ -27,61 +27,9 @@ const pid = (value) => { if (typeof value !== "number" || !Number.isSafeInteger(
27
27
  throw new TypeError("Herdr process ID must be a positive process identifier."); return value; };
28
28
  const counter = (value, name) => { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
29
29
  throw new TypeError(`${name} must be a nonnegative safe integer.`); return value; };
30
- const nativeProcessEvidence = (value, agent) => {
31
- const executable = (candidate) => {
32
- if (typeof candidate !== "string")
33
- return false;
34
- const leaf = candidate.split(/[\\/]/).at(-1)?.toLowerCase();
35
- return leaf === agent || leaf === `${agent}.exe` || leaf === `${agent}.js`;
36
- };
37
- if (value.argv !== undefined) {
38
- const argv = array(value.argv, "Native process arguments");
39
- if (argv.some((argument) => typeof argument !== "string"))
40
- throw new TypeError("Native process arguments must be strings.");
41
- if (executable(argv[0]))
42
- return true;
43
- const interpreter = typeof argv[0] === "string" ? argv[0].split(/[\\/]/).at(-1)?.toLowerCase() : undefined;
44
- if ((interpreter === "node" || interpreter === "node.exe") && executable(argv[1]))
45
- return true;
46
- }
47
- return executable(value.argv0) || executable(value.name);
48
- };
49
- const nativeInteractiveProcessEvidence = (value, agent) => {
50
- if (!Array.isArray(value.argv) || !value.argv.every((argument) => typeof argument === "string"))
51
- return false;
52
- const argv = value.argv;
53
- const leaf = (argument) => argument?.split(/[\\/]/).at(-1)?.toLowerCase();
54
- const native = (argument) => [agent, `${agent}.exe`, `${agent}.js`].includes(leaf(argument));
55
- const offset = native(argv[0]) ? 1 : ["node", "node.exe"].includes(leaf(argv[0])) && native(argv[1]) ? 2 : 0;
56
- if (offset === 0)
57
- return false;
58
- const args = argv.slice(offset);
59
- // Only an interactive invocation can fence deferred input. Subcommands such
60
- // as `codex exec` and their descendants are not the original UI process.
61
- if (agent === "codex" && args[0] === "--no-daemon")
62
- args.shift();
63
- if (agent === "codex" && args[0] === "resume" && typeof args[1] === "string" && canonicalId(args[1]) !== null)
64
- args.splice(0, 2);
65
- if (agent === "copilot" && args[0]?.startsWith("--resume=") && canonicalId(args[0].slice("--resume=".length)) !== null)
66
- args.shift();
67
- if (args[0] === "--model" && typeof args[1] === "string" && args[1] !== "")
68
- args.splice(0, 2);
69
- try {
70
- validateHerdrAgentArguments(agent, args);
71
- return true;
72
- }
73
- catch {
74
- return false;
75
- }
76
- };
77
30
  const expectedResult = (value, type) => { if (value.type !== type)
78
31
  throw new TypeError(`Expected Herdr ${type} result; received ${value.type}.`); return value; };
79
- const sameBinding = (left, right) => left.session === right.session && left.workspaceId === right.workspaceId && left.paneId === right.paneId && left.terminalId === right.terminalId;
80
32
  const contextIdentity = (context) => {
81
- if (context.agent !== "codex" && context.agent !== "copilot")
82
- throw new TypeError("Execution agent must be codex or copilot.");
83
- if (!path.isAbsolute(context.cwd) || /[\r\n\0]/.test(context.cwd))
84
- throw new TypeError("The task cwd must be an absolute path without line breaks or NUL.");
85
33
  const identifier = (value, label) => { const result = canonicalId(value); if (result === null)
86
34
  throw new TypeError(`${label} must be a UUID.`); return result; };
87
35
  return { serverOrigin: normalizeServerUrl(context.serverOrigin), projectId: identifier(context.projectId, "Project ID"), taskId: identifier(context.taskId, "Task ID"), claimId: identifier(context.claimId, "Claim ID"), worktreeId: identifier(context.worktreeId, "Worktree ID") };
@@ -107,14 +55,12 @@ export class HerdrAgentActionError extends Error {
107
55
  action;
108
56
  submission;
109
57
  promptSubmission;
110
- launchEvidence;
111
58
  constructor(message, action, submission, options) {
112
59
  super(message, options);
113
60
  this.action = action;
114
61
  this.submission = submission;
115
62
  this.name = "HerdrAgentActionError";
116
63
  this.promptSubmission = options?.promptSubmission === undefined ? null : options.promptSubmission;
117
- this.launchEvidence = options?.launchEvidence === undefined ? null : options.launchEvidence;
118
64
  }
119
65
  }
120
66
  const rejectionCodes = {
@@ -146,20 +92,12 @@ export const createHerdrAgentManager = (options) => {
146
92
  if (Object.keys(suppliedEnvironment).length !== xdgFields.length || xdgFields.some((field) => !Object.hasOwn(suppliedEnvironment, field) || typeof suppliedEnvironment[field] !== "string" || suppliedEnvironment[field].includes("\0")))
147
93
  throw new TypeError("Task workspace environment must contain only XDG_CONFIG_HOME and XDG_STATE_HOME string values without NUL.");
148
94
  const workspaceEnvironment = { XDG_CONFIG_HOME: suppliedEnvironment.XDG_CONFIG_HOME, XDG_STATE_HOME: suppliedEnvironment.XDG_STATE_HOME };
149
- const validWorkspaceEvidence = (context, binding, evidence) => {
150
- if (evidence === undefined || evidence === null || typeof evidence.binding !== "object" || evidence.binding === null || !sameBinding(evidence.binding, binding) || evidence.ownerLabel !== herdrExecutionLabel(context))
151
- return false;
152
- const environment = evidence.environment;
153
- if (typeof environment !== "object" || environment === null || Array.isArray(environment))
154
- return false;
155
- const values = environment;
156
- return Object.keys(values).length === xdgFields.length && xdgFields.every((field) => Object.hasOwn(values, field) && values[field] === workspaceEnvironment[field]);
157
- };
158
95
  const isExecuting = options.processExecuting === undefined ? processExecuting : options.processExecuting;
159
96
  const pollInterval = options.pollIntervalMs === undefined ? 100 : options.pollIntervalMs;
160
97
  if (!Number.isSafeInteger(pollInterval) || pollInterval <= 0 || pollInterval > 1000)
161
98
  throw new TypeError("The Herdr observation interval must be between 1 and 1000 milliseconds.");
162
99
  const inputTails = new Map();
100
+ const stoppingProcesses = new Map();
163
101
  const serialized = async (key, operation, action) => {
164
102
  operation.check();
165
103
  const previous = inputTails.get(key);
@@ -196,6 +134,24 @@ export const createHerdrAgentManager = (options) => {
196
134
  if (operation.signal.aborted)
197
135
  abort();
198
136
  });
137
+ const finishStop = async (key, operation) => {
138
+ const pending = stoppingProcesses.get(key);
139
+ if (pending === undefined)
140
+ return;
141
+ while (true) {
142
+ let running = false;
143
+ for (const processId of pending.processIds) {
144
+ operation.check();
145
+ if (await isExecuting(processId))
146
+ running = true;
147
+ }
148
+ if (!running) {
149
+ stoppingProcesses.delete(key);
150
+ return;
151
+ }
152
+ await wait(operation);
153
+ }
154
+ };
199
155
  const snapshot = async (operation) => {
200
156
  const result = expectedResult(await options.rpc.request("session.snapshot", {}, operation), "session_snapshot");
201
157
  const data = object(result.snapshot, "Herdr snapshot");
@@ -289,7 +245,7 @@ export const createHerdrAgentManager = (options) => {
289
245
  let before;
290
246
  const verifyInputAgent = (current) => {
291
247
  const knownAgent = current.kind === context.agent || current.nativeSession?.agent === context.agent || (initialPrompt && current.name === herdrExecutionAgentName(context));
292
- if (!knownAgent || current.launchPending || (!initialPrompt && current.nativeSession === null) || (initialPrompt && !current.interactiveReady) || current.status === "blocked")
248
+ if (!knownAgent || current.launchPending || ((initialPrompt || current.nativeSession === null) && !current.interactiveReady) || current.status === "blocked")
293
249
  throw new Error("The original native agent is not ready for input.");
294
250
  };
295
251
  try {
@@ -323,72 +279,8 @@ export const createHerdrAgentManager = (options) => {
323
279
  }
324
280
  return { submission: "submitted", observation: before };
325
281
  };
326
- const verifyStopObservation = (context, binding, evidence) => {
327
- if (!sameBinding(binding, evidence.binding) || evidence.ownerLabel !== herdrExecutionLabel(context) || evidence.agent !== context.agent)
328
- throw new Error("The saved process observation belongs to another execution.");
329
- pid(evidence.shellPid);
330
- if (evidence.nativeSessionId !== null && canonicalId(evidence.nativeSessionId) === null)
331
- throw new Error("The saved native session identity is invalid.");
332
- if (!Array.isArray(evidence.processIds) || evidence.processIds.length === 0 || new Set(evidence.processIds).size !== evidence.processIds.length || evidence.processIds.some((value) => pid(value) === evidence.shellPid))
333
- throw new Error("The original native process observation is invalid.");
334
- if (evidence.nativeProcessIds !== null && (!Array.isArray(evidence.nativeProcessIds) || evidence.nativeProcessIds.length === 0 || new Set(evidence.nativeProcessIds).size !== evidence.nativeProcessIds.length || evidence.nativeProcessIds.some((value) => pid(value) === evidence.shellPid || !evidence.processIds.includes(value))))
335
- throw new Error("The original interactive native process observation is invalid.");
336
- if (!Number.isSafeInteger(evidence.observedAt) || evidence.observedAt < 0)
337
- throw new Error("The original process observation time is invalid.");
338
- };
339
- const absentProcesses = async (evidence, operation) => {
340
- for (const processId of evidence.processIds) {
341
- operation.check();
342
- if (await isExecuting(processId))
343
- throw new Error("The original native process is still executing or its absence is unconfirmed.");
344
- }
345
- };
346
- const verifyOriginalStopScope = (context, binding, evidence) => {
347
- verifyStopObservation(context, binding, evidence);
348
- if (binding.session !== session)
349
- throw new Error("The original stop observation belongs to another Herdr session.");
350
- for (const field of ["session", "workspaceId", "paneId", "terminalId"])
351
- text(binding[field], `Original Herdr ${field}`);
352
- };
353
- const originalProcessesAbsent = async (evidence, operation) => {
354
- await absentProcesses(evidence, operation);
355
- operation.check();
356
- if (await isExecuting(evidence.shellPid))
357
- throw new Error("The original task shell is still executing or its absence is unconfirmed.");
358
- };
359
- const requireOriginalResourceAbsent = (data, binding) => {
360
- const workspaceIds = new Set();
361
- for (const workspace of data.workspaces) {
362
- const id = text(workspace.workspace_id, "Observed workspace ID");
363
- if (typeof workspace.label !== "string" || workspaceIds.has(id))
364
- throw new Error("The Herdr workspace snapshot is malformed or ambiguous.");
365
- workspaceIds.add(id);
366
- if (id === binding.workspaceId)
367
- throw new Error("The original task workspace is still present.");
368
- }
369
- if ([...data.panes, ...data.agents].some((resource) => resource.workspace_id === binding.workspaceId || resource.pane_id === binding.paneId || resource.terminal_id === binding.terminalId))
370
- throw new Error("The original task pane, terminal or agent is still present in the Herdr snapshot.");
371
- };
372
- const emptyOwnedPane = async (context, binding, operation, mode) => {
373
- const current = await observation(context, binding, operation);
374
- if (current.kind !== null || current.launchPending || current.interactiveReady || (mode === "unlaunched" && (current.name !== null || current.nativeSession !== null)))
375
- throw new Error(mode === "unlaunched" ? "The task pane is not an unlaunched plain shell." : "The restored task pane is not an inactive shell.");
376
- const owned = await readOwned(context, binding, operation);
377
- if (mode === "unlaunched" && owned.data.panes.filter((pane) => pane.workspace_id === binding.workspaceId).length !== 1)
378
- throw new Error("The task workspace does not contain a single unlaunched shell.");
379
- const result = expectedResult(await options.rpc.request("pane.process_info", { pane_id: binding.paneId }, operation), "pane_process_info");
380
- const info = object(result.process_info, "Empty task pane process observation");
381
- if (info.pane_id !== binding.paneId)
382
- throw new Error("The empty-shell process observation belongs to another pane.");
383
- const shellPid = pid(info.shell_pid);
384
- if (info.foreground_process_group_id !== undefined && pid(info.foreground_process_group_id) !== shellPid)
385
- throw new Error("The task shell has a different foreground process group.");
386
- const processes = info.foreground_processes === undefined ? [] : array(info.foreground_processes, "Empty task foreground processes");
387
- if (processes.some((value) => pid(object(value, "Empty task foreground process").pid) !== shellPid))
388
- throw new Error("The task shell still hosts foreground work.");
389
- return { current, data: owned.data };
390
- };
391
282
  return {
283
+ pendingStops: () => [...stoppingProcesses.values()].map(({ context, binding }) => ({ context: { ...context }, binding: { ...binding } })),
392
284
  listResources: async (operation) => {
393
285
  const data = await snapshot(operation);
394
286
  const result = [];
@@ -454,7 +346,7 @@ export const createHerdrAgentManager = (options) => {
454
346
  let createPossible = false;
455
347
  try {
456
348
  const label = herdrExecutionLabel(context);
457
- await realpath(context.cwd);
349
+ const expectedCwd = path.normalize(await realpath(context.cwd));
458
350
  const data = await snapshot(operation);
459
351
  const matching = data.workspaces.filter((workspace) => workspace.label === label);
460
352
  if (matching.length > 1)
@@ -467,260 +359,121 @@ export const createHerdrAgentManager = (options) => {
467
359
  const pane = panes[0];
468
360
  const binding = { session, workspaceId, paneId: text(pane.pane_id, "Task pane ID"), terminalId: text(pane.terminal_id, "Task terminal ID") };
469
361
  await readOwned(context, binding, operation);
470
- return { binding, workspaceEvidence: null };
362
+ return binding;
471
363
  }
472
- const expectedCwd = path.normalize(await realpath(context.cwd));
473
364
  for (const pane of data.panes) {
474
- if (typeof pane.cwd !== "string")
475
- continue;
476
- const reportedCwd = path.normalize(path.resolve(pane.cwd));
477
- if (reportedCwd === expectedCwd || reportedCwd === path.normalize(path.resolve(context.cwd)))
365
+ if (typeof pane.cwd === "string" && [expectedCwd, path.normalize(path.resolve(context.cwd))].includes(path.normalize(path.resolve(pane.cwd))))
478
366
  throw new Error("The requested task cwd already belongs to a different Herdr workspace.");
479
367
  }
480
368
  createPossible = true;
481
369
  const result = await mutation("workspace.create", { cwd: context.cwd, focus: false, label, env: { ...workspaceEnvironment } }, "workspace_created", operation);
482
- try {
483
- const workspace = object(result.workspace, "Created workspace");
484
- const pane = object(result.root_pane, "Created task pane");
485
- const binding = { session, workspaceId: text(workspace.workspace_id, "Created workspace ID"), paneId: text(pane.pane_id, "Created pane ID"), terminalId: text(pane.terminal_id, "Created terminal ID") };
486
- await readOwned(context, binding, operation);
487
- return { binding, workspaceEvidence: { binding: { ...binding }, ownerLabel: label, environment: { ...workspaceEnvironment } } };
488
- }
489
- catch (cause) {
490
- throw new HerdrAgentActionError("The created task workspace could not be verified.", "workspace.create", "unknown", { cause });
491
- }
370
+ const workspace = object(result.workspace, "Created workspace");
371
+ const pane = object(result.root_pane, "Created task pane");
372
+ const binding = { session, workspaceId: text(workspace.workspace_id, "Created workspace ID"), paneId: text(pane.pane_id, "Created pane ID"), terminalId: text(pane.terminal_id, "Created terminal ID") };
373
+ await readOwned(context, binding, operation);
374
+ return binding;
492
375
  }
493
376
  catch (cause) {
494
- if (createPossible && cause instanceof HerdrAgentActionError)
377
+ if (cause instanceof HerdrAgentActionError)
495
378
  throw cause;
496
379
  throw new HerdrAgentActionError(cause instanceof Error ? cause.message : "The task workspace could not be prepared.", "workspace.create", createPossible ? "unknown" : "not-submitted", { cause });
497
380
  }
498
381
  },
499
382
  observe: observation,
500
- start: async (context, binding, input, operation) => {
501
- let launchEvidence = null;
502
- let promptSubmission = "not-submitted";
503
- let nativeStartPossible = false;
383
+ start: (context, binding, input, operation) => serialized(binding.workspaceId, operation, async () => {
384
+ if (input.configuration.agent !== context.agent)
385
+ throw new TypeError("The agent configuration differs from the execution identity.");
386
+ const args = buildHerdrAgentArguments(input.configuration, input.resumeSessionId);
387
+ if (typeof input.prompt !== "string" || input.prompt.length === 0)
388
+ throw new TypeError("The startup prompt must be nonempty text.");
389
+ const before = await observation(context, binding, operation);
390
+ if (before.kind !== null || before.launchPending || before.interactiveReady)
391
+ return before;
392
+ await options.preflight.verifyStart({ context, configuration: input.configuration, ...(input.resumeSessionId === undefined ? {} : { resumeSessionId: input.resumeSessionId }) }, operation);
393
+ const timeout = Math.floor(Math.min(30_000, operation.remainingMs()));
394
+ if (timeout <= 3000)
395
+ throw new HerdrAgentActionError("The remaining operation budget cannot start a native agent.", "agent.start", "not-submitted");
396
+ const started = await mutation("agent.start", { name: herdrExecutionAgentName(context), kind: context.agent, pane_id: binding.paneId, args, timeout_ms: timeout }, "agent_started", operation);
397
+ const acknowledged = object(started.agent, "Started native agent");
398
+ const argv = array(started.argv, "Native launch arguments");
399
+ const expectedArgv = [context.agent, ...args];
400
+ if (acknowledged.pane_id !== binding.paneId || acknowledged.terminal_id !== binding.terminalId || acknowledged.workspace_id !== binding.workspaceId || argv.length !== expectedArgv.length || argv.some((value, index) => value !== expectedArgv[index]))
401
+ throw new HerdrAgentActionError("The native start result differs from the requested target or arguments.", "agent.start", "unknown");
504
402
  try {
505
- const prior = input.launchEvidence;
506
- if (prior !== undefined && prior.referenceBaseline === "empty" && prior.agent === context.agent && prior.ownerLabel === herdrExecutionLabel(context) && sameBinding(prior.binding, binding) && Array.isArray(prior.argv) && prior.argv[0] === context.agent && prior.argv.every((value) => typeof value === "string"))
507
- launchEvidence = prior;
508
- return await serialized(binding.paneId, operation, async () => {
509
- if (input.configuration.agent !== context.agent)
510
- throw new TypeError("The frozen agent configuration differs from the execution identity.");
511
- const args = buildHerdrAgentArguments(input.configuration, input.resumeSessionId);
512
- if (typeof input.prompt !== "string" || input.prompt.length === 0)
513
- throw new TypeError("The caller startup prompt must be nonempty text.");
514
- const before = await observation(context, binding, operation);
515
- const existing = before.kind !== null || before.name !== null || before.launchPending || before.interactiveReady;
516
- if (input.resumeSessionId !== undefined && before.nativeSession !== null && before.nativeSession.sessionId !== canonicalId(input.resumeSessionId))
517
- throw new Error("The retained native session differs from the original recovery session.");
518
- const rebind = (reason) => ({ kind: "rebind-required", reason, observation: before, promptSubmission: "not-submitted", launchEvidence: null });
519
- if (!before.launchPending && !before.interactiveReady && before.kind === null && before.nativeSession !== null)
520
- return rebind("retained-native-reference");
521
- let workspaceEvidence;
522
- if (existing) {
523
- // A reconnect observes the existing input history. Only the caller's persisted
524
- // never-submitted fact and verified empty-reference launch can authorize deferred first input.
525
- if (input.initialPromptNotSubmitted !== true)
526
- return { kind: "reconnected", observation: before, promptSubmission: "not-submitted", launchEvidence: null };
527
- const evidence = input.launchEvidence;
528
- const expectedArgv = [context.agent, ...args];
529
- if (evidence === undefined || evidence.referenceBaseline !== "empty" || evidence.ownerLabel !== herdrExecutionLabel(context) || evidence.agent !== context.agent || !sameBinding(evidence.binding, binding) || !Array.isArray(evidence.argv) || evidence.argv.length !== expectedArgv.length || evidence.argv.some((value, index) => value !== expectedArgv[index]))
530
- return rebind("unverified-launch");
531
- if (!validWorkspaceEvidence(context, binding, evidence.workspaceEvidence) || (input.workspaceEvidence !== undefined && input.workspaceEvidence !== null && !validWorkspaceEvidence(context, binding, input.workspaceEvidence)))
532
- return rebind("unverified-workspace-environment");
533
- workspaceEvidence = evidence.workspaceEvidence;
534
- launchEvidence = evidence;
535
- }
536
- else if (input.initialPromptNotSubmitted === true) {
537
- return rebind("unverified-launch");
538
- }
539
- else if (before.nativeSession !== null) {
540
- return rebind("retained-native-reference");
541
- }
542
- else {
543
- if (!validWorkspaceEvidence(context, binding, input.workspaceEvidence))
544
- return rebind("unverified-workspace-environment");
545
- if (input.launchEvidence !== undefined && !validWorkspaceEvidence(context, binding, input.launchEvidence.workspaceEvidence))
546
- return rebind("unverified-workspace-environment");
547
- workspaceEvidence = input.workspaceEvidence;
548
- }
549
- await options.preflight.verifyStart({ context, configuration: input.configuration, ...(input.resumeSessionId === undefined ? {} : { resumeSessionId: input.resumeSessionId }) }, operation);
550
- if (!existing) {
551
- const timeout = Math.floor(Math.min(30_000, operation.remainingMs()));
552
- if (timeout <= 3000)
553
- throw new HerdrAgentActionError("The remaining operation budget cannot start a native agent.", "agent.start", "not-submitted", { promptSubmission: "not-submitted" });
554
- nativeStartPossible = true;
555
- const started = await mutation("agent.start", { name: herdrExecutionAgentName(context), kind: context.agent, pane_id: binding.paneId, args, timeout_ms: timeout }, "agent_started", operation);
556
- const acknowledged = object(started.agent, "Native launch receipt");
557
- const argv = array(started.argv, "Native launch arguments");
558
- const expectedArgv = [context.agent, ...args];
559
- if (acknowledged.pane_id !== binding.paneId || acknowledged.terminal_id !== binding.terminalId || acknowledged.workspace_id !== binding.workspaceId || argv.length !== expectedArgv.length || argv.some((value, index) => value !== expectedArgv[index]))
560
- throw new Error("The native launch receipt does not preserve its original target and frozen arguments.");
561
- launchEvidence = { binding: { ...binding }, ownerLabel: herdrExecutionLabel(context), agent: context.agent, argv: [...expectedArgv], referenceBaseline: "empty", workspaceEvidence };
562
- }
563
- while (true) {
564
- const current = await observation(context, binding, operation);
565
- if (current.status === "blocked")
566
- return { kind: "waiting", observation: current, promptSubmission: "not-submitted", launchEvidence };
567
- const ready = !current.launchPending && current.interactiveReady && (current.kind === context.agent || current.nativeSession?.agent === context.agent || current.name === herdrExecutionAgentName(context));
568
- const originalSessionReady = input.resumeSessionId === undefined || current.nativeSession === null || current.nativeSession.sessionId === canonicalId(input.resumeSessionId);
569
- if (input.resumeSessionId !== undefined && current.nativeSession !== null && !originalSessionReady)
570
- throw new Error("The resumed agent reported a different native session.");
571
- if (ready && originalSessionReady) {
572
- // Native SessionStart can run on the first user turn, including resume.
573
- // The verified explicit launch authorizes one input; its reported identity is checked afterward.
574
- // https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/core/src/session/turn.rs#L340-L342
575
- await submit(context, binding, input.prompt, operation, true);
576
- promptSubmission = "submitted";
577
- while (true) {
578
- const reported = await observation(context, binding, operation);
579
- if (reported.nativeSession !== null) {
580
- if (input.resumeSessionId !== undefined && reported.nativeSession.sessionId !== canonicalId(input.resumeSessionId))
581
- throw new Error("The resumed native identity changed after prompt submission.");
582
- return { kind: "started", observation: reported, promptSubmission: "submitted", launchEvidence };
583
- }
584
- if (reported.status === "blocked")
585
- return { kind: "waiting", observation: reported, promptSubmission: "submitted", launchEvidence };
586
- await wait(operation);
587
- }
588
- }
589
- await wait(operation);
590
- }
591
- });
403
+ while (true) {
404
+ const current = await observation(context, binding, operation);
405
+ if (current.status === "blocked")
406
+ throw new HerdrAgentActionError("The native agent requires operator action before its first input.", "agent.prompt", "not-submitted", { promptSubmission: "not-submitted" });
407
+ if (input.resumeSessionId !== undefined && current.nativeSession !== null && current.nativeSession.sessionId !== canonicalId(input.resumeSessionId))
408
+ throw new HerdrAgentActionError("The resumed agent reported a different native session.", "agent.start", "unknown");
409
+ if (!current.launchPending && current.interactiveReady && (current.kind === context.agent || current.nativeSession?.agent === context.agent || current.name === herdrExecutionAgentName(context)))
410
+ break;
411
+ await wait(operation);
412
+ }
592
413
  }
593
414
  catch (cause) {
594
- const evidence = launchEvidence === null ? {} : { launchEvidence };
595
415
  if (cause instanceof HerdrAgentActionError)
596
- throw new HerdrAgentActionError(cause.message, cause.action, cause.submission, { cause, promptSubmission: cause.promptSubmission === null ? promptSubmission : cause.promptSubmission, ...evidence });
597
- throw new HerdrAgentActionError(cause instanceof Error ? cause.message : "The native launch or initial prompt could not be confirmed; observe the original pane before any further action.", "agent.start", nativeStartPossible ? "unknown" : "not-submitted", { cause, promptSubmission, ...evidence });
598
- }
599
- },
600
- submitPrompt: (context, binding, prompt, operation) => serialized(binding.paneId, operation, () => submit(context, binding, prompt, operation)),
601
- captureStop: async (context, binding, operation) => {
602
- const current = await observation(context, binding, operation);
603
- const result = expectedResult(await options.rpc.request("pane.process_info", { pane_id: binding.paneId }, operation), "pane_process_info");
604
- const info = object(result.process_info, "Pane process observation");
605
- if (info.pane_id !== binding.paneId)
606
- throw new Error("The process observation belongs to another pane.");
607
- const shellPid = pid(info.shell_pid);
608
- const processes = info.foreground_processes === undefined ? [] : array(info.foreground_processes, "Foreground processes");
609
- const records = processes.map((value) => object(value, "Foreground process"));
610
- const processIds = [...new Set(records.map((value) => pid(value.pid)).filter((value) => value !== shellPid))];
611
- const keys = [...new Set(records.filter((value) => pid(value.pid) !== shellPid && nativeInteractiveProcessEvidence(value, context.agent)).map((value) => pid(value.pid)))];
612
- if (processIds.length === 0)
613
- throw new Error("The original native process identities are unavailable; pane absence or an empty process list cannot establish stopped confirmation.");
614
- if (current.launchPending && (current.name !== herdrExecutionAgentName(context) || !records.some((value) => pid(value.pid) !== shellPid && nativeProcessEvidence(value, context.agent))))
615
- throw new Error("The pending task launch has no concrete original native process evidence.");
616
- return { binding, ownerLabel: herdrExecutionLabel(context), agent: context.agent, nativeSessionId: current.nativeSession?.sessionId ?? null, shellPid, processIds, nativeProcessIds: keys.length === 0 ? null : keys, observedAt: Date.now() };
617
- },
618
- requestGracefulStop: (context, binding, operation) => serialized(binding.paneId, operation, () => submit(context, binding, "/exit", operation)),
619
- // This observes only the current shell. Historical never-submitted launch state
620
- // remains a separate durable caller precondition for deriving agentStopped.
621
- confirmEmptyPane: async (context, binding, operation) => {
622
- await emptyOwnedPane(context, binding, operation, "unlaunched");
623
- return { noNativeProcess: true };
624
- },
625
- confirmStopped: async (context, binding, evidence, operation) => {
626
- verifyStopObservation(context, binding, evidence);
627
- const current = await readOwned(context, binding, operation, true);
628
- await absentProcesses(evidence, operation);
629
- if (current.pane !== null) {
630
- const result = expectedResult(await options.rpc.request("pane.process_info", { pane_id: binding.paneId }, operation), "pane_process_info");
631
- const info = object(result.process_info, "Current pane process observation");
632
- if (info.pane_id !== binding.paneId)
633
- throw new Error("The current process observation belongs to another pane.");
634
- const currentShellPid = pid(info.shell_pid);
635
- if (currentShellPid !== evidence.shellPid && await isExecuting(evidence.shellPid))
636
- throw new Error("The original task shell is still executing or its absence is unconfirmed.");
637
- const currentProcesses = info.foreground_processes === undefined ? [] : array(info.foreground_processes, "Current foreground processes");
638
- if (currentProcesses.some((value) => pid(object(value, "Current foreground process").pid) !== currentShellPid))
639
- throw new Error("The original pane still hosts foreground work.");
416
+ throw cause;
417
+ throw new HerdrAgentActionError("The native process could not become ready before its first input.", "agent.prompt", "not-submitted", { cause, promptSubmission: "not-submitted" });
640
418
  }
641
- return { agentStopped: true };
642
- },
643
- confirmMissingWorkspaceStopped: async (context, originalBinding, evidence, operation) => {
644
- verifyOriginalStopScope(context, originalBinding, evidence);
645
- const data = await snapshot(operation);
646
- requireOriginalResourceAbsent(data, originalBinding);
647
- if (data.workspaces.some((workspace) => workspace.label === herdrExecutionLabel(context)))
648
- throw new Error("The original task label identifies another resource.");
649
- await originalProcessesAbsent(evidence, operation);
650
- return { agentStopped: true };
651
- },
652
- confirmRestoredStop: async (context, originalBinding, restoredBinding, evidence, operation) => {
653
- verifyOriginalStopScope(context, originalBinding, evidence);
654
- if (originalBinding.session !== restoredBinding.session || originalBinding.workspaceId !== restoredBinding.workspaceId || originalBinding.paneId !== restoredBinding.paneId)
655
- throw new Error("The restored shell does not have the original task workspace and pane association.");
656
- const { current } = await emptyOwnedPane(context, restoredBinding, operation, "restored");
657
- if (evidence.nativeSessionId !== null && current.nativeSession !== null && evidence.nativeSessionId !== current.nativeSession.sessionId)
658
- throw new Error("The restored shell retains a different original native session.");
659
- await originalProcessesAbsent(evidence, operation);
660
- return { agentStopped: true, binding: { ...restoredBinding } };
661
- },
662
- confirmReplacedStop: async (context, originalBinding, replacementBinding, evidence, operation) => {
663
- verifyOriginalStopScope(context, originalBinding, evidence);
664
- if (replacementBinding.session !== originalBinding.session)
665
- throw new Error("The replacement belongs to another Herdr session.");
666
- const { data } = await emptyOwnedPane(context, replacementBinding, operation, "unlaunched");
667
- requireOriginalResourceAbsent(data, originalBinding);
668
- await originalProcessesAbsent(evidence, operation);
669
- return { agentStopped: true, binding: { ...replacementBinding } };
670
- },
671
- forceStop: (context, binding, evidence, operation) => serialized(binding.paneId, operation, async () => {
672
- verifyStopObservation(context, binding, evidence);
673
- const { data, pane } = await readOwned(context, binding, operation, true);
674
- if (pane !== null) {
419
+ await submit(context, binding, input.prompt, operation, true);
420
+ // SessionStart may be emitted on the first input rather than process launch.
421
+ // https://github.com/openai/codex/blob/6aaac694803004a63c6f393d153c96d22521a6b9/codex-rs/core/src/session/turn.rs#L340-L342
422
+ while (true) {
675
423
  const current = await observation(context, binding, operation);
676
- if (evidence.nativeSessionId !== null && current.nativeSession !== null && current.nativeSession.sessionId !== evidence.nativeSessionId)
677
- throw new Error("The task pane now identifies another native session; the original stop observation was retained.");
678
- const processResult = expectedResult(await options.rpc.request("pane.process_info", { pane_id: binding.paneId }, operation), "pane_process_info");
679
- const processInfo = object(processResult.process_info, "Current stop process observation");
680
- if (processInfo.pane_id !== binding.paneId || pid(processInfo.shell_pid) !== evidence.shellPid)
681
- throw new Error("The original task shell identity has changed.");
682
- const foreground = processInfo.foreground_processes === undefined ? [] : array(processInfo.foreground_processes, "Current stop processes");
683
- const currentPids = foreground.map((value) => pid(object(value, "Current stop process").pid)).filter((value) => value !== evidence.shellPid);
684
- if (currentPids.length !== 0 && !currentPids.some((value) => evidence.processIds.includes(value)))
685
- throw new Error("The task pane now hosts processes outside the original stop observation.");
686
- const guardLabel = `daynight-stop:${binding.terminalId}`;
687
- const guards = data.tabs.filter((tab) => tab.workspace_id === binding.workspaceId && tab.label === guardLabel);
688
- if (guards.length > 1)
689
- throw new Error("The task stop anchor is duplicated.");
690
- if (guards.length === 0) {
691
- const result = await mutation("tab.create", { workspace_id: binding.workspaceId, cwd: context.cwd, focus: false, label: guardLabel, env: { ...workspaceEnvironment } }, "tab_created", operation);
692
- const tab = object(result.tab, "Created stop anchor tab");
693
- const root = object(result.root_pane, "Created stop anchor pane");
694
- if (tab.workspace_id !== binding.workspaceId || tab.label !== guardLabel || root.workspace_id !== binding.workspaceId || root.pane_id === binding.paneId || root.agent !== undefined)
695
- throw new HerdrAgentActionError("The stop anchor does not identify an empty task-owned pane.", "tab.create", "unknown");
424
+ if (current.nativeSession !== null) {
425
+ if (input.resumeSessionId !== undefined && current.nativeSession.sessionId !== canonicalId(input.resumeSessionId))
426
+ throw new Error("The resumed native identity differs from the requested session.");
427
+ return current;
696
428
  }
697
- const anchored = await readOwned(context, binding, operation);
698
- const ownedPanes = anchored.data.panes.filter((candidate) => candidate.workspace_id === binding.workspaceId);
699
- if (ownedPanes.length < 2 || !anchored.data.tabs.some((tab) => tab.workspace_id === binding.workspaceId && tab.label === guardLabel))
700
- throw new Error("The task workspace has no verified stop anchor.");
701
- await mutation("pane.close", { pane_id: binding.paneId }, "ok", operation);
429
+ if (current.status === "blocked")
430
+ return current;
431
+ await wait(operation);
702
432
  }
703
- await readOwned(context, binding, operation, true);
704
- await absentProcesses(evidence, operation);
705
- return { agentStopped: true, originalPaneClosed: true };
706
433
  }),
707
- closeWorkspace: async (context, binding, proof, operation) => {
708
- if (proof.agentStopped !== true)
709
- throw new Error("Task workspace closure requires confirmed original agent stop.");
434
+ submitPrompt: (context, binding, prompt, operation) => serialized(binding.workspaceId, operation, () => submit(context, binding, prompt, operation)),
435
+ stopAndClose: (context, binding, operation) => serialized(binding.workspaceId, operation, async () => {
436
+ if (binding.session !== session)
437
+ throw new Error("The task binding belongs to another Herdr session.");
710
438
  const before = await snapshot(operation);
711
- const original = before.workspaces.find((workspace) => workspace.workspace_id === binding.workspaceId);
712
- if (original === undefined) {
713
- if (before.workspaces.some((workspace) => workspace.label === herdrExecutionLabel(context)))
714
- throw new Error("The task label now identifies another workspace; the original binding was retained.");
715
- return { workspaceClosed: true };
439
+ const label = herdrExecutionLabel(context);
440
+ const key = JSON.stringify([binding.session, binding.workspaceId, label]);
441
+ const matching = before.workspaces.filter((workspace) => workspace.workspace_id === binding.workspaceId || workspace.label === label);
442
+ if (matching.length === 0) {
443
+ if ([...before.panes, ...before.agents].some((resource) => resource.workspace_id === binding.workspaceId || resource.pane_id === binding.paneId || resource.terminal_id === binding.terminalId))
444
+ throw new Error("The missing task workspace still has associated resources.");
445
+ return finishStop(key, operation);
446
+ }
447
+ if (matching.length !== 1 || matching[0].workspace_id !== binding.workspaceId || matching[0].label !== label)
448
+ throw new Error("The task workspace identity changed before stopping.");
449
+ const currentPanes = before.panes.filter((pane) => pane.workspace_id === binding.workspaceId);
450
+ // Closing removes discovery evidence before all processes necessarily exit.
451
+ // Retain this process-local responsibility across failed close/wait calls.
452
+ let pending = stoppingProcesses.get(key);
453
+ if (pending === undefined) {
454
+ pending = { context: { ...context }, binding: { ...binding }, processIds: new Set() };
455
+ stoppingProcesses.set(key, pending);
456
+ }
457
+ for (const pane of currentPanes) {
458
+ const paneId = text(pane.pane_id, "Stop pane ID");
459
+ if (paneId === binding.paneId && pane.terminal_id !== binding.terminalId)
460
+ throw new Error("The task terminal identity changed before stopping.");
461
+ await verifyCwd(pane.cwd, context);
462
+ const result = expectedResult(await options.rpc.request("pane.process_info", { pane_id: paneId }, operation), "pane_process_info");
463
+ const info = object(result.process_info, "Current stop process observation");
464
+ if (info.pane_id !== paneId)
465
+ throw new Error("The stop process observation belongs to another pane.");
466
+ pending.processIds.add(pid(info.shell_pid));
467
+ const foreground = info.foreground_processes === undefined ? [] : array(info.foreground_processes, "Current foreground processes");
468
+ for (const entry of foreground)
469
+ pending.processIds.add(pid(object(entry, "Current foreground process").pid));
716
470
  }
717
- await readOwned(context, binding, operation, true);
718
471
  await mutation("workspace.close", { workspace_id: binding.workspaceId, close_group: false }, "ok", operation);
719
472
  const after = await snapshot(operation);
720
- if (after.workspaces.some((workspace) => workspace.workspace_id === binding.workspaceId || workspace.label === herdrExecutionLabel(context)))
721
- throw new HerdrAgentActionError("The original workspace still exists after closure.", "workspace.close", "unknown");
722
- return { workspaceClosed: true };
723
- },
473
+ if (after.workspaces.some((workspace) => workspace.workspace_id === binding.workspaceId || workspace.label === label) || [...after.panes, ...after.agents].some((resource) => resource.workspace_id === binding.workspaceId))
474
+ throw new HerdrAgentActionError("The task workspace still exists after closure.", "workspace.close", "unknown");
475
+ await finishStop(key, operation);
476
+ }),
724
477
  };
725
478
  };
726
479
  //# sourceMappingURL=agents.js.map