team 0.1.1 → 0.2.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 +38 -12
- package/dist/ansi.d.ts +13 -0
- package/dist/ansi.js +75 -0
- package/dist/approve/approval.d.ts +11 -0
- package/dist/approve/approval.js +80 -8
- package/dist/approve/fingerprint.d.ts +29 -6
- package/dist/approve/fingerprint.js +57 -32
- package/dist/budgets/gate.d.ts +5 -4
- package/dist/budgets/gate.js +23 -14
- package/dist/budgets/readings.d.ts +57 -9
- package/dist/budgets/readings.js +134 -18
- package/dist/budgets/run.js +8 -2
- package/dist/budgets/table.d.ts +4 -2
- package/dist/budgets/table.js +29 -11
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +8 -5
- package/dist/commands/add.d.ts +0 -1
- package/dist/commands/add.js +18 -9
- package/dist/commands/approve.js +17 -1
- package/dist/commands/doctor.d.ts +17 -1
- package/dist/commands/doctor.js +128 -8
- package/dist/commands/down.d.ts +6 -2
- package/dist/commands/down.js +40 -14
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.js +26 -4
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +22 -3
- package/dist/commands/up.d.ts +3 -1
- package/dist/commands/up.js +12 -6
- package/dist/commands/watch.d.ts +4 -0
- package/dist/commands/watch.js +99 -19
- package/dist/conformance/adapter.d.ts +5 -0
- package/dist/conformance/adapter.js +44 -0
- package/dist/file/check.d.ts +20 -0
- package/dist/file/check.js +112 -0
- package/dist/file/lines.js +13 -2
- package/dist/file/sections/budgets.d.ts +15 -0
- package/dist/file/sections/budgets.js +180 -0
- package/dist/file/sections/coordinator.d.ts +2 -0
- package/dist/file/sections/coordinator.js +11 -0
- package/dist/file/sections/format.d.ts +2 -0
- package/dist/file/sections/format.js +13 -0
- package/dist/file/sections/identity.d.ts +2 -0
- package/dist/file/sections/identity.js +103 -0
- package/dist/file/sections/index.d.ts +9 -0
- package/dist/file/sections/index.js +43 -0
- package/dist/file/sections/lead.d.ts +4 -0
- package/dist/file/sections/lead.js +18 -0
- package/dist/file/sections/limits.d.ts +2 -0
- package/dist/file/sections/limits.js +43 -0
- package/dist/file/sections/machine.d.ts +2 -0
- package/dist/file/sections/machine.js +38 -0
- package/dist/file/sections/operator.d.ts +2 -0
- package/dist/file/sections/operator.js +11 -0
- package/dist/file/sections/project.d.ts +2 -0
- package/dist/file/sections/project.js +13 -0
- package/dist/file/sections/rules.d.ts +2 -0
- package/dist/file/sections/rules.js +9 -0
- package/dist/file/sections/seats.d.ts +7 -0
- package/dist/file/sections/seats.js +161 -0
- package/dist/file/sections/section.d.ts +32 -0
- package/dist/file/sections/section.js +7 -0
- package/dist/file/sections/session.d.ts +2 -0
- package/dist/file/sections/session.js +16 -0
- package/dist/file/sections/tools.d.ts +2 -0
- package/dist/file/sections/tools.js +43 -0
- package/dist/file/sections/trust.d.ts +2 -0
- package/dist/file/sections/trust.js +20 -0
- package/dist/file/sections/units.d.ts +16 -0
- package/dist/file/sections/units.js +8 -0
- package/dist/file/sections/visibility.d.ts +2 -0
- package/dist/file/sections/visibility.js +12 -0
- package/dist/file/sections/watch-checks.d.ts +9 -0
- package/dist/file/sections/watch-checks.js +14 -0
- package/dist/file/sections/watch.d.ts +14 -0
- package/dist/file/sections/watch.js +111 -0
- package/dist/file/sections/workspace.d.ts +4 -0
- package/dist/file/sections/workspace.js +77 -0
- package/dist/file/types.d.ts +6 -0
- package/dist/file/validate.d.ts +4 -12
- package/dist/file/validate.js +45 -668
- package/dist/herdr.d.ts +5 -0
- package/dist/herdr.js +51 -12
- package/dist/launch/agent.d.ts +1 -0
- package/dist/launch/agent.js +13 -0
- package/dist/launch/deliver.d.ts +7 -1
- package/dist/launch/deliver.js +159 -7
- package/dist/launch/execute.d.ts +4 -2
- package/dist/launch/execute.js +36 -13
- package/dist/launch/plan.js +16 -2
- package/dist/launch/rules.d.ts +4 -2
- package/dist/launch/rules.js +17 -3
- package/dist/profiles/antigravity.yaml +25 -2
- package/dist/profiles/claude-code.yaml +51 -6
- package/dist/profiles/codex.yaml +14 -1
- package/dist/profiles/cursor.yaml +46 -3
- package/dist/profiles/overrides.d.ts +67 -0
- package/dist/profiles/overrides.js +190 -0
- package/dist/profiles/profile.d.ts +3 -0
- package/dist/profiles/profile.js +5 -1
- package/dist/state.d.ts +9 -4
- package/dist/state.js +37 -0
- package/dist/status/compare.js +7 -4
- package/dist/status/statusline.js +3 -1
- package/dist/store/store.d.ts +5 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/dist/watch/check.d.ts +1 -0
- package/dist/watch/checks/budget.js +50 -51
- package/dist/watch/dialect.d.ts +6 -1
- package/dist/watch/dialect.js +68 -12
- package/dist/watch/pass.d.ts +10 -2
- package/dist/watch/pass.js +70 -29
- package/dist/watch/screen-core.d.ts +41 -4
- package/dist/watch/screen-core.js +510 -89
- package/dist/watch/screen-data.d.ts +34 -0
- package/dist/watch/screen-file.d.ts +8 -2
- package/dist/watch/screen-file.js +344 -39
- package/dist/watch/screen-profile.d.ts +19 -0
- package/dist/watch/screen-profile.js +1 -0
- package/dist/watch/screen.d.ts +25 -6
- package/dist/watch/screen.js +48 -9
- package/examples/team.yaml +8 -0
- package/package.json +9 -3
- package/schema/team.schema.json +498 -0
package/dist/herdr.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type HerdrAgent = {
|
|
|
8
8
|
};
|
|
9
9
|
export declare function agentList(session?: string): HerdrAgent[] | null;
|
|
10
10
|
export declare function paneRootPid(pane: string, session?: string): number | null;
|
|
11
|
+
export declare function foregroundArgv0(result: unknown): string[] | null;
|
|
11
12
|
export declare function paneForeground(pane: string, session?: string): string[] | null;
|
|
12
13
|
export type HerdrWorkspace = {
|
|
13
14
|
id: string;
|
|
@@ -26,6 +27,10 @@ export declare function paneRun(pane: string, command: string, session?: string)
|
|
|
26
27
|
export declare function agentRename(pane: string, name: string, session?: string): boolean;
|
|
27
28
|
export declare function workspaceClose(workspace: string, session?: string): boolean;
|
|
28
29
|
export declare function sessionStop(name: string): boolean;
|
|
30
|
+
export declare function sessionDelete(name: string): boolean;
|
|
31
|
+
export type PaneExec = (args: string[]) => string;
|
|
32
|
+
/** Swaps the process call paneRead makes; null restores the real one. */
|
|
33
|
+
export declare function setPaneExec(exec: PaneExec | null): void;
|
|
29
34
|
export declare function paneRead(pane: string, lines: number, session?: string): string | null;
|
|
30
35
|
export declare function herdrCommand(session: string | undefined, ...args: string[]): string;
|
|
31
36
|
export declare function typeText(pane: string, text: string, session?: string): boolean;
|
package/dist/herdr.js
CHANGED
|
@@ -33,15 +33,19 @@ export function paneRootPid(pane, session) {
|
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
// argv0 of each foreground process, or null when the pane can't be
|
|
37
|
-
//
|
|
36
|
+
// argv0 of each foreground process, in herdr's order, or null when the pane can't be
|
|
37
|
+
// read. `name` is not the process: a Claude entry's name is its version. After a CLI
|
|
38
|
+
// exits, herdr keeps the pane and the foreground process is the shell.
|
|
39
|
+
export function foregroundArgv0(result) {
|
|
40
|
+
const list = result
|
|
41
|
+
?.process_info?.foreground_processes;
|
|
42
|
+
if (!Array.isArray(list))
|
|
43
|
+
return null;
|
|
44
|
+
return list.map((proc) => (typeof proc?.argv0 === 'string' ? proc.argv0 : ''));
|
|
45
|
+
}
|
|
38
46
|
export function paneForeground(pane, session) {
|
|
39
47
|
try {
|
|
40
|
-
|
|
41
|
-
const list = result.process_info?.foreground_processes;
|
|
42
|
-
if (!Array.isArray(list))
|
|
43
|
-
return null;
|
|
44
|
-
return list.map((proc) => (typeof proc.argv0 === 'string' ? proc.argv0 : ''));
|
|
48
|
+
return foregroundArgv0(run(['pane', 'process-info', '--pane', pane], session));
|
|
45
49
|
}
|
|
46
50
|
catch {
|
|
47
51
|
return null;
|
|
@@ -167,7 +171,7 @@ export function workspaceClose(workspace, session) {
|
|
|
167
171
|
const body = parsed(out);
|
|
168
172
|
return body !== null && body.type !== 'error';
|
|
169
173
|
}
|
|
170
|
-
// Stops a session
|
|
174
|
+
// Stops a session, which herdr keeps listed as stopped.
|
|
171
175
|
export function sessionStop(name) {
|
|
172
176
|
const out = capture(['session', 'stop', name]);
|
|
173
177
|
if (out === null)
|
|
@@ -175,15 +179,50 @@ export function sessionStop(name) {
|
|
|
175
179
|
const body = parsed(out);
|
|
176
180
|
return body === null || body.type !== 'error';
|
|
177
181
|
}
|
|
182
|
+
// Clears a stopped session — the one case `team` deletes: `down`, on the session it has itself
|
|
183
|
+
// just stopped in the same run, so the next `up` starts from the beginning. `up` never deletes:
|
|
184
|
+
// its refusal names this command and leaves it to the owner.
|
|
185
|
+
export function sessionDelete(name) {
|
|
186
|
+
const out = capture(['session', 'delete', name]);
|
|
187
|
+
if (out === null)
|
|
188
|
+
return false;
|
|
189
|
+
const body = parsed(out);
|
|
190
|
+
return body !== null && body.type !== 'error';
|
|
191
|
+
}
|
|
192
|
+
const shellPaneExec = (args) => execFileSync('herdr', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 10_000 });
|
|
193
|
+
let paneExec = shellPaneExec;
|
|
194
|
+
/** Swaps the process call paneRead makes; null restores the real one. */
|
|
195
|
+
export function setPaneExec(exec) {
|
|
196
|
+
paneExec = exec ?? shellPaneExec;
|
|
197
|
+
}
|
|
178
198
|
// The visible lines of a pane, or null. `session` undefined reaches the caller's own server.
|
|
199
|
+
// The lines keep their ANSI styling, CRLF folded to LF: a greyed suggestion and typed text
|
|
200
|
+
// read the same as plain text, and only their styling tells them apart, so the readers that
|
|
201
|
+
// want plain text strip it (`stripSgr`). An herdr without the `--format` option refuses the
|
|
202
|
+
// styled call and is read by the exact call main makes today — no flag at all — where
|
|
203
|
+
// unstyled text can never read as dim and the placeholder list alone decides. A herdr that
|
|
204
|
+
// hangs is not asked twice: one timeout reads null.
|
|
179
205
|
export function paneRead(pane, lines, session) {
|
|
206
|
+
const full = [...(session ? ['--session', session] : []), 'pane', 'read', pane, '--source', 'visible', '--lines', String(lines)];
|
|
207
|
+
let styled;
|
|
180
208
|
try {
|
|
181
|
-
|
|
182
|
-
return execFileSync('herdr', full, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 10_000 });
|
|
209
|
+
styled = paneExec([...full, '--format', 'ansi']);
|
|
183
210
|
}
|
|
184
|
-
catch {
|
|
185
|
-
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (!(error instanceof Error))
|
|
213
|
+
return null;
|
|
214
|
+
// execFileSync reports its own timeout as an Error with code ETIMEDOUT — signal SIGTERM,
|
|
215
|
+
// status null, and no killed field. Observed under Bun, from a run, not from memory.
|
|
216
|
+
if (error.code === 'ETIMEDOUT')
|
|
217
|
+
return null;
|
|
218
|
+
try {
|
|
219
|
+
return paneExec(full);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
186
224
|
}
|
|
225
|
+
return styled.replace(/\r\n/g, '\n').replace(/\r$/, '');
|
|
187
226
|
}
|
|
188
227
|
// The words to type for a herdr command, for a repair line.
|
|
189
228
|
export function herdrCommand(session, ...args) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function reportedLiveAgent(foreground: readonly string[] | null, processNames: readonly string[]): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Herdr reports a live agent when one foreground argv0 is that CLI. An unreadable
|
|
2
|
+
// pane, or the shell left after the CLI has exited, is not one: those are the panes
|
|
3
|
+
// a nudge, a first message, or an exit must not be typed into.
|
|
4
|
+
//
|
|
5
|
+
// Null is not a live agent, so nothing is typed, and the watch's quota read asks this
|
|
6
|
+
// same question: a figure is only read where the CLI was seen. `paneStillRunning`
|
|
7
|
+
// reads that same null as still there — an unreadable list is not a departure — and
|
|
8
|
+
// that reading is only for the departure wait in `down` and `remove`.
|
|
9
|
+
export function reportedLiveAgent(foreground, processNames) {
|
|
10
|
+
if (!foreground || processNames.length === 0)
|
|
11
|
+
return false;
|
|
12
|
+
return foreground.some((name) => processNames.includes(name));
|
|
13
|
+
}
|
package/dist/launch/deliver.d.ts
CHANGED
|
@@ -3,8 +3,14 @@ export interface Delivery {
|
|
|
3
3
|
status(): string | null;
|
|
4
4
|
type(text: string): boolean;
|
|
5
5
|
enter(): boolean;
|
|
6
|
+
/** Foreground argv0 names, or null when the pane can't be read. */
|
|
7
|
+
foreground(): string[] | null;
|
|
6
8
|
now(): number;
|
|
7
9
|
sleep(ms: number): Promise<void>;
|
|
8
10
|
}
|
|
11
|
+
/** Whether the pane's box holds exactly `text` right now: an unsent composer, no fold marker, and
|
|
12
|
+
* rows that read back as the text. The one check the watch's nudge and the exit typing share
|
|
13
|
+
* with delivery — false for anything not observed, never `true` on trust. */
|
|
14
|
+
export declare function boxHoldsText(cli: string, text: string, screen: string | undefined): boolean;
|
|
9
15
|
/** A first message is accepted only after working is observed with the composer empty again. */
|
|
10
|
-
export declare function deliverRules(cli: string, text: string, seconds: number, io: Delivery): Promise<boolean>;
|
|
16
|
+
export declare function deliverRules(cli: string, text: string, seconds: number, io: Delivery): Promise<boolean | 'no-agent'>;
|
package/dist/launch/deliver.js
CHANGED
|
@@ -1,26 +1,178 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { reportedLiveAgent } from "./agent.js";
|
|
2
|
+
import { profileFor } from "../profiles/index.js";
|
|
3
|
+
import { classifyComposer, readBox, readFold, readFoldMark, readScreen } from "../watch/screen.js";
|
|
4
|
+
/** The rows a composer shows for `text` at `width` columns: a hard wrap, a line longer than the
|
|
5
|
+
* width split into width-sized chunks, no word wrapping. The fold's tail and count are checked
|
|
6
|
+
* against this model; the fixtures README names the same one. */
|
|
7
|
+
function rowsOf(text, width) {
|
|
8
|
+
const rows = [];
|
|
9
|
+
for (const line of text.split('\n')) {
|
|
10
|
+
if (line.length === 0) {
|
|
11
|
+
rows.push('');
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
for (let at = 0; at < line.length; at += width)
|
|
15
|
+
rows.push(line.slice(at, at + width));
|
|
16
|
+
}
|
|
17
|
+
return rows;
|
|
18
|
+
}
|
|
19
|
+
/** Whether the folded box is exactly `text` at the width the box reports: its visible tail rows
|
|
20
|
+
* are the text's last rows, and its hidden-row count closes the gap. Anything else is not the
|
|
21
|
+
* typed text and gets no Enter. */
|
|
22
|
+
function holdsText(text, fold) {
|
|
23
|
+
if (fold.width <= 0 || fold.rows.length === 0)
|
|
24
|
+
return false;
|
|
25
|
+
const all = rowsOf(text, fold.width);
|
|
26
|
+
if (fold.rows.length >= all.length)
|
|
27
|
+
return false;
|
|
28
|
+
if (fold.count !== all.length - fold.rows.length)
|
|
29
|
+
return false;
|
|
30
|
+
const tail = all.slice(-fold.rows.length);
|
|
31
|
+
return fold.rows.every((row, i) => row.trimEnd() === (tail[i] ?? '').trimEnd());
|
|
32
|
+
}
|
|
33
|
+
/** The whitespace a row break may stand for: the pane replaces it by the break, or adds the
|
|
34
|
+
* continuation row's indentation where the text has none. */
|
|
35
|
+
const BREAK = /[ \t\n\r]/;
|
|
36
|
+
/** Where the text's next row starts after `pos`, or null when `row` is not the text's row there.
|
|
37
|
+
* A row that is the text's at that very position is always read as such — that normalises
|
|
38
|
+
* nothing. A `word` boundary additionally skips the whitespace run the pane broke at before the
|
|
39
|
+
* row; a `hard` one reads the text's next characters as the row carries them; a composer with no
|
|
40
|
+
* rule (`null`) reads either way, since no capture showed how that one wraps. Every character
|
|
41
|
+
* inside a row must match the text as it is: only the row break may stand for whitespace. */
|
|
42
|
+
function continues(text, pos, row, kind) {
|
|
43
|
+
if (text.startsWith(row, pos))
|
|
44
|
+
return pos + row.length;
|
|
45
|
+
if (kind === 'hard')
|
|
46
|
+
return null;
|
|
47
|
+
let at = pos;
|
|
48
|
+
while (at < text.length && BREAK.test(text[at] ?? ''))
|
|
49
|
+
at++;
|
|
50
|
+
return at > pos && text.startsWith(row, at) ? at + row.length : null;
|
|
51
|
+
}
|
|
52
|
+
/** Where a blank row sits in the typed text: the pane draws an empty row only for a blank line
|
|
53
|
+
* the text itself has. The line's own content — nothing a box row shows, so only spaces and
|
|
54
|
+
* tabs, which a pane need not paint — is consumed; an interior blank line ends at the next
|
|
55
|
+
* newline, a blank last line ends with the text. A blank row anywhere else is not the text's
|
|
56
|
+
* own and is refused, and a blank line the box does not show a row for is refused after the
|
|
57
|
+
* last row (the trailing-newline check in `holdsBox`). */
|
|
58
|
+
function blankLine(text, pos) {
|
|
59
|
+
if (text[pos] !== '\n')
|
|
60
|
+
return null;
|
|
61
|
+
let at = pos + 1;
|
|
62
|
+
while (at < text.length && (text[at] === ' ' || text[at] === '\t'))
|
|
63
|
+
at++;
|
|
64
|
+
return at === text.length || text[at] === '\n' ? at : null;
|
|
65
|
+
}
|
|
66
|
+
/** Whether the box shows exactly `text`: the first line after the prompt and every continuation
|
|
67
|
+
* row at the box's own column — no more rows, no fewer, none changed. A continuation row that
|
|
68
|
+
* does not start at that column is not the text's own row and the box is not trusted. Only the
|
|
69
|
+
* whitespace a row break itself may stand for is normalised — the run a word break was made at,
|
|
70
|
+
* or a blank line the text itself has — and inside a row every character must match, runs of
|
|
71
|
+
* spaces included. A box that is not the text waits and is refused at the deadline, never
|
|
72
|
+
* entered on trust. */
|
|
73
|
+
function holdsBox(text, box) {
|
|
74
|
+
const pad = ' '.repeat(box.indent);
|
|
75
|
+
const rows = [];
|
|
76
|
+
for (const row of box.rows) {
|
|
77
|
+
if (row === '') {
|
|
78
|
+
rows.push('');
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!row.startsWith(pad))
|
|
82
|
+
return false;
|
|
83
|
+
rows.push(row.slice(pad.length));
|
|
84
|
+
}
|
|
85
|
+
const kind = box.wrap?.kind ?? null;
|
|
86
|
+
const visual = [box.first, ...rows];
|
|
87
|
+
let at = 0;
|
|
88
|
+
for (let i = 0; i < visual.length; i++) {
|
|
89
|
+
const row = visual[i] ?? '';
|
|
90
|
+
if (i === 0) {
|
|
91
|
+
if (!text.startsWith(row))
|
|
92
|
+
return false;
|
|
93
|
+
at = row.length;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (row === '') {
|
|
97
|
+
const next = blankLine(text, at);
|
|
98
|
+
if (next === null)
|
|
99
|
+
return false;
|
|
100
|
+
at = next;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const next = continues(text, at, row, kind);
|
|
104
|
+
if (next === null)
|
|
105
|
+
return false;
|
|
106
|
+
at = next;
|
|
107
|
+
}
|
|
108
|
+
// What is left after the box's last row. Trailing spaces and tabs are not observable in a
|
|
109
|
+
// box — a pane may not paint the end of a row — so they are not required to match. A newline
|
|
110
|
+
// is observable: it is the box's next row, and every row has been read above; one left over
|
|
111
|
+
// is a row the box does not show, and the box is not the typed text.
|
|
112
|
+
return /^[ \t]*$/.test(text.slice(at));
|
|
113
|
+
}
|
|
114
|
+
/** What the box holds right now: `ready` to submit, still rendering (`wait`), or a state that is
|
|
115
|
+
* never the typed text (`no`). A folded box — and one showing a fold marker whatever its count —
|
|
116
|
+
* is ready only when its tail and hidden-row count are the ones the text renders to at the width
|
|
117
|
+
* the box reports. An ordinary box is ready only when its rows read back as exactly the text
|
|
118
|
+
* typed; anything else waits, and a wait that outlives the deadline is a refusal, not an Enter. */
|
|
119
|
+
function boxState(cli, text, screen) {
|
|
120
|
+
const kind = readScreen(cli, screen).kind;
|
|
121
|
+
const fold = readFold(cli, screen);
|
|
122
|
+
if (fold || readFoldMark(cli, screen)) {
|
|
123
|
+
if (kind !== 'unsent' && kind !== 'unknown')
|
|
124
|
+
return 'no';
|
|
125
|
+
return fold !== null && holdsText(text, fold) ? 'ready' : 'wait';
|
|
126
|
+
}
|
|
127
|
+
if (kind === 'unsent') {
|
|
128
|
+
const box = readBox(cli, screen);
|
|
129
|
+
return box !== null && holdsBox(text, box) ? 'ready' : 'wait';
|
|
130
|
+
}
|
|
131
|
+
return kind === 'idle' ? 'wait' : 'no';
|
|
132
|
+
}
|
|
133
|
+
/** Whether the pane's box holds exactly `text` right now: an unsent composer, no fold marker, and
|
|
134
|
+
* rows that read back as the text. The one check the watch's nudge and the exit typing share
|
|
135
|
+
* with delivery — false for anything not observed, never `true` on trust. */
|
|
136
|
+
export function boxHoldsText(cli, text, screen) {
|
|
137
|
+
if (readFoldMark(cli, screen))
|
|
138
|
+
return false;
|
|
139
|
+
if (readScreen(cli, screen).kind !== 'unsent')
|
|
140
|
+
return false;
|
|
141
|
+
const box = readBox(cli, screen);
|
|
142
|
+
return box !== null && holdsBox(text, box);
|
|
143
|
+
}
|
|
2
144
|
/** A first message is accepted only after working is observed with the composer empty again. */
|
|
3
145
|
export async function deliverRules(cli, text, seconds, io) {
|
|
146
|
+
const names = profileFor(cli)?.processNames ?? [];
|
|
147
|
+
const live = () => reportedLiveAgent(io.foreground(), names);
|
|
4
148
|
const free = () => ['idle', 'done'].includes(io.status() ?? '');
|
|
149
|
+
if (!live())
|
|
150
|
+
return 'no-agent';
|
|
5
151
|
if (!free() || readScreen(cli, io.screen()).kind !== 'idle' || !io.type(text))
|
|
6
152
|
return false;
|
|
7
153
|
const deadline = io.now() + seconds * 1000;
|
|
8
|
-
// Terminal rendering can lag send-text. Never
|
|
154
|
+
// Terminal rendering can lag send-text. Never press Enter until the box is verified to hold the
|
|
155
|
+
// text: an ordinary box reads `unsent`, and a box that folded the paste counts only when the
|
|
156
|
+
// tail and hidden-row count it shows are the ones the typed text renders to.
|
|
9
157
|
for (;;) {
|
|
10
|
-
const kind = readScreen(cli, io.screen()).kind;
|
|
11
158
|
if (!free())
|
|
12
159
|
return false;
|
|
13
|
-
|
|
160
|
+
const state = boxState(cli, text, io.screen());
|
|
161
|
+
if (state === 'ready')
|
|
14
162
|
break;
|
|
15
|
-
if (
|
|
163
|
+
if (state === 'no' || io.now() >= deadline)
|
|
16
164
|
return false;
|
|
17
165
|
const before = io.now();
|
|
18
166
|
await io.sleep(100);
|
|
19
167
|
if (io.now() <= before)
|
|
20
168
|
return false;
|
|
21
169
|
}
|
|
22
|
-
// Re-read immediately before Enter
|
|
23
|
-
|
|
170
|
+
// Re-read immediately before Enter. The agent is asked again: it may have exited since the
|
|
171
|
+
// paste, and a dialog that appeared gets no key; a folded box that no longer matches the text
|
|
172
|
+
// gets none either.
|
|
173
|
+
if (!live())
|
|
174
|
+
return 'no-agent';
|
|
175
|
+
if (!free() || boxState(cli, text, io.screen()) !== 'ready' || !io.enter())
|
|
24
176
|
return false;
|
|
25
177
|
for (;;) {
|
|
26
178
|
const status = io.status();
|
package/dist/launch/execute.d.ts
CHANGED
|
@@ -11,11 +11,13 @@ export type Host = {
|
|
|
11
11
|
workspace: string;
|
|
12
12
|
} | null;
|
|
13
13
|
paneRun(session: string, pane: string, command: string): boolean;
|
|
14
|
-
typeLine(session: string, pane: string, text: string): boolean;
|
|
15
|
-
deliverRules?(session: string, pane: string, cli: string, text: string, seconds: number): Promise<boolean>;
|
|
14
|
+
typeLine(session: string, pane: string, text: string): boolean | 'no-agent';
|
|
15
|
+
deliverRules?(session: string, pane: string, cli: string, text: string, seconds: number): Promise<boolean | 'no-agent'>;
|
|
16
16
|
renameAgent(session: string, pane: string, name: string): boolean;
|
|
17
17
|
closeWorkspace(session: string, workspace: string): boolean;
|
|
18
18
|
stopSession(session: string): boolean;
|
|
19
|
+
/** Clears the session this run has just stopped. Present on `down` only: `up` never deletes. */
|
|
20
|
+
deleteSession?(session: string): boolean;
|
|
19
21
|
kill(pid: number): boolean;
|
|
20
22
|
/** Pane ids herdr lists an agent in, or null when the list can't be read. */
|
|
21
23
|
agentPanes(session: string): string[] | null;
|
package/dist/launch/execute.js
CHANGED
|
@@ -29,14 +29,15 @@ export async function executePlan(steps, session, host) {
|
|
|
29
29
|
host.say(`${seat}: ${what}\n`);
|
|
30
30
|
host.log(seat, what);
|
|
31
31
|
};
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
// The display label may be shared. The seat's name is the only key; the watch has no seat.
|
|
33
|
+
const place = (key, pane, workspace) => {
|
|
34
|
+
const have = places.get(key);
|
|
34
35
|
if (have)
|
|
35
36
|
return have;
|
|
36
37
|
if (!pane)
|
|
37
38
|
return null;
|
|
38
39
|
const made = { pane, workspace };
|
|
39
|
-
places.set(
|
|
40
|
+
places.set(key, made);
|
|
40
41
|
return made;
|
|
41
42
|
};
|
|
42
43
|
for (const step of steps) {
|
|
@@ -116,7 +117,7 @@ export async function executePlan(steps, session, host) {
|
|
|
116
117
|
}
|
|
117
118
|
break;
|
|
118
119
|
}
|
|
119
|
-
places.set(op.label, made);
|
|
120
|
+
places.set(op.seat ?? op.label, made);
|
|
120
121
|
if (op.seat) {
|
|
121
122
|
host.record(op.seat, { stage: 'launched', pane: made.pane, workspace: made.workspace });
|
|
122
123
|
host.running(op.seat);
|
|
@@ -132,7 +133,7 @@ export async function executePlan(steps, session, host) {
|
|
|
132
133
|
finish(op.seat, why);
|
|
133
134
|
break;
|
|
134
135
|
}
|
|
135
|
-
const here = place(op.
|
|
136
|
+
const here = place(op.seat, op.pane);
|
|
136
137
|
if (!here || !host.paneRun(session, here.pane, op.command)) {
|
|
137
138
|
dropped.add(op.seat);
|
|
138
139
|
finish(op.seat, 'its launch command did not run; left at launched');
|
|
@@ -144,7 +145,7 @@ export async function executePlan(steps, session, host) {
|
|
|
144
145
|
case 'idle': {
|
|
145
146
|
if (op.notice)
|
|
146
147
|
host.say(`${op.seat}: ${op.notice}\n`);
|
|
147
|
-
const here = place(op.
|
|
148
|
+
const here = place(op.seat, op.pane, op.workspace);
|
|
148
149
|
if (!here) {
|
|
149
150
|
dropped.add(op.seat);
|
|
150
151
|
finish(op.seat, 'has no pane to read; left at launched');
|
|
@@ -167,7 +168,7 @@ export async function executePlan(steps, session, host) {
|
|
|
167
168
|
}
|
|
168
169
|
if (outcome === 'idle')
|
|
169
170
|
break;
|
|
170
|
-
const workspace = here.workspace ?? places.get(op.
|
|
171
|
+
const workspace = here.workspace ?? places.get(op.seat)?.workspace;
|
|
171
172
|
if (outcome === 'permission' || outcome === 'trust' || outcome === 'question') {
|
|
172
173
|
// A trust question is closed with no key and no text. The same for a permission or a question.
|
|
173
174
|
if (workspace)
|
|
@@ -185,7 +186,7 @@ export async function executePlan(steps, session, host) {
|
|
|
185
186
|
break;
|
|
186
187
|
}
|
|
187
188
|
case 'rename': {
|
|
188
|
-
const here = place(op.
|
|
189
|
+
const here = place(op.seat, op.pane);
|
|
189
190
|
if (!here) {
|
|
190
191
|
dropped.add(op.seat);
|
|
191
192
|
finish(op.seat, 'has no pane to name; left at launched');
|
|
@@ -207,8 +208,14 @@ export async function executePlan(steps, session, host) {
|
|
|
207
208
|
case 'deliver': {
|
|
208
209
|
if (op.notice)
|
|
209
210
|
host.say(`${op.seat}: ${op.notice}\n`);
|
|
210
|
-
const here = place(op.
|
|
211
|
-
|
|
211
|
+
const here = place(op.seat, op.pane);
|
|
212
|
+
const delivered = here ? await host.deliverRules?.(session, here.pane, op.cli, op.rules, op.seconds) : false;
|
|
213
|
+
if (delivered === 'no-agent') {
|
|
214
|
+
dropped.add(op.seat);
|
|
215
|
+
finish(op.seat, 'no live agent in its pane; its rules were not delivered');
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
if (!here || !delivered) {
|
|
212
219
|
dropped.add(op.seat);
|
|
213
220
|
finish(op.seat, 'its rules were not delivered; left at named');
|
|
214
221
|
break;
|
|
@@ -230,7 +237,14 @@ export async function executePlan(steps, session, host) {
|
|
|
230
237
|
break;
|
|
231
238
|
}
|
|
232
239
|
case 'type': {
|
|
233
|
-
|
|
240
|
+
const typed = host.typeLine(session, op.pane, op.text);
|
|
241
|
+
if (typed === 'no-agent') {
|
|
242
|
+
held = true;
|
|
243
|
+
dropped.add(op.seat);
|
|
244
|
+
finish(op.seat, 'no live agent in its pane; its exit was not typed');
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
if (!typed) {
|
|
234
248
|
held = true;
|
|
235
249
|
dropped.add(op.seat);
|
|
236
250
|
finish(op.seat, 'its exit was not typed; left as it is');
|
|
@@ -270,10 +284,19 @@ export async function executePlan(steps, session, host) {
|
|
|
270
284
|
host.say(`session ${op.session}: not stopped, something was left in it\n`);
|
|
271
285
|
break;
|
|
272
286
|
}
|
|
273
|
-
if (!host.stopSession(op.session))
|
|
287
|
+
if (!host.stopSession(op.session)) {
|
|
274
288
|
host.say(`session ${op.session}: it did not stop\n`);
|
|
275
|
-
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
// Without a `deleteSession` the stop is the whole step. `down` goes one further and clears
|
|
292
|
+
// the session it has itself just stopped; a clear that does not happen names the command.
|
|
293
|
+
if (host.deleteSession === undefined)
|
|
276
294
|
host.say(`session ${op.session}: stopped\n`);
|
|
295
|
+
else if (host.deleteSession(op.session))
|
|
296
|
+
host.say(`session ${op.session}: stopped and cleared\n`);
|
|
297
|
+
else {
|
|
298
|
+
host.say(`session ${op.session}: stopped; it did not clear, run \`herdr session delete ${op.session}\`\n`);
|
|
299
|
+
}
|
|
277
300
|
break;
|
|
278
301
|
}
|
|
279
302
|
default:
|
package/dist/launch/plan.js
CHANGED
|
@@ -46,7 +46,7 @@ export function upPlan(input) {
|
|
|
46
46
|
steps.push({ kind: 'skip', text: `${seat.name}: already ready; left as it is` });
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
49
|
-
const pane = seat.pane ?? paneOf(seat.
|
|
49
|
+
const pane = seat.pane ?? paneOf(seat.name);
|
|
50
50
|
const cwd = join(input.root, seat.cwd);
|
|
51
51
|
const fresh = seat.stage === undefined || !seat.pane;
|
|
52
52
|
// The same condition as the launch step below. A seat already running keeps
|
|
@@ -202,6 +202,15 @@ export function downPlan(input) {
|
|
|
202
202
|
});
|
|
203
203
|
continue;
|
|
204
204
|
}
|
|
205
|
+
if (seat.cli === 'unknown') {
|
|
206
|
+
steps.push({
|
|
207
|
+
kind: 'skip',
|
|
208
|
+
text: `${seat.name}: the state doesn't say which CLI it runs, so it can't be asked to exit; ` +
|
|
209
|
+
'left running (`team down --abandon` closes it without typing)',
|
|
210
|
+
});
|
|
211
|
+
left++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
205
214
|
steps.push({
|
|
206
215
|
kind: 'skip',
|
|
207
216
|
text: `${seat.name}: no launch profile for \`${seat.cli}\` in this version; left running`,
|
|
@@ -259,7 +268,12 @@ export function downPlan(input) {
|
|
|
259
268
|
});
|
|
260
269
|
}
|
|
261
270
|
else {
|
|
262
|
-
steps.push({
|
|
271
|
+
steps.push({
|
|
272
|
+
kind: 'run',
|
|
273
|
+
argv: ['herdr', 'session', 'stop', session],
|
|
274
|
+
note: 'stopped, then cleared: the session this run stopped, so a later `up` starts from the beginning',
|
|
275
|
+
do: { do: 'stop', session },
|
|
276
|
+
});
|
|
263
277
|
}
|
|
264
278
|
return steps;
|
|
265
279
|
}
|
package/dist/launch/rules.d.ts
CHANGED
|
@@ -21,5 +21,7 @@ export interface RulesInput {
|
|
|
21
21
|
* workspace's.
|
|
22
22
|
*/
|
|
23
23
|
export declare function seatRules(input: RulesInput): string[];
|
|
24
|
-
/**
|
|
25
|
-
export
|
|
24
|
+
/** How the rules reach a seat: typed as its first message, or carried as a launch option. */
|
|
25
|
+
export type RulesDelivery = 'message' | 'option';
|
|
26
|
+
/** The rules as the one text a launch option or a first message carries, the closing line last. */
|
|
27
|
+
export declare function rulesText(input: RulesInput, delivery: RulesDelivery): string;
|
package/dist/launch/rules.js
CHANGED
|
@@ -30,7 +30,21 @@ export function seatRules(input) {
|
|
|
30
30
|
}
|
|
31
31
|
return [...own, ...input.rules, ...signing, ...working];
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The line each form ends with. The message says what the rules are and never that they are not a
|
|
35
|
+
* task, so some models start complying by investigating instead of waiting for a brief. A first
|
|
36
|
+
* message is answered once, so it can ask for the ready reply; a system prompt (a launch option)
|
|
37
|
+
* stays in force on every later turn, so it must not: a Claude seat could answer "ready" to its
|
|
38
|
+
* real brief.
|
|
39
|
+
*/
|
|
40
|
+
const CLOSING = {
|
|
41
|
+
message: 'These are standing rules, not a task: reply ready and wait for your brief.',
|
|
42
|
+
option: 'These are standing rules, not a task.',
|
|
43
|
+
};
|
|
44
|
+
/** The rules as the one text a launch option or a first message carries, the closing line last. */
|
|
45
|
+
export function rulesText(input, delivery) {
|
|
46
|
+
// A file whose own rules already carry the form's closing line gets it once: as the closing line.
|
|
47
|
+
const closing = CLOSING[delivery];
|
|
48
|
+
const rules = seatRules(input).filter((rule) => rule !== closing);
|
|
49
|
+
return ['Rules for this session, from the team file:', ...rules.map((rule) => `- ${rule}`), closing].join('\n');
|
|
36
50
|
}
|
|
@@ -43,6 +43,12 @@ screen:
|
|
|
43
43
|
- 'Do you trust the contents of this project\?'
|
|
44
44
|
- '>\s*Yes, I trust this folder'
|
|
45
45
|
below_last_rule: 'enter Confirm'
|
|
46
|
+
# The captured trust dialog fills the pane: there is no composer rule.
|
|
47
|
+
- all:
|
|
48
|
+
- 'Do you trust the contents of this project\?'
|
|
49
|
+
- '>\s*Yes, I trust this folder'
|
|
50
|
+
- 'enter Confirm'
|
|
51
|
+
without_rule: true
|
|
46
52
|
permission:
|
|
47
53
|
ignore_case: true
|
|
48
54
|
rules:
|
|
@@ -50,13 +56,30 @@ screen:
|
|
|
50
56
|
- 'Requesting permission for:'
|
|
51
57
|
- '^\s*>\s*[0-9]\.\s+Yes\b'
|
|
52
58
|
below_last_rule: '↑/↓ Navigate'
|
|
59
|
+
# A long command pushes the one rule out of the window. The dialog is the
|
|
60
|
+
# three lines, with no composer rule left to sit below.
|
|
61
|
+
- all:
|
|
62
|
+
- 'Requesting permission for:'
|
|
63
|
+
- '^\s*>\s*[0-9]\.\s+Yes\b'
|
|
64
|
+
- '↑/↓ Navigate'
|
|
65
|
+
without_rule: true
|
|
53
66
|
working:
|
|
54
67
|
- any: ['\bGenerating\.\.\.']
|
|
55
68
|
composer:
|
|
56
69
|
mode: two-rules-footer-below
|
|
57
70
|
ignore_case: true
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
# The input row is the box's first row under its opening rule, carrying the prompt at the
|
|
72
|
+
# pane's first column — unsent.txt draws `> Rules...` there, content at the second — and a
|
|
73
|
+
# prompt glyph on a continuation row is content, never the input row. The reader anchors
|
|
74
|
+
# this pattern to the captured column.
|
|
75
|
+
prompt: '^>\s*'
|
|
76
|
+
# Both rules are unbroken runs of ─ from the pane's first column (unsent.txt draws them at
|
|
77
|
+
# rows 7 and 12). A rule-looking row the box holds is drawn at the content column and is
|
|
78
|
+
# content, never the frame.
|
|
79
|
+
rule: '^[─━]{8,}\s*$'
|
|
80
|
+
# A long paste folds: the row reads `↑ N more lines` (with the prompt glyph on the
|
|
81
|
+
# input row's own marker), the rows below it are the tail. Capture 1 is N.
|
|
82
|
+
fold: '^\s*(?:>\s*)?↑\s*([0-9]+)\s+more lines\s*$'
|
|
60
83
|
footers:
|
|
61
84
|
- '\? for shortcuts'
|
|
62
85
|
- 'Gemini\s+[0-9]'
|