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
|
@@ -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".
|
|
@@ -50,20 +54,61 @@ screen:
|
|
|
50
54
|
footer: 'Esc to cancel · Tab to amend'
|
|
51
55
|
below_last_rule: 'Esc to cancel · Tab to amend'
|
|
52
56
|
- below_last_rule: '^\s*❯?\s*[0-9]\. (Yes|No)\b'
|
|
57
|
+
# A pane with no composer rule whose choice is the dialog, including the
|
|
58
|
+
# unmarked "1. Yes" while up is running. A choice quoted above an empty
|
|
59
|
+
# prompt is a transcript: the empty prompt comes after it.
|
|
60
|
+
- all:
|
|
61
|
+
- '^\s*❯?\s*[0-9]\. (Yes|No)\b'
|
|
62
|
+
without_rule: true
|
|
63
|
+
none_after:
|
|
64
|
+
anchor: '^\s*❯?\s*[0-9]\. (Yes|No)\b'
|
|
65
|
+
patterns:
|
|
66
|
+
- '^\s*[❯›>]\s*$'
|
|
53
67
|
question:
|
|
68
|
+
# The phrase is the dialog's last line, as a permission footer is. Prose
|
|
69
|
+
# above the box is not a question.
|
|
70
|
+
- any:
|
|
71
|
+
- 'Enter to select'
|
|
72
|
+
- '↑/↓ to navigate'
|
|
73
|
+
- 'Enter to confirm'
|
|
74
|
+
- 'Esc to cancel'
|
|
75
|
+
on_footer: true
|
|
76
|
+
- below_last_rule: 'Enter to select|↑/↓ to navigate|Enter to confirm|Esc to cancel'
|
|
77
|
+
# The footer has a status line under it and the rule is out of the window.
|
|
78
|
+
# A phrase quoted above an empty prompt is a transcript.
|
|
54
79
|
- any:
|
|
55
80
|
- 'Enter to select'
|
|
56
81
|
- '↑/↓ to navigate'
|
|
57
82
|
- 'Enter to confirm'
|
|
58
83
|
- 'Esc to cancel'
|
|
84
|
+
without_rule: true
|
|
85
|
+
none_after:
|
|
86
|
+
anchor: 'Enter to select|↑/↓ to navigate|Enter to confirm|Esc to cancel'
|
|
87
|
+
patterns:
|
|
88
|
+
- '^\s*[❯›>]\s*$'
|
|
59
89
|
working:
|
|
60
90
|
- any:
|
|
61
91
|
- '\besc to interrupt\b'
|
|
62
92
|
- '^[^\S\n]*[✢✳✶✻✼✽][^\n]*…\s*\(\s*\d'
|
|
63
93
|
composer:
|
|
64
94
|
mode: box-to-rule
|
|
65
|
-
prompt
|
|
66
|
-
|
|
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.*$'
|
|
108
|
+
# Claude Code greys every suggestion it shows — "Try …", "Resume briefly.", any other —
|
|
109
|
+
# with faint styling, while typed text carries none. The list below stays the fallback
|
|
110
|
+
# for a source without styling.
|
|
111
|
+
placeholder_style: dim
|
|
67
112
|
placeholders:
|
|
68
113
|
- equals: ''
|
|
69
114
|
- prefix: 'Try "'
|
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,18 +48,61 @@ screen:
|
|
|
48
48
|
- '^\s*→'
|
|
49
49
|
- '^\s+Grok\s+[0-9]'
|
|
50
50
|
working:
|
|
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.
|
|
51
60
|
- any:
|
|
52
|
-
- 'ctrl\+c to stop'
|
|
53
|
-
- '^\s*[⠀-⣿]+\s
|
|
61
|
+
- '^\s*→.*\s{2,}ctrl\+c to stop\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*❯'
|
|
54
79
|
composer:
|
|
55
80
|
mode: status-then-one
|
|
56
81
|
status_line: '^\s+Grok\s+[0-9]'
|
|
57
|
-
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}→'
|
|
58
87
|
strip_suffix: '\s*ctrl\+c to stop\s*$'
|
|
59
88
|
placeholders:
|
|
60
89
|
- equals: ''
|
|
61
90
|
- equals: Plan, search, build anything
|
|
62
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
|
|
63
106
|
fallback:
|
|
64
107
|
- all:
|
|
65
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;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// The owner's override file. It sits beside the approval and may only add dialog
|
|
2
|
+
// patterns and quota patterns to a profile this version ships. It cannot take a
|
|
3
|
+
// shipped pattern out, and it cannot change a composer, a prompt, a footer, a
|
|
4
|
+
// launch line or the order of the stages: anything else in the file is refused
|
|
5
|
+
// with its line. An edit takes effect only once `approve` records the text: until
|
|
6
|
+
// then the approved copy stays in force, and a copy that cannot be read leaves
|
|
7
|
+
// the shipped profiles.
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { profileFor, quotaFor, quotaList } from "./profile.js";
|
|
11
|
+
import { readApproval, storePath } from "../store/store.js";
|
|
12
|
+
import { addedRules } from "../watch/screen-file.js";
|
|
13
|
+
import { classifyData, screenData } from "../watch/screen.js";
|
|
14
|
+
import { YamlError, parseYaml } from "../yaml.js";
|
|
15
|
+
export const DIALOG_STAGES = ['unknown', 'trust', 'permission', 'question'];
|
|
16
|
+
/** `<store>/overrides.yaml`, the same folder as the approval for this root. */
|
|
17
|
+
export function overridesPath(project, root, home) {
|
|
18
|
+
return join(storePath(project, root, home), 'overrides.yaml');
|
|
19
|
+
}
|
|
20
|
+
/** The file's added patterns, or the line that refuses it. One error, the first. */
|
|
21
|
+
export function parseOverrides(text) {
|
|
22
|
+
try {
|
|
23
|
+
return { ok: true, profiles: profilesOf(parseYaml(text)) };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof YamlError)
|
|
27
|
+
return { ok: false, errors: [{ line: error.line, message: error.message }] };
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Shipped rules stay first, and the added ones follow. The composer and the
|
|
33
|
+
* chrome are the shipped profile's own objects, so an override cannot retune
|
|
34
|
+
* them by merging.
|
|
35
|
+
*/
|
|
36
|
+
export function mergeScreen(base, added) {
|
|
37
|
+
const next = { ...base };
|
|
38
|
+
for (const name of DIALOG_STAGES) {
|
|
39
|
+
const extra = added.screen[name];
|
|
40
|
+
if (!extra)
|
|
41
|
+
continue;
|
|
42
|
+
const existing = base[name];
|
|
43
|
+
next[name] = { rules: [...(existing?.rules ?? []), ...extra.rules] };
|
|
44
|
+
}
|
|
45
|
+
return next;
|
|
46
|
+
}
|
|
47
|
+
const KEPT = new Set(['permission', 'trust', 'question']);
|
|
48
|
+
const DIALOG = new Set(['unknown', 'trust', 'permission', 'question']);
|
|
49
|
+
/**
|
|
50
|
+
* An added pattern may move a reading toward a dialog. It may not make a screen
|
|
51
|
+
* idle or unsent when the shipped profile does not, and it may not stop a
|
|
52
|
+
* shipped permission, trust or question pattern from matching.
|
|
53
|
+
*/
|
|
54
|
+
export function guardReading(shipped, merged) {
|
|
55
|
+
if (KEPT.has(shipped.kind))
|
|
56
|
+
return shipped;
|
|
57
|
+
if ((merged.kind === 'idle' || merged.kind === 'unsent') && merged.kind !== shipped.kind)
|
|
58
|
+
return shipped;
|
|
59
|
+
if (!DIALOG.has(merged.kind))
|
|
60
|
+
return shipped;
|
|
61
|
+
return merged;
|
|
62
|
+
}
|
|
63
|
+
/** The shipped reading, then the same window with this profile's added patterns, guarded. */
|
|
64
|
+
export function classifyWith(cli, screen, profiles) {
|
|
65
|
+
if (screen === undefined)
|
|
66
|
+
return { kind: 'unknown' };
|
|
67
|
+
const base = screenData(cli);
|
|
68
|
+
if (!base)
|
|
69
|
+
return { kind: 'unknown' };
|
|
70
|
+
const lines = screen.split('\n');
|
|
71
|
+
const shipped = classifyData(base, lines);
|
|
72
|
+
const added = profiles.find((profile) => profile.cli === cli);
|
|
73
|
+
if (!added || DIALOG_STAGES.every((stage) => added.screen[stage] === undefined))
|
|
74
|
+
return shipped;
|
|
75
|
+
return guardReading(shipped, classifyData(mergeScreen(base, added), lines));
|
|
76
|
+
}
|
|
77
|
+
/** Shipped quota patterns, then the override's. The shipped ones are never dropped. */
|
|
78
|
+
export function quotaWith(cli, profiles) {
|
|
79
|
+
const added = profiles.find((profile) => profile.cli === cli)?.quota ?? [];
|
|
80
|
+
return [...quotaFor(cli), ...added];
|
|
81
|
+
}
|
|
82
|
+
/** What `approve` records when the override file itself is the change. */
|
|
83
|
+
export const OVERRIDE_CHANGED = '`overrides` changed';
|
|
84
|
+
/** A stored copy that does not parse. The shipped profiles stay in force. */
|
|
85
|
+
export const OVERRIDE_UNREADABLE = "the approved overrides can't be read";
|
|
86
|
+
/** The override file's text, and why it was refused. A missing file is an empty text and no problem. */
|
|
87
|
+
export function overrideFile(project, root, home) {
|
|
88
|
+
const path = overridesPath(project, root, home);
|
|
89
|
+
try {
|
|
90
|
+
const text = readFileSync(path, 'utf8');
|
|
91
|
+
const parsed = parseOverrides(text);
|
|
92
|
+
return { path, text, problems: parsed.ok ? [] : parsed.errors.map((problem) => overrideProblem(path, problem)) };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error.code === 'ENOENT')
|
|
96
|
+
return { path, text: null, problems: [] };
|
|
97
|
+
return { path, text: null, problems: [overrideProblem(path, { line: 0, message: "can't be read" })] };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The patterns in force for this project. Keyed on the project root, the same
|
|
102
|
+
* store as the team-file approval: another checkout of the same name has its
|
|
103
|
+
* own. No file, or a stored copy that cannot be read, leaves the shipped profiles.
|
|
104
|
+
* The live file is used only when it is the text `approve` recorded.
|
|
105
|
+
*/
|
|
106
|
+
export function overridesInForce(project, root, home) {
|
|
107
|
+
const record = readApproval(storePath(project, root, home));
|
|
108
|
+
const recorded = record !== null && Object.hasOwn(record.approval, 'overrides');
|
|
109
|
+
const stored = recorded ? record?.approval.overrides : undefined;
|
|
110
|
+
const approvedText = typeof stored === 'string' ? stored : null;
|
|
111
|
+
const live = overrideFile(project, root, home);
|
|
112
|
+
const differences = [];
|
|
113
|
+
const profiles = [];
|
|
114
|
+
if (approvedText !== null) {
|
|
115
|
+
const parsed = parseOverrides(approvedText);
|
|
116
|
+
if (parsed.ok)
|
|
117
|
+
profiles.push(...parsed.profiles);
|
|
118
|
+
else
|
|
119
|
+
differences.push(OVERRIDE_UNREADABLE);
|
|
120
|
+
}
|
|
121
|
+
else if (recorded && stored !== null && stored !== undefined) {
|
|
122
|
+
differences.push(OVERRIDE_UNREADABLE);
|
|
123
|
+
}
|
|
124
|
+
if (live.text !== approvedText) {
|
|
125
|
+
if (record === null) {
|
|
126
|
+
if (live.text !== null)
|
|
127
|
+
differences.push('the overrides were never approved');
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
differences.push(OVERRIDE_CHANGED);
|
|
131
|
+
}
|
|
132
|
+
return { profiles, differences, problems: live.problems };
|
|
133
|
+
}
|
|
134
|
+
export function overrideProblem(path, problem) {
|
|
135
|
+
return problem.line ? `${path}: line ${problem.line}: ${problem.message}` : `${path}: ${problem.message}`;
|
|
136
|
+
}
|
|
137
|
+
function profilesOf(root) {
|
|
138
|
+
const entries = mapping(root, 'an override file');
|
|
139
|
+
const format = required(entries, 'format', root.line);
|
|
140
|
+
if (format.value.kind !== 'scalar' || format.value.value !== 1)
|
|
141
|
+
fail(format.line, '"format" must be 1');
|
|
142
|
+
only(entries, ['format', 'profiles']);
|
|
143
|
+
const profiles = required(entries, 'profiles', root.line);
|
|
144
|
+
const named = mapping(profiles.value, '"profiles"');
|
|
145
|
+
if (named.length === 0)
|
|
146
|
+
fail(profiles.line, '"profiles" must name a profile');
|
|
147
|
+
return named.map(profileOf);
|
|
148
|
+
}
|
|
149
|
+
function profileOf(entry) {
|
|
150
|
+
if (!profileFor(entry.key))
|
|
151
|
+
fail(entry.line, `unknown profile "${entry.key}"`);
|
|
152
|
+
const entries = mapping(entry.value, 'a profile override');
|
|
153
|
+
only(entries, ['screen', 'quota']);
|
|
154
|
+
const screen = optional(entries, 'screen');
|
|
155
|
+
const quota = optional(entries, 'quota');
|
|
156
|
+
if (!screen && !quota)
|
|
157
|
+
fail(entry.line, 'a profile override adds dialog patterns or quota patterns');
|
|
158
|
+
const stages = screen ? screenOf(screen.value) : {};
|
|
159
|
+
if (screen && Object.keys(stages).length === 0 && !quota) {
|
|
160
|
+
fail(screen.line, 'a profile override adds dialog patterns or quota patterns');
|
|
161
|
+
}
|
|
162
|
+
return { cli: entry.key, screen: stages, quota: quota ? quotaList(quota.value) : [] };
|
|
163
|
+
}
|
|
164
|
+
function screenOf(node) {
|
|
165
|
+
const entries = mapping(node, 'screen');
|
|
166
|
+
only(entries, DIALOG_STAGES);
|
|
167
|
+
const screen = {};
|
|
168
|
+
for (const entry of entries)
|
|
169
|
+
screen[entry.key] = { rules: addedRules(entry.value) };
|
|
170
|
+
return screen;
|
|
171
|
+
}
|
|
172
|
+
function mapping(node, what) {
|
|
173
|
+
if (node.kind !== 'map')
|
|
174
|
+
fail(node.line, `${what} must be a map`);
|
|
175
|
+
return node.entries;
|
|
176
|
+
}
|
|
177
|
+
function only(entries, allowed) {
|
|
178
|
+
for (const entry of entries)
|
|
179
|
+
if (!allowed.includes(entry.key))
|
|
180
|
+
fail(entry.line, `unknown key "${entry.key}"`);
|
|
181
|
+
}
|
|
182
|
+
function required(entries, key, line) {
|
|
183
|
+
return optional(entries, key) ?? fail(line, `missing "${key}"`);
|
|
184
|
+
}
|
|
185
|
+
function optional(entries, key) {
|
|
186
|
+
return entries.find((entry) => entry.key === key);
|
|
187
|
+
}
|
|
188
|
+
function fail(line, message) {
|
|
189
|
+
throw new YamlError(line, message);
|
|
190
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type QuotaPattern } from './quota.ts';
|
|
2
|
+
import { type YamlNode } from '../yaml.ts';
|
|
2
3
|
/**
|
|
3
4
|
* A launch profile: what `team` knows about one CLI, so a seat can't start
|
|
4
5
|
* blocked. Keyed by the seat's `cli`.
|
|
@@ -53,6 +54,8 @@ export declare function profileFor(cli: string): Profile | null;
|
|
|
53
54
|
export declare function quotaFor(cli: string): readonly QuotaPattern[];
|
|
54
55
|
/** Patterns from a YAML list, for a profile snippet. A bad pattern throws. */
|
|
55
56
|
export declare function quotaPatterns(text: string): QuotaPattern[];
|
|
57
|
+
/** Quota patterns from a list already parsed. A bad pattern throws. */
|
|
58
|
+
export declare function quotaList(node: YamlNode): QuotaPattern[];
|
|
56
59
|
/** The model a status line names. `unreadable` is a line the rules claim that does not name one. */
|
|
57
60
|
export declare function statusOnLine(cli: string, line: string): {
|
|
58
61
|
model: string;
|
package/dist/profiles/profile.js
CHANGED
|
@@ -61,6 +61,10 @@ export function quotaFor(cli) {
|
|
|
61
61
|
export function quotaPatterns(text) {
|
|
62
62
|
return quotaOf(parseYaml(text));
|
|
63
63
|
}
|
|
64
|
+
/** Quota patterns from a list already parsed. A bad pattern throws. */
|
|
65
|
+
export function quotaList(node) {
|
|
66
|
+
return quotaOf(node);
|
|
67
|
+
}
|
|
64
68
|
/** The model a status line names. `unreadable` is a line the rules claim that does not name one. */
|
|
65
69
|
export function statusOnLine(cli, line) {
|
|
66
70
|
const rules = SHIPPED[cli]?.status;
|
|
@@ -84,7 +88,7 @@ function launchOf(root) {
|
|
|
84
88
|
const format = required(entries, 'format', root.line);
|
|
85
89
|
if (format.value.kind !== 'scalar' || format.value.value !== 1)
|
|
86
90
|
fail(format.line, '"format" must be 1');
|
|
87
|
-
only(entries, ['format', 'cli', 'screen', 'quota', ...LAUNCH_KEYS]);
|
|
91
|
+
only(entries, ['format', 'cli', 'screen', 'screen_module', 'quota', ...LAUNCH_KEYS]);
|
|
88
92
|
const cli = text(required(entries, 'cli', root.line), 'cli');
|
|
89
93
|
required(entries, 'screen', root.line);
|
|
90
94
|
const quotaEntry = optional(entries, 'quota');
|
package/dist/state.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { StoredReading, StoredSpend } from './budgets/readings.ts';
|
|
2
2
|
export type SeatState = {
|
|
3
3
|
stage: 'launched' | 'named' | 'ready';
|
|
4
|
+
/** The CLI the seat was launched with: `down` stops it under it when the file renamed it. */
|
|
5
|
+
cli?: string;
|
|
4
6
|
pane?: string;
|
|
5
7
|
/** The herdr workspace, so a later command can close it without listing agents. */
|
|
6
8
|
workspace?: string;
|
|
@@ -35,10 +37,6 @@ export type SessionState = {
|
|
|
35
37
|
nudge?: {
|
|
36
38
|
pending_since: string | null;
|
|
37
39
|
};
|
|
38
|
-
/** The last screen readings for this project, keyed by account, window and seat. */
|
|
39
|
-
budgets?: Record<string, StoredReading>;
|
|
40
|
-
/** The last spend check readings for this project, keyed by account. */
|
|
41
|
-
spend?: Record<string, StoredSpend>;
|
|
42
40
|
};
|
|
43
41
|
export type State = {
|
|
44
42
|
format: 1;
|
|
@@ -46,6 +44,13 @@ export type State = {
|
|
|
46
44
|
read_at: string;
|
|
47
45
|
file: string;
|
|
48
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* The project's last readings, whatever session saw them: a screen reading keyed by account,
|
|
49
|
+
* window and seat, a check reading by account and window (§ 4.4).
|
|
50
|
+
*/
|
|
51
|
+
budgets?: Record<string, StoredReading>;
|
|
52
|
+
/** The project's last spend check readings, keyed by account. */
|
|
53
|
+
spend?: Record<string, StoredSpend>;
|
|
49
54
|
sessions: Record<string, SessionState>;
|
|
50
55
|
};
|
|
51
56
|
export declare const STATE_FILE = "team.state.json";
|
package/dist/state.js
CHANGED
|
@@ -26,9 +26,46 @@ export function readState(dir) {
|
|
|
26
26
|
for (const session of Object.values(state.sessions)) {
|
|
27
27
|
session.seats ??= {};
|
|
28
28
|
session.worktrees ??= {};
|
|
29
|
+
// Read old, write new: readings used to be held under a session. They are the project's
|
|
30
|
+
// (§ 4.4), so every session's records are merged to the top level here — no reader loses one,
|
|
31
|
+
// and the next write persists the new shape.
|
|
32
|
+
const held = session;
|
|
33
|
+
if (held.budgets) {
|
|
34
|
+
state.budgets = mergeReadings(state.budgets, held.budgets);
|
|
35
|
+
delete held.budgets;
|
|
36
|
+
}
|
|
37
|
+
if (held.spend) {
|
|
38
|
+
state.spend = mergeSpend(state.spend, held.spend);
|
|
39
|
+
delete held.spend;
|
|
40
|
+
}
|
|
29
41
|
}
|
|
30
42
|
return state;
|
|
31
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* One record per slot when two sessions hold the same account, window and seat: the reading that
|
|
46
|
+
* counts wins — a confirmed one, and among those the newest change (§ 4.3).
|
|
47
|
+
*/
|
|
48
|
+
function mergeReadings(top, held) {
|
|
49
|
+
const merged = top ?? {};
|
|
50
|
+
for (const [key, reading] of Object.entries(held)) {
|
|
51
|
+
const current = merged[key];
|
|
52
|
+
merged[key] = !current
|
|
53
|
+
|| (reading.confirmed && !current.confirmed)
|
|
54
|
+
|| (reading.confirmed === current.confirmed && Date.parse(reading.changedAt) > Date.parse(current.changedAt))
|
|
55
|
+
? reading
|
|
56
|
+
: current;
|
|
57
|
+
}
|
|
58
|
+
return merged;
|
|
59
|
+
}
|
|
60
|
+
/** The same for spend: one record per account, the reading whose check ran last winning. */
|
|
61
|
+
function mergeSpend(top, held) {
|
|
62
|
+
const merged = top ?? {};
|
|
63
|
+
for (const [key, reading] of Object.entries(held)) {
|
|
64
|
+
const current = merged[key];
|
|
65
|
+
merged[key] = !current || Date.parse(reading.at) > Date.parse(current.at) ? reading : current;
|
|
66
|
+
}
|
|
67
|
+
return merged;
|
|
68
|
+
}
|
|
32
69
|
// Reads the state, lets `change` edit it, and writes it back, all under the lock.
|
|
33
70
|
export function updateState(dir, change) {
|
|
34
71
|
return withLock(dir, () => {
|
package/dist/status/compare.js
CHANGED
|
@@ -35,14 +35,17 @@ export function compare(team, session, state, live, now = new Date(), watch = te
|
|
|
35
35
|
rows.push({ name: seat.name, state: 'stopped', model: seat.display, pane: '-' });
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
|
-
// An agent sits in the
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
// An agent sits in the workspace recorded for this name, under another name or under none.
|
|
39
|
+
// The display label is not a key: two seats may share it.
|
|
40
|
+
const stray = recorded?.workspace
|
|
41
|
+
? live.agents.find((candidate) => !claimed.has(candidate.pane) && candidate.workspace === recorded.workspace
|
|
42
|
+
&& !team.seats.some((other) => other.name === candidate.name))
|
|
43
|
+
: undefined;
|
|
41
44
|
if (stray) {
|
|
42
45
|
claimed.add(stray.pane);
|
|
43
46
|
rows.push({ name: seat.name, state: 'wrong name', model: seat.display, pane: stray.pane });
|
|
44
47
|
differences.push({
|
|
45
|
-
what: `${seat.name}: the agent in
|
|
48
|
+
what: `${seat.name}: the agent in ${stray.pane} is ${stray.name ? `named "${stray.name}"` : 'unnamed'}`,
|
|
46
49
|
repair: herdrCommand(session, 'agent', 'rename', stray.pane, seat.name),
|
|
47
50
|
});
|
|
48
51
|
continue;
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
// visible text into the file's two fields, `model` and `version`, or null when the text doesn't
|
|
3
3
|
// show them: "unread" is never a mismatch. The last six lines are the window. A line the rules
|
|
4
4
|
// claim but cannot name clears an earlier match; that is how an unknown Codex footer stays unread.
|
|
5
|
+
import { stripSgr } from "../ansi.js";
|
|
5
6
|
import { statusOnLine } from "../profiles/profile.js";
|
|
6
7
|
export function runningModel(cli, screen) {
|
|
7
8
|
let found = null;
|
|
8
|
-
|
|
9
|
+
// The pane's text may keep its ANSI styling; the model is read from the plain form.
|
|
10
|
+
for (const line of stripSgr(screen).split('\n').slice(-6)) {
|
|
9
11
|
const hit = statusOnLine(cli, line);
|
|
10
12
|
if (hit === 'unreadable')
|
|
11
13
|
found = null;
|
package/dist/store/store.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export interface Approval {
|
|
|
23
23
|
path: string;
|
|
24
24
|
hash: string;
|
|
25
25
|
}>;
|
|
26
|
+
/**
|
|
27
|
+
* The override file as approved, or null when the owner approved there being
|
|
28
|
+
* none. Absent on a record written before the file existed.
|
|
29
|
+
*/
|
|
30
|
+
overrides?: string | null;
|
|
26
31
|
}
|
|
27
32
|
export interface LedgerEntry {
|
|
28
33
|
display: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function version(): string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
// The one place the CLI's version is read. `--version` prints it and `team init` pins the
|
|
3
|
+
// schema URL's tag to it, so the URL can never name a release the CLI doesn't report.
|
|
4
|
+
export function version() {
|
|
5
|
+
const text = readFileSync(new URL('../package.json', import.meta.url), 'utf8');
|
|
6
|
+
return JSON.parse(text).version;
|
|
7
|
+
}
|