@bakers-build/addon-herdr 0.0.0-stage → 0.1.0-next.38
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 +21 -0
- package/README.md +4 -2
- package/dist/src/agent-host.d.ts +45 -0
- package/dist/src/agent-host.js +9 -0
- package/dist/src/agents.d.ts +3 -0
- package/dist/src/agents.js +132 -0
- package/dist/src/config.d.ts +77 -0
- package/dist/src/config.js +29 -0
- package/dist/src/frozen-marker.d.ts +8 -0
- package/dist/src/frozen-marker.js +26 -0
- package/dist/src/fs.d.ts +22 -0
- package/dist/src/fs.js +58 -0
- package/dist/src/index.d.ts +219 -0
- package/dist/src/index.js +809 -0
- package/dist/src/launch-script.d.ts +34 -0
- package/dist/src/launch-script.js +98 -0
- package/dist/src/pane-layout.d.ts +45 -0
- package/dist/src/pane-layout.js +78 -0
- package/dist/src/panes.d.ts +31 -0
- package/dist/src/panes.js +87 -0
- package/dist/src/preset.d.ts +6 -0
- package/dist/src/preset.js +18 -0
- package/dist/src/process-host.d.ts +6 -0
- package/dist/src/process-host.js +46 -0
- package/dist/src/root-anchor.d.ts +13 -0
- package/dist/src/root-anchor.js +12 -0
- package/dist/src/workspace-list.d.ts +19 -0
- package/dist/src/workspace-list.js +29 -0
- package/dist/src/workspace.d.ts +185 -0
- package/dist/src/workspace.js +415 -0
- package/package.json +27 -4
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { BbExecutor, ExecutorCapabilities, ExecutorPlugin, HookContext, HookName, HookResult, SpawnedSession, SpawnRunInput, WorktreeTeardown } from '@bakers-build/conductor/contract';
|
|
2
|
+
import type { ConductorLogger } from '@bakers-build/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 { herdrProcessHost } from './process-host.js';
|
|
10
|
+
export { HerdrWorkspace, type HerdrWorkspaceOptions, herdrWorkspace, type OpenLayout, } from './workspace.js';
|
|
11
|
+
export { parseWorkspaceList, type WorkspaceEntry, } from './workspace-list.js';
|
|
12
|
+
/** Runs a lifecycle-hook shell command in a cwd; rejects on non-zero exit. */
|
|
13
|
+
export type ShellRunner = (command: string, cwd: string, env?: Record<string, string>) => Promise<void>;
|
|
14
|
+
/** Runs `git <args>` in a cwd; resolves with stdout, rejects on non-zero exit. */
|
|
15
|
+
export type GitRunner = (args: string[], cwd: string) => Promise<string>;
|
|
16
|
+
export declare class HerdrExecutor implements BbExecutor {
|
|
17
|
+
private readonly options;
|
|
18
|
+
private readonly execHerdr;
|
|
19
|
+
private readonly logger;
|
|
20
|
+
private readonly runShell;
|
|
21
|
+
private readonly runGit;
|
|
22
|
+
readonly id = "herdr";
|
|
23
|
+
constructor(options: HerdrExecutorOptions, execHerdr?: HerdrExec, logger?: ConductorLogger, runShell?: ShellRunner, runGit?: GitRunner);
|
|
24
|
+
capabilities(): ExecutorCapabilities;
|
|
25
|
+
/**
|
|
26
|
+
* Run the lifecycle hook `name` best-effort — the single catch point for all
|
|
27
|
+
* lifecycle hooks (see {@link BbExecutor.runHook}). No configured command →
|
|
28
|
+
* silent no-op. A failing command → log loudly (hook + worktree + ticket +
|
|
29
|
+
* err) and return. NEVER throws, so a hook failure can't abort dispatch or
|
|
30
|
+
* wedge a run.
|
|
31
|
+
*
|
|
32
|
+
* BB-237: it also RETURNS what happened ({@link HookResult}). Not throwing
|
|
33
|
+
* kept a hook from wedging a run, but swallowing the outcome entirely left the
|
|
34
|
+
* caller unable to tell a provisioned worktree from a broken one — so an agent
|
|
35
|
+
* was dispatched into a worktree whose bootstrap had aborted, with nothing but
|
|
36
|
+
* a log line (which, on a TTY, went nowhere) to say so.
|
|
37
|
+
*/
|
|
38
|
+
runHook(name: HookName, cwd: string, ctx: HookContext, env?: Record<string, string>): Promise<HookResult>;
|
|
39
|
+
/**
|
|
40
|
+
* List worktrees, ALWAYS anchored to the configured parent repo root
|
|
41
|
+
* (BB-211). `herdr worktree list` is machine-global, but run WITHOUT
|
|
42
|
+
* `--cwd` it resolves herdr's own ambient (previously selected / persisted /
|
|
43
|
+
* process / shell / daemon) repository context. When a preceding
|
|
44
|
+
* `bakers-cleanup-*` integration test selected then deleted a
|
|
45
|
+
* `/tmp/bakers-cleanup-*` repo, that stale context survives and the unanchored
|
|
46
|
+
* list exits (`cannot change to '/tmp/bakers-cleanup-…'`), aborting dispatch.
|
|
47
|
+
* Anchoring every list with `--cwd this.options.root` makes listing
|
|
48
|
+
* independent of that ambient context. This is the SINGLE choke point for
|
|
49
|
+
* all executor `worktree list` call paths — the sibling
|
|
50
|
+
* {@link HerdrWorkspace.ensure} anchors identically. The `--cwd` is only
|
|
51
|
+
* omitted when no root is configured (rootless test callers).
|
|
52
|
+
*
|
|
53
|
+
* On a herdr failure it throws an error naming the explicit configured root
|
|
54
|
+
* and the operation (AC-9) instead of surfacing herdr's raw stale-context
|
|
55
|
+
* message, so a future context failure is diagnosable.
|
|
56
|
+
*/
|
|
57
|
+
private listWorktrees;
|
|
58
|
+
/**
|
|
59
|
+
* Find the worktree entry (path + open_workspace_id) for a branch via
|
|
60
|
+
* worktree list. Returns null if the branch has no worktree.
|
|
61
|
+
*/
|
|
62
|
+
private findWorktreeByBranch;
|
|
63
|
+
/**
|
|
64
|
+
* Find the open_workspace_id for a branch via worktree list.
|
|
65
|
+
* Returns null if no workspace is open for that branch.
|
|
66
|
+
*/
|
|
67
|
+
private findWorkspaceByBranch;
|
|
68
|
+
/**
|
|
69
|
+
* Find the open_workspace_id for a worktree by its stable `worktreePath`
|
|
70
|
+
* (falling back to `branch`; see {@link resolveWorktreeEntry}). Unlike
|
|
71
|
+
* {@link findWorkspaceByBranch}, this does not rely on the mutable branch —
|
|
72
|
+
* it is the startRun lookup the BB-114 fix hangs on. Returns null when the
|
|
73
|
+
* worktree is not listed as open.
|
|
74
|
+
*/
|
|
75
|
+
private findOpenWorkspace;
|
|
76
|
+
/** Parent repo root for git worktree admin ops (falls back to `fallback`). */
|
|
77
|
+
private gitRoot;
|
|
78
|
+
/**
|
|
79
|
+
* Sweep orphaned `.git/worktrees/<name>` admin entries at the parent repo
|
|
80
|
+
* root — the ones whose on-disk dir has vanished (BB-141: they show up as
|
|
81
|
+
* `prunable` in `git worktree list`). Best-effort: a prune failure must never
|
|
82
|
+
* fail teardown.
|
|
83
|
+
*/
|
|
84
|
+
private pruneWorktrees;
|
|
85
|
+
/**
|
|
86
|
+
* Remove an on-disk worktree GIT-AWARELY (BB-141 RC-1). The old fallback
|
|
87
|
+
* raw-`rm`'d the directory and left the parent repo's `.git/worktrees/<name>`
|
|
88
|
+
* admin entry behind → a `prunable` orphan. This:
|
|
89
|
+
* 1. restores owner-write (Drupal hardens web/sites/default 0555 +
|
|
90
|
+
* settings.php 0444 — chmod-restorable by the owning user, no sudo);
|
|
91
|
+
* 2. `git worktree remove --force <path>` from the PARENT repo root, which
|
|
92
|
+
* unlinks the directory AND drops the `.git/worktrees/<name>` entry —
|
|
93
|
+
* but ONLY when the path really is a registered worktree (BB-293):
|
|
94
|
+
* against an unregistered path git exits 128 and the shared exec helper
|
|
95
|
+
* logs `ERROR: exec failed` for a condition this code expects and
|
|
96
|
+
* handles. The guard lives here rather than at the caller because this
|
|
97
|
+
* method has a second call site (the path resolved via
|
|
98
|
+
* {@link findGitWorktree});
|
|
99
|
+
* 3. guarantees the directory is gone (a raw force-remove) in case git
|
|
100
|
+
* refused or was skipped — a no-op if git already removed it;
|
|
101
|
+
* 4. prunes the repo so any now-stale admin entry is swept.
|
|
102
|
+
*
|
|
103
|
+
* Returns whether the directory is gone afterwards. A reclaim that cannot
|
|
104
|
+
* remove it REPORTS instead of throwing (BB-293, spec D5): the raw EACCES
|
|
105
|
+
* used to propagate all the way out of {@link removeWorktree} and reach the
|
|
106
|
+
* reaper as an opaque exception.
|
|
107
|
+
*/
|
|
108
|
+
private gitAwareRemove;
|
|
109
|
+
/**
|
|
110
|
+
* When herdr has lost track of a worktree and its `worktreePath` was never
|
|
111
|
+
* persisted, the PARENT repo's `git worktree list` still knows it (BB-141
|
|
112
|
+
* RC-2 — the ~28 never-touched leftovers). Resolve the lost worktree's on-disk
|
|
113
|
+
* path there, preferring an exact `worktreePath` match, then a branch match;
|
|
114
|
+
* never the main worktree. Returns null when nothing matches. Best-effort: a
|
|
115
|
+
* git failure resolves to null.
|
|
116
|
+
*
|
|
117
|
+
* `cwd` is only where git is RUN. The main worktree is excluded by git's own
|
|
118
|
+
* porcelain ordering (it is always the first record) rather than by comparing
|
|
119
|
+
* against `cwd`, because the two coincide only when a parent root is
|
|
120
|
+
* configured. Without one the probe falls back to running git inside the
|
|
121
|
+
* worktree itself, and a `path !== cwd` filter then dropped the very entry
|
|
122
|
+
* being asked about — so `registered: true` was unreachable for a rootless
|
|
123
|
+
* executor (BB-293 review, finding 1). The configured root is excluded too,
|
|
124
|
+
* belt and braces, so a caller that anchors elsewhere still never resolves the
|
|
125
|
+
* main checkout as a ticket worktree.
|
|
126
|
+
*/
|
|
127
|
+
private findGitWorktree;
|
|
128
|
+
/**
|
|
129
|
+
* Ask — passively — whether this worktree still exists, in BOTH dimensions
|
|
130
|
+
* that can come apart (BB-293): git's registration, and the directory on
|
|
131
|
+
* disk. The reported failure was exactly a case where they disagreed — the
|
|
132
|
+
* directory was there, the registration was not — so a single-dimension
|
|
133
|
+
* check cannot decide what to do.
|
|
134
|
+
*
|
|
135
|
+
* Passive by construction: it writes nothing, calls no herdr, and NEVER
|
|
136
|
+
* throws. A git failure yields `registered: false`, degrading the verdict to
|
|
137
|
+
* the on-disk answer rather than failing the teardown.
|
|
138
|
+
*
|
|
139
|
+
* The persisted `worktreePath` is the stable key and wins when given;
|
|
140
|
+
* `registered` then describes THAT path, never a branch match pointing
|
|
141
|
+
* somewhere else (the checked-out branch is mutable — BB-114).
|
|
142
|
+
*
|
|
143
|
+
* git is run at the configured parent root when there is one, else inside the
|
|
144
|
+
* worktree itself — `git worktree list` enumerates the whole repo from any of
|
|
145
|
+
* its worktrees, so the rootless answer is the same list.
|
|
146
|
+
*/
|
|
147
|
+
private probeWorktree;
|
|
148
|
+
/**
|
|
149
|
+
* BB-166 focus guard. herdr's `worktree remove` deletes the ticket
|
|
150
|
+
* worktree's on-disk directory; if that workspace is the CURRENTLY FOCUSED
|
|
151
|
+
* one, the interactive client is left in a now-deleted `cwd` and herdr spawns
|
|
152
|
+
* a stray empty workspace for the dead path. So before the remove, if the
|
|
153
|
+
* target workspace is focused, switch focus to the main-checkout (non-linked)
|
|
154
|
+
* workspace of the SAME repo. A background reap of a non-focused worktree
|
|
155
|
+
* (or one herdr no longer lists) focuses nothing.
|
|
156
|
+
*
|
|
157
|
+
* Returns whether it is SAFE to proceed with the remove: `false` when a
|
|
158
|
+
* required focus switch cannot be resolved or verified — the caller then
|
|
159
|
+
* aborts (returns `false` → `cleaned_up=0`) so the reaper retries later,
|
|
160
|
+
* rather than deleting the focused path out from under the user.
|
|
161
|
+
*/
|
|
162
|
+
private ensureFocusSafeToRemove;
|
|
163
|
+
/**
|
|
164
|
+
* Tear down a ticket's entire worktree (git worktree + hosted workspace).
|
|
165
|
+
* `herdr worktree remove` is keyed by an open workspace id, so resolve the
|
|
166
|
+
* worktree first — by its stable `worktreePath`, falling back to `branch`
|
|
167
|
+
* (see {@link resolveWorktreeEntry}): if its workspace is already open use
|
|
168
|
+
* that id; otherwise open the on-disk worktree to obtain one (mirrors
|
|
169
|
+
* startRun's open-if-needed).
|
|
170
|
+
*
|
|
171
|
+
* `herdr worktree list` is machine-global — it enumerates every repo's
|
|
172
|
+
* worktrees on the host, most of which are not ours. So a no-match does NOT
|
|
173
|
+
* mean "error"; it means herdr no longer tracks this ticket's worktree. When
|
|
174
|
+
* herdr has lost track, teardown is resolved off git itself (never a foreign
|
|
175
|
+
* worktree), git-awarely so no `.git/worktrees/<name>` metadata is orphaned
|
|
176
|
+
* (BB-141 RC-1). Since BB-293 that resolution starts by ASKING — a passive
|
|
177
|
+
* {@link probeWorktree} reads both dimensions of existence and the four cases
|
|
178
|
+
* branch explicitly:
|
|
179
|
+
* - neither registered nor on disk → nothing to do at all;
|
|
180
|
+
* - registered but gone from disk → sweep the prunable admin entry;
|
|
181
|
+
* - on disk but not registered → reclaim the directory, WITHOUT asking git
|
|
182
|
+
* to remove a worktree it does not know (the bogus `ERROR: exec failed`);
|
|
183
|
+
* - both → reclaim it git-awarely, `git worktree remove` included.
|
|
184
|
+
* A path that resolves in NEITHER dimension is not "absent" — it is
|
|
185
|
+
* unresolved, and still reported `unverified` (BB-141 RC-2).
|
|
186
|
+
*
|
|
187
|
+
* Reports what the teardown DID (BB-293): `removed` when the worktree is
|
|
188
|
+
* gone because this host removed it (or reclaimed an on-disk orphan, or swept
|
|
189
|
+
* the prunable admin entry it left behind), `already_absent` when a RESOLVED
|
|
190
|
+
* worktree existed in neither dimension, and `unverified` ONLY when nothing
|
|
191
|
+
* was resolvable or the directory could not be reclaimed — the reaper then
|
|
192
|
+
* leaves the ticket on its work list for a later retry instead of falsely
|
|
193
|
+
* flagging it cleaned (BB-141 RC-2).
|
|
194
|
+
*/
|
|
195
|
+
removeWorktree(branch: string, worktreePath?: string): Promise<WorktreeTeardown>;
|
|
196
|
+
startRun(input: SpawnRunInput): Promise<SpawnedSession>;
|
|
197
|
+
/** Best-effort cleanup: close only the just-created tab. Swallows errors. */
|
|
198
|
+
private rollback;
|
|
199
|
+
/**
|
|
200
|
+
* No-op: at finalise time the agent is idle and its transcript is already on
|
|
201
|
+
* disk; the {@link cleanupRun} tab-close kills the PTY, so the agent dies
|
|
202
|
+
* implicitly. Kept to satisfy the run lifecycle seam.
|
|
203
|
+
*/
|
|
204
|
+
stopRun(_branch: string): Promise<void>;
|
|
205
|
+
/**
|
|
206
|
+
* Close ONLY the branch-workspace tab whose label carries the `#<runId>`
|
|
207
|
+
* token (`herdr tab close <tab_id>`), killing that PTY (BB-183). The run
|
|
208
|
+
* identity lives in the tab label, so teardown is run-scoped: a sibling run's
|
|
209
|
+
* tab and any non-run tab in the same workspace are left untouched, and a
|
|
210
|
+
* missing match is a quiet no-op. Best-effort: a failing close is swallowed.
|
|
211
|
+
* No `/exit`/C-c and no `(done)` rename — the matched tab simply goes away.
|
|
212
|
+
* The branch worktree is untouched (that is the reap/{@link removeWorktree}
|
|
213
|
+
* lifecycle).
|
|
214
|
+
*/
|
|
215
|
+
cleanupRun(branch: string, runId: number): Promise<void>;
|
|
216
|
+
private commandFor;
|
|
217
|
+
}
|
|
218
|
+
export declare function herdrExecutor(options?: HerdrExecutorOptions): ExecutorPlugin;
|
|
219
|
+
export default herdrExecutor;
|