@groundnuty/macf-core 0.2.44 → 0.2.45
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/dist/discovery.d.ts +59 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +103 -0
- package/dist/discovery.js.map +1 -0
- package/dist/fleet-driver.d.ts +135 -0
- package/dist/fleet-driver.d.ts.map +1 -0
- package/dist/fleet-driver.js +44 -0
- package/dist/fleet-driver.js.map +1 -0
- package/dist/fleet-reconcile.d.ts +180 -0
- package/dist/fleet-reconcile.d.ts.map +1 -0
- package/dist/fleet-reconcile.js +249 -0
- package/dist/fleet-reconcile.js.map +1 -0
- package/dist/fleet-upgrade.d.ts +171 -0
- package/dist/fleet-upgrade.d.ts.map +1 -0
- package/dist/fleet-upgrade.js +177 -0
- package/dist/fleet-upgrade.js.map +1 -0
- package/dist/guest.d.ts +100 -0
- package/dist/guest.d.ts.map +1 -0
- package/dist/guest.js +120 -0
- package/dist/guest.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/norm.d.ts +2 -2
- package/dist/prompt-responses.d.ts +208 -0
- package/dist/prompt-responses.d.ts.map +1 -0
- package/dist/prompt-responses.js +274 -0
- package/dist/prompt-responses.js.map +1 -0
- package/dist/stall-signatures.d.ts +139 -0
- package/dist/stall-signatures.d.ts.map +1 -0
- package/dist/stall-signatures.js +219 -0
- package/dist/stall-signatures.js.map +1 -0
- package/dist/verify-green.d.ts +62 -0
- package/dist/verify-green.d.ts.map +1 -0
- package/dist/verify-green.js +53 -0
- package/dist/verify-green.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive-prompt auto-responder — config schema, the Inv-2 ceremony-only
|
|
3
|
+
* classifier, and the pure Inv-1/Inv-3 frame matcher (DR-033, groundnuty/macf#645).
|
|
4
|
+
*
|
|
5
|
+
* SAFETY-CRITICAL. This module holds the *pure* logic behind the auto-responder:
|
|
6
|
+
* the config the operator curates in `.claude/.macf/prompt-responses.json`, the
|
|
7
|
+
* rules for what may go on the allowlist, and the frame-matching that decides
|
|
8
|
+
* whether a captured tmux pane is a KNOWN ceremony prompt we may answer. The
|
|
9
|
+
* shell watcher (`scripts/macf-prompt-watcher.sh`) reimplements the SAME
|
|
10
|
+
* algorithm in bash+jq for runtime; this module is the tested reference spec +
|
|
11
|
+
* the validator the CLI runs at `macf init/update/rules refresh`. The two MUST
|
|
12
|
+
* stay in lockstep — a change here is a change there.
|
|
13
|
+
*
|
|
14
|
+
* The three constitutional invariants (DR-033 §Decision):
|
|
15
|
+
*
|
|
16
|
+
* Inv 1 — allowlist-only. Only auto-answer a prompt matching a vetted entry.
|
|
17
|
+
* An unrecognized prompt-like frame → the caller ALERTs and does NOT answer.
|
|
18
|
+
* `matchPromptFrame` returns `unknown-prompt` for that case; the watcher
|
|
19
|
+
* turns it into a loud forensic-log + stderr alert. Silence-on-unknown beats
|
|
20
|
+
* a wrong answer.
|
|
21
|
+
*
|
|
22
|
+
* Inv 2 — ceremony-acks ONLY. The config loader HARD-REFUSES (drops) any entry
|
|
23
|
+
* whose signature material contains an authorization/destructive substring
|
|
24
|
+
* (`delete` / `overwrite` / `trust`) and LOUD-WARNS (keeps, operator-owned
|
|
25
|
+
* risk) on `(y/n)` / `allow` / `permission` / `grant`. This constrains what
|
|
26
|
+
* may go ON the allowlist — the stronger sibling of Inv 1.
|
|
27
|
+
*
|
|
28
|
+
* Inv 3 — the signature GUARANTEES the `send` still means the intended option.
|
|
29
|
+
* A structured signature (`frame_contains` + `option_text` at the `send`
|
|
30
|
+
* ordinal's rendered position) — NOT a fragile substring. A menu reorder /
|
|
31
|
+
* reword / insert breaks the match → falls through to Inv 1 (alert, don't
|
|
32
|
+
* answer) rather than firing a now-wrong ordinal. The signature is not "what
|
|
33
|
+
* prompt is this"; it is "is the world still exactly as the `send` assumes".
|
|
34
|
+
*/
|
|
35
|
+
import { z } from 'zod';
|
|
36
|
+
import { MacfError } from './errors.js';
|
|
37
|
+
/** Config schema version. Bump on a breaking shape change. */
|
|
38
|
+
export const PROMPT_RESPONSES_SCHEMA_VERSION = '1';
|
|
39
|
+
/**
|
|
40
|
+
* Inv 2 — HARD-REFUSE substrings. An entry whose signature material contains any
|
|
41
|
+
* of these (case-insensitive) is DROPPED at load: the auto-responder must never
|
|
42
|
+
* answer a destructive-confirmation or a folder/tool-trust prompt, even if an
|
|
43
|
+
* operator tries to allowlist it (DR-033 OQ1 lean: hard-refuse the most
|
|
44
|
+
* dangerous). Kept in lockstep with `PROMPT_REFUSE_SUBSTRINGS` in the watcher.
|
|
45
|
+
* `revoke`/`remove` are the security/destructive siblings of `delete`/`trust`
|
|
46
|
+
* ("revoke token?" / "remove collaborator?") — same never-even-if-allowlisted
|
|
47
|
+
* class (science review of macf#645).
|
|
48
|
+
*/
|
|
49
|
+
export const PROMPT_REFUSE_SUBSTRINGS = ['delete', 'overwrite', 'trust', 'revoke', 'remove'];
|
|
50
|
+
/**
|
|
51
|
+
* Inv 2 — LOUD-WARN substrings. An entry whose signature material contains any
|
|
52
|
+
* of these is KEPT (operator-owned risk) but flagged loudly at load: these lean
|
|
53
|
+
* authorization-shaped, so the operator should confirm they meant a ceremony ack
|
|
54
|
+
* and not a permission grant. Kept in lockstep with the watcher.
|
|
55
|
+
*/
|
|
56
|
+
export const PROMPT_WARN_SUBSTRINGS = ['(y/n)', 'allow', 'permission', 'grant'];
|
|
57
|
+
/** Thrown when the prompt-responses config fails schema validation. */
|
|
58
|
+
export class PromptResponderError extends MacfError {
|
|
59
|
+
constructor(message) {
|
|
60
|
+
super('PROMPT_RESPONDER_ERROR', message);
|
|
61
|
+
this.name = 'PromptResponderError';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A single allowlist entry — the STRUCTURED signature that resolves DR-033 OQ2
|
|
66
|
+
* (Inv 3). `frame_contains` identifies WHICH prompt (all substrings must be
|
|
67
|
+
* present); `option_text` is the EXACT rendered text at the `send` ordinal's
|
|
68
|
+
* line, so `send`↔option binding is machine-checkable, not a fragile substring.
|
|
69
|
+
*
|
|
70
|
+
* - `frame_contains` — substrings that MUST ALL be present in the captured
|
|
71
|
+
* frame (Inv 1 identity). Requiring all present also doubles as the
|
|
72
|
+
* "wait for the full frame rendered" gate.
|
|
73
|
+
* - `option_text` — the exact rendered option text at the `send` position
|
|
74
|
+
* (Inv 3 binding). Must appear on a menu line numbered `send`.
|
|
75
|
+
* - `send` — the keystroke(s) to send, typically a menu ordinal ("1" / "2").
|
|
76
|
+
* - `verify_contains` — OPTIONAL expected substring of the POST-answer
|
|
77
|
+
* next-screen (Inv-A "verify the RIGHT outcome"). When set, the watcher
|
|
78
|
+
* asserts it appears after sending; when unset the watcher verifies
|
|
79
|
+
* signature-clearance only + logs that the outcome check was weaker.
|
|
80
|
+
* - `max_fires` — per-prompt fire cap (default 1). An auto-responder that
|
|
81
|
+
* fires repeatedly on a recurring pattern is itself a hazard.
|
|
82
|
+
*
|
|
83
|
+
* `.strict()` so an operator typo in a field name (e.g. `frame_contain`) is a
|
|
84
|
+
* loud validation error, not a silently-ignored field.
|
|
85
|
+
*/
|
|
86
|
+
export const PromptResponseEntrySchema = z
|
|
87
|
+
.object({
|
|
88
|
+
name: z.string().min(1),
|
|
89
|
+
frame_contains: z.array(z.string().min(1)).min(1),
|
|
90
|
+
option_text: z.string().min(1),
|
|
91
|
+
send: z.string().min(1),
|
|
92
|
+
verify_contains: z.string().min(1).optional(),
|
|
93
|
+
max_fires: z.number().int().positive().default(1),
|
|
94
|
+
})
|
|
95
|
+
.strict();
|
|
96
|
+
/**
|
|
97
|
+
* The whole config. Top-level is deliberately NON-strict (unknown keys stripped,
|
|
98
|
+
* not rejected) so the shipped seed can carry a `_comment` operator-note field
|
|
99
|
+
* without failing validation. Entries stay strict.
|
|
100
|
+
*/
|
|
101
|
+
export const PromptResponsesConfigSchema = z.object({
|
|
102
|
+
schema_version: z.literal(PROMPT_RESPONSES_SCHEMA_VERSION),
|
|
103
|
+
entries: z.array(PromptResponseEntrySchema).default([]),
|
|
104
|
+
});
|
|
105
|
+
/**
|
|
106
|
+
* The signature material Inv-2 classification scans: the entry's identifying
|
|
107
|
+
* fields, lowercased + joined. Deliberately includes `name` + every
|
|
108
|
+
* `frame_contains` needle + `option_text` — an authorization-shaped substring
|
|
109
|
+
* anywhere in what the entry MATCHES ON is enough to refuse/warn.
|
|
110
|
+
*/
|
|
111
|
+
function signatureText(entry) {
|
|
112
|
+
return [entry.name, ...entry.frame_contains, entry.option_text].join('\n').toLowerCase();
|
|
113
|
+
}
|
|
114
|
+
/** First refuse/warn substring present in the entry, or undefined. */
|
|
115
|
+
function firstSubstring(entry, list) {
|
|
116
|
+
const hay = signatureText(entry);
|
|
117
|
+
return list.find((s) => hay.includes(s));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Classify an entry against the Inv-2 substring lists. `refuse` wins over `warn`
|
|
121
|
+
* (the destructive/trust list is checked first). Substring match is intentional
|
|
122
|
+
* and over-refuse-safe: better to reject a benignly-worded entry than to admit a
|
|
123
|
+
* dangerous one.
|
|
124
|
+
*/
|
|
125
|
+
export function classifyPromptSignature(entry) {
|
|
126
|
+
if (firstSubstring(entry, PROMPT_REFUSE_SUBSTRINGS) !== undefined)
|
|
127
|
+
return 'refuse';
|
|
128
|
+
if (firstSubstring(entry, PROMPT_WARN_SUBSTRINGS) !== undefined)
|
|
129
|
+
return 'warn';
|
|
130
|
+
return 'ok';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Validate + Inv-2-filter a parsed config object. Throws `PromptResponderError`
|
|
134
|
+
* on a schema-invalid shape (loud — never silently proceed on a malformed
|
|
135
|
+
* config). Refuse-classified entries are dropped from `accepted`; warn-classified
|
|
136
|
+
* entries stay in `accepted` (operator-owned risk) but are also listed in
|
|
137
|
+
* `warned`.
|
|
138
|
+
*/
|
|
139
|
+
export function loadPromptResponses(raw) {
|
|
140
|
+
const parsed = PromptResponsesConfigSchema.safeParse(raw);
|
|
141
|
+
if (!parsed.success) {
|
|
142
|
+
throw new PromptResponderError(`prompt-responses config is invalid: ${parsed.error.issues
|
|
143
|
+
.map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`)
|
|
144
|
+
.join('; ')}`);
|
|
145
|
+
}
|
|
146
|
+
const accepted = [];
|
|
147
|
+
const refused = [];
|
|
148
|
+
const warned = [];
|
|
149
|
+
for (const entry of parsed.data.entries) {
|
|
150
|
+
const cls = classifyPromptSignature(entry);
|
|
151
|
+
if (cls === 'refuse') {
|
|
152
|
+
refused.push({ entry, matched: firstSubstring(entry, PROMPT_REFUSE_SUBSTRINGS) ?? '' });
|
|
153
|
+
continue; // Inv 2 hard-refuse: DROP — never expose to the matcher.
|
|
154
|
+
}
|
|
155
|
+
if (cls === 'warn') {
|
|
156
|
+
warned.push({ entry, matched: firstSubstring(entry, PROMPT_WARN_SUBSTRINGS) ?? '' });
|
|
157
|
+
}
|
|
158
|
+
accepted.push(entry);
|
|
159
|
+
}
|
|
160
|
+
return { accepted, refused, warned };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Prompt-like frame detector (Inv 1). A frame that matches no allowlist entry
|
|
164
|
+
* but LOOKS like an interactive prompt (a `❯` selector or a `(y/n)`-style
|
|
165
|
+
* confirmation) is the dangerous case: an unknown prompt we must NOT answer.
|
|
166
|
+
* Broad on purpose — false-positive (alert on a non-prompt) is cheap; a
|
|
167
|
+
* false-negative (miss a real unknown prompt) is the hazard.
|
|
168
|
+
*/
|
|
169
|
+
export function looksPromptLike(frame) {
|
|
170
|
+
if (frame.includes('❯'))
|
|
171
|
+
return true;
|
|
172
|
+
return /\((?:y\/n|y\/N|Y\/n)\)|\[(?:y\/N|Y\/n|y\/n)\]/.test(frame);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Inv 3 binding: does `optionText` render on a menu line numbered `send`?
|
|
176
|
+
*
|
|
177
|
+
* For a numeric `send` (the common menu-ordinal case), the line containing
|
|
178
|
+
* `optionText` must, after stripping any leading selector glyph / whitespace
|
|
179
|
+
* (`❯ 1. …`), START with the `send` ordinal followed by a boundary — so a menu
|
|
180
|
+
* reorder that moves `optionText` to a differently-numbered line breaks the
|
|
181
|
+
* match. For a non-numeric `send` there is no ordinal to bind to, so presence of
|
|
182
|
+
* `optionText` is the binding (weaker; the seeds + realistic launch menus are
|
|
183
|
+
* numbered).
|
|
184
|
+
*/
|
|
185
|
+
export function optionOnSendLine(frame, optionText, send) {
|
|
186
|
+
const numeric = /^[0-9]+$/.test(send);
|
|
187
|
+
for (const line of frame.split(/\r?\n/)) {
|
|
188
|
+
if (!line.includes(optionText))
|
|
189
|
+
continue;
|
|
190
|
+
if (!numeric)
|
|
191
|
+
return true;
|
|
192
|
+
// Strip the leading run of non-alphanumeric chars (whitespace + any
|
|
193
|
+
// selector glyph, incl. multibyte `❯`), then require the line to START with
|
|
194
|
+
// `send` followed by a non-alphanumeric boundary (or end-of-line), so `1`
|
|
195
|
+
// matches `1.` / `1)` / `1 ` but NOT `10`. Kept identical to the watcher's
|
|
196
|
+
// awk `_option_on_send_line`.
|
|
197
|
+
const stripped = line.replace(/^[^0-9A-Za-z]+/, '');
|
|
198
|
+
if (!stripped.startsWith(send))
|
|
199
|
+
continue;
|
|
200
|
+
const rest = stripped.charAt(send.length); // '' when send is the whole token
|
|
201
|
+
if (rest === '' || !/[0-9A-Za-z]/.test(rest))
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Does the frame match this specific allowlist entry? All `frame_contains`
|
|
208
|
+
* present (Inv 1 identity + full-render gate) AND `option_text` present AND
|
|
209
|
+
* `option_text` on a line numbered `send` (Inv 3 binding).
|
|
210
|
+
*/
|
|
211
|
+
export function entryMatchesFrame(frame, entry) {
|
|
212
|
+
for (const needle of entry.frame_contains) {
|
|
213
|
+
if (!frame.includes(needle))
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (!frame.includes(entry.option_text))
|
|
217
|
+
return false;
|
|
218
|
+
return optionOnSendLine(frame, entry.option_text, entry.send);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Match a captured tmux frame against the accepted allowlist (Inv 1 + Inv 3).
|
|
222
|
+
* Returns the first matching entry; else `unknown-prompt` when the frame looks
|
|
223
|
+
* prompt-like (→ caller alerts, never answers); else `none` (no prompt / the
|
|
224
|
+
* agent's own output).
|
|
225
|
+
*/
|
|
226
|
+
export function matchPromptFrame(frame, entries) {
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
if (entryMatchesFrame(frame, entry))
|
|
229
|
+
return { kind: 'match', entry };
|
|
230
|
+
}
|
|
231
|
+
return looksPromptLike(frame) ? { kind: 'unknown-prompt' } : { kind: 'none' };
|
|
232
|
+
}
|
|
233
|
+
/** Per-prompt fire cap (Inv operational guard): may this entry fire again? */
|
|
234
|
+
export function canFire(entry, firedCount) {
|
|
235
|
+
return firedCount < entry.max_fires;
|
|
236
|
+
}
|
|
237
|
+
export function verifyOutcome(afterFrame, entry) {
|
|
238
|
+
if (entryMatchesFrame(afterFrame, entry))
|
|
239
|
+
return 'not-cleared';
|
|
240
|
+
if (entry.verify_contains !== undefined) {
|
|
241
|
+
return afterFrame.includes(entry.verify_contains) ? 'ok' : 'wrong-outcome';
|
|
242
|
+
}
|
|
243
|
+
return 'weak-ok';
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The canonical seed config `macf init` writes when no `prompt-responses.json`
|
|
247
|
+
* exists yet. Two ceremony-ack entries vetted to Inv 2 (neither signature
|
|
248
|
+
* contains a refuse/warn substring). The exact rendered text of a launch prompt
|
|
249
|
+
* can only be confirmed by an OPERATOR live-verify (the prompts don't render in
|
|
250
|
+
* `-p` mode), so a seed that doesn't exactly match a real prompt fails SAFE —
|
|
251
|
+
* the frame falls through to Inv 1 (unknown-prompt alert), never a wrong answer.
|
|
252
|
+
* Operators tune the exact `frame_contains` / `option_text` against the real
|
|
253
|
+
* pane, then the ceremony auto-clears.
|
|
254
|
+
*/
|
|
255
|
+
export const PROMPT_RESPONSES_SEED = {
|
|
256
|
+
schema_version: PROMPT_RESPONSES_SCHEMA_VERSION,
|
|
257
|
+
entries: [
|
|
258
|
+
{
|
|
259
|
+
name: 'dev-channels',
|
|
260
|
+
frame_contains: ['I am using this for local development'],
|
|
261
|
+
option_text: 'I am using this for local development',
|
|
262
|
+
send: '1',
|
|
263
|
+
max_fires: 1,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'resume-summary',
|
|
267
|
+
frame_contains: ['Resume full session as-is'],
|
|
268
|
+
option_text: 'Resume full session as-is',
|
|
269
|
+
send: '2',
|
|
270
|
+
max_fires: 1,
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
//# sourceMappingURL=prompt-responses.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-responses.js","sourceRoot":"","sources":["../src/prompt-responses.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stall-signature allowlist — config schema, the pure pane-matcher, and the
|
|
3
|
+
* per-action fire-cap logic behind `macf fleet resume` (DR-037 / groundnuty/macf#686,
|
|
4
|
+
* porting `groundnuty/macf-devops-toolkit:fleet/resume.sh` + `stall-signatures.json`).
|
|
5
|
+
*
|
|
6
|
+
* SAFETY-CRITICAL. `macf fleet resume` acts on an IDLE agent — an idle agent is one
|
|
7
|
+
* of three things and only the pane tells them apart (operator, 2026-06-28):
|
|
8
|
+
*
|
|
9
|
+
* - idle-CLEAN (no signature matched) → legitimately idle/done → DO NOTHING.
|
|
10
|
+
* - idle-STALLED (rate-limit / turn-abort) → `action: nudge` — resume the SAME
|
|
11
|
+
* session (a nudge preserves in-progress work; a restart loses it + re-hits a
|
|
12
|
+
* rate-limit). The gentlest recovery.
|
|
13
|
+
* - idle-BLOCKED (permission / trust / → `action: report` — a DURABLE operator
|
|
14
|
+
* skill / memory prompt) alert, NEVER auto-answered (an
|
|
15
|
+
* authorization decision needs a human;
|
|
16
|
+
* DR-033 ceremony-not-authorization).
|
|
17
|
+
*
|
|
18
|
+
* The safety contract is the sibling of DR-033's: **allowlist-only, never a blind
|
|
19
|
+
* nudge.** We act ONLY on an idle agent whose recent pane matches a KNOWN, vetted
|
|
20
|
+
* signature. An unmatched idle pane is legitimately idle/done → never touched (no
|
|
21
|
+
* spam). An unknown-but-blocking prompt falls through to no-match → skip; the
|
|
22
|
+
* operator adds a `report` signature as the TUI evolves (the allowlist is DATA).
|
|
23
|
+
*
|
|
24
|
+
* This module holds the PURE logic — the config the operator curates in
|
|
25
|
+
* `.claude/.macf/stall-signatures.json`, the schema/validator the CLI runs at
|
|
26
|
+
* `macf init/update/rules refresh`, and the frame-matcher the `fleet resume`
|
|
27
|
+
* decision layer drives off. The tested reference is the devops `resume.sh` bash
|
|
28
|
+
* (which reimplements the same match-then-dispatch in bash + jq); the two MUST
|
|
29
|
+
* stay in lockstep.
|
|
30
|
+
*/
|
|
31
|
+
import { z } from 'zod';
|
|
32
|
+
import { MacfError } from './errors.js';
|
|
33
|
+
/** A matched stall's dispatched action: nudge a stalled agent, or report a blocked one. */
|
|
34
|
+
export declare const STALL_ACTIONS: readonly ["nudge", "report"];
|
|
35
|
+
export type StallAction = (typeof STALL_ACTIONS)[number];
|
|
36
|
+
/**
|
|
37
|
+
* Per-action default fire cap when an entry omits `max_fires` — a nudge is a
|
|
38
|
+
* gentle repeatable resume (retry a few times, then escalate), a report is a
|
|
39
|
+
* one-shot durable alert per episode (re-alerting spams the operator). Mirrors
|
|
40
|
+
* the devops `resume.sh` defaults (`nudge`=3, `report`=1).
|
|
41
|
+
*/
|
|
42
|
+
export declare const DEFAULT_NUDGE_MAX_FIRES = 3;
|
|
43
|
+
export declare const DEFAULT_REPORT_MAX_FIRES = 1;
|
|
44
|
+
/** Thrown when the stall-signatures config fails schema / regex validation. */
|
|
45
|
+
export declare class StallSignaturesError extends MacfError {
|
|
46
|
+
constructor(message: string);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A single stall-signature allowlist entry.
|
|
50
|
+
*
|
|
51
|
+
* - `name` — the signature's stable identifier (rendered in the plan / the
|
|
52
|
+
* alert title, so a fire-cap counter can key off it).
|
|
53
|
+
* - `signature` — a regular-expression source matched CASE-INSENSITIVELY against
|
|
54
|
+
* the captured idle pane (the ERE the devops bash greps with `grep -qiE`; the
|
|
55
|
+
* seeds' alternation/`\?`/`\.` forms are JS-RegExp-compatible). Validated for
|
|
56
|
+
* compilability at load — a malformed pattern is a LOUD config error, never a
|
|
57
|
+
* silent non-match.
|
|
58
|
+
* - `action` — `nudge` (resume a stalled agent) or `report` (durable alert for a
|
|
59
|
+
* blocked agent; NEVER auto-answered). Defaults to `nudge`.
|
|
60
|
+
* - `nudge` — the text sent into the TUI for `action: nudge` (default: a generic
|
|
61
|
+
* "please continue").
|
|
62
|
+
* - `report` — the operator-facing summary for `action: report` (default: a
|
|
63
|
+
* generic "blocked on an operator-input prompt").
|
|
64
|
+
* - `max_fires` — per-episode fire cap; omitted → the per-action default.
|
|
65
|
+
* - `comment` — operator documentation (why this signature; caveats). Ignored by
|
|
66
|
+
* the matcher; kept in the schema so the seed's `comment` fields validate.
|
|
67
|
+
*
|
|
68
|
+
* `.strict()` so an operator typo in a field name (e.g. `signatures`) is a loud
|
|
69
|
+
* validation error, not a silently-ignored field.
|
|
70
|
+
*/
|
|
71
|
+
export declare const StallSignatureEntrySchema: z.ZodObject<{
|
|
72
|
+
name: z.ZodString;
|
|
73
|
+
signature: z.ZodString;
|
|
74
|
+
action: z.ZodDefault<z.ZodEnum<{
|
|
75
|
+
nudge: "nudge";
|
|
76
|
+
report: "report";
|
|
77
|
+
}>>;
|
|
78
|
+
nudge: z.ZodOptional<z.ZodString>;
|
|
79
|
+
report: z.ZodOptional<z.ZodString>;
|
|
80
|
+
max_fires: z.ZodOptional<z.ZodNumber>;
|
|
81
|
+
comment: z.ZodOptional<z.ZodString>;
|
|
82
|
+
}, z.core.$strict>;
|
|
83
|
+
export type StallSignatureEntry = z.infer<typeof StallSignatureEntrySchema>;
|
|
84
|
+
/**
|
|
85
|
+
* The whole config: a bare array of entries — the exact shape of the devops
|
|
86
|
+
* `fleet/stall-signatures.json` reference (so the two files are interchangeable).
|
|
87
|
+
*/
|
|
88
|
+
export declare const StallSignaturesConfigSchema: z.ZodArray<z.ZodObject<{
|
|
89
|
+
name: z.ZodString;
|
|
90
|
+
signature: z.ZodString;
|
|
91
|
+
action: z.ZodDefault<z.ZodEnum<{
|
|
92
|
+
nudge: "nudge";
|
|
93
|
+
report: "report";
|
|
94
|
+
}>>;
|
|
95
|
+
nudge: z.ZodOptional<z.ZodString>;
|
|
96
|
+
report: z.ZodOptional<z.ZodString>;
|
|
97
|
+
max_fires: z.ZodOptional<z.ZodNumber>;
|
|
98
|
+
comment: z.ZodOptional<z.ZodString>;
|
|
99
|
+
}, z.core.$strict>>;
|
|
100
|
+
export type StallSignaturesConfig = z.infer<typeof StallSignaturesConfigSchema>;
|
|
101
|
+
/**
|
|
102
|
+
* Validate + regex-check a parsed config. Throws `StallSignaturesError` on a
|
|
103
|
+
* schema-invalid shape OR an uncompilable `signature` (loud — a malformed
|
|
104
|
+
* allowlist must never silently degrade to "matches nothing"). Returns the
|
|
105
|
+
* accepted entries.
|
|
106
|
+
*/
|
|
107
|
+
export declare function loadStallSignatures(raw: unknown): readonly StallSignatureEntry[];
|
|
108
|
+
/**
|
|
109
|
+
* The per-episode fire cap for an entry — its explicit `max_fires`, else the
|
|
110
|
+
* per-action default (nudge 3 / report 1).
|
|
111
|
+
*/
|
|
112
|
+
export declare function resolveMaxFires(entry: StallSignatureEntry): number;
|
|
113
|
+
/** Fire-cap guard: may this stall-signature fire again given its prior fire count? */
|
|
114
|
+
export declare function canFireStall(entry: StallSignatureEntry, firedCount: number): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* First allowlist entry whose `signature` regex matches the captured pane
|
|
117
|
+
* (case-insensitive), or `null` when the pane matches nothing — the idle-CLEAN
|
|
118
|
+
* case the caller treats as "legitimately idle/done, never touched". Pure.
|
|
119
|
+
*
|
|
120
|
+
* A signature that fails to compile is skipped (defensively) rather than
|
|
121
|
+
* throwing here — `loadStallSignatures` is the fail-loud gate; the matcher stays
|
|
122
|
+
* total so a single bad entry never breaks a whole sweep.
|
|
123
|
+
*/
|
|
124
|
+
export declare function matchStallSignature(pane: string, entries: readonly StallSignatureEntry[]): StallSignatureEntry | null;
|
|
125
|
+
/**
|
|
126
|
+
* The canonical seed `macf init` writes when no `stall-signatures.json` exists.
|
|
127
|
+
* Ported verbatim from `groundnuty/macf-devops-toolkit:fleet/stall-signatures.json`
|
|
128
|
+
* (the tested reference). Two `nudge` signatures (transient rate-limit / turn-abort
|
|
129
|
+
* — resume the same session) and three `report` signatures (permission / trust /
|
|
130
|
+
* skill-or-memory prompts — authorization decisions that must NEVER be
|
|
131
|
+
* auto-answered, only surfaced as a durable operator alert).
|
|
132
|
+
*
|
|
133
|
+
* Signature strings are best-effort across Claude Code versions — the allowlist is
|
|
134
|
+
* DATA; an operator tunes it against the real pane as the TUI evolves. An imperfect
|
|
135
|
+
* signature fails SAFE (falls through to no-match → the agent is left untouched,
|
|
136
|
+
* never a wrong action).
|
|
137
|
+
*/
|
|
138
|
+
export declare const STALL_SIGNATURES_SEED: StallSignaturesConfig;
|
|
139
|
+
//# sourceMappingURL=stall-signatures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stall-signatures.d.ts","sourceRoot":"","sources":["../src/stall-signatures.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,2FAA2F;AAC3F,eAAO,MAAM,aAAa,8BAA+B,CAAC;AAC1D,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,+EAA+E;AAC/E,qBAAa,oBAAqB,SAAQ,SAAS;gBACrC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;kBAU3B,CAAC;AACZ,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E;;;GAGG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;mBAAqC,CAAC;AAC9E,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEhF;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,mBAAmB,EAAE,CAsBhF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,mBAAmB,GAAG,MAAM,CAGlE;AAED,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAEpF;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,mBAAmB,EAAE,GACtC,mBAAmB,GAAG,IAAI,CAW5B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,EAAE,qBA8DnC,CAAC"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stall-signature allowlist — config schema, the pure pane-matcher, and the
|
|
3
|
+
* per-action fire-cap logic behind `macf fleet resume` (DR-037 / groundnuty/macf#686,
|
|
4
|
+
* porting `groundnuty/macf-devops-toolkit:fleet/resume.sh` + `stall-signatures.json`).
|
|
5
|
+
*
|
|
6
|
+
* SAFETY-CRITICAL. `macf fleet resume` acts on an IDLE agent — an idle agent is one
|
|
7
|
+
* of three things and only the pane tells them apart (operator, 2026-06-28):
|
|
8
|
+
*
|
|
9
|
+
* - idle-CLEAN (no signature matched) → legitimately idle/done → DO NOTHING.
|
|
10
|
+
* - idle-STALLED (rate-limit / turn-abort) → `action: nudge` — resume the SAME
|
|
11
|
+
* session (a nudge preserves in-progress work; a restart loses it + re-hits a
|
|
12
|
+
* rate-limit). The gentlest recovery.
|
|
13
|
+
* - idle-BLOCKED (permission / trust / → `action: report` — a DURABLE operator
|
|
14
|
+
* skill / memory prompt) alert, NEVER auto-answered (an
|
|
15
|
+
* authorization decision needs a human;
|
|
16
|
+
* DR-033 ceremony-not-authorization).
|
|
17
|
+
*
|
|
18
|
+
* The safety contract is the sibling of DR-033's: **allowlist-only, never a blind
|
|
19
|
+
* nudge.** We act ONLY on an idle agent whose recent pane matches a KNOWN, vetted
|
|
20
|
+
* signature. An unmatched idle pane is legitimately idle/done → never touched (no
|
|
21
|
+
* spam). An unknown-but-blocking prompt falls through to no-match → skip; the
|
|
22
|
+
* operator adds a `report` signature as the TUI evolves (the allowlist is DATA).
|
|
23
|
+
*
|
|
24
|
+
* This module holds the PURE logic — the config the operator curates in
|
|
25
|
+
* `.claude/.macf/stall-signatures.json`, the schema/validator the CLI runs at
|
|
26
|
+
* `macf init/update/rules refresh`, and the frame-matcher the `fleet resume`
|
|
27
|
+
* decision layer drives off. The tested reference is the devops `resume.sh` bash
|
|
28
|
+
* (which reimplements the same match-then-dispatch in bash + jq); the two MUST
|
|
29
|
+
* stay in lockstep.
|
|
30
|
+
*/
|
|
31
|
+
import { z } from 'zod';
|
|
32
|
+
import { MacfError } from './errors.js';
|
|
33
|
+
/** A matched stall's dispatched action: nudge a stalled agent, or report a blocked one. */
|
|
34
|
+
export const STALL_ACTIONS = ['nudge', 'report'];
|
|
35
|
+
/**
|
|
36
|
+
* Per-action default fire cap when an entry omits `max_fires` — a nudge is a
|
|
37
|
+
* gentle repeatable resume (retry a few times, then escalate), a report is a
|
|
38
|
+
* one-shot durable alert per episode (re-alerting spams the operator). Mirrors
|
|
39
|
+
* the devops `resume.sh` defaults (`nudge`=3, `report`=1).
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_NUDGE_MAX_FIRES = 3;
|
|
42
|
+
export const DEFAULT_REPORT_MAX_FIRES = 1;
|
|
43
|
+
/** Thrown when the stall-signatures config fails schema / regex validation. */
|
|
44
|
+
export class StallSignaturesError extends MacfError {
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super('STALL_SIGNATURES_ERROR', message);
|
|
47
|
+
this.name = 'StallSignaturesError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A single stall-signature allowlist entry.
|
|
52
|
+
*
|
|
53
|
+
* - `name` — the signature's stable identifier (rendered in the plan / the
|
|
54
|
+
* alert title, so a fire-cap counter can key off it).
|
|
55
|
+
* - `signature` — a regular-expression source matched CASE-INSENSITIVELY against
|
|
56
|
+
* the captured idle pane (the ERE the devops bash greps with `grep -qiE`; the
|
|
57
|
+
* seeds' alternation/`\?`/`\.` forms are JS-RegExp-compatible). Validated for
|
|
58
|
+
* compilability at load — a malformed pattern is a LOUD config error, never a
|
|
59
|
+
* silent non-match.
|
|
60
|
+
* - `action` — `nudge` (resume a stalled agent) or `report` (durable alert for a
|
|
61
|
+
* blocked agent; NEVER auto-answered). Defaults to `nudge`.
|
|
62
|
+
* - `nudge` — the text sent into the TUI for `action: nudge` (default: a generic
|
|
63
|
+
* "please continue").
|
|
64
|
+
* - `report` — the operator-facing summary for `action: report` (default: a
|
|
65
|
+
* generic "blocked on an operator-input prompt").
|
|
66
|
+
* - `max_fires` — per-episode fire cap; omitted → the per-action default.
|
|
67
|
+
* - `comment` — operator documentation (why this signature; caveats). Ignored by
|
|
68
|
+
* the matcher; kept in the schema so the seed's `comment` fields validate.
|
|
69
|
+
*
|
|
70
|
+
* `.strict()` so an operator typo in a field name (e.g. `signatures`) is a loud
|
|
71
|
+
* validation error, not a silently-ignored field.
|
|
72
|
+
*/
|
|
73
|
+
export const StallSignatureEntrySchema = z
|
|
74
|
+
.object({
|
|
75
|
+
name: z.string().min(1),
|
|
76
|
+
signature: z.string().min(1),
|
|
77
|
+
action: z.enum(STALL_ACTIONS).default('nudge'),
|
|
78
|
+
nudge: z.string().min(1).optional(),
|
|
79
|
+
report: z.string().min(1).optional(),
|
|
80
|
+
max_fires: z.number().int().positive().optional(),
|
|
81
|
+
comment: z.string().optional(),
|
|
82
|
+
})
|
|
83
|
+
.strict();
|
|
84
|
+
/**
|
|
85
|
+
* The whole config: a bare array of entries — the exact shape of the devops
|
|
86
|
+
* `fleet/stall-signatures.json` reference (so the two files are interchangeable).
|
|
87
|
+
*/
|
|
88
|
+
export const StallSignaturesConfigSchema = z.array(StallSignatureEntrySchema);
|
|
89
|
+
/**
|
|
90
|
+
* Validate + regex-check a parsed config. Throws `StallSignaturesError` on a
|
|
91
|
+
* schema-invalid shape OR an uncompilable `signature` (loud — a malformed
|
|
92
|
+
* allowlist must never silently degrade to "matches nothing"). Returns the
|
|
93
|
+
* accepted entries.
|
|
94
|
+
*/
|
|
95
|
+
export function loadStallSignatures(raw) {
|
|
96
|
+
const parsed = StallSignaturesConfigSchema.safeParse(raw);
|
|
97
|
+
if (!parsed.success) {
|
|
98
|
+
throw new StallSignaturesError(`stall-signatures config is invalid: ${parsed.error.issues
|
|
99
|
+
.map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`)
|
|
100
|
+
.join('; ')}`);
|
|
101
|
+
}
|
|
102
|
+
for (const entry of parsed.data) {
|
|
103
|
+
try {
|
|
104
|
+
// Validate compilability up front (fail-loud). Case-insensitive to mirror
|
|
105
|
+
// the devops `grep -iE`.
|
|
106
|
+
new RegExp(entry.signature, 'i');
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
110
|
+
throw new StallSignaturesError(`stall-signatures entry "${entry.name}" has an invalid regex signature: ${msg}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return parsed.data;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The per-episode fire cap for an entry — its explicit `max_fires`, else the
|
|
117
|
+
* per-action default (nudge 3 / report 1).
|
|
118
|
+
*/
|
|
119
|
+
export function resolveMaxFires(entry) {
|
|
120
|
+
if (entry.max_fires !== undefined)
|
|
121
|
+
return entry.max_fires;
|
|
122
|
+
return entry.action === 'report' ? DEFAULT_REPORT_MAX_FIRES : DEFAULT_NUDGE_MAX_FIRES;
|
|
123
|
+
}
|
|
124
|
+
/** Fire-cap guard: may this stall-signature fire again given its prior fire count? */
|
|
125
|
+
export function canFireStall(entry, firedCount) {
|
|
126
|
+
return firedCount < resolveMaxFires(entry);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* First allowlist entry whose `signature` regex matches the captured pane
|
|
130
|
+
* (case-insensitive), or `null` when the pane matches nothing — the idle-CLEAN
|
|
131
|
+
* case the caller treats as "legitimately idle/done, never touched". Pure.
|
|
132
|
+
*
|
|
133
|
+
* A signature that fails to compile is skipped (defensively) rather than
|
|
134
|
+
* throwing here — `loadStallSignatures` is the fail-loud gate; the matcher stays
|
|
135
|
+
* total so a single bad entry never breaks a whole sweep.
|
|
136
|
+
*/
|
|
137
|
+
export function matchStallSignature(pane, entries) {
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
let re;
|
|
140
|
+
try {
|
|
141
|
+
re = new RegExp(entry.signature, 'i');
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (re.test(pane))
|
|
147
|
+
return entry;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The canonical seed `macf init` writes when no `stall-signatures.json` exists.
|
|
153
|
+
* Ported verbatim from `groundnuty/macf-devops-toolkit:fleet/stall-signatures.json`
|
|
154
|
+
* (the tested reference). Two `nudge` signatures (transient rate-limit / turn-abort
|
|
155
|
+
* — resume the same session) and three `report` signatures (permission / trust /
|
|
156
|
+
* skill-or-memory prompts — authorization decisions that must NEVER be
|
|
157
|
+
* auto-answered, only surfaced as a durable operator alert).
|
|
158
|
+
*
|
|
159
|
+
* Signature strings are best-effort across Claude Code versions — the allowlist is
|
|
160
|
+
* DATA; an operator tunes it against the real pane as the TUI evolves. An imperfect
|
|
161
|
+
* signature fails SAFE (falls through to no-match → the agent is left untouched,
|
|
162
|
+
* never a wrong action).
|
|
163
|
+
*/
|
|
164
|
+
export const STALL_SIGNATURES_SEED = [
|
|
165
|
+
{
|
|
166
|
+
name: 'rate-limit',
|
|
167
|
+
signature: '(temporarily limiting requests|Rate limited|API Error.*limiting)',
|
|
168
|
+
action: 'nudge',
|
|
169
|
+
nudge: 'You appear to have stalled on a transient API rate-limit (server-side, not ' +
|
|
170
|
+
'your usage quota). The limit should have cleared — please continue your work ' +
|
|
171
|
+
'where you left off.',
|
|
172
|
+
max_fires: 4,
|
|
173
|
+
comment: 'Anthropic server-side capacity throttle (NOT the account quota). Leaves the ' +
|
|
174
|
+
'agent idle mid-flow. Nudge resumes the SAME session (preserves work); restart ' +
|
|
175
|
+
'would lose it + re-hit the limit. verify-resumed self-corrects a nudge into a ' +
|
|
176
|
+
'still-active throttle (back off + retry).',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'turn-aborted',
|
|
180
|
+
signature: '(Request was aborted|aborted by user|API Error: Connection error)',
|
|
181
|
+
action: 'nudge',
|
|
182
|
+
nudge: 'Your last turn aborted before completing. Please continue your work where you left off.',
|
|
183
|
+
max_fires: 3,
|
|
184
|
+
comment: 'A turn that died on a transient connection/abort error, leaving the agent ' +
|
|
185
|
+
'idle. Same resume-not-restart logic.',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'permission-prompt',
|
|
189
|
+
signature: '(Do you want to proceed\\?|Do you want to make this edit\\?|Do you want to create|Yes, and don.t ask again)',
|
|
190
|
+
action: 'report',
|
|
191
|
+
report: 'blocked on a tool-permission prompt ("Do you want to proceed?") — needs the ' +
|
|
192
|
+
'operator to approve/deny. Cannot be auto-answered (an authorization decision, ' +
|
|
193
|
+
'per DR-033 ceremony-not-authorization).',
|
|
194
|
+
max_fires: 1,
|
|
195
|
+
comment: 'Claude Code tool-permission dialog. action=report (NOT nudge/answer): ' +
|
|
196
|
+
'authorization prompts require human judgment, so the correct automation is to ' +
|
|
197
|
+
'make the silent block LOUD, never to answer it. macf-devops-toolkit#132.',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'trust-folder-prompt',
|
|
201
|
+
signature: '(Do you trust the files in this folder\\?|trust the files in this)',
|
|
202
|
+
action: 'report',
|
|
203
|
+
report: 'blocked on a folder-trust prompt — needs the operator to confirm trust. ' +
|
|
204
|
+
'Authorization decision; cannot be auto-answered.',
|
|
205
|
+
max_fires: 1,
|
|
206
|
+
comment: "Claude Code 'trust this folder' dialog on a new/changed workspace. action=report. #132.",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'skill-or-memory-prompt',
|
|
210
|
+
signature: '(Allow .* to run\\?|Save this to memory\\?|Add this to memory\\?|Allow this skill)',
|
|
211
|
+
action: 'report',
|
|
212
|
+
report: 'blocked on a skill-approval or memory-accept prompt — needs the operator\'s ' +
|
|
213
|
+
'decision. Cannot be auto-answered.',
|
|
214
|
+
max_fires: 1,
|
|
215
|
+
comment: 'Skill-approval / memory-accept dialogs. action=report. Signature strings are ' +
|
|
216
|
+
'best-effort across CC versions — the allowlist is DATA, tune as the TUI evolves. #132.',
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
//# sourceMappingURL=stall-signatures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stall-signatures.js","sourceRoot":"","sources":["../src/stall-signatures.ts"],"names":[],"mappings":"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"}
|