team 0.1.2 → 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 +22 -11
- package/dist/approve/approval.js +14 -9
- package/dist/approve/fingerprint.d.ts +14 -2
- package/dist/approve/fingerprint.js +21 -21
- package/dist/budgets/gate.js +7 -2
- package/dist/budgets/readings.d.ts +6 -2
- package/dist/budgets/readings.js +28 -2
- package/dist/budgets/run.js +8 -2
- package/dist/budgets/table.d.ts +2 -0
- package/dist/budgets/table.js +10 -5
- 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 +7 -5
- package/dist/commands/approve.js +17 -1
- package/dist/commands/doctor.d.ts +15 -1
- package/dist/commands/doctor.js +126 -8
- package/dist/commands/down.d.ts +4 -0
- package/dist/commands/down.js +32 -13
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/remove.js +5 -2
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +19 -1
- package/dist/commands/up.js +3 -1
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +28 -4
- 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 +1 -0
- package/dist/file/validate.d.ts +4 -12
- package/dist/file/validate.js +45 -688
- package/dist/herdr.d.ts +1 -0
- package/dist/herdr.js +11 -1
- package/dist/launch/agent.js +4 -3
- package/dist/launch/deliver.d.ts +4 -0
- package/dist/launch/deliver.js +151 -8
- package/dist/launch/execute.d.ts +2 -0
- package/dist/launch/execute.js +21 -11
- package/dist/launch/plan.js +16 -2
- package/dist/profiles/antigravity.yaml +12 -2
- package/dist/profiles/claude-code.yaml +21 -6
- package/dist/profiles/codex.yaml +14 -1
- package/dist/profiles/cursor.yaml +45 -5
- 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 +2 -0
- package/dist/status/compare.js +7 -4
- package/dist/store/store.d.ts +5 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/dist/watch/checks/budget.js +49 -53
- package/dist/watch/dialect.d.ts +6 -1
- package/dist/watch/dialect.js +68 -12
- package/dist/watch/pass.d.ts +8 -1
- package/dist/watch/pass.js +19 -9
- package/dist/watch/screen-core.d.ts +41 -4
- package/dist/watch/screen-core.js +458 -93
- package/dist/watch/screen-data.d.ts +25 -0
- package/dist/watch/screen-file.d.ts +8 -2
- package/dist/watch/screen-file.js +325 -38
- package/dist/watch/screen-profile.d.ts +19 -0
- package/dist/watch/screen-profile.js +1 -0
- package/dist/watch/screen.d.ts +22 -4
- package/dist/watch/screen.js +39 -5
- package/package.json +9 -3
- package/schema/team.schema.json +498 -0
package/dist/herdr.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare function paneRun(pane: string, command: string, session?: string)
|
|
|
27
27
|
export declare function agentRename(pane: string, name: string, session?: string): boolean;
|
|
28
28
|
export declare function workspaceClose(workspace: string, session?: string): boolean;
|
|
29
29
|
export declare function sessionStop(name: string): boolean;
|
|
30
|
+
export declare function sessionDelete(name: string): boolean;
|
|
30
31
|
export type PaneExec = (args: string[]) => string;
|
|
31
32
|
/** Swaps the process call paneRead makes; null restores the real one. */
|
|
32
33
|
export declare function setPaneExec(exec: PaneExec | null): void;
|
package/dist/herdr.js
CHANGED
|
@@ -171,7 +171,7 @@ export function workspaceClose(workspace, session) {
|
|
|
171
171
|
const body = parsed(out);
|
|
172
172
|
return body !== null && body.type !== 'error';
|
|
173
173
|
}
|
|
174
|
-
// Stops a session
|
|
174
|
+
// Stops a session, which herdr keeps listed as stopped.
|
|
175
175
|
export function sessionStop(name) {
|
|
176
176
|
const out = capture(['session', 'stop', name]);
|
|
177
177
|
if (out === null)
|
|
@@ -179,6 +179,16 @@ export function sessionStop(name) {
|
|
|
179
179
|
const body = parsed(out);
|
|
180
180
|
return body === null || body.type !== 'error';
|
|
181
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
|
+
}
|
|
182
192
|
const shellPaneExec = (args) => execFileSync('herdr', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 10_000 });
|
|
183
193
|
let paneExec = shellPaneExec;
|
|
184
194
|
/** Swaps the process call paneRead makes; null restores the real one. */
|
package/dist/launch/agent.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// pane, or the shell left after the CLI has exited, is not one: those are the panes
|
|
3
3
|
// a nudge, a first message, or an exit must not be typed into.
|
|
4
4
|
//
|
|
5
|
-
// Null is not a live agent, so nothing is typed
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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`.
|
|
8
9
|
export function reportedLiveAgent(foreground, processNames) {
|
|
9
10
|
if (!foreground || processNames.length === 0)
|
|
10
11
|
return false;
|
package/dist/launch/deliver.d.ts
CHANGED
|
@@ -8,5 +8,9 @@ export interface Delivery {
|
|
|
8
8
|
now(): number;
|
|
9
9
|
sleep(ms: number): Promise<void>;
|
|
10
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;
|
|
11
15
|
/** A first message is accepted only after working is observed with the composer empty again. */
|
|
12
16
|
export declare function deliverRules(cli: string, text: string, seconds: number, io: Delivery): Promise<boolean | 'no-agent'>;
|
package/dist/launch/deliver.js
CHANGED
|
@@ -1,6 +1,146 @@
|
|
|
1
1
|
import { reportedLiveAgent } from "./agent.js";
|
|
2
2
|
import { profileFor } from "../profiles/index.js";
|
|
3
|
-
import { classifyComposer, readScreen } from "../watch/screen.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
|
+
}
|
|
4
144
|
/** A first message is accepted only after working is observed with the composer empty again. */
|
|
5
145
|
export async function deliverRules(cli, text, seconds, io) {
|
|
6
146
|
const names = profileFor(cli)?.processNames ?? [];
|
|
@@ -11,25 +151,28 @@ export async function deliverRules(cli, text, seconds, io) {
|
|
|
11
151
|
if (!free() || readScreen(cli, io.screen()).kind !== 'idle' || !io.type(text))
|
|
12
152
|
return false;
|
|
13
153
|
const deadline = io.now() + seconds * 1000;
|
|
14
|
-
// 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.
|
|
15
157
|
for (;;) {
|
|
16
|
-
const kind = readScreen(cli, io.screen()).kind;
|
|
17
158
|
if (!free())
|
|
18
159
|
return false;
|
|
19
|
-
|
|
160
|
+
const state = boxState(cli, text, io.screen());
|
|
161
|
+
if (state === 'ready')
|
|
20
162
|
break;
|
|
21
|
-
if (
|
|
163
|
+
if (state === 'no' || io.now() >= deadline)
|
|
22
164
|
return false;
|
|
23
165
|
const before = io.now();
|
|
24
166
|
await io.sleep(100);
|
|
25
167
|
if (io.now() <= before)
|
|
26
168
|
return false;
|
|
27
169
|
}
|
|
28
|
-
// Re-read immediately before Enter. The agent is asked again: it may have exited
|
|
29
|
-
//
|
|
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.
|
|
30
173
|
if (!live())
|
|
31
174
|
return 'no-agent';
|
|
32
|
-
if (!free() ||
|
|
175
|
+
if (!free() || boxState(cli, text, io.screen()) !== 'ready' || !io.enter())
|
|
33
176
|
return false;
|
|
34
177
|
for (;;) {
|
|
35
178
|
const status = io.status();
|
package/dist/launch/execute.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type Host = {
|
|
|
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,7 +208,7 @@ 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
|
+
const here = place(op.seat, op.pane);
|
|
211
212
|
const delivered = here ? await host.deliverRules?.(session, here.pane, op.cli, op.rules, op.seconds) : false;
|
|
212
213
|
if (delivered === 'no-agent') {
|
|
213
214
|
dropped.add(op.seat);
|
|
@@ -283,10 +284,19 @@ export async function executePlan(steps, session, host) {
|
|
|
283
284
|
host.say(`session ${op.session}: not stopped, something was left in it\n`);
|
|
284
285
|
break;
|
|
285
286
|
}
|
|
286
|
-
if (!host.stopSession(op.session))
|
|
287
|
+
if (!host.stopSession(op.session)) {
|
|
287
288
|
host.say(`session ${op.session}: it did not stop\n`);
|
|
288
|
-
|
|
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)
|
|
289
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
|
+
}
|
|
290
300
|
break;
|
|
291
301
|
}
|
|
292
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
|
}
|
|
@@ -68,8 +68,18 @@ screen:
|
|
|
68
68
|
composer:
|
|
69
69
|
mode: two-rules-footer-below
|
|
70
70
|
ignore_case: true
|
|
71
|
-
|
|
72
|
-
|
|
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*$'
|
|
73
83
|
footers:
|
|
74
84
|
- '\? for shortcuts'
|
|
75
85
|
- 'Gemini\s+[0-9]'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Claude Code's screen. The
|
|
2
|
-
# classifier ignored case on those
|
|
3
|
-
#
|
|
1
|
+
# Claude Code's screen. The trust question carries ignore_case on its own pattern:
|
|
2
|
+
# the old classifier ignored case on those lines and not on the "1. Yes" choice,
|
|
3
|
+
# and a rule-wide or stage-wide flag would have reached the choice too.
|
|
4
4
|
format: 1
|
|
5
5
|
cli: claude-code
|
|
6
6
|
binary: claude
|
|
@@ -39,8 +39,12 @@ status_model:
|
|
|
39
39
|
screen:
|
|
40
40
|
trust:
|
|
41
41
|
- all:
|
|
42
|
-
- '(?:
|
|
42
|
+
- match: '(?:Do you trust (?:this|the) folder\?|One you trust\?)'
|
|
43
|
+
ignore_case: true
|
|
43
44
|
- '^\s*[❯›>]?\s*1\.\s+Yes\b'
|
|
45
|
+
- all:
|
|
46
|
+
- '^\s*[❯›>]?\s*Yes, I trust this folder\b'
|
|
47
|
+
footer: 'Enter to confirm · Esc to cancel'
|
|
44
48
|
# A quoted question above the composer's rule is not a dialog. The footer is the
|
|
45
49
|
# captured last line and needs the question. A Yes/No choice below the last rule
|
|
46
50
|
# is the other shape; the mark is optional, as on an unmarked "2. No".
|
|
@@ -88,8 +92,19 @@ screen:
|
|
|
88
92
|
- '^[^\S\n]*[✢✳✶✻✼✽][^\n]*…\s*\(\s*\d'
|
|
89
93
|
composer:
|
|
90
94
|
mode: box-to-rule
|
|
91
|
-
prompt
|
|
92
|
-
|
|
95
|
+
# The prompt sits at the pane's first column, as unsent-typed-ansi.txt draws `❯ Fix the`;
|
|
96
|
+
# the pane draws continuation rows at the content column — two spaces — so a prompt glyph
|
|
97
|
+
# on one of those is content, never the input row. The reader takes the box's first row
|
|
98
|
+
# under its opening rule and anchors this pattern to the captured column: a row the box
|
|
99
|
+
# holds can never be mistaken for the input.
|
|
100
|
+
prompt: '^[❯›>]'
|
|
101
|
+
# The box's two rules, as unsent-typed-ansi.txt draws them: one unbroken run of ─ from the
|
|
102
|
+
# pane's first column, the closing rule the window's last rule row. A rule-looking row the
|
|
103
|
+
# box holds is drawn at the content column and is content, never the frame.
|
|
104
|
+
rule: '^[─━]{8,}$'
|
|
105
|
+
footers:
|
|
106
|
+
- '^\s{2,}.*·.*(?:Opus|Sonnet|Haiku|Fable)\s+[0-9]+.*$'
|
|
107
|
+
- '^\s{2,}(?:⏵⏵\s+)?bypass permissions\b.*$'
|
|
93
108
|
# Claude Code greys every suggestion it shows — "Try …", "Resume briefly.", any other —
|
|
94
109
|
# with faint styling, while typed text carries none. The list below stays the fallback
|
|
95
110
|
# for a source without styling.
|
package/dist/profiles/codex.yaml
CHANGED
|
@@ -44,6 +44,9 @@ status_model:
|
|
|
44
44
|
model: 'GPT {2:title}'
|
|
45
45
|
version: '{1}'
|
|
46
46
|
# A later footer that is not one of those four names is unread, as the old id map was.
|
|
47
|
+
# The `{1}` names a group this pattern does not have, on purpose: a template that cannot
|
|
48
|
+
# fill reads the line as naming no model. There is no plainer spelling — an empty string
|
|
49
|
+
# is refused and a literal would read as a found model, not as unread.
|
|
47
50
|
- match: '^\s+GPT-[0-9][\w.-]*\s[^·]*·'
|
|
48
51
|
model: '{1}'
|
|
49
52
|
version: '{1}'
|
|
@@ -71,7 +74,17 @@ screen:
|
|
|
71
74
|
composer:
|
|
72
75
|
mode: status-last
|
|
73
76
|
status_line: '^\s+GPT-[0-9][\w.-]*\s+[^·]*·'
|
|
74
|
-
prompt
|
|
77
|
+
# The input row carries the prompt at the pane's first column — unsent.txt draws `› Rules.`
|
|
78
|
+
# there, content on the rows below at the second, and exit-typed.txt keeps the old
|
|
79
|
+
# transcript and menu rows above it — and a prompt glyph on a content row is never the
|
|
80
|
+
# input row. The read scans up from the status line and anchors this pattern to the
|
|
81
|
+
# captured column.
|
|
82
|
+
prompt: '^›(?:\s|$)'
|
|
75
83
|
placeholders:
|
|
76
84
|
- equals: ''
|
|
77
85
|
- equals: Ask Codex to do anything
|
|
86
|
+
# unsent.txt and exit-typed.txt show one empty row under the text, between it and the
|
|
87
|
+
# status line: the drop the pane draws of its own, inside the box's frame. The box read
|
|
88
|
+
# strips up to one trailing empty row as that frame; a row beyond it is one the typed
|
|
89
|
+
# text does not have.
|
|
90
|
+
frame_rows: 1
|
|
@@ -48,21 +48,61 @@ screen:
|
|
|
48
48
|
- '^\s*→'
|
|
49
49
|
- '^\s+Grok\s+[0-9]'
|
|
50
50
|
working:
|
|
51
|
-
# The running turn is the braille spinner
|
|
52
|
-
# "ctrl+c to stop"
|
|
53
|
-
#
|
|
51
|
+
# The running turn is the braille spinner (whatever verb it paints), the
|
|
52
|
+
# prompt line ending in "ctrl+c to stop", or the follow-up queue: the box's
|
|
53
|
+
# `enter …` hint row with, below it, the composer's own row — empty, or its
|
|
54
|
+
# placeholder. The order is the frame's: what follows the hint row may be
|
|
55
|
+
# chrome or that unpainted composer row and nothing else, so a typed row
|
|
56
|
+
# under the box falls to the composer read and says unsent. A transcript
|
|
57
|
+
# that quotes the box paints its prose lines there, so the queue rule reads
|
|
58
|
+
# them as content and leaves the screen alone. Idle and rules-accepted
|
|
59
|
+
# captures paint none of these.
|
|
54
60
|
- any:
|
|
55
61
|
- '^\s*→.*\s{2,}ctrl\+c to stop\s*$'
|
|
56
|
-
- '^\s*[⠀-⣿]+\s
|
|
62
|
+
- '^\s*[⠀-⣿]+\s+\S'
|
|
63
|
+
- all:
|
|
64
|
+
- '^\s*│\s*enter (?:steer|interrupt and send) · ↑ select/edit · esc cancel\s*│\s*$'
|
|
65
|
+
- '^\s*→\s*(?:Add a follow-up|Plan, search, build anything)?\s*$'
|
|
66
|
+
none_after:
|
|
67
|
+
anchor: '^\s*│\s*enter (?:steer|interrupt and send) · ↑ select/edit · esc cancel\s*│\s*$'
|
|
68
|
+
patterns:
|
|
69
|
+
- match: '^\s*\S'
|
|
70
|
+
except:
|
|
71
|
+
- '^\s*[│┌└─┐┘├┤]'
|
|
72
|
+
- '^\s*[⠀-⣿]'
|
|
73
|
+
- '^\s*Tip:'
|
|
74
|
+
- '^\s*→\s*(?:Add a follow-up|Plan, search, build anything)?\s*$'
|
|
75
|
+
- '^\s+Grok\s'
|
|
76
|
+
- '^\s*<workspace>'
|
|
77
|
+
- '^\s*~/'
|
|
78
|
+
- '^\s*❯'
|
|
57
79
|
composer:
|
|
58
80
|
mode: status-then-one
|
|
59
81
|
status_line: '^\s+Grok\s+[0-9]'
|
|
60
|
-
prompt
|
|
82
|
+
# The input row carries the prompt at the pane's second column — unsent.txt and
|
|
83
|
+
# follow-up-queue-typed.txt draw ` → Plan, search, build anything` there, continuations at
|
|
84
|
+
# the fourth — and a prompt glyph on a content or popup row is never the input row. The
|
|
85
|
+
# read scans up from the status line and anchors this pattern to the captured column.
|
|
86
|
+
prompt: '^ {2}→'
|
|
61
87
|
strip_suffix: '\s*ctrl\+c to stop\s*$'
|
|
62
88
|
placeholders:
|
|
63
89
|
- equals: ''
|
|
64
90
|
- equals: Plan, search, build anything
|
|
65
91
|
- equals: Add a follow-up
|
|
92
|
+
# A line wider than the pane's content column continues on the next row at the text
|
|
93
|
+
# column, broken at a space: unsent.txt is its sentence word-wrapped this way on a
|
|
94
|
+
# 51-column pane, and the box is compared with the typed text before any Enter. Only
|
|
95
|
+
# the whitespace the row break itself stands for is normalised — the run the break was
|
|
96
|
+
# made at, or a blank line the text itself has; inside a row, every character must
|
|
97
|
+
# match, runs of spaces included.
|
|
98
|
+
wrap:
|
|
99
|
+
continuation: text-column
|
|
100
|
+
kind: word
|
|
101
|
+
# unsent.txt and follow-up-queue-typed.txt show two empty rows under the text, between
|
|
102
|
+
# it and the status line: the drop the pane draws of its own, inside the box's frame.
|
|
103
|
+
# The box read strips up to two trailing empty rows as that frame; a row beyond them is
|
|
104
|
+
# one the typed text does not have.
|
|
105
|
+
frame_rows: 2
|
|
66
106
|
fallback:
|
|
67
107
|
- all:
|
|
68
108
|
- match: '^\s*→\s+\S'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { QuotaPattern } from './quota.ts';
|
|
2
|
+
import type { ScreenData, Stage } from '../watch/screen-data.ts';
|
|
3
|
+
import { type Screen } from '../watch/screen.ts';
|
|
4
|
+
export declare const DIALOG_STAGES: readonly ["unknown", "trust", "permission", "question"];
|
|
5
|
+
export type DialogStage = (typeof DIALOG_STAGES)[number];
|
|
6
|
+
export type ProfileOverride = {
|
|
7
|
+
cli: string;
|
|
8
|
+
screen: Partial<Record<DialogStage, Stage>>;
|
|
9
|
+
quota: QuotaPattern[];
|
|
10
|
+
};
|
|
11
|
+
export type OverrideProblem = {
|
|
12
|
+
line: number;
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
export type OverrideParse = {
|
|
16
|
+
ok: true;
|
|
17
|
+
profiles: ProfileOverride[];
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
errors: OverrideProblem[];
|
|
21
|
+
};
|
|
22
|
+
/** `<store>/overrides.yaml`, the same folder as the approval for this root. */
|
|
23
|
+
export declare function overridesPath(project: string, root: string, home: string): string;
|
|
24
|
+
/** The file's added patterns, or the line that refuses it. One error, the first. */
|
|
25
|
+
export declare function parseOverrides(text: string): OverrideParse;
|
|
26
|
+
/**
|
|
27
|
+
* Shipped rules stay first, and the added ones follow. The composer and the
|
|
28
|
+
* chrome are the shipped profile's own objects, so an override cannot retune
|
|
29
|
+
* them by merging.
|
|
30
|
+
*/
|
|
31
|
+
export declare function mergeScreen(base: ScreenData, added: ProfileOverride): ScreenData;
|
|
32
|
+
/**
|
|
33
|
+
* An added pattern may move a reading toward a dialog. It may not make a screen
|
|
34
|
+
* idle or unsent when the shipped profile does not, and it may not stop a
|
|
35
|
+
* shipped permission, trust or question pattern from matching.
|
|
36
|
+
*/
|
|
37
|
+
export declare function guardReading(shipped: Screen, merged: Screen): Screen;
|
|
38
|
+
/** The shipped reading, then the same window with this profile's added patterns, guarded. */
|
|
39
|
+
export declare function classifyWith(cli: string, screen: string | undefined, profiles: readonly ProfileOverride[]): Screen;
|
|
40
|
+
/** Shipped quota patterns, then the override's. The shipped ones are never dropped. */
|
|
41
|
+
export declare function quotaWith(cli: string, profiles: readonly ProfileOverride[]): readonly QuotaPattern[];
|
|
42
|
+
/** What `approve` records when the override file itself is the change. */
|
|
43
|
+
export declare const OVERRIDE_CHANGED = "`overrides` changed";
|
|
44
|
+
/** A stored copy that does not parse. The shipped profiles stay in force. */
|
|
45
|
+
export declare const OVERRIDE_UNREADABLE = "the approved overrides can't be read";
|
|
46
|
+
export type OverrideForce = {
|
|
47
|
+
/** The approved patterns. Empty when none were approved, or the copy cannot be read. */
|
|
48
|
+
profiles: ProfileOverride[];
|
|
49
|
+
/** Drift from the approved copy. Empty when the file is the approved one, or there is none. */
|
|
50
|
+
differences: string[];
|
|
51
|
+
/** The live file, when it cannot be read or parsed. Already carrying the path and the line. */
|
|
52
|
+
problems: string[];
|
|
53
|
+
};
|
|
54
|
+
/** The override file's text, and why it was refused. A missing file is an empty text and no problem. */
|
|
55
|
+
export declare function overrideFile(project: string, root: string, home: string): {
|
|
56
|
+
path: string;
|
|
57
|
+
text: string | null;
|
|
58
|
+
problems: string[];
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The patterns in force for this project. Keyed on the project root, the same
|
|
62
|
+
* store as the team-file approval: another checkout of the same name has its
|
|
63
|
+
* own. No file, or a stored copy that cannot be read, leaves the shipped profiles.
|
|
64
|
+
* The live file is used only when it is the text `approve` recorded.
|
|
65
|
+
*/
|
|
66
|
+
export declare function overridesInForce(project: string, root: string, home: string): OverrideForce;
|
|
67
|
+
export declare function overrideProblem(path: string, problem: OverrideProblem): string;
|