@bakers-build/addon-herdr-core 0.0.0-stage → 0.6.1-next.7
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/index.d.ts +56 -0
- package/dist/src/index.js +198 -0
- package/dist/src/preset.d.ts +3 -0
- package/dist/src/preset.js +6 -0
- package/package.json +31 -4
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
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @bakers-build/addon-herdr-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
bakers.build herdr-core integration: executor running each agent in a herdr-core (hc) terminal.
|
|
4
|
+
|
|
5
|
+
Part of the bakers.build CLI. Install the meta package `@bakers-build/bakers` to get the `bakers` CLI with all addons. Source: https://github.com/bakers-build/bakers-build (bakers-cli/).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type LaunchWatchOptions } from '@bakers-build/addon-herdr/shared';
|
|
2
|
+
import type { BbExecutor, ExecutorCapabilities, ExecutorPlugin, SpawnedSession, SpawnRunInput } from '@bakers-build/conductor/contract';
|
|
3
|
+
import { type ConductorLogger } from '@bakers-build/core';
|
|
4
|
+
/** Runs `hc <args>`; resolves with stdout, rejects on a non-zero exit. */
|
|
5
|
+
export type HcExec = (args: string[]) => Promise<string>;
|
|
6
|
+
export interface HerdrCoreExecutorOptions {
|
|
7
|
+
/** Fallback agent command when the conductor supplies none. Default `claude`. */
|
|
8
|
+
command?: string;
|
|
9
|
+
/** As for herdr: `startTimeoutMs` (default 60 s), then `timeoutMs` (4 s). */
|
|
10
|
+
launchWatch?: Omit<LaunchWatchOptions, 'readPane'>;
|
|
11
|
+
}
|
|
12
|
+
/** The oldest `hc` whose `run --bg --no-parent --outlive-parent` this addon uses. */
|
|
13
|
+
export declare const MIN_HC_VERSION = "0.1.18";
|
|
14
|
+
/** `hc` is not on PATH. */
|
|
15
|
+
export declare class HcNotFound extends Error {
|
|
16
|
+
constructor();
|
|
17
|
+
}
|
|
18
|
+
/** The real CLI, resolved through PATH. */
|
|
19
|
+
export declare const defaultHc: HcExec;
|
|
20
|
+
/** Rejects unless `hc --version` reports at least {@link MIN_HC_VERSION}. */
|
|
21
|
+
export declare function assertHcVersion(hc: HcExec): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* The run's terminal name, `<identifier>-<runId>`: hc allows 1-32 of
|
|
24
|
+
* `a-z0-9-_`, unique among live terminals. The run id suffix is never cut.
|
|
25
|
+
*/
|
|
26
|
+
export declare function terminalName(identifier: string, runId: number): string;
|
|
27
|
+
/**
|
|
28
|
+
* Executor hosting each run's agent in a herdr-core terminal (`hc`). The
|
|
29
|
+
* terminal lives in the herdr-core server, so it survives the Herdr app quitting
|
|
30
|
+
* or crashing. It is found again by its `bakers.branch` and `bakers.run` labels.
|
|
31
|
+
* herdr-core has no workspace notion: the worktree comes from the workspace
|
|
32
|
+
* slot, and there is no `openWorkspace`.
|
|
33
|
+
*/
|
|
34
|
+
export declare class HerdrCoreExecutor implements BbExecutor {
|
|
35
|
+
private readonly options;
|
|
36
|
+
private readonly hc;
|
|
37
|
+
private readonly logger;
|
|
38
|
+
readonly id = "herdr-core";
|
|
39
|
+
constructor(options: HerdrCoreExecutorOptions, hc?: HcExec, logger?: ConductorLogger);
|
|
40
|
+
capabilities(): ExecutorCapabilities;
|
|
41
|
+
startRun(input: SpawnRunInput): Promise<SpawnedSession>;
|
|
42
|
+
/** No-op: `cleanupRun` ends the program; the transcript is already on disk. */
|
|
43
|
+
stopRun(_branch: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* End and remove the terminals of ONE run, found by its branch and run
|
|
46
|
+
* labels; a sibling run's terminal is left alone. Best-effort: a failure is
|
|
47
|
+
* logged, never thrown, and a missing match is warned about, since it may
|
|
48
|
+
* leave an agent running.
|
|
49
|
+
*/
|
|
50
|
+
cleanupRun(branch: string, runId: number): Promise<void>;
|
|
51
|
+
/** `hc kill` (when live) then `hc rm`; each step best-effort. */
|
|
52
|
+
private drop;
|
|
53
|
+
private commandFor;
|
|
54
|
+
}
|
|
55
|
+
export declare function herdrCoreExecutor(options?: HerdrCoreExecutorOptions): ExecutorPlugin;
|
|
56
|
+
export default herdrCoreExecutor;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
import { watchLaunch, writeLaunchScript, } from '@bakers-build/addon-herdr/shared';
|
|
3
|
+
import { ExecError, exec, shellQuote, } from '@bakers-build/core';
|
|
4
|
+
/** The oldest `hc` whose `run --bg --no-parent --outlive-parent` this addon uses. */
|
|
5
|
+
export const MIN_HC_VERSION = '0.1.18';
|
|
6
|
+
/** `hc` is not on PATH. */
|
|
7
|
+
export class HcNotFound extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(`hc not found on PATH: install herdr-core ${MIN_HC_VERSION} or later (https://herdr.dev/core/)`);
|
|
10
|
+
this.name = 'HcNotFound';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** The real CLI, resolved through PATH. */
|
|
14
|
+
export const defaultHc = async (args) => {
|
|
15
|
+
try {
|
|
16
|
+
return await exec('hc', args);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
const cause = (err instanceof ExecError ? err.cause : err);
|
|
20
|
+
if (cause?.code === 'ENOENT')
|
|
21
|
+
throw new HcNotFound();
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const noopLogger = {
|
|
26
|
+
debug() { },
|
|
27
|
+
info() { },
|
|
28
|
+
warn() { },
|
|
29
|
+
error() { },
|
|
30
|
+
};
|
|
31
|
+
function versionParts(version) {
|
|
32
|
+
return version.split('.').map(Number);
|
|
33
|
+
}
|
|
34
|
+
/** Rejects unless `hc --version` reports at least {@link MIN_HC_VERSION}. */
|
|
35
|
+
export async function assertHcVersion(hc) {
|
|
36
|
+
const out = await hc(['--version']);
|
|
37
|
+
const found = out.match(/(\d+)\.(\d+)\.(\d+)/)?.[0];
|
|
38
|
+
if (!found) {
|
|
39
|
+
throw new Error(`hc --version printed no version: ${out.trim()}`);
|
|
40
|
+
}
|
|
41
|
+
const have = versionParts(found);
|
|
42
|
+
const need = versionParts(MIN_HC_VERSION);
|
|
43
|
+
for (let i = 0; i < need.length; i++) {
|
|
44
|
+
if ((have[i] ?? 0) > (need[i] ?? 0))
|
|
45
|
+
return;
|
|
46
|
+
if ((have[i] ?? 0) < (need[i] ?? 0)) {
|
|
47
|
+
throw new Error(`hc ${found} is too old: upgrade herdr-core to ${MIN_HC_VERSION} or later (hc update)`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The run's terminal name, `<identifier>-<runId>`: hc allows 1-32 of
|
|
53
|
+
* `a-z0-9-_`, unique among live terminals. The run id suffix is never cut.
|
|
54
|
+
*/
|
|
55
|
+
export function terminalName(identifier, runId) {
|
|
56
|
+
const suffix = `-${runId}`;
|
|
57
|
+
const key = identifier.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
|
|
58
|
+
return `${key.slice(0, Math.max(1, 32 - suffix.length))}${suffix}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `hc ls --json` prints `{hidden_finished, terminals: [{address, name, state}]}`
|
|
62
|
+
* (hc 0.1.18); `address` (`t4`) is unique even among finished terminals.
|
|
63
|
+
*/
|
|
64
|
+
function parseTerminals(output) {
|
|
65
|
+
const list = JSON.parse(output)
|
|
66
|
+
?.terminals;
|
|
67
|
+
if (!Array.isArray(list))
|
|
68
|
+
return [];
|
|
69
|
+
return list.flatMap((t) => {
|
|
70
|
+
const r = t;
|
|
71
|
+
return typeof r?.address === 'string' && r.address
|
|
72
|
+
? [{ id: r.address, live: r.state !== 'finished' }]
|
|
73
|
+
: [];
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Executor hosting each run's agent in a herdr-core terminal (`hc`). The
|
|
78
|
+
* terminal lives in the herdr-core server, so it survives the Herdr app quitting
|
|
79
|
+
* or crashing. It is found again by its `bakers.branch` and `bakers.run` labels.
|
|
80
|
+
* herdr-core has no workspace notion: the worktree comes from the workspace
|
|
81
|
+
* slot, and there is no `openWorkspace`.
|
|
82
|
+
*/
|
|
83
|
+
export class HerdrCoreExecutor {
|
|
84
|
+
options;
|
|
85
|
+
hc;
|
|
86
|
+
logger;
|
|
87
|
+
id = 'herdr-core';
|
|
88
|
+
constructor(options, hc = defaultHc, logger = noopLogger) {
|
|
89
|
+
this.options = options;
|
|
90
|
+
this.hc = hc;
|
|
91
|
+
this.logger = logger;
|
|
92
|
+
}
|
|
93
|
+
capabilities() {
|
|
94
|
+
return { persistent: true };
|
|
95
|
+
}
|
|
96
|
+
async startRun(input) {
|
|
97
|
+
await assertHcVersion(this.hc);
|
|
98
|
+
const name = terminalName(input.ticket.identifier, input.run.id);
|
|
99
|
+
let launch;
|
|
100
|
+
let started = false;
|
|
101
|
+
try {
|
|
102
|
+
// The run's env travels inside the 0600 launch script, never in argv.
|
|
103
|
+
launch = writeLaunchScript(this.commandFor(input));
|
|
104
|
+
await this.hc([
|
|
105
|
+
'run',
|
|
106
|
+
'--bg',
|
|
107
|
+
'--no-parent',
|
|
108
|
+
'--outlive-parent',
|
|
109
|
+
'--cwd',
|
|
110
|
+
input.workspacePath,
|
|
111
|
+
'--name',
|
|
112
|
+
name,
|
|
113
|
+
'--label',
|
|
114
|
+
`bakers.branch=${input.ticket.branchName}`,
|
|
115
|
+
'--label',
|
|
116
|
+
`bakers.run=${input.run.id}`,
|
|
117
|
+
'--label',
|
|
118
|
+
`bakers.ticket=${input.ticket.identifier}`,
|
|
119
|
+
'--json',
|
|
120
|
+
'sh',
|
|
121
|
+
launch.wrapPath,
|
|
122
|
+
]);
|
|
123
|
+
started = true;
|
|
124
|
+
await watchLaunch(launch, {
|
|
125
|
+
...(this.options.launchWatch ?? {}),
|
|
126
|
+
readPane: () => this.hc(['read', name, '--lines', '40']),
|
|
127
|
+
});
|
|
128
|
+
return { sessionRef: name };
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
if (launch)
|
|
132
|
+
rmSync(launch.dir, { recursive: true, force: true });
|
|
133
|
+
if (started)
|
|
134
|
+
await this.drop(name, true);
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** No-op: `cleanupRun` ends the program; the transcript is already on disk. */
|
|
139
|
+
async stopRun(_branch) { }
|
|
140
|
+
/**
|
|
141
|
+
* End and remove the terminals of ONE run, found by its branch and run
|
|
142
|
+
* labels; a sibling run's terminal is left alone. Best-effort: a failure is
|
|
143
|
+
* logged, never thrown, and a missing match is warned about, since it may
|
|
144
|
+
* leave an agent running.
|
|
145
|
+
*/
|
|
146
|
+
async cleanupRun(branch, runId) {
|
|
147
|
+
try {
|
|
148
|
+
const terminals = parseTerminals(await this.hc([
|
|
149
|
+
'ls',
|
|
150
|
+
'--all',
|
|
151
|
+
'--label',
|
|
152
|
+
`bakers.branch=${branch}`,
|
|
153
|
+
'--label',
|
|
154
|
+
`bakers.run=${runId}`,
|
|
155
|
+
'--json',
|
|
156
|
+
]));
|
|
157
|
+
if (terminals.length === 0) {
|
|
158
|
+
this.logger.warn({ branch, runId }, 'no herdr-core terminal found for this run; its agent may still be running');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
for (const t of terminals)
|
|
162
|
+
await this.drop(t.id, t.live);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
this.logger.warn({ branch, runId, err: String(err) }, 'herdr-core cleanup failed (best-effort)');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** `hc kill` (when live) then `hc rm`; each step best-effort. */
|
|
169
|
+
async drop(terminal, live) {
|
|
170
|
+
if (live) {
|
|
171
|
+
await this.hc(['kill', terminal, '--json']).catch(() => { });
|
|
172
|
+
}
|
|
173
|
+
await this.hc(['rm', terminal, '--json']).catch(() => { });
|
|
174
|
+
}
|
|
175
|
+
commandFor(input) {
|
|
176
|
+
const env = Object.entries(input.env ?? {}).map(([key, value]) => {
|
|
177
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
178
|
+
throw new Error(`invalid environment variable name for herdr-core: ${key}`);
|
|
179
|
+
}
|
|
180
|
+
return `${key}=${shellQuote(value)}`;
|
|
181
|
+
});
|
|
182
|
+
const command = input.command ?? this.options.command ?? 'claude';
|
|
183
|
+
return [...env, command].join(' ');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
export function herdrCoreExecutor(options = {}) {
|
|
187
|
+
return {
|
|
188
|
+
kind: 'executor',
|
|
189
|
+
id: 'herdr-core',
|
|
190
|
+
requiredModules: [],
|
|
191
|
+
async createExecutor(_config, deps) {
|
|
192
|
+
return new HerdrCoreExecutor({ ...options, command: options.command ?? 'claude' }, defaultHc, deps.logger);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// The class is exported too, so the default export is what lets the config
|
|
197
|
+
// resolver pick the factory without an `export:` key.
|
|
198
|
+
export default herdrCoreExecutor;
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakers-build/addon-herdr-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.6.1-next.7",
|
|
4
|
+
"description": "bakers.build herdr-core integration: executor running each agent in a herdr-core (hc) terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/src/index.js",
|
|
9
|
+
"./preset": "./dist/src/preset.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/src"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/bakers-build/bakers-build.git",
|
|
20
|
+
"directory": "bakers-cli/addons/herdr-core"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@bakers-build/addon-herdr": "^0.6.1-next.7"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@bakers-build/conductor": "^0.6.1-next.7",
|
|
27
|
+
"@bakers-build/core": "^0.6.1-next.7"
|
|
28
|
+
},
|
|
29
|
+
"bakersBuild": {
|
|
30
|
+
"commit": "b89398b187bf51f4f9ca5bfc1150469f73a879e6",
|
|
31
|
+
"channel": "next"
|
|
32
|
+
}
|
|
33
|
+
}
|