@bli-cockpit/cli 0.2.240 → 0.2.242

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.
@@ -0,0 +1,88 @@
1
+ /**
2
+ * What `cockpit manager` tells a person after a refusal, and the limits it tells them (BLI-5777).
3
+ *
4
+ * A leaf module (it imports nothing) so the repo-shape tests can read it without pulling in the collector. Everything here
5
+ * is a COPY of something the manager server owns, and the CLI cannot import the server:
6
+ *
7
+ * - `MANAGER_LIMITS_COPY` and `LONG_OVERDUE_DAYS_COPY`: the server's `TOWER_MANAGER_SUMMARY_LIMITS` and `SUMMARY_LONG_OVERDUE_DAYS`;
8
+ * - `SUMMARY_REFUSAL_NAMES`: the server's `TOWER_MANAGER_SUMMARY_REFUSALS`, same names in the same order;
9
+ * - `ITEM_REFUSAL_NAMES`: the server's `ReviewRefusal` (`db/review.ts`) and `item_busy`, less `not_found` (the `not_found` reason answers it);
10
+ * - `READ_ONLY_REFUSAL_NAMES`: the door's `ReadOnlyRefusalReason`.
11
+ *
12
+ * `apps/dashboard/src/repo-shape/manager-copies.test.ts` fails the moment a copy differs from the server's, and each hint map
13
+ * is typed against its own name list (`satisfies Record<…>`), so a name added to the list without a hint fails `tsc`.
14
+ * A new module like this one joins `scripts/build-public-cli.mjs` `runtimeFiles` or the packed CLI breaks while the tests stay green.
15
+ */
16
+ export const MANAGER_LIMITS_COPY = { headlineMax: 140, bulletsMinPerson: 1, bulletsMax: 5, bulletMax: 160, nextMax: 200, blockerMax: 200 };
17
+ /** More than this many days past due, an open action makes the card at risk: a copy of the server's `SUMMARY_LONG_OVERDUE_DAYS`. */
18
+ export const LONG_OVERDUE_DAYS_COPY = 14;
19
+ const L = MANAGER_LIMITS_COPY;
20
+ export const SUMMARY_REFUSAL_NAMES = [
21
+ "summary_changed", "summary_empty", "summary_incomplete", "summary_health_invalid", "summary_headline_empty", "summary_headline_too_long",
22
+ "summary_bullets_count", "summary_bullet_empty", "summary_bullet_too_long", "summary_bullet_date_invalid", "summary_next_too_long",
23
+ "summary_blocker_too_long", "summary_blocked_needs_blocker", "summary_blocker_needs_blocked",
24
+ ];
25
+ /** The way forward after each named refusal of a "Where we are" edit; `home` is the ready-to-run `cockpit manager home <client> --json`. */
26
+ export function summaryHints(home) {
27
+ // A hint is the next step in this terminal's own words (the flag to pass, the command to run). It never says the server's sentence again.
28
+ return {
29
+ summary_changed: `Read the card again with ${home} and retry with its summary.updatedAt as --version (none only when it has no card).`,
30
+ summary_empty: "Pass at least one of --health, --headline, --bullet, --next, --blocker.",
31
+ summary_incomplete: "Pass --health, --headline and --bullet together, with --version none.",
32
+ summary_health_invalid: "Pass --health with one of those words, in lower case with underscores.",
33
+ summary_headline_empty: "A blank or invisible-only --headline counts as empty.",
34
+ summary_headline_too_long: `Shorten --headline: the limit (${L.headlineMax}) counts characters, not bytes.`,
35
+ summary_bullets_count: "Each --bullet is one bullet, and together they replace the whole list.",
36
+ summary_bullet_empty: "Put words after a --bullet's date, when it has one.",
37
+ summary_bullet_too_long: `Shorten the long --bullet (the limit is ${L.bulletMax}).`,
38
+ summary_bullet_date_invalid: "Start a dated --bullet with \"YYYY-MM-DD: \" and a real day.",
39
+ summary_next_too_long: `Shorten --next (the limit is ${L.nextMax}).`,
40
+ summary_blocker_too_long: `Shorten --blocker (the limit is ${L.blockerMax}).`,
41
+ summary_blocked_needs_blocker: "Add --blocker <what blocks it>, or pass another --health.",
42
+ summary_blocker_needs_blocked: "Add --health blocked, or leave --blocker out (`--blocker none` clears the stored one).",
43
+ };
44
+ }
45
+ export const ITEM_REFUSAL_NAMES = [
46
+ "item_changed", "item_busy", "item_archived", "item_not_archived", "item_dismissed", "person_item_not_dismissable",
47
+ "no_proposal", "proposal_replaced", "timeline_not_live", "request_reused", "proposal_decided", "proposal_not_addable",
48
+ ];
49
+ /** Refusals that mean "the item is not as you last saw it": reading it again is the way forward. */
50
+ export const RESEEN_REFUSALS = new Set(["item_changed", "item_busy", "proposal_decided", "no_proposal"]);
51
+ /** The way forward after each named refusal of an item write. `flag` is " --timeline" for a timeline item, else "". */
52
+ export function itemHints(id, flag) {
53
+ const reread = `Run \`cockpit manager show ${id}${flag}\` for the current version.`;
54
+ return {
55
+ item_changed: reread,
56
+ item_busy: reread,
57
+ no_proposal: reread,
58
+ proposal_decided: reread,
59
+ // The request id IS the item's id (an add made it): show it; a retry already made it, and a different item needs no request id.
60
+ request_reused: "The request id is that item's id: `cockpit manager show <request-id>` shows it. If you were retrying an add, it is already there: don't add it again. To add a different item, run the add without --request-id.",
61
+ proposal_replaced: `Run \`cockpit manager show ${id}${flag}\` to see the suggested changes waiting and their ids.`,
62
+ proposal_not_addable: `Run \`cockpit manager proposal ${id} <proposal-id> accept --version <v>\` (or dismiss).`,
63
+ item_archived: `Bring it back with \`cockpit manager unarchive ${id}${flag} --version <v>\` (\`list <client>${flag} --archived\` prints its version).`,
64
+ item_dismissed: `Run \`cockpit manager approve ${id}${flag} --version <v> --restore\` (\`list <client>${flag} --review dismissed\` prints its version).`,
65
+ person_item_not_dismissable: `Run \`cockpit manager archive ${id}${flag} --version <v>\`.`,
66
+ timeline_not_live: "`cockpit manager list <client> --timeline` lists the client's live timeline items.",
67
+ item_not_archived: "Nothing to bring back: it is in the lists already.",
68
+ };
69
+ }
70
+ /**
71
+ * The door's own refusals of a read-only device token (`ReadOnlyRefusalReason`): they reach the terminal as the `reason`
72
+ * (BLI-5777; before, the 403 carried no name and the CLI printed `http_403`).
73
+ */
74
+ export const READ_ONLY_REFUSAL_NAMES = ["read_only_device_cannot_write", "read_only_device_door_not_listed", "read_only_device_on_admin"];
75
+ export const READ_ONLY_HINTS = {
76
+ read_only_device_cannot_write: " Sign in as the person who is asking with `cockpit login`.",
77
+ read_only_device_door_not_listed: " A brain session's key may not read Tower Manager: sign in as the person who is asking with `cockpit login`.",
78
+ read_only_device_on_admin: " Mint the read-only key for a member instead.",
79
+ };
80
+ /** A read's workflow id (`sched_wf_…`, `wf-…`): what `lastRead.workflowRunId` holds in `client --json` and `home --json`. It is not a run's receipt id. */
81
+ export const looksLikeWorkflowId = (value) => /^(?:[a-z]+_)?wf[-_]/i.test(value);
82
+ /** Where a run's receipt id really comes from, for the refusal of anything else (`run <run-id>`). */
83
+ export function runIdRefusal(value) {
84
+ const where = "A run id looks like 8f14e45f-ceea-467a-9575-1d3a4e7b2c10. It comes from `cockpit manager queue <client> --json` (a row's changedByRunId), `cockpit manager show <id>` (a suggested change's runId) or `cockpit manager client <client> --json` (lastBriefRebuild.runId); `list` does not print run ids.";
85
+ return looksLikeWorkflowId(value)
86
+ ? `That is a read's workflow id (the lastRead.workflowRunId that \`client --json\` and \`home --json\` print), not a run's receipt id. ${where}`
87
+ : where;
88
+ }
@@ -0,0 +1,269 @@
1
+ {
2
+ "schema": 1,
3
+ "about": "Tower Manager's limits, rules and refusal names, generated from the manager server's constants (apps/dashboard/src/repo-shape/manager-limits-lib.ts). A skill or doc that quotes a number or a refusal name checks it against this list.",
4
+ "limits": [
5
+ {
6
+ "id": "addDetailCut",
7
+ "value": 200,
8
+ "unit": "characters",
9
+ "source": "the CLI's manager-render.ts savedTextLines: an add prints its detail cut to 200 characters (a display cut, not a server limit)"
10
+ },
11
+ {
12
+ "id": "attentionPart",
13
+ "value": 25,
14
+ "unit": "items",
15
+ "source": "TOWER_MANAGER_ATTENTION_PART_LIMIT"
16
+ },
17
+ {
18
+ "id": "blockerMax",
19
+ "value": 200,
20
+ "unit": "characters",
21
+ "source": "TOWER_MANAGER_SUMMARY_LIMITS.blockerMax"
22
+ },
23
+ {
24
+ "id": "bulletMax",
25
+ "value": 160,
26
+ "unit": "characters",
27
+ "source": "TOWER_MANAGER_SUMMARY_LIMITS.bulletMax"
28
+ },
29
+ {
30
+ "id": "bulletsMax",
31
+ "value": 5,
32
+ "unit": "bullets",
33
+ "source": "TOWER_MANAGER_SUMMARY_LIMITS.bulletsMax"
34
+ },
35
+ {
36
+ "id": "bulletsMin",
37
+ "value": 1,
38
+ "unit": "bullets",
39
+ "source": "TOWER_MANAGER_SUMMARY_LIMITS.bulletsMinPerson"
40
+ },
41
+ {
42
+ "id": "comingUp",
43
+ "value": 20,
44
+ "unit": "items",
45
+ "source": "TOWER_MANAGER_HOME_LIMITS.comingUp"
46
+ },
47
+ {
48
+ "id": "headlineMax",
49
+ "value": 140,
50
+ "unit": "characters",
51
+ "source": "TOWER_MANAGER_SUMMARY_LIMITS.headlineMax"
52
+ },
53
+ {
54
+ "id": "inTowerDays",
55
+ "value": 7,
56
+ "unit": "days",
57
+ "source": "TOWER_MANAGER_IN_TOWER_DAYS"
58
+ },
59
+ {
60
+ "id": "inTowerItems",
61
+ "value": 50,
62
+ "unit": "items",
63
+ "source": "TOWER_MANAGER_IN_TOWER_LIMIT"
64
+ },
65
+ {
66
+ "id": "listRows",
67
+ "value": 500,
68
+ "unit": "rows",
69
+ "source": "db/items.ts LIST_LIMIT"
70
+ },
71
+ {
72
+ "id": "longOverdueDays",
73
+ "value": 14,
74
+ "unit": "days",
75
+ "source": "SUMMARY_LONG_OVERDUE_DAYS"
76
+ },
77
+ {
78
+ "id": "needsAttention",
79
+ "value": 50,
80
+ "unit": "items",
81
+ "source": "TOWER_MANAGER_HOME_LIMITS.needsAttention"
82
+ },
83
+ {
84
+ "id": "needsAttentionDays",
85
+ "value": 7,
86
+ "unit": "days",
87
+ "source": "db/home.ts: due_at <= today + 6, so today and six days ahead (a window of 7 days)"
88
+ },
89
+ {
90
+ "id": "nextMax",
91
+ "value": 200,
92
+ "unit": "characters",
93
+ "source": "TOWER_MANAGER_SUMMARY_LIMITS.nextMax"
94
+ },
95
+ {
96
+ "id": "pageDefault",
97
+ "value": 50,
98
+ "unit": "rows",
99
+ "source": "MANAGER_LIST_PAGE.default (the MCP list tools' own default)"
100
+ },
101
+ {
102
+ "id": "pageMax",
103
+ "value": 200,
104
+ "unit": "rows",
105
+ "source": "MANAGER_LIST_PAGE.max (the MCP list tools' own cap)"
106
+ },
107
+ {
108
+ "id": "sourceNoteMax",
109
+ "value": 1000,
110
+ "unit": "characters",
111
+ "source": "routes/validation.ts: sourceNote .max(1000, ...)"
112
+ },
113
+ {
114
+ "id": "whatChanged",
115
+ "value": 100,
116
+ "unit": "items",
117
+ "source": "TOWER_MANAGER_HOME_LIMITS.whatChanged"
118
+ }
119
+ ],
120
+ "rules": [
121
+ {
122
+ "id": "long_overdue_makes_card_at_risk",
123
+ "text": "An open action more than 14 days past its due date (Tower's today, America/Vancouver) makes the card at_risk, or blocked with a named blocker; never on_track."
124
+ },
125
+ {
126
+ "id": "blocker_only_on_blocked",
127
+ "text": "A blocker goes only on a Blocked card: a blocker sent with any other health is refused (summary_blocker_needs_blocked), and sending only a new health drops the stored one; blocked always has a blocker."
128
+ },
129
+ {
130
+ "id": "every_item_write_names_its_version",
131
+ "text": "Every write on an existing item names the version (updatedAt) it last read; a stale one is refused by name and nothing is written."
132
+ }
133
+ ],
134
+ "refusals": {
135
+ "summary": [
136
+ "summary_changed",
137
+ "summary_empty",
138
+ "summary_incomplete",
139
+ "summary_health_invalid",
140
+ "summary_headline_empty",
141
+ "summary_headline_too_long",
142
+ "summary_bullets_count",
143
+ "summary_bullet_empty",
144
+ "summary_bullet_too_long",
145
+ "summary_bullet_date_invalid",
146
+ "summary_next_too_long",
147
+ "summary_blocker_too_long",
148
+ "summary_blocked_needs_blocker",
149
+ "summary_blocker_needs_blocked"
150
+ ],
151
+ "item": [
152
+ "item_archived",
153
+ "item_busy",
154
+ "item_changed",
155
+ "item_dismissed",
156
+ "item_not_archived",
157
+ "no_proposal",
158
+ "person_item_not_dismissable",
159
+ "proposal_decided",
160
+ "proposal_not_addable",
161
+ "proposal_replaced",
162
+ "request_reused",
163
+ "timeline_not_live"
164
+ ],
165
+ "owner": [
166
+ "owner_already_merged",
167
+ "owner_changed",
168
+ "owner_has_aliases",
169
+ "owner_not_merged",
170
+ "owner_same",
171
+ "owner_target_merged",
172
+ "owner_unknown"
173
+ ],
174
+ "read_only_door": [
175
+ "read_only_device_cannot_write",
176
+ "read_only_device_door_not_listed",
177
+ "read_only_device_on_admin"
178
+ ],
179
+ "door": [
180
+ "inference_not_configured",
181
+ "manager_body_too_large",
182
+ "manager_body_unreadable",
183
+ "manager_operation_not_on_this_door",
184
+ "manager_path_malformed",
185
+ "manager_role_denied",
186
+ "manager_route_not_found",
187
+ "manager_timeout",
188
+ "manager_unreachable",
189
+ "team_lookup_failed",
190
+ "team_membership_required"
191
+ ]
192
+ },
193
+ "refusalPrefixes": [
194
+ "summary_",
195
+ "item_",
196
+ "proposal_",
197
+ "owner_",
198
+ "read_only_device_",
199
+ "request_",
200
+ "manager_",
201
+ "team_",
202
+ "timeline_not_",
203
+ "person_item_",
204
+ "no_proposal"
205
+ ],
206
+ "tools": [
207
+ "archived",
208
+ "blocker",
209
+ "bullets",
210
+ "client_id",
211
+ "date",
212
+ "decision",
213
+ "detail",
214
+ "due",
215
+ "expected_updated_at",
216
+ "headline",
217
+ "health",
218
+ "item_id",
219
+ "kind",
220
+ "limit",
221
+ "manager_add",
222
+ "manager_approve",
223
+ "manager_archive",
224
+ "manager_client",
225
+ "manager_clients",
226
+ "manager_dismiss",
227
+ "manager_done",
228
+ "manager_drop",
229
+ "manager_edit",
230
+ "manager_home",
231
+ "manager_link",
232
+ "manager_list",
233
+ "manager_proposal",
234
+ "manager_queue",
235
+ "manager_reopen",
236
+ "manager_run",
237
+ "manager_set_summary",
238
+ "manager_show",
239
+ "manager_unarchive",
240
+ "meeting_id",
241
+ "needs_review",
242
+ "next",
243
+ "no_owner",
244
+ "overdue",
245
+ "owner",
246
+ "owner_name",
247
+ "owners",
248
+ "proposal_id",
249
+ "request_id",
250
+ "restore",
251
+ "review",
252
+ "run_id",
253
+ "search",
254
+ "source_note",
255
+ "source_url",
256
+ "status",
257
+ "timeline",
258
+ "title",
259
+ "version"
260
+ ],
261
+ "doorRefusedOperations": [
262
+ "create_client",
263
+ "merge_owner",
264
+ "rebuild_brief",
265
+ "sync_all",
266
+ "sync_client",
267
+ "unmerge_owner"
268
+ ]
269
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Why a read or a summary check stopped, in the words the manager page uses (`REASON_SENTENCES` in
3
+ * `apps/dashboard/src/app/(cockpit)/manager/manager-client-bar.tsx`, BLI-5777 batch 4). The CLI used to print the raw
4
+ * label ("notion_root_unreadable"). `manager-reason-words.test.ts` (dashboard repo-shape) fails when a reason the
5
+ * server can write has no sentence here, or when a sentence here is not the page's.
6
+ */
7
+ export const READ_REASON_SENTENCES = {
8
+ running: "Still running.",
9
+ ok: "Finished.",
10
+ client_locked: "Another run was already reading this client, so this one stood aside.",
11
+ client_not_found: "This client no longer exists.",
12
+ client_disabled: "This client is disabled.",
13
+ notion_not_configured: "Notion is not connected on this Tower deployment.",
14
+ notion_root_unreadable: "Tower cannot read this client's Notion page. Share it with the Tower Manager Dashboard integration again, then sync.",
15
+ notion_unavailable: "Notion did not answer. The next sync will try again.",
16
+ model_failed: "The model did not answer. Nothing was lost; the next sync reads the same pages again.",
17
+ model_timeout: "The model took too long to answer. Nothing was lost; the next sync reads the same pages again, fewer at a time.",
18
+ model_output_rejected: "The model's answer was not usable. Nothing was lost; the next sync reads the same pages again.",
19
+ root_owned_by_other_client: "This client's Notion page sits inside another client's tree, which holds it. Nothing was read.",
20
+ interrupted: "Interrupted when Tower restarted; will retry. The daily sync carries it on, or Sync now.",
21
+ batch_too_dense: "Those pages held more than one answer can carry, so they are being read again in smaller batches.",
22
+ database_error: "Tower could not save the result. The next sync will try again.",
23
+ unexpected_error: "The run stopped on an unexpected error. The next sync will try again.",
24
+ };
25
+ /** The page's sentence for a reason, with the label beside it so an agent can name it; an unknown label is printed as it is. */
26
+ export function readReasonText(reason) {
27
+ const sentence = READ_REASON_SENTENCES[reason];
28
+ return sentence ? `${sentence} (${reason})` : reason;
29
+ }
30
+ /**
31
+ * Why the AI's answer for a "Where we are" card was not used, in words (BLI-5777): every refusal name a summary check can record (the
32
+ * answer reader's `SummaryAnswerRefusal`, and the card's own `TOWER_MANAGER_SUMMARY_REFUSALS`). `manager-cli-mcp-rules.test.ts` fails
33
+ * when a name the server has no words here, so a new refusal cannot ship as a bare code.
34
+ */
35
+ export const SUMMARY_REFUSAL_WORDS = {
36
+ not_an_answer: "it was not a card at all",
37
+ health_invalid: "its health was none of on_track, at_risk, blocked",
38
+ headline_empty: "it had no headline",
39
+ bullets_empty: "it had no bullets",
40
+ summary_changed: "the card was changed after the version the edit named",
41
+ summary_empty: "there was nothing to change",
42
+ summary_incomplete: "a first card needs health, headline and a bullet",
43
+ summary_health_invalid: "its health was none of on_track, at_risk, blocked",
44
+ summary_headline_empty: "the headline was empty",
45
+ summary_headline_too_long: "the headline was over 140 characters",
46
+ summary_bullets_count: "too many or too few bullets",
47
+ summary_bullet_empty: "a bullet had no words",
48
+ summary_bullet_too_long: "a bullet was over 160 characters",
49
+ summary_bullet_date_invalid: "a bullet's date was not a real day",
50
+ summary_next_too_long: "next was over 200 characters",
51
+ summary_blocker_too_long: "the blocker was over 200 characters",
52
+ summary_blocked_needs_blocker: "blocked with no blocker",
53
+ summary_blocker_needs_blocked: "a blocker on a card that is not blocked",
54
+ };
55
+ /** Why a summary check kept the card that was there instead of writing one (the receipt's `kept_reason`), in words. */
56
+ export const SUMMARY_KEPT_WORDS = {
57
+ model_call_failed: "the model call failed, so the card that was there stays",
58
+ model_answer_refused: "the AI's answer was refused, so the card that was there stays",
59
+ card_changed_meanwhile: "someone wrote the card while the check ran, so theirs stays; it is newer",
60
+ };
61
+ /** "The AI's answer broke a card rule (too many or too few bullets: summary_bullets_count), so it was not used." The name stays after the words. */
62
+ export function summaryRefusalText(refusal) {
63
+ const words = SUMMARY_REFUSAL_WORDS[refusal];
64
+ return words ? `The AI's answer broke a card rule (${words}: ${refusal}), so it was not used.` : `The AI's answer was refused (${refusal}), so it was not used.`;
65
+ }
66
+ /** "the model call failed, so the card that was there stays (model_call_failed)"; an unknown reason is said as it is. */
67
+ export function keptReasonText(reason) {
68
+ const words = SUMMARY_KEPT_WORDS[reason];
69
+ return words ? `${words} (${reason})` : reason;
70
+ }
71
+ /** What started a summary check (the receipt's `trigger`), in words; an unknown one is said as it is. */
72
+ export const SUMMARY_TRIGGER_WORDS = {
73
+ read: "a read",
74
+ brief_rebuild: "Rebuild brief",
75
+ };
76
+ export function summaryTriggerText(trigger) {
77
+ return SUMMARY_TRIGGER_WORDS[trigger] ?? trigger;
78
+ }