@markjaquith/agency 3.10.1 → 3.11.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.
package/README.md CHANGED
@@ -140,7 +140,8 @@ whether the server plugin registered writable-checkout skills. It uses a native
140
140
  toast and does not submit a prompt to an LLM. When no writable checkout skill
141
141
  directory is available, server initialization is reported as indeterminate
142
142
  rather than inferred from plugin discovery.
143
- OpenCode discovers the config and plugin from task and epic launch directories.
143
+ OpenCode discovers the config and plugin from Agency launch directories and
144
+ their ancestors.
144
145
  The plugin uses OpenCode's plural discovery directory and exports both the V1
145
146
  server function and the `opencode2` module wrapper. Integration sync migrates a
146
147
  checksum-valid legacy singular-path plugin and preserves customized files.
@@ -394,9 +395,13 @@ identify the invoking session, so Agency deliberately avoids leaking one
394
395
  session's worker identity into another; the validated marker and injected system
395
396
  instruction remain the V2 fallback when the long-lived server did not inherit
396
397
  the client's launch environment.
397
- The `opencode2` and `opencode` agents remain rooted in their task or epic
398
- working directory so the workbase `AGENTS.md` and managed OpenCode config are
399
- discovered normally.
398
+ For execution units, the built-in `pi` agent and the `opencode2` and `opencode`
399
+ agents running OpenCode V2 launch from the authoritative writable checkout so
400
+ their project and Git interfaces reflect the implementation repository.
401
+ OpenCode V1 stops configuration discovery at the checkout's Git root, so V1
402
+ launches remain rooted in the task or phase directory. Epic and multi-phase task orchestration remains
403
+ rooted in its Agency document directory. Ancestor discovery still supplies the
404
+ workbase `AGENTS.md` and managed OpenCode config.
400
405
  Agency's managed OpenCode plugin grants the active workbase external-directory
401
406
  access and exposes existing checkout-local `.claude/skills`, `.agents/skills`,
402
407
  and `.opencode/{skill,skills}` definitions. V1 adds those source directories to
@@ -406,8 +411,9 @@ hook. The global Pi
406
411
  extension provides equivalent whole-workbase context and additionally discovers
407
412
  checkout-local `.pi/skills` through Pi's `resources_discover` lifecycle.
408
413
  `agency work` supplies the checkout directly; plain OpenCode and Pi launches
409
- resolve a materialized execution-unit
410
- checkout through `agency context`. A multi-phase
414
+ from a task or phase directory resolve a materialized execution-unit checkout
415
+ through `agency context` and instruct the agent to change to it before
416
+ implementation or Git operations. A multi-phase
411
417
  task root has no single checkout, so launch from its phase directory when using
412
418
  plain OpenCode or Pi. Other checkout-local configuration is not composed.
413
419
  `--print-command` prints the cwd, command template, and non-secret environment
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "3.10.1",
3
+ "version": "3.11.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs"
2
- import { dirname, join, resolve } from "node:path"
2
+ import { dirname, join, resolve, sep } from "node:path"
3
3
 
