@markjaquith/agency 3.4.1 → 3.4.2

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
@@ -318,11 +318,16 @@ set `agent` in `$XDG_CONFIG_HOME/agency/agency.json` (or
318
318
  The selection precedence is `--agent` (including the legacy `--opencode` and
319
319
  `--claude` aliases), then `AGENCY_AGENT`, then `agent`, then automatic
320
320
  detection. Supported global values are `opencode2`, `opencode`, `pi`, and
321
- `claude`; an unavailable configured agent fails rather than falling back. A launch
322
- is fresh unless `AGENCY_SESSION_ID` is already set; resumed launches use the agent's
323
- `resumeCommand` when configured. The built-in presets use `--continue` only for
324
- resumed launches. By default Agency opens the agent without a prompt. `--auto`
325
- uses its autonomous command and sends the generated task, phase, or epic prompt.
321
+ `claude`; an unavailable configured agent fails rather than falling back. A
322
+ launch is fresh unless `AGENCY_SESSION_ID` is already set; resumed launches use
323
+ the agent's `resumeCommand` when configured. The built-in presets use
324
+ `--continue` only for resumed launches, except autonomous OpenCode V2 launches,
325
+ which open a fresh TUI session so the generated continuation prompt cannot be
326
+ routed to an unrelated prior session. By default Agency opens the agent without
327
+ a prompt. `--auto` uses its autonomous command and sends the generated task,
328
+ phase, or epic prompt. OpenCode V2 receives a launch-only environment marker;
329
+ Agency's managed TUI companion waits for the populated composer and dispatches
330
+ its native submit command once.
326
331
 
327
332
  Custom agents are direct argv commands, never shell snippets:
328
333
 
@@ -352,6 +357,9 @@ Every agent receives the same `AGENCY_AGENT`, `AGENCY_SESSION_ID`,
352
357
  `AGENCY_WORKBASE`, `AGENCY_TARGET`,
353
358
  `AGENCY_TASK_ID`, `AGENCY_PHASE_ID`, and `AGENCY_PROMPT` environment. Configured
354
359
  environment is added without overriding these normalized values.
360
+ Autonomous OpenCode V2 launches additionally receive
361
+ `AGENCY_TUI_AUTOSUBMIT=1`; the client-side managed TUI companion consumes it,
362
+ and manual and non-V2 launches do not receive it.
355
363
  Execution-unit agents also receive `AGENCY_WRITABLE_CHECKOUT` with the
356
364
  authoritative writable checkout path.
357
365
  `AGENCY_PROMPT` is empty unless `--auto` is set.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "3.4.1",
3
+ "version": "3.4.2",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -18,6 +18,7 @@ interface IntegrationResult {
18
18
  | "opencode-plugin"
19
19
  | "opencode-tui"
20
20
  | "opencode-tui-plugin"
21
+ | "opencode-v2-tui-plugin"
21
22
  readonly path: string
22
23
  readonly state: string
23
24
  readonly diagnostic: string
@@ -32,6 +33,7 @@ const integrationNames = {
32
33
  "opencode-plugin": "OpenCode workbase plugin",
33
34
  "opencode-tui": "OpenCode TUI config",
34
35
  "opencode-tui-plugin": "OpenCode /agency-debug",
36
+ "opencode-v2-tui-plugin": "OpenCode V2 TUI companion",
35
37
  } as const
36
38
 
37
39
  const logHumanResult = (
@@ -23,6 +23,12 @@ import {
23
23
  canUpdateManagedWorkbaseOpencodeTuiPlugin,
24
24
  managedWorkbaseOpencodeTuiPlugin,
25
25
  } from "../workbase/opencode-tui-plugin-file"
26
+ import {
27
+ canUpdateManagedWorkbaseOpencodeV2TuiFile,
28
+ managedWorkbaseOpencodeV2TuiIndex,
29
+ managedWorkbaseOpencodeV2TuiPackage,
30
+ managedWorkbaseOpencodeV2TuiPlugin,
31
+ } from "../workbase/opencode-v2-tui-plugin-file"
26
32
 
27
33
  const managedHeaderPattern =
28
34
  /^\/\/ agency-managed: sha256=([a-f0-9]{64})\r?\n\r?\n/
@@ -46,6 +52,7 @@ interface IntegrationFileStatus {
46
52
  | "opencode-plugin"
47
53
  | "opencode-tui"
48
54
  | "opencode-tui-plugin"
55
+ | "opencode-v2-tui-plugin"
49
56
  readonly path: string
50
57
  readonly state: IntegrationFileState
51
58
  readonly diagnostic: string
@@ -144,6 +151,25 @@ const describe = (
144
151
  "Run 'agency integration sync' to install /agency-debug.",
145
152
  }
146
153
  }
154
+ if (name === "opencode-v2-tui-plugin") {
155
+ return state === "managed"
156
+ ? {
157
+ diagnostic: "Agency's managed OpenCode V2 TUI companion is current.",
158
+ remediation: null,
159
+ }
160
+ : state === "customized"
161
+ ? {
162
+ diagnostic:
163
+ "A user-owned OpenCode V2 TUI companion is present and was preserved.",
164
+ remediation: null,
165
+ }
166
+ : {
167
+ diagnostic:
168
+ "The managed OpenCode V2 TUI companion needs synchronization.",
169
+ remediation:
170
+ "Run 'agency integration sync' to install V2 autonomous prompt submission.",
171
+ }
172
+ }
147
173
  return state === "missing" || state === "drifted"
148
174
  ? {
149
175
  diagnostic: "Managed workbase instructions need synchronization.",
@@ -202,6 +228,10 @@ const inspect = (root: string) =>
202
228
  "agency-repository-skills.ts",
203
229
  )
204
230
  const tuiPluginPath = join(opencodeDirectory, "tui", "agency-debug.ts")
231
+ const v2TuiDirectory = join(opencodeDirectory, "plugins", "agency-tui")
232
+ const v2TuiPackagePath = join(v2TuiDirectory, "package.json")
233
+ const v2TuiIndexPath = join(v2TuiDirectory, "index.ts")
234
+ const v2TuiPluginPath = join(v2TuiDirectory, "tui.ts")
205
235
  const [
206
236
  agents,
207
237
  opencode,
@@ -211,6 +241,9 @@ const inspect = (root: string) =>
211
241
  tui,
212
242
  tuiJson,
213
243
  tuiPlugin,
244
+ v2TuiPackage,
245
+ v2TuiIndex,
246
+ v2TuiPlugin,
214
247
  ] = yield* Effect.all(
215
248
  [
216
249
  fs.inspectFile(agentsPath),
@@ -221,6 +254,9 @@ const inspect = (root: string) =>
221
254
  fs.inspectFile(tuiPath),
222
255
  fs.inspectFile(tuiJsonPath),
223
256
  fs.inspectFile(tuiPluginPath),
257
+ fs.inspectFile(v2TuiPackagePath),
258
+ fs.inspectFile(v2TuiIndexPath),
259
+ fs.inspectFile(v2TuiPluginPath),
224
260
  ] as const,
225
261
  { concurrency: 8 },
226
262
  )
@@ -317,6 +353,30 @@ const inspect = (root: string) =>
317
353
  files.push(fileStatus("opencode-tui-plugin", tuiPluginPath, "missing"))
318
354
  }
319
355
 
356
+ const v2TuiFiles = [
357
+ [v2TuiPackage, managedWorkbaseOpencodeV2TuiPackage],
358
+ [v2TuiIndex, managedWorkbaseOpencodeV2TuiIndex],
359
+ [v2TuiPlugin, managedWorkbaseOpencodeV2TuiPlugin],
360
+ ] as const
361
+ const v2TuiStates = v2TuiFiles.map(([file, managed]) => {
362
+ if (file.kind === "missing") return "missing" as const
363
+ if (file.kind !== "file") return "customized" as const
364
+ if (file.content === managed) return "managed" as const
365
+ return canUpdateManagedWorkbaseOpencodeV2TuiFile(file.content)
366
+ ? ("drifted" as const)
367
+ : ("customized" as const)
368
+ })
369
+ const v2TuiState = v2TuiStates.every((state) => state === "missing")
370
+ ? "missing"
371
+ : v2TuiStates.every((state) => state === "managed")
372
+ ? "managed"
373
+ : v2TuiStates.some((state) => state === "customized")
374
+ ? "customized"
375
+ : "drifted"
376
+ files.push(
377
+ fileStatus("opencode-v2-tui-plugin", v2TuiPluginPath, v2TuiState),
378
+ )
379
+
320
380
  return { files, legacyPlugin }
321
381
  })
