@markjaquith/agency 3.5.1 → 3.6.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.
@@ -37,6 +37,7 @@ import {
37
37
  readValidationEvidence,
38
38
  } from "../workbase/execution-contract"
39
39
  import { ValidationFailedError } from "./validate"
40
+ import { prepareOpenCodeLaunch } from "../workbase/opencode-launch"
40
41
 
41
42
  export interface WorkOptions extends BaseCommandOptions {
42
43
  readonly directory?: string
@@ -386,12 +387,22 @@ export const work = (
386
387
  ...agentEnvironment(agent, variables),
387
388
  }
388
389
  if (writablePath) environment.AGENCY_WRITABLE_CHECKOUT = writablePath
390
+ const managedOpenCodeAuto =
391
+ options.auto &&
392
+ (agent === "opencode" || agent === "opencode2") &&
393
+ !config.agents?.[agent]
389
394
  if (options.printCommand) {
390
395
  log(
391
396
  JSON.stringify(
392
397
  {
393
398
  cwd: launchPath,
394
399
  argv: resolved.argv,
400
+ ...(managedOpenCodeAuto
401
+ ? {
402
+ startup:
403
+ "OpenCode V2: submit via API, then attach with --session; V1: use argv",
404
+ }
405
+ : {}),
395
406
  environment: printableEnvironment(environment),
396
407
  },
397
408
  null,
@@ -449,14 +460,17 @@ export const work = (
449
460
  )
450
461
  }
451
462
  }
463
+ const launchArgv = managedOpenCodeAuto
464
+ ? yield* prepareOpenCodeLaunch(resolved.argv, launchPath, environment)
465
+ : resolved.argv
452
466
  for (const [key, value] of Object.entries(environment)) {
453
467
  process.env[key] = value
454
468
  }
455
469
  verboseLog(
456
- `Launching command: ${formatCommand(resolved.argv)} (cwd: ${launchPath})`,
470
+ `Launching command: ${formatCommand(launchArgv)} (cwd: ${launchPath})`,
457
471
  )
458
472
  try {
459
- launch(cli, resolved.argv, launchPath, environment)
473
+ launch(cli, launchArgv, launchPath, environment)
460
474
  } finally {
461
475
  for (const key of Object.keys(environment)) {
462
476
  const previous = previousEnvironment[key]
@@ -6,6 +6,7 @@ export interface Choice<T> {
6
6
  readonly plainLabel?: string
7
7
  readonly depth?: number
8
8
  readonly segments?: readonly ChoiceSegment[]
9
+ readonly details?: ChoiceDetails
9
10
  readonly value: T
10
11
  }
11
12
 
@@ -14,6 +15,12 @@ export interface ChoiceSegment {
14
15
  readonly color?: string
15
16
  }
16
17
 
18
+ export interface ChoiceDetails {
19
+ readonly title: readonly ChoiceSegment[]
20
+ readonly metadata: readonly ChoiceSegment[]
21
+ readonly description: string
22
+ }
23
+
17
24
  export type ChooserErrorReason =
18
25
  | "invalid-choices"
19
26
  | "input-unavailable"
package/src/utils/exec.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { dlopen, FFIType, ptr } from "bun:ffi"
2
2
 
3
3
  /**
4
- * Native exec implementation using Bun FFI to call POSIX execvp.
4
+ * PATH-resolving native exec using POSIX execve and an explicit environment.
5
5
  * This completely replaces the current process with the specified command.
6
6
  *
7
7
  * IMPORTANT: This function will never return if successful. The process
@@ -12,12 +12,18 @@ import { dlopen, FFIType, ptr } from "bun:ffi"
12
12
  * @throws Error if exec fails (e.g., command not found)
13
13
  */
14
14
  export function execvp(file: string, args: string[]): never {
15
- // Open libc to access execvp (platform-specific library paths)
15
+ const executable = Bun.which(file)
16
+ if (!executable) throw new Error(`Unable to find executable '${file}'`)
17
+ // Bun's process.env mutations are not reflected in libc's environ. Passing
18
+ // envp explicitly preserves launch identity and deletions across replacement.
19
+ const variables = Object.entries(process.env)
20
+ .filter((entry): entry is [string, string] => entry[1] !== undefined)
21
+ .map(([key, value]) => `${key}=${value}`)
16
22
  const libcPath =
17
23
  process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6"
18
24
  const libc = dlopen(libcPath, {
19
- execvp: {
20
- args: [FFIType.cstring, FFIType.ptr],
25
+ execve: {
26
+ args: [FFIType.cstring, FFIType.ptr, FFIType.ptr],
21
27
  returns: FFIType.int,
22
28
  },
23
29
  })
@@ -36,10 +42,19 @@ export function execvp(file: string, args: string[]): never {
36
42
  }
37
43
  // Null-terminate the pointer array
38
44
  ptrs[args.length] = 0n
45
+ const environmentStrings = variables.map((value) => Buffer.from(value + "\0"))
46
+ const environmentPointers = new BigUint64Array(environmentStrings.length + 1)
47
+ for (let i = 0; i < environmentStrings.length; i++) {
48
+ environmentPointers[i] = BigInt(ptr(environmentStrings[i]!))
49
+ }
39
50
 
40
51
  // Call execvp - this will replace the current process if successful
41
- const fileBuffer = Buffer.from(file + "\0")
42
- const result = libc.symbols.execvp(ptr(fileBuffer), ptr(ptrs))
52
+ const fileBuffer = Buffer.from(executable + "\0")
53
+ const result = libc.symbols.execve(
54
+ ptr(fileBuffer),
55
+ ptr(ptrs),
56
+ ptr(environmentPointers),
57
+ )
43
58
 
44
59
  // If we reach here, exec failed
45
60
  throw new Error(