@gaia-ai/addon-herdr 0.6.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 keytec GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @gaia-ai/addon-herdr
2
+
3
+ GAIA herdr integration: spawn executor + per-ticket worktree workspace.
4
+
5
+ Part of the GAIA CLI. Install the meta package `@gaia-ai/gaia` to get the `gaia` CLI with all addons. Source: https://git.key-tec.de/keytec/gaia (gaia-cli/).
@@ -0,0 +1,45 @@
1
+ /** A hosted agent as reported by the host. `id` is the host-owned stable handle
2
+ * (survives a TUI restart); `group` is the opaque grouping key it was launched
3
+ * under; `label` is a human tag for the list. */
4
+ export interface HostedAgent {
5
+ id: string;
6
+ group: string;
7
+ label: string;
8
+ }
9
+ /**
10
+ * The INTENT-level agent host herdr provides. The caller says "launch an agent
11
+ * in this group", "show that agent", "list this group" — it never names panes,
12
+ * tabs, splits or zoom. The host (herdr) owns the whole layout policy. `group`
13
+ * is OPAQUE (gaia passes the project slug). `command` is the resolved shell
14
+ * command the agent runs (the renderer's prompt is turned into a command by the
15
+ * `gaia ui` entry before it reaches here — herdr stays agent-binary-agnostic).
16
+ */
17
+ export interface AgentLaunchHost {
18
+ /**
19
+ * Start an agent and return its stable handle. The host decides where the
20
+ * session is placed and shows it; the caller only supplies intent + a group.
21
+ */
22
+ launch(opts: {
23
+ group: string;
24
+ cwd: string;
25
+ command: string;
26
+ env?: Record<string, string | undefined> | undefined;
27
+ label?: string | undefined;
28
+ }): Promise<HostedAgent>;
29
+ /** The live agents of a group — the source of truth for the list, rebuilt
30
+ * from the host on demand so sessions survive a TUI restart. */
31
+ list(group: string): Promise<HostedAgent[]>;
32
+ /** Bring one agent to the foreground (the host zooms/switches as it sees fit). */
33
+ focus(id: string): Promise<void>;
34
+ /** Stop showing a group's agents and return focus to the caller. Durable —
35
+ * the agents keep running; this never terminates them. */
36
+ hide(group: string): Promise<void>;
37
+ /** Terminate one agent for good. */
38
+ kill(id: string): Promise<void>;
39
+ /** Send literal text to an agent's input. */
40
+ sendText(id: string, text: string): Promise<void>;
41
+ }
42
+ /** A workspace (or other plugin) that can host agents at the intent level. */
43
+ export interface AgentLaunchHostProvider {
44
+ agentHost: AgentLaunchHost;
45
+ }
@@ -0,0 +1,9 @@
1
+ // Herdr's own agent-host contract (GAIA-194 AC-3). It used to live in
2
+ // `@gaia-ai/core` as `AgentLaunchHost`/`HostedAgent`, but the agent-host seam is
3
+ // a TUI/renderer concern, not a conductor-engine one — so core dropped it and
4
+ // herdr types its impl LOCALLY. This interface imports nothing removed and
5
+ // nothing from `@gaia-ai/addon-gaia-ui`: the renderer owns its own
6
+ // prompt-level `TuiAgentService`, and the conductor `gaia ui` entry adapts this
7
+ // mechanism-level (`command`-based) host to it. Structurally compatible, no
8
+ // cross-package type coupling.
9
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { AgentLaunchHost } from './agent-host.js';
2
+ import type { HerdrExec } from './pane-layout.js';
3
+ export declare function herdrAgentHost(execHerdr: HerdrExec): AgentLaunchHost;
@@ -0,0 +1,111 @@
1
+ import { envArgs, findTabIdByLabel, parsePaneInfo, parsePanesInTab, parseTabCreate, } from './panes.js';
2
+ /** The single tab that holds a group's agent panes. */
3
+ const groupTabLabel = (group) => `gaia-agents:${group}`;
4
+ export function herdrAgentHost(execHerdr) {
5
+ // The caller's home tab, returned to on hide. Captured lazily on the first
6
+ // launch — that is when the caller is focused on its own tab (pressing `n`),
7
+ // and it keeps construction side-effect-free. Best-effort: a probe failure
8
+ // just leaves hide without a home to return to.
9
+ let homeTab = null;
10
+ const captureHome = async () => {
11
+ if (homeTab !== null)
12
+ return;
13
+ try {
14
+ homeTab = parsePaneInfo(await execHerdr(['pane', 'current'])).tabId;
15
+ }
16
+ catch {
17
+ // no home tab known; hide() becomes a no-op focus
18
+ }
19
+ };
20
+ // Cache each group's tab id to skip the `tab list` round-trip once known.
21
+ const tabByGroup = new Map();
22
+ const resolveTab = async (group) => {
23
+ const cached = tabByGroup.get(group);
24
+ if (cached)
25
+ return cached;
26
+ const found = findTabIdByLabel(await execHerdr(['tab', 'list']), groupTabLabel(group));
27
+ if (found)
28
+ tabByGroup.set(group, found);
29
+ return found;
30
+ };
31
+ return {
32
+ async launch({ group, cwd, command, env, label }) {
33
+ await captureHome();
34
+ const e = env ?? {};
35
+ // Find the group's tab and split off a LIVE pane (newest-first; a stale
36
+ // pane — a previous agent died/was closed — is skipped). None survive, or
37
+ // no tab yet → create the group tab (its root pane is the agent).
38
+ let pane = null;
39
+ const tabId = await resolveTab(group);
40
+ if (tabId) {
41
+ const panes = parsePanesInTab(await execHerdr(['pane', 'list']), tabId);
42
+ for (const p of [...panes].reverse()) {
43
+ try {
44
+ pane = parsePaneInfo(await execHerdr([
45
+ 'pane',
46
+ 'split',
47
+ p.paneId,
48
+ '--direction',
49
+ 'right',
50
+ '--no-focus',
51
+ '--cwd',
52
+ cwd,
53
+ ...envArgs(e),
54
+ ]));
55
+ break;
56
+ }
57
+ catch {
58
+ // stale pane — try the next
59
+ }
60
+ }
61
+ if (!pane)
62
+ tabByGroup.delete(group);
63
+ }
64
+ if (!pane) {
65
+ pane = parseTabCreate(await execHerdr([
66
+ 'tab',
67
+ 'create',
68
+ '--cwd',
69
+ cwd,
70
+ '--no-focus',
71
+ '--label',
72
+ groupTabLabel(group),
73
+ ...envArgs(e),
74
+ ]));
75
+ tabByGroup.set(group, pane.tabId);
76
+ }
77
+ await execHerdr(['pane', 'run', pane.paneId, command]);
78
+ const human = label ?? 'agent';
79
+ await execHerdr(['pane', 'rename', pane.paneId, human]).catch(() => { });
80
+ await execHerdr(['pane', 'zoom', pane.paneId, '--on']).catch(() => { });
81
+ return { id: pane.paneId, group, label: human };
82
+ },
83
+ async list(group) {
84
+ const tabId = await resolveTab(group);
85
+ if (!tabId)
86
+ return [];
87
+ return parsePanesInTab(await execHerdr(['pane', 'list']), tabId).map((p) => ({ id: p.paneId, group, label: p.label }));
88
+ },
89
+ async focus(id) {
90
+ await execHerdr(['pane', 'zoom', id, '--on']);
91
+ },
92
+ async hide(group) {
93
+ // Un-zoom the group's panes (so it reads as the tiled overview next time)
94
+ // and return focus to the caller's home tab. The agents keep running.
95
+ const tabId = await resolveTab(group);
96
+ if (tabId) {
97
+ for (const p of parsePanesInTab(await execHerdr(['pane', 'list']), tabId)) {
98
+ await execHerdr(['pane', 'zoom', p.paneId, '--off']).catch(() => { });
99
+ }
100
+ }
101
+ if (homeTab)
102
+ await execHerdr(['tab', 'focus', homeTab]).catch(() => { });
103
+ },
104
+ async kill(id) {
105
+ await execHerdr(['pane', 'close', id]);
106
+ },
107
+ async sendText(id, text) {
108
+ await execHerdr(['pane', 'send-text', id, text]);
109
+ },
110
+ };
111
+ }
@@ -0,0 +1,55 @@
1
+ import type { RootGitExec } from './root-anchor.js';
2
+ export interface PaneSpec {
3
+ direction: 'right' | 'down';
4
+ command: string;
5
+ focus?: boolean;
6
+ }
7
+ export interface StateConfig {
8
+ /**
9
+ * Tab label template. Must contain {identifier} — the conductor matches a
10
+ * run's tab by its ticket identifier (stop/state), so the identifier is the
11
+ * stable anchor in the visible name. Other vars: {title}, {titleSlug},
12
+ * {runUuid}, {repoName}, {workspacePath}.
13
+ */
14
+ tabLabel?: string;
15
+ workspaceLabel?: string;
16
+ /** Extra layout panes (diff/logs); NOT the agent command. */
17
+ panes?: PaneSpec[];
18
+ }
19
+ export interface HerdrExecutorOptions {
20
+ /** Legacy fallback for input.command. */
21
+ command?: string;
22
+ /**
23
+ * Lifecycle-hook commands, keyed like `config.hooks` (after_create,
24
+ * before_run, after_run, after_done). The factory copies `config.hooks` here
25
+ * so the executor — the single best-effort catch point — can run them.
26
+ */
27
+ hooks?: {
28
+ after_create?: string;
29
+ before_run?: string;
30
+ after_run?: string;
31
+ after_done?: string;
32
+ };
33
+ /**
34
+ * The parent repo root (main clone) — herdr `--cwd` target for reattaching a
35
+ * closed worktree. herdr rejects a `worktree open` whose --cwd is a linked
36
+ * worktree (`linked_worktree_source`): open/new must start from the parent
37
+ * workspace. When unset the factory derives it from the git common dir
38
+ * (GAIA-177), exactly like the sibling `herdrWorkspace` plugin; this override
39
+ * always wins.
40
+ */
41
+ root?: string;
42
+ /** Overrideable git exec for root derivation (tests). */
43
+ execGit?: RootGitExec;
44
+ default?: StateConfig;
45
+ states?: Record<string, StateConfig>;
46
+ }
47
+ export interface ResolvedConfig {
48
+ tabLabel: string;
49
+ workspaceLabel: string;
50
+ panes: PaneSpec[];
51
+ }
52
+ export declare const BUILTIN_DEFAULTS: ResolvedConfig;
53
+ export declare function resolveStateConfig(options: HerdrExecutorOptions, state: string): ResolvedConfig;
54
+ export declare function renderTemplate(tpl: string, vars: Record<string, string>): string;
55
+ export declare function assertTabLabelTemplate(tabLabel: string): void;
@@ -0,0 +1,27 @@
1
+ export const BUILTIN_DEFAULTS = {
2
+ tabLabel: '{identifier} · {titleSlug}',
3
+ workspaceLabel: '{repoName}',
4
+ panes: [],
5
+ };
6
+ export function resolveStateConfig(options, state) {
7
+ const base = options.default ?? {};
8
+ const stateCfg = options.states?.[state] ?? {};
9
+ return {
10
+ tabLabel: stateCfg.tabLabel ?? base.tabLabel ?? BUILTIN_DEFAULTS.tabLabel,
11
+ workspaceLabel: stateCfg.workspaceLabel ??
12
+ base.workspaceLabel ??
13
+ BUILTIN_DEFAULTS.workspaceLabel,
14
+ panes: stateCfg.panes ?? base.panes ?? BUILTIN_DEFAULTS.panes,
15
+ };
16
+ }
17
+ export function renderTemplate(tpl, vars) {
18
+ return tpl.replace(/\{(\w+)\}/g, (whole, key) => {
19
+ const value = vars[key];
20
+ return value === undefined ? whole : value;
21
+ });
22
+ }
23
+ export function assertTabLabelTemplate(tabLabel) {
24
+ if (!tabLabel.includes('{identifier}')) {
25
+ throw new Error(`herdr tabLabel must contain {identifier} for stop/state matching: ${tabLabel}`);
26
+ }
27
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Recursively re-add the owner-write bit to a directory tree.
3
+ *
4
+ * A GAIA worktree is a Drupal install, and Drupal hardens `web/sites/default`
5
+ * to `0555` and `settings.php` / `files/.htaccess` to `0444`. A directory
6
+ * without its write bit cannot have its entries unlinked, so removing the
7
+ * worktree (`git worktree remove --force` / `rm -rf`, however herdr does it)
8
+ * gets EACCES. Restoring `u+w` on every dir/file first is what makes removal
9
+ * succeed — this is not an ownership problem, only the permission bits.
10
+ *
11
+ * Best-effort per entry: a single un-chmod-able entry (already gone, foreign
12
+ * owner) must not abort the walk. Symlinks are never followed — chmod would
13
+ * touch the link target, which may live outside the tree.
14
+ */
15
+ export declare function restoreWritable(path: string): void;
16
+ /**
17
+ * Remove a directory tree that may be hardened read-only by Drupal:
18
+ * {@link restoreWritable} first, then `rm -rf`. Used where the conductor owns
19
+ * the unlink; when herdr performs the removal, call {@link restoreWritable}
20
+ * alone beforehand. A missing path is a no-op.
21
+ */
22
+ export declare function forceRemoveDir(path: string): void;
package/dist/src/fs.js ADDED
@@ -0,0 +1,58 @@
1
+ import { chmodSync, lstatSync, readdirSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Recursively re-add the owner-write bit to a directory tree.
5
+ *
6
+ * A GAIA worktree is a Drupal install, and Drupal hardens `web/sites/default`
7
+ * to `0555` and `settings.php` / `files/.htaccess` to `0444`. A directory
8
+ * without its write bit cannot have its entries unlinked, so removing the
9
+ * worktree (`git worktree remove --force` / `rm -rf`, however herdr does it)
10
+ * gets EACCES. Restoring `u+w` on every dir/file first is what makes removal
11
+ * succeed — this is not an ownership problem, only the permission bits.
12
+ *
13
+ * Best-effort per entry: a single un-chmod-able entry (already gone, foreign
14
+ * owner) must not abort the walk. Symlinks are never followed — chmod would
15
+ * touch the link target, which may live outside the tree.
16
+ */
17
+ export function restoreWritable(path) {
18
+ let stat;
19
+ try {
20
+ stat = lstatSync(path);
21
+ }
22
+ catch {
23
+ return; // gone / inaccessible — nothing to restore
24
+ }
25
+ if (stat.isSymbolicLink()) {
26
+ return; // don't follow: the target lives elsewhere
27
+ }
28
+ try {
29
+ // Add owner-write, preserving every other bit (read/execute stay intact so
30
+ // a directory remains traversable and readable for the recursion below).
31
+ chmodSync(path, stat.mode | 0o200);
32
+ }
33
+ catch {
34
+ // best-effort: one un-chmod-able entry must not abort the walk
35
+ }
36
+ if (stat.isDirectory()) {
37
+ let entries;
38
+ try {
39
+ entries = readdirSync(path);
40
+ }
41
+ catch {
42
+ return;
43
+ }
44
+ for (const entry of entries) {
45
+ restoreWritable(join(path, entry));
46
+ }
47
+ }
48
+ }
49
+ /**
50
+ * Remove a directory tree that may be hardened read-only by Drupal:
51
+ * {@link restoreWritable} first, then `rm -rf`. Used where the conductor owns
52
+ * the unlink; when herdr performs the removal, call {@link restoreWritable}
53
+ * alone beforehand. A missing path is a no-op.
54
+ */
55
+ export function forceRemoveDir(path) {
56
+ restoreWritable(path);
57
+ rmSync(path, { recursive: true, force: true });
58
+ }
@@ -0,0 +1,172 @@
1
+ import type { ExecutorCapabilities, ExecutorPlugin, GaiaExecutor, HookContext, HookName, SpawnedSession, SpawnRunInput } from '@gaia-ai/conductor/contract';
2
+ import type { ConductorLogger } from '@gaia-ai/core';
3
+ import { type HerdrExecutorOptions } from './config.js';
4
+ import { type HerdrExec } from './pane-layout.js';
5
+ export type { AgentLaunchHost, AgentLaunchHostProvider, HostedAgent, } from './agent-host.js';
6
+ export { herdrAgentHost } from './agents.js';
7
+ export type { HerdrExec } from './pane-layout.js';
8
+ export { envArgs, findTabIdByLabel, parsePaneInfo, parsePanesInTab, parseTabCreate, } from './panes.js';
9
+ export { HerdrWorkspace, type HerdrWorkspaceOptions, herdrWorkspace, type OpenLayout, } from './workspace.js';
10
+ /** Runs a lifecycle-hook shell command in a cwd; rejects on non-zero exit. */
11
+ export type ShellRunner = (command: string, cwd: string, env?: Record<string, string>) => Promise<void>;
12
+ /** Runs `git <args>` in a cwd; resolves with stdout, rejects on non-zero exit. */
13
+ export type GitRunner = (args: string[], cwd: string) => Promise<string>;
14
+ export declare class HerdrExecutor implements GaiaExecutor {
15
+ private readonly options;
16
+ private readonly execHerdr;
17
+ private readonly logger;
18
+ private readonly runShell;
19
+ private readonly runGit;
20
+ readonly id = "herdr";
21
+ constructor(options: HerdrExecutorOptions, execHerdr?: HerdrExec, logger?: ConductorLogger, runShell?: ShellRunner, runGit?: GitRunner);
22
+ capabilities(): ExecutorCapabilities;
23
+ /**
24
+ * Run the lifecycle hook `name` best-effort — the single catch point for all
25
+ * lifecycle hooks (see {@link GaiaExecutor.runHook}). No configured command →
26
+ * silent no-op. A failing command → log loudly (hook + worktree + ticket +
27
+ * err) and return. NEVER throws, so a hook failure can't abort dispatch or
28
+ * wedge a run.
29
+ */
30
+ runHook(name: HookName, cwd: string, ctx: HookContext, env?: Record<string, string>): Promise<void>;
31
+ /**
32
+ * List worktrees, ALWAYS anchored to the configured parent repo root
33
+ * (GAIA-211). `herdr worktree list` is machine-global, but run WITHOUT
34
+ * `--cwd` it resolves herdr's own ambient (previously selected / persisted /
35
+ * process / shell / daemon) repository context. When a preceding
36
+ * `gaia-cleanup-*` integration test selected then deleted a
37
+ * `/tmp/gaia-cleanup-*` repo, that stale context survives and the unanchored
38
+ * list exits (`cannot change to '/tmp/gaia-cleanup-…'`), aborting dispatch.
39
+ * Anchoring every list with `--cwd this.options.root` makes listing
40
+ * independent of that ambient context. This is the SINGLE choke point for
41
+ * all executor `worktree list` call paths — the sibling
42
+ * {@link HerdrWorkspace.ensure} anchors identically. The `--cwd` is only
43
+ * omitted when no root is configured (rootless test callers).
44
+ *
45
+ * On a herdr failure it throws an error naming the explicit configured root
46
+ * and the operation (AC-9) instead of surfacing herdr's raw stale-context
47
+ * message, so a future context failure is diagnosable.
48
+ */
49
+ private listWorktrees;
50
+ /**
51
+ * Find the worktree entry (path + open_workspace_id) for a branch via
52
+ * worktree list. Returns null if the branch has no worktree.
53
+ */
54
+ private findWorktreeByBranch;
55
+ /**
56
+ * Find the open_workspace_id for a branch via worktree list.
57
+ * Returns null if no workspace is open for that branch.
58
+ */
59
+ private findWorkspaceByBranch;
60
+ /**
61
+ * Find the open_workspace_id for a worktree by its stable `worktreePath`
62
+ * (falling back to `branch`; see {@link resolveWorktreeEntry}). Unlike
63
+ * {@link findWorkspaceByBranch}, this does not rely on the mutable branch —
64
+ * it is the startRun lookup the GAIA-114 fix hangs on. Returns null when the
65
+ * worktree is not listed as open.
66
+ */
67
+ private findOpenWorkspace;
68
+ /** Parent repo root for git worktree admin ops (falls back to `fallback`). */
69
+ private gitRoot;
70
+ /**
71
+ * Sweep orphaned `.git/worktrees/<name>` admin entries at the parent repo
72
+ * root — the ones whose on-disk dir has vanished (GAIA-141: they show up as
73
+ * `prunable` in `git worktree list`). Best-effort: a prune failure must never
74
+ * fail teardown.
75
+ */
76
+ private pruneWorktrees;
77
+ /**
78
+ * Remove an on-disk worktree GIT-AWARELY (GAIA-141 RC-1). The old fallback
79
+ * raw-`rm`'d the directory and left the parent repo's `.git/worktrees/<name>`
80
+ * admin entry behind → a `prunable` orphan. This:
81
+ * 1. restores owner-write (Drupal hardens web/sites/default 0555 +
82
+ * settings.php 0444 — chmod-restorable by the owning user, no sudo);
83
+ * 2. `git worktree remove --force <path>` from the PARENT repo root, which
84
+ * unlinks the directory AND drops the `.git/worktrees/<name>` entry;
85
+ * 3. guarantees the directory is gone (a raw force-remove) in case git
86
+ * refused (the path was never a registered worktree) — a no-op if git
87
+ * already removed it;
88
+ * 4. prunes the repo so any now-stale admin entry is swept.
89
+ */
90
+ private gitAwareRemove;
91
+ /**
92
+ * When herdr has lost track of a worktree and its `worktreePath` was never
93
+ * persisted, the PARENT repo's `git worktree list` still knows it (GAIA-141
94
+ * RC-2 — the ~28 never-touched leftovers). Resolve the lost worktree's on-disk
95
+ * path there, preferring an exact `worktreePath` match, then a branch match;
96
+ * never the main worktree (the repo root itself). Returns null when nothing
97
+ * matches. Best-effort: a git failure resolves to null.
98
+ */
99
+ private findGitWorktree;
100
+ /**
101
+ * GAIA-166 focus guard. herdr's `worktree remove` deletes the ticket
102
+ * worktree's on-disk directory; if that workspace is the CURRENTLY FOCUSED
103
+ * one, the interactive client is left in a now-deleted `cwd` and herdr spawns
104
+ * a stray empty workspace for the dead path. So before the remove, if the
105
+ * target workspace is focused, switch focus to the main-checkout (non-linked)
106
+ * workspace of the SAME repo. A background reap of a non-focused worktree
107
+ * (or one herdr no longer lists) focuses nothing.
108
+ *
109
+ * Returns whether it is SAFE to proceed with the remove: `false` when a
110
+ * required focus switch cannot be resolved or verified — the caller then
111
+ * aborts (returns `false` → `cleaned_up=0`) so the reaper retries later,
112
+ * rather than deleting the focused path out from under the user.
113
+ */
114
+ private ensureFocusSafeToRemove;
115
+ /**
116
+ * Tear down a ticket's entire worktree (git worktree + hosted workspace).
117
+ * `herdr worktree remove` is keyed by an open workspace id, so resolve the
118
+ * worktree first — by its stable `worktreePath`, falling back to `branch`
119
+ * (see {@link resolveWorktreeEntry}): if its workspace is already open use
120
+ * that id; otherwise open the on-disk worktree to obtain one (mirrors
121
+ * startRun's open-if-needed).
122
+ *
123
+ * `herdr worktree list` is machine-global — it enumerates every repo's
124
+ * worktrees on the host, most of which are not ours. So a no-match does NOT
125
+ * mean "error"; it means herdr no longer tracks this ticket's worktree. When
126
+ * herdr has lost track, teardown is resolved off git itself (never a foreign
127
+ * worktree), git-awarely so no `.git/worktrees/<name>` metadata is orphaned
128
+ * (GAIA-141 RC-1):
129
+ * - `worktreePath` still on disk → reclaim it git-awarely;
130
+ * - `worktreePath` known but gone from disk → already torn down; sweep any
131
+ * orphaned metadata and report verified;
132
+ * - `worktreePath` unknown → resolve the lost worktree via the parent repo's
133
+ * `git worktree list` (GAIA-141 RC-2) and reclaim it git-awarely.
134
+ *
135
+ * Returns whether the teardown was VERIFIED on THIS host: `true` when the
136
+ * worktree is gone (removed by us, an on-disk orphan reclaimed, or confirmed
137
+ * already absent), `false` ONLY when nothing was resolvable and the teardown
138
+ * could not be verified — the reaper then leaves the ticket on its work list
139
+ * for a later retry instead of falsely flagging it cleaned (GAIA-141 RC-2).
140
+ */
141
+ removeWorktree(branch: string, worktreePath?: string): Promise<boolean>;
142
+ startRun(input: SpawnRunInput): Promise<SpawnedSession>;
143
+ /** Best-effort cleanup: close only the just-created tab. Swallows errors. */
144
+ private rollback;
145
+ /**
146
+ * No-op: at finalise time the agent is idle and its transcript is already on
147
+ * disk; the {@link cleanupRun} tab-close kills the PTY, so the agent dies
148
+ * implicitly. Kept to satisfy the run lifecycle seam.
149
+ */
150
+ stopRun(_branch: string): Promise<void>;
151
+ /**
152
+ * Close ONLY the branch-workspace tab whose label carries the `#<runId>`
153
+ * token (`herdr tab close <tab_id>`), killing that PTY (GAIA-183). The run
154
+ * identity lives in the tab label, so teardown is run-scoped: a sibling run's
155
+ * tab and any non-run tab in the same workspace are left untouched, and a
156
+ * missing match is a quiet no-op. Best-effort: a failing close is swallowed.
157
+ * No `/exit`/C-c and no `(done)` rename — the matched tab simply goes away.
158
+ * The branch worktree is untouched (that is the reap/{@link removeWorktree}
159
+ * lifecycle).
160
+ */
161
+ cleanupRun(branch: string, runId: number): Promise<void>;
162
+ private commandFor;
163
+ /**
164
+ * The pane-run command with every env VALUE masked (key names kept). Used
165
+ * only for the exec log so a failed `pane run` never carries a secret DSN —
166
+ * consistent with the conductor's key-name-only env logging (GAIA-99).
167
+ */
168
+ private redactedCommandFor;
169
+ private assembleCommand;
170
+ }
171
+ export declare function herdrExecutor(options?: HerdrExecutorOptions): ExecutorPlugin;
172
+ export default herdrExecutor;