322
382
 
@@ -429,6 +489,21 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
429
489
  status.path,
430
490
  managedWorkbaseOpencodeTuiPlugin,
431
491
  )
492
+ } else if (status.name === "opencode-v2-tui-plugin") {
493
+ const directory = dirname(status.path)
494
+ yield* fs.createDirectory(directory)
495
+ yield* fs.writeFile(
496
+ join(directory, "package.json"),
497
+ managedWorkbaseOpencodeV2TuiPackage,
498
+ )
499
+ yield* fs.writeFile(
500
+ join(directory, "index.ts"),
501
+ managedWorkbaseOpencodeV2TuiIndex,
502
+ )
503
+ yield* fs.writeFile(
504
+ status.path,
505
+ managedWorkbaseOpencodeV2TuiPlugin,
506
+ )
432
507
  }
433
508
  }
434
509
  files.push({
@@ -31,7 +31,7 @@ const BUILTIN_AGENTS: Readonly<Record<string, AgentDefinition>> = {
31
31
  command: ["opencode2"],
32
32
  autoCommand: ["opencode2", "--prompt", "{prompt}"],
33
33
  resumeCommand: ["opencode2", "--continue"],
34
- autoResumeCommand: ["opencode2", "--continue", "--prompt", "{prompt}"],
34
+ autoResumeCommand: ["opencode2", "--prompt", "{prompt}"],
35
35
  },
36
36
  opencode: {
37
37
  command: ["opencode"],
@@ -125,6 +125,9 @@ export const agentEnvironment = (
125
125
  AGENCY_TASK_ID: variables.task,
126
126
  AGENCY_PHASE_ID: variables.phase,
127
127
  AGENCY_PROMPT: variables.prompt,
128
+ ...(agent === "opencode2" && variables.prompt
129
+ ? { AGENCY_TUI_AUTOSUBMIT: "1" }
130
+ : {}),
128
131
  })
129
132
 
130
133
  const SECRET_NAME =
@@ -0,0 +1,134 @@
1
+ import { createHash } from "node:crypto"
2
+
3
+ const managedHeaderPattern =
4
+ /^\/\/ agency-managed: sha256=([a-f0-9]{64})\r?\n\r?\n/
5
+
6
+ const checksum = (content: string) =>
7
+ createHash("sha256").update(content).digest("hex")
8
+
9
+ const managed = (content: string) =>
10
+ `// agency-managed: sha256=${checksum(content)}\n\n${content}`
11
+
12
+ const packageBody = `${JSON.stringify(
13
+ {
14
+ name: "agency-tui",
15
+ private: true,
16
+ type: "module",
17
+ exports: {
18
+ ".": "./index.ts",
19
+ "./tui": "./tui.ts",
20
+ },
21
+ },
22
+ null,
23
+ 2,
24
+ )}\n`
25
+
26
+ const indexBody = `const plugin = {
27
+ id: "agency.tui.loader",
28
+ setup() {},
29
+ async server() {
30
+ return {}
31
+ },
32
+ }
33
+
34
+ export default plugin
35
+ `
36
+
37
+ const tuiBody = `const autosubmitTimeoutMs = 10_000
38
+ const autosubmitRetryMs = 25
39
+
40
+ type AutosubmitContext = {
41
+ keymap: {
42
+ commands(): readonly { id?: string }[]
43
+ dispatch(id: string): unknown
44
+ }
45
+ renderer: { currentFocusedEditor?: unknown }
46
+ ui: {
47
+ router: { current(): { type: string } }
48
+ toast: {
49
+ show(input: {
50
+ variant: "error"
51
+ title: string
52
+ message: string
53
+ duration: number
54
+ }): void
55
+ }
56
+ }
57
+ }
58
+
59
+ export const createAgencyAutosubmit = (
60
+ options: { timeoutMs?: number; retryMs?: number } = {},
61
+ ) => {
62
+ let started = false
63
+
64
+ return (context: AutosubmitContext) => {
65
+ if (started) return () => {}
66
+ if (process.env.AGENCY_TUI_AUTOSUBMIT !== "1") return () => {}
67
+ if (!process.env.AGENCY_PROMPT) return () => {}
68
+ started = true
69
+ delete process.env.AGENCY_TUI_AUTOSUBMIT
70
+
71
+ const timeoutMs = options.timeoutMs ?? autosubmitTimeoutMs
72
+ const retryMs = options.retryMs ?? autosubmitRetryMs
73
+ const deadline = Date.now() + timeoutMs
74
+ let timer: ReturnType<typeof setTimeout> | undefined
75
+ let stopped = false
76
+
77
+ const stop = () => {
78
+ stopped = true
79
+ if (timer) clearTimeout(timer)
80
+ }
81
+
82
+ const attempt = () => {
83
+ if (stopped) return
84
+ if (context.ui.router.current().type !== "home") {
85
+ stop()
86
+ return
87
+ }
88
+
89
+ const submitReady = context.keymap
90
+ .commands()
91
+ .some((command) => command.id === "prompt.submit")
92
+ if (context.renderer.currentFocusedEditor && submitReady) {
93
+ stop()
94
+ context.keymap.dispatch("prompt.submit")
95
+ return
96
+ }
97
+
98
+ if (Date.now() >= deadline) {
99
+ stop()
100
+ context.ui.toast.show({
101
+ variant: "error",
102
+ title: "Agency launch",
103
+ message: "The task prompt is ready but could not be submitted automatically.",
104
+ duration: 8_000,
105
+ })
106
+ return
107
+ }
108
+ timer = setTimeout(attempt, retryMs)
109
+ }
110
+
111
+ attempt()
112
+ return stop
113
+ }
114
+ }
115
+
116
+ const autosubmit = createAgencyAutosubmit()
117
+
118
+ export default {
119
+ id: "agency.tui",
120
+ setup(context: AutosubmitContext) {
121
+ return autosubmit(context)
122
+ },
123
+ }
124
+ `
125
+
126
+ export const managedWorkbaseOpencodeV2TuiPackage = packageBody
127
+ export const managedWorkbaseOpencodeV2TuiIndex = managed(indexBody)
128
+ export const managedWorkbaseOpencodeV2TuiPlugin = managed(tuiBody)
129
+
130
+ export const canUpdateManagedWorkbaseOpencodeV2TuiFile = (content: string) => {
131
+ const match = content.match(managedHeaderPattern)
132
+ if (!match?.[1]) return false
133
+ return checksum(content.slice(match[0].length)) === match[1]
134
+ }