4
4
  type ExtensionAPI = {
5
5
  exec: (
@@ -26,7 +26,9 @@ type ExtensionAPI = {
26
26
  type AgencyContext = {
27
27
  root?: string
28
28
  checkout?: string
29
+ documentDirectory?: string
29
30
  target?: string
31
+ working?: boolean
30
32
  }
31
33
 
32
34
  const contextTarget = (result: Record<string, any>): string | undefined => {
@@ -78,14 +80,18 @@ const agencyContext = async (
78
80
  result.workbase?.root !== root ||
79
81
  !target ||
80
82
  (target.startsWith("execution-unit:") &&
81
- (!result.authority?.writable?.checkoutPath || status !== "working"))
83
+ !result.authority?.writable?.checkoutPath)
82
84
  )
83
85
  return
84
86
 
85
87
  return {
86
88
  root,
87
89
  checkout: result.authority?.writable?.checkoutPath,
90
+ documentDirectory: result.target?.path
91
+ ? dirname(result.target.path)
92
+ : undefined,
88
93
  target,
94
+ working: status === "working",
89
95
  }
90
96
  }
91
97
 
@@ -150,12 +156,27 @@ export default function agencyExtension(pi: ExtensionAPI) {
150
156
  : undefined
151
157
  const activeTarget = process.env.AGENCY_TARGET
152
158
  const worker =
153
- activeTarget && context.target === activeTarget
159
+ activeTarget && context.target === activeTarget && context.working
154
160
  ? `Agency verified this Pi session as the active worker for ${activeTarget}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.`
155
161
  : undefined
156
162
  const access = `The complete Agency workbase is available at ${context.root}. Use absolute paths under that root when workbase context is needed. Agency context remains the authority for writes.`
157
- const implementation = context.checkout
158
- ? `Pi remains rooted in the task or phase directory for Agency instructions and context. Treat ${context.checkout} as the default implementation directory for source reads, edits, repository status, builds, tests, formatting, and other repository-local commands. Run Agency lifecycle and context commands from the task or phase directory. Any reference checkouts reported by Agency context are read-only.`
163
+ const checkout = context.checkout ? resolve(context.checkout) : undefined
164
+ const current = resolve(ctx.cwd)
165
+ const repository =
166
+ checkout &&
167
+ (current === checkout || current.startsWith(`${checkout}${sep}`))
168
+ const implementation = checkout
169
+ ? [
170
+ repository
171
+ ? `Pi is rooted in Agency's authoritative writable checkout at ${checkout}. Use this directory for source reads, edits, Git status and other Git operations, builds, tests, and formatting.`
172
+ : `Agency's authoritative writable checkout is ${checkout}. Before doing implementation work or Git operations, change the working directory to that checkout. Set each tool's working directory to it when supported; otherwise use absolute paths.`,
173
+ context.documentDirectory
174
+ ? `Run Agency lifecycle commands from ${context.documentDirectory}; agency context may also be queried from the checkout.`
175
+ : undefined,
176
+ "Any reference checkouts reported by Agency context are read-only.",
177
+ ]
178
+ .filter(Boolean)
179
+ .join(" ")
159
180
  : undefined
160
181
 
161
182
  return {
@@ -37,7 +37,10 @@ import {
37
37
  readValidationEvidence,
38
38
  } from "../workbase/execution-contract"
39
39
  import { ValidationFailedError } from "./validate"
40
- import { prepareOpenCodeLaunch } from "../workbase/opencode-launch"
40
+ import {
41
+ openCodeDiscoversAncestorConfig,
42
+ prepareOpenCodeLaunch,
43
+ } from "../workbase/opencode-launch"
41
44
 
42
45
  export interface WorkOptions extends BaseCommandOptions {
43
46
  readonly directory?: string
@@ -378,6 +381,15 @@ export const work = (
378
381
  options.auto,
379
382
  )
380
383
  cli = resolved.argv[0]!
384
+ if (writablePath && !config.agents?.[agent]) {
385
+ if (
386
+ agent === "pi" ||
387
+ ((agent === "opencode2" || agent === "opencode") &&
388
+ (yield* openCodeDiscoversAncestorConfig(cli)))
389
+ ) {
390
+ launchPath = writablePath
391
+ }
392
+ }
381
393
  const environment = {
382
394
  ...resolved.environment,
383
395
  ...agentEnvironment(agent, variables),
@@ -81,6 +81,8 @@ var createReadlineEditing = (getInput, onInput) => {
81
81
  };
82
82
  var InteractiveTextPrompt = (props) => {
83
83
  let input;
84
+ const dimensions = useTerminalDimensions();
85
+ const inputWidth = () => dimensions().width + 1;
84
86
  const editing = createReadlineEditing(() => input);
85
87
  useKeyboard((key) => {
86
88
  if (key.propagationStopped)
@@ -110,6 +112,7 @@ var InteractiveTextPrompt = (props) => {
110
112
  _$setProp(_el$, "flexDirection", "column");
111
113
  _$setProp(_el$, "width", "100%");
112
114
  _$setProp(_el$, "height", "100%");
115
+ _$setProp(_el$, "overflow", "hidden");
113
116
  _$insert(_el$, (() => {
114
117
  var _c$ = _$memo(() => !!(props.fullScreen && !props.embedded));
115
118
  return () => _c$() && [(() => {
@@ -152,17 +155,18 @@ var InteractiveTextPrompt = (props) => {
152
155
  _$setProp(_el$4, "flexShrink", 0);
153
156
  _$insert(_el$4, () => props.embedded ? "enter submit | shift-enter newline | esc clear/back" : "enter submit | shift-enter newline | esc cancel");
154
157
  _$effect((_p$) => {
155
- var _v$ = macchiato.base, _v$2 = macchiato.blue, _v$3 = props.fullScreen ? 0 : 2, _v$4 = props.fullScreen ? 1 : 0, _v$5 = macchiato.mantle, _v$6 = macchiato.surface0, _v$7 = macchiato.text, _v$8 = macchiato.text, _v$9 = macchiato.blue, _v$0 = macchiato.overlay1;
158
+ var _v$ = macchiato.base, _v$2 = macchiato.blue, _v$3 = inputWidth(), _v$4 = props.fullScreen ? 0 : 2, _v$5 = props.fullScreen ? 1 : 0, _v$6 = macchiato.mantle, _v$7 = macchiato.surface0, _v$8 = macchiato.text, _v$9 = macchiato.text, _v$0 = macchiato.blue, _v$1 = macchiato.overlay1;
156
159
  _v$ !== _p$.e && (_p$.e = _$setProp(_el$, "backgroundColor", _v$, _p$.e));
157
160
  _v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "fg", _v$2, _p$.t));
158
- _v$3 !== _p$.a && (_p$.a = _$setProp(_el$3, "height", _v$3, _p$.a));
159
- _v$4 !== _p$.o && (_p$.o = _$setProp(_el$3, "flexGrow", _v$4, _p$.o));
160
- _v$5 !== _p$.i && (_p$.i = _$setProp(_el$3, "backgroundColor", _v$5, _p$.i));
161
- _v$6 !== _p$.n && (_p$.n = _$setProp(_el$3, "focusedBackgroundColor", _v$6, _p$.n));
162
- _v$7 !== _p$.s && (_p$.s = _$setProp(_el$3, "textColor", _v$7, _p$.s));
163
- _v$8 !== _p$.h && (_p$.h = _$setProp(_el$3, "focusedTextColor", _v$8, _p$.h));
164
- _v$9 !== _p$.r && (_p$.r = _$setProp(_el$3, "cursorColor", _v$9, _p$.r));
165
- _v$0 !== _p$.d && (_p$.d = _$setProp(_el$4, "fg", _v$0, _p$.d));
161
+ _v$3 !== _p$.a && (_p$.a = _$setProp(_el$3, "width", _v$3, _p$.a));
162
+ _v$4 !== _p$.o && (_p$.o = _$setProp(_el$3, "height", _v$4, _p$.o));
163
+ _v$5 !== _p$.i && (_p$.i = _$setProp(_el$3, "flexGrow", _v$5, _p$.i));
164
+ _v$6 !== _p$.n && (_p$.n = _$setProp(_el$3, "backgroundColor", _v$6, _p$.n));
165
+ _v$7 !== _p$.s && (_p$.s = _$setProp(_el$3, "focusedBackgroundColor", _v$7, _p$.s));
166
+ _v$8 !== _p$.h && (_p$.h = _$setProp(_el$3, "textColor", _v$8, _p$.h));
167
+ _v$9 !== _p$.r && (_p$.r = _$setProp(_el$3, "focusedTextColor", _v$9, _p$.r));
168
+ _v$0 !== _p$.d && (_p$.d = _$setProp(_el$3, "cursorColor", _v$0, _p$.d));
169
+ _v$1 !== _p$.l && (_p$.l = _$setProp(_el$4, "fg", _v$1, _p$.l));
166
170
  return _p$;
167
171
  }, {
168
172
  e: undefined,
@@ -174,7 +178,8 @@ var InteractiveTextPrompt = (props) => {
174
178
  s: undefined,
175
179
  h: undefined,
176
180
  r: undefined,
177
- d: undefined
181
+ d: undefined,
182
+ l: undefined
178
183
  });
179
184
  return _el$;
180
185
  })();
@@ -497,15 +502,15 @@ var InteractiveSelectPrompt = (props) => {
497
502
  })()
498
503
  }), null);
499
504
  _$effect((_p$) => {
500
- var _v$24 = index === selected() ? macchiato.text : macchiato.subtext0, _v$25 = index === selected() ? macchiato.surface1 : background(), _v$26 = {
505
+ var _v$25 = index === selected() ? macchiato.text : macchiato.subtext0, _v$26 = index === selected() ? macchiato.surface1 : background(), _v$27 = {
501
506
  fg: index === selected() ? macchiato.blue : undefined
502
- }, _v$27 = {
507
+ }, _v$28 = {
503
508
  fg: macchiato.overlay1
504
509
  };
505
- _v$24 !== _p$.e && (_p$.e = _$setProp(_el$26, "fg", _v$24, _p$.e));
506
- _v$25 !== _p$.t && (_p$.t = _$setProp(_el$26, "bg", _v$25, _p$.t));
507
- _v$26 !== _p$.a && (_p$.a = _$setProp(_el$27, "style", _v$26, _p$.a));
508
- _v$27 !== _p$.o && (_p$.o = _$setProp(_el$28, "style", _v$27, _p$.o));
510
+ _v$25 !== _p$.e && (_p$.e = _$setProp(_el$26, "fg", _v$25, _p$.e));
511
+ _v$26 !== _p$.t && (_p$.t = _$setProp(_el$26, "bg", _v$26, _p$.t));
512
+ _v$27 !== _p$.a && (_p$.a = _$setProp(_el$27, "style", _v$27, _p$.a));
513
+ _v$28 !== _p$.o && (_p$.o = _$setProp(_el$28, "style", _v$28, _p$.o));
509
514
  return _p$;
510
515
  }, {
511
516
  e: undefined,
@@ -517,9 +522,9 @@ var InteractiveSelectPrompt = (props) => {
517
522
  })();
518
523
  })());
519
524
  _$effect((_p$) => {
520
- var _v$22 = rowHeight(), _v$23 = index === selected() ? macchiato.surface1 : background();
521
- _v$22 !== _p$.e && (_p$.e = _$setProp(_el$19, "height", _v$22, _p$.e));
522
- _v$23 !== _p$.t && (_p$.t = _$setProp(_el$19, "backgroundColor", _v$23, _p$.t));
525
+ var _v$23 = rowHeight(), _v$24 = index === selected() ? macchiato.surface1 : background();
526
+ _v$23 !== _p$.e && (_p$.e = _$setProp(_el$19, "height", _v$23, _p$.e));
527
+ _v$24 !== _p$.t && (_p$.t = _$setProp(_el$19, "backgroundColor", _v$24, _p$.t));
523
528
  return _p$;
524
529
  }, {
525
530
  e: undefined,
@@ -529,20 +534,20 @@ var InteractiveSelectPrompt = (props) => {
529
534
  })()
530
535
  }));
531
536
  _$effect((_p$) => {
532
- var _v$1 = background(), _v$10 = brandHeight(), _v$11 = macchiato.blue, _v$12 = macchiato.blue, _v$13 = props.active === false ? macchiato.mantle : props.embedded ? macchiato.surface1 : macchiato.surface0, _v$14 = props.active !== false, _v$15 = macchiato.overlay0, _v$16 = macchiato.mantle, _v$17 = props.embedded ? macchiato.surface1 : macchiato.surface0, _v$18 = macchiato.text, _v$19 = macchiato.text, _v$20 = macchiato.blue, _v$21 = gapHeight();
533
- _v$1 !== _p$.e && (_p$.e = _$setProp(_el$8, "backgroundColor", _v$1, _p$.e));
534
- _v$10 !== _p$.t && (_p$.t = _$setProp(_el$9, "height", _v$10, _p$.t));
535
- _v$11 !== _p$.a && (_p$.a = _$setProp(_el$0, "fg", _v$11, _p$.a));
536
- _v$12 !== _p$.o && (_p$.o = _$setProp(_el$10, "fg", _v$12, _p$.o));
537
- _v$13 !== _p$.i && (_p$.i = _$setProp(_el$12, "backgroundColor", _v$13, _p$.i));
538
- _v$14 !== _p$.n && (_p$.n = _$setProp(_el$13, "focused", _v$14, _p$.n));
539
- _v$15 !== _p$.s && (_p$.s = _$setProp(_el$13, "placeholderColor", _v$15, _p$.s));
540
- _v$16 !== _p$.h && (_p$.h = _$setProp(_el$13, "backgroundColor", _v$16, _p$.h));
541
- _v$17 !== _p$.r && (_p$.r = _$setProp(_el$13, "focusedBackgroundColor", _v$17, _p$.r));
542
- _v$18 !== _p$.d && (_p$.d = _$setProp(_el$13, "textColor", _v$18, _p$.d));
543
- _v$19 !== _p$.l && (_p$.l = _$setProp(_el$13, "focusedTextColor", _v$19, _p$.l));
544
- _v$20 !== _p$.u && (_p$.u = _$setProp(_el$13, "cursorColor", _v$20, _p$.u));
545
- _v$21 !== _p$.c && (_p$.c = _$setProp(_el$14, "height", _v$21, _p$.c));
537
+ var _v$10 = background(), _v$11 = brandHeight(), _v$12 = macchiato.blue, _v$13 = macchiato.blue, _v$14 = props.active === false ? macchiato.mantle : props.embedded ? macchiato.surface1 : macchiato.surface0, _v$15 = props.active !== false, _v$16 = macchiato.overlay0, _v$17 = macchiato.mantle, _v$18 = props.embedded ? macchiato.surface1 : macchiato.surface0, _v$19 = macchiato.text, _v$20 = macchiato.text, _v$21 = macchiato.blue, _v$22 = gapHeight();
538
+ _v$10 !== _p$.e && (_p$.e = _$setProp(_el$8, "backgroundColor", _v$10, _p$.e));
539
+ _v$11 !== _p$.t && (_p$.t = _$setProp(_el$9, "height", _v$11, _p$.t));
540
+ _v$12 !== _p$.a && (_p$.a = _$setProp(_el$0, "fg", _v$12, _p$.a));
541
+ _v$13 !== _p$.o && (_p$.o = _$setProp(_el$10, "fg", _v$13, _p$.o));
542
+ _v$14 !== _p$.i && (_p$.i = _$setProp(_el$12, "backgroundColor", _v$14, _p$.i));
543
+ _v$15 !== _p$.n && (_p$.n = _$setProp(_el$13, "focused", _v$15, _p$.n));
544
+ _v$16 !== _p$.s && (_p$.s = _$setProp(_el$13, "placeholderColor", _v$16, _p$.s));
545
+ _v$17 !== _p$.h && (_p$.h = _$setProp(_el$13, "backgroundColor", _v$17, _p$.h));
546
+ _v$18 !== _p$.r && (_p$.r = _$setProp(_el$13, "focusedBackgroundColor", _v$18, _p$.r));
547
+ _v$19 !== _p$.d && (_p$.d = _$setProp(_el$13, "textColor", _v$19, _p$.d));
548
+ _v$20 !== _p$.l && (_p$.l = _$setProp(_el$13, "focusedTextColor", _v$20, _p$.l));
549
+ _v$21 !== _p$.u && (_p$.u = _$setProp(_el$13, "cursorColor", _v$21, _p$.u));
550
+ _v$22 !== _p$.c && (_p$.c = _$setProp(_el$14, "height", _v$22, _p$.c));
546
551
  return _p$;
547
552
  }, {
548
553
  e: undefined,
@@ -605,12 +610,12 @@ var InteractiveTabbedPrompt = (props) => {
605
610
  _$insert(_el$38, () => index() === active() ? "\u258E" : " ");
606
611
  _$insert(_el$37, () => ` ${tab.label} `, null);
607
612
  _$effect((_p$) => {
608
- var _v$30 = index() === active() ? macchiato.text : macchiato.overlay1, _v$31 = index() === active() ? macchiato.base : macchiato.surface0, _v$32 = {
613
+ var _v$31 = index() === active() ? macchiato.text : macchiato.overlay1, _v$32 = index() === active() ? macchiato.base : macchiato.surface0, _v$33 = {
609
614
  fg: macchiato.blue
610
615
  };
611
- _v$30 !== _p$.e && (_p$.e = _$setProp(_el$37, "fg", _v$30, _p$.e));
612
- _v$31 !== _p$.t && (_p$.t = _$setProp(_el$37, "bg", _v$31, _p$.t));
613
- _v$32 !== _p$.a && (_p$.a = _$setProp(_el$38, "style", _v$32, _p$.a));
616
+ _v$31 !== _p$.e && (_p$.e = _$setProp(_el$37, "fg", _v$31, _p$.e));
617
+ _v$32 !== _p$.t && (_p$.t = _$setProp(_el$37, "bg", _v$32, _p$.t));
618
+ _v$33 !== _p$.a && (_p$.a = _$setProp(_el$38, "style", _v$33, _p$.a));
614
619
  return _p$;
615
620
  }, {
616
621
  e: undefined,
@@ -702,9 +707,9 @@ var InteractiveTabbedPrompt = (props) => {
702
707
  });
703
708
  })(), null);
704
709
  _$effect((_p$) => {
705
- var _v$28 = macchiato.surface0, _v$29 = macchiato.base;
706
- _v$28 !== _p$.e && (_p$.e = _$setProp(_el$30, "backgroundColor", _v$28, _p$.e));
707
- _v$29 !== _p$.t && (_p$.t = _$setProp(_el$35, "backgroundColor", _v$29, _p$.t));
710
+ var _v$29 = macchiato.surface0, _v$30 = macchiato.base;
711
+ _v$29 !== _p$.e && (_p$.e = _$setProp(_el$30, "backgroundColor", _v$29, _p$.e));
712
+ _v$30 !== _p$.t && (_p$.t = _$setProp(_el$35, "backgroundColor", _v$30, _p$.t));
708
713
  return _p$;
709
714
  }, {
710
715
  e: undefined,
@@ -769,9 +774,9 @@ var createInteractiveSession = async (onCancel = () => {}, initialTabs = []) =>
769
774
  _$setProp(_el$40, "height", "100%");
770
775
  _$insert(_el$41, () => props.prompt);
771
776
  _$effect((_p$) => {
772
- var _v$33 = macchiato.base, _v$34 = macchiato.text;
773
- _v$33 !== _p$.e && (_p$.e = _$setProp(_el$40, "backgroundColor", _v$33, _p$.e));
774
- _v$34 !== _p$.t && (_p$.t = _$setProp(_el$41, "fg", _v$34, _p$.t));
777
+ var _v$34 = macchiato.base, _v$35 = macchiato.text;
778
+ _v$34 !== _p$.e && (_p$.e = _$setProp(_el$40, "backgroundColor", _v$34, _p$.e));
779
+ _v$35 !== _p$.t && (_p$.t = _$setProp(_el$41, "fg", _v$35, _p$.t));
775
780
  return _p$;
776
781
  }, {
777
782
  e: undefined,
@@ -303,9 +303,12 @@ before launching an agent.
303
303
 
304
304
  OpenCode can access the complete workbase tree, but this filesystem permission
305
305
  does not expand Agency write authority beyond the checkout reported by
306
- `agency context`. OpenCode remains rooted in the task or epic directory so the
307
- workbase instructions and config compose normally. The managed plugin resolves
308
- the writable checkout from launch context or `agency context`, then exposes its
309
- supported skill directories through the applicable OpenCode plugin API; this
310
- does not make other checkout-local OpenCode configuration authoritative. Agents
311
- must follow the authority reported by `agency context`.
306
+ `agency context`. Pi and OpenCode V2 execution units launched by `agency work`
307
+ are rooted in the authoritative writable checkout so their project and Git
308
+ status reflect the implementation repository; OpenCode V1 remains rooted in the
309
+ task or phase directory. Orchestration remains rooted in its
310
+ Agency document directory. The managed integrations resolve the writable
311
+ checkout from launch context or `agency context`, guide task-directory sessions
312
+ to use it for implementation, and expose its supported skill directories through
313
+ the applicable plugin API; this does not make other checkout-local configuration
314
+ authoritative. Agents must follow the authority reported by `agency context`.
@@ -15,6 +15,21 @@ const Admitted = Schema.Struct({
15
15
  }),
16
16
  })
17
17
 
18
+ // V1 stops config discovery at the Git root, so a checkout-rooted V1 launch
19
+ // would miss the workbase's managed OpenCode config and plugin.
20
+ export const openCodeDiscoversAncestorConfig = (cli: string) =>
21
+ Effect.gen(function* () {
22
+ const fs = yield* FileSystemService
23
+ const result = yield* fs.runCommand([cli, "--version"], {
24
+ captureOutput: true,
25
+ timeoutMs: 60_000,
26
+ })
27
+ return (
28
+ result.exitCode === 0 &&
29
+ /^(?:opencode\s+v?)?2\./.test(result.stdout.trim())
30
+ )
31
+ }).pipe(Effect.orElseSucceed(() => false))
32
+
18
33
  // V2's TUI --prompt only seeds the composer. Submit through the same CLI's
19
34
  // authenticated service connection, then attach without a prompt. Reloading or
20
35
  // reconnecting the TUI cannot replay the launch input. Only built-in Agency
@@ -27,6 +27,7 @@ const workerLaunchPattern = /^Agency worker launch target: ([^.\\s]+)\\./
27
27
  type AgencyContext = {
28
28
  root?: string
29
29
  checkout?: string
30
+ documentDirectory?: string
30
31
  target?: string
31
32
  task?: string
32
33
  phase?: string
@@ -139,9 +140,27 @@ const skillInfo = (path: string) => {
139
140
  }
140
141
  }
141
142
 
143
+ const implementationInstructions = (
144
+ client: "OpenCode" | "Pi",
145
+ context: AgencyContext | undefined,
146
+ directory: string,
147
+ ) => {
148
+ if (!context?.checkout) return
149
+ const checkout = resolve(context.checkout)
150
+ const current = resolve(directory)
151
+ const repository = current === checkout || current.startsWith(\`\${checkout}\${sep}\`)
152
+ ? \`\${client} is rooted in Agency's authoritative writable checkout at \${checkout}. Use this directory for source reads, edits, Git status and other Git operations, builds, tests, and formatting.\`
153
+ : \`Agency's authoritative writable checkout is \${checkout}. Before doing implementation work or Git operations, change the working directory to that checkout. Set each tool's working directory to it when supported; otherwise use absolute paths.\`
154
+ const lifecycle = context.documentDirectory
155
+ ? \`Run Agency lifecycle commands from \${context.documentDirectory}; agency context may also be queried from the checkout. Any reference checkouts reported by Agency context are read-only.\`
156
+ : "Any reference checkouts reported by Agency context are read-only."
157
+ return \`\${repository} \${lifecycle}\`
158
+ }
159
+
142
160
  const agencyContext = async (
143
161
  directory: string,
144
162
  useEnvironmentTarget = true,
163
+ requireWorking = false,
145
164
  ): Promise<AgencyContext | undefined> => {
146
165
  const task = useEnvironmentTarget ? process.env.AGENCY_TASK_ID : undefined
147
166
  const phase = useEnvironmentTarget ? process.env.AGENCY_PHASE_ID : undefined
@@ -161,18 +180,26 @@ const agencyContext = async (
161
180
  const result = envelope.result ?? {}
162
181
  const target = contextTarget(result)
163
182
  const document = result.target?.path
183
+ const checkout = result.authority?.writable?.checkoutPath
164
184
  const status = result.documents?.phase?.data?.status ?? result.documents?.task?.data?.status
185
+ const current = resolve(directory)
186
+ const documentDirectory = document ? dirname(document) : undefined
187
+ const matchesLocation =
188
+ documentDirectory === current ||
189
+ (checkout &&
190
+ (current === resolve(checkout) || current.startsWith(\`\${resolve(checkout)}\${sep}\`)))
165
191
  if (
166
192
  result.validation?.valid !== true ||
167
193
  !target ||
168
194
  !document ||
169
- dirname(document) !== resolve(directory) ||
170
- (target.startsWith("execution-unit:") &&
195
+ !matchesLocation ||
196
+ (requireWorking && target.startsWith("execution-unit:") &&
171
197
  (!result.authority?.writable?.checkoutPath || status !== "working"))
172
198
  ) return
173
199
  return {
174
200
  root: result.workbase?.root,
175
- checkout: result.authority?.writable?.checkoutPath,
201
+ checkout,
202
+ documentDirectory,
176
203
  target,
177
204
  task: result.target?.taskId,
178
205
  phase: result.target?.phaseId,
@@ -181,10 +208,11 @@ const agencyContext = async (
181
208
 
182
209
  const plugin: V1Plugin = async ({ directory }) => {
183
210
  const workerSessions = new Map<string, AgencyContext>()
211
+ const locationContext = agencyContext(directory, false).catch(() => undefined)
184
212
 
185
213
  return {
186
214
  config: async (config: any) => {
187
- const context = await agencyContext(directory).catch(() => undefined)
215
+ const context = await locationContext
188
216
  const root = process.env.AGENCY_WORKBASE ?? context?.root ?? discoverWorkbase(directory)
189
217
  const checkout = process.env.AGENCY_WRITABLE_CHECKOUT ?? context?.checkout ?? discoverCheckout(directory, root)
190
218
 
@@ -220,7 +248,7 @@ const plugin: V1Plugin = async ({ directory }) => {
220
248
  "chat.message": async ({ sessionID }: { sessionID: string }, output: any) => {
221
249
  const launchTarget = workerLaunchTarget(output.parts)
222
250
  if (!launchTarget) return
223
- const context = await agencyContext(directory, false).catch(() => undefined)
251
+ const context = await agencyContext(directory, false, true).catch(() => undefined)
224
252
  if (context?.target !== launchTarget) return
225
253
  workerSessions.set(sessionID, context)
226
254
  },
@@ -229,16 +257,16 @@ const plugin: V1Plugin = async ({ directory }) => {
229
257
  output: any,
230
258
  ) => {
231
259
  if (!sessionID) return
232
- const context = workerSessions.get(sessionID)
233
- if (!context?.target) return
234
- output.system.push(
235
- [
236
- \`Agency verified this OpenCode session as the active worker for \${context.target}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.\`,
237
- context.checkout
238
- ? \`OpenCode remains rooted in the task or phase directory for Agency instructions and context. Treat \${context.checkout} as the default implementation directory: use it for source reads, edits, repository status, builds, tests, formatting, and other repository-local commands. Set each tool's working directory to that checkout when supported; otherwise use absolute paths. Run Agency lifecycle and context commands from the task or phase directory. Any reference checkouts reported by Agency context are read-only.\`
239
- : undefined,
240
- ].filter(Boolean).join(" "),
241
- )
260
+ const worker = workerSessions.get(sessionID)
261
+ const context = worker ?? await locationContext
262
+ const implementation = implementationInstructions("OpenCode", context, directory)
263
+ if (!worker?.target && !implementation) return
264
+ output.system.push([
265
+ worker?.target
266
+ ? \`Agency verified this OpenCode session as the active worker for \${worker.target}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.\`
267
+ : undefined,
268
+ implementation,
269
+ ].filter(Boolean).join(" "))
242
270
  },
243
271
  "shell.env": async ({ sessionID }: { sessionID?: string }, output: any) => {
244
272
  if (!sessionID) return
@@ -259,7 +287,7 @@ export const AgencyPlugin = plugin
259
287
 
260
288
  const setup = async (context: V2PluginContext) => {
261
289
  const directory = context.location.directory
262
- const agency = await agencyContext(directory).catch(() => undefined)
290
+ const agency = await agencyContext(directory, false).catch(() => undefined)
263
291
  const root = agency?.root ?? resolve(dirname(fileURLToPath(import.meta.url)), "../..")
264
292
  const instructionsPath = join(root, ".agency", "AGENTS.md")
265
293
  const instructions = existsSync(instructionsPath)
@@ -302,23 +330,21 @@ const setup = async (context: V2PluginContext) => {
302
330
  await context.session.hook("prompt", async (event) => {
303
331
  const launchTarget = event.prompt.text.match(workerLaunchPattern)?.[1]
304
332
  if (!launchTarget) return
305
- const current = await agencyContext(directory, false).catch(() => undefined)
333
+ const current = await agencyContext(directory, false, true).catch(() => undefined)
306
334
  if (!current || current.target !== launchTarget) return
307
335
  workerSessions.set(event.sessionID, current)
308
336
  })
337
+ const implementation = implementationInstructions("OpenCode", agency, directory)
309
338
  await context.session.hook("context", (event) => {
310
339
  if (instructions) event.system.push({ type: "text", text: instructions })
311
340
  const current = workerSessions.get(event.sessionID)
312
- if (!current?.target) return
313
- event.system.push({
314
- type: "text",
315
- text: [
316
- \`Agency verified this OpenCode session as the active worker for \${current.target}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.\`,
317
- current.checkout
318
- ? \`OpenCode remains rooted in the task or phase directory for Agency instructions and context. Treat \${current.checkout} as the default implementation directory: use it for source reads, edits, repository status, builds, tests, formatting, and other repository-local commands. Set each tool's working directory to that checkout when supported; otherwise use absolute paths. Run Agency lifecycle and context commands from the task or phase directory. Any reference checkouts reported by Agency context are read-only.\`
319
- : undefined,
320
- ].filter(Boolean).join(" "),
321
- })
341
+ const text = [
342
+ current?.target
343
+ ? \`Agency verified this OpenCode session as the active worker for \${current.target}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.\`
344
+ : undefined,
345
+ implementationInstructions("OpenCode", current, directory) ?? implementation,
346
+ ].filter(Boolean).join(" ")
347
+ if (text) event.system.push({ type: "text", text })
322
348
  })
323
349
  }
324
350