wogiflow 2.29.4 → 2.29.6
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/.workflow/templates/partials/methodology-rules.hbs +74 -0
- package/README.md +1 -1
- package/lib/wogi-claude +34 -3
- package/lib/wogi-claude-expect.exp +30 -5
- package/package.json +2 -2
- package/scripts/flow-defer-auth.js +103 -0
- package/scripts/flow-utils.js +52 -0
- package/scripts/hooks/core/deferral-classifier.js +129 -0
- package/scripts/hooks/core/deferral-gate.js +379 -0
- package/scripts/hooks/core/deletion-log.js +426 -0
- package/scripts/hooks/core/pre-tool-orchestrator.js +58 -0
- package/scripts/hooks/core/research-evidence-gate.js +11 -1
- package/scripts/hooks/core/research-required-classifier.js +205 -0
- package/scripts/hooks/core/research-required-gate.js +235 -0
- package/scripts/hooks/core/session-context.js +21 -0
- package/scripts/hooks/core/task-boundary-reset.js +132 -1
- package/scripts/hooks/entry/claude-code/post-tool-use.js +34 -0
- package/scripts/hooks/entry/claude-code/stop.js +26 -0
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +39 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Mechanical Deferral Authorization Gate (wf-f9912af6)
|
|
5
|
+
*
|
|
6
|
+
* Prevents the AI from silently writing `status: deferred*` to review/audit
|
|
7
|
+
* findings. The CLAUDE.md "Review-Findings Anti-Deferral" rule was honor-
|
|
8
|
+
* system; this gate makes it mechanical.
|
|
9
|
+
*
|
|
10
|
+
* Pattern: PreToolUse intercepts Write/Edit/Bash to .workflow/state/last-review.json
|
|
11
|
+
* (and last-audit.json). It compares the new content against the prior on-disk
|
|
12
|
+
* content. If any finding's status transitions INTO a `deferred*` state and no
|
|
13
|
+
* valid authorization marker is present, the write is blocked.
|
|
14
|
+
*
|
|
15
|
+
* Authorization comes from one of:
|
|
16
|
+
* - User's UserPromptSubmit message contains explicit defer phrases
|
|
17
|
+
* ("defer X", "fix critical only", "ship as-is", etc.) → classifier
|
|
18
|
+
* writes deferral-authorization.json
|
|
19
|
+
* - Explicit CLI: `node scripts/flow-defer-auth.js grant ...`
|
|
20
|
+
*
|
|
21
|
+
* Negative intent ("fix everything", "no deferrals", "I don't want tech debt")
|
|
22
|
+
* writes a no-defer-pin.json that HARD-BLOCKS deferrals for the current turn,
|
|
23
|
+
* overriding any auth marker.
|
|
24
|
+
*
|
|
25
|
+
* Fail-open: any parse error, missing config, or unexpected exception falls
|
|
26
|
+
* through (allow the write). The block path is for confirmed deferral attempts
|
|
27
|
+
* with no auth — every other case allows the write.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
const { PATHS } = require('../../flow-utils');
|
|
33
|
+
const { safeJsonParse } = require('../../flow-io');
|
|
34
|
+
|
|
35
|
+
const AUTH_FILE = 'deferral-authorization.json';
|
|
36
|
+
const NO_DEFER_PIN_FILE = 'no-defer-pin.json';
|
|
37
|
+
const BLOCK_LOG_FILE = 'deferral-block-log.json';
|
|
38
|
+
const DEFAULT_TTL_SECONDS = 600; // 10 minutes
|
|
39
|
+
const TARGET_BASENAMES = new Set(['last-review.json', 'last-audit.json']);
|
|
40
|
+
|
|
41
|
+
function getAuthPath() { return path.join(PATHS.state, AUTH_FILE); }
|
|
42
|
+
function getNoDeferPinPath() { return path.join(PATHS.state, NO_DEFER_PIN_FILE); }
|
|
43
|
+
function getBlockLogPath() { return path.join(PATHS.state, BLOCK_LOG_FILE); }
|
|
44
|
+
|
|
45
|
+
function isGateEnabled(config) {
|
|
46
|
+
const cfg = config?.deferralGate;
|
|
47
|
+
if (cfg === false) return false;
|
|
48
|
+
if (cfg && typeof cfg === 'object' && cfg.enabled === false) return false;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getAuthTtlSeconds(config) {
|
|
53
|
+
const v = config?.deferralGate?.authTtlSeconds;
|
|
54
|
+
if (typeof v === 'number' && v > 0 && Number.isFinite(v)) return v;
|
|
55
|
+
return DEFAULT_TTL_SECONDS;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Match deferral-style status values. Conservative: any string starting with
|
|
60
|
+
* "deferred" (case-insensitive), plus common synonyms.
|
|
61
|
+
*/
|
|
62
|
+
const DEFERRAL_STATUS_RX = /^(?:deferred(?:[-_].*)?|wont-?fix|won-?t-?fix|skipped|dismissed-low-priority)$/i;
|
|
63
|
+
|
|
64
|
+
function isDeferralStatus(status) {
|
|
65
|
+
return typeof status === 'string' && DEFERRAL_STATUS_RX.test(status.trim());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Identify findings whose status transitions INTO a deferral state.
|
|
70
|
+
* Pre-existing deferrals (same finding, same status) are grandfathered.
|
|
71
|
+
*
|
|
72
|
+
* @param {Object|null} prevContent - parsed prior file content, or null if file didn't exist
|
|
73
|
+
* @param {Object} newContent - parsed new file content
|
|
74
|
+
* @returns {Array<{id: string, prevStatus: string|null, newStatus: string}>}
|
|
75
|
+
*/
|
|
76
|
+
function detectDeferralChanges(prevContent, newContent) {
|
|
77
|
+
const changes = [];
|
|
78
|
+
const newFindings = Array.isArray(newContent?.findings) ? newContent.findings : [];
|
|
79
|
+
const prevByIdMap = new Map();
|
|
80
|
+
if (prevContent && Array.isArray(prevContent.findings)) {
|
|
81
|
+
for (const f of prevContent.findings) {
|
|
82
|
+
if (f && typeof f.id === 'string') prevByIdMap.set(f.id, f);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const f of newFindings) {
|
|
86
|
+
if (!f || typeof f.id !== 'string' || !isDeferralStatus(f.status)) continue;
|
|
87
|
+
const prev = prevByIdMap.get(f.id);
|
|
88
|
+
const prevStatus = prev?.status || null;
|
|
89
|
+
if (prevStatus && isDeferralStatus(prevStatus)) continue; // grandfathered
|
|
90
|
+
changes.push({ id: f.id, prevStatus, newStatus: f.status });
|
|
91
|
+
}
|
|
92
|
+
return changes;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function loadAuth() {
|
|
96
|
+
const auth = safeJsonParse(getAuthPath(), null);
|
|
97
|
+
if (!auth || typeof auth !== 'object') return null;
|
|
98
|
+
// Expiry check
|
|
99
|
+
if (auth.expiresAt) {
|
|
100
|
+
const exp = Date.parse(auth.expiresAt);
|
|
101
|
+
if (Number.isFinite(exp) && exp < Date.now()) return null;
|
|
102
|
+
}
|
|
103
|
+
return auth;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function loadNoDeferPin() {
|
|
107
|
+
const pin = safeJsonParse(getNoDeferPinPath(), null);
|
|
108
|
+
if (!pin || typeof pin !== 'object') return null;
|
|
109
|
+
if (pin.expiresAt) {
|
|
110
|
+
const exp = Date.parse(pin.expiresAt);
|
|
111
|
+
if (Number.isFinite(exp) && exp < Date.now()) return null;
|
|
112
|
+
}
|
|
113
|
+
return pin;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function clearAuth() {
|
|
117
|
+
try { fs.unlinkSync(getAuthPath()); } catch (_err) { /* fine if absent */ }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function clearNoDeferPin() {
|
|
121
|
+
try { fs.unlinkSync(getNoDeferPinPath()); } catch (_err) { /* fine if absent */ }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeAuth({ scope = 'all', source = 'unspecified', grantedBy = 'user-prompt', ttlSec, config } = {}) {
|
|
125
|
+
try {
|
|
126
|
+
const ttl = Number.isFinite(ttlSec) ? ttlSec : getAuthTtlSeconds(config);
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
const payload = {
|
|
129
|
+
version: 1,
|
|
130
|
+
grantedAt: new Date(now).toISOString(),
|
|
131
|
+
expiresAt: new Date(now + ttl * 1000).toISOString(),
|
|
132
|
+
scope,
|
|
133
|
+
grantedBy,
|
|
134
|
+
source: typeof source === 'string' ? source.slice(0, 1000) : 'unspecified'
|
|
135
|
+
};
|
|
136
|
+
fs.mkdirSync(path.dirname(getAuthPath()), { recursive: true });
|
|
137
|
+
const tmp = `${getAuthPath()}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
|
|
138
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
139
|
+
fs.renameSync(tmp, getAuthPath());
|
|
140
|
+
return payload;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (process.env.DEBUG) console.error(`[deferral-gate] writeAuth failed: ${err.message}`);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function writeNoDeferPin({ source = 'unspecified', ttlSec = 1800 } = {}) {
|
|
148
|
+
try {
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const payload = {
|
|
151
|
+
version: 1,
|
|
152
|
+
pinnedAt: new Date(now).toISOString(),
|
|
153
|
+
expiresAt: new Date(now + ttlSec * 1000).toISOString(),
|
|
154
|
+
source: typeof source === 'string' ? source.slice(0, 1000) : 'unspecified'
|
|
155
|
+
};
|
|
156
|
+
fs.mkdirSync(path.dirname(getNoDeferPinPath()), { recursive: true });
|
|
157
|
+
const tmp = `${getNoDeferPinPath()}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
|
|
158
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
159
|
+
fs.renameSync(tmp, getNoDeferPinPath());
|
|
160
|
+
// Clear any auth — negative intent overrides positive
|
|
161
|
+
clearAuth();
|
|
162
|
+
return payload;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
if (process.env.DEBUG) console.error(`[deferral-gate] writeNoDeferPin failed: ${err.message}`);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Authorization check — given a list of finding IDs being deferred, does the
|
|
171
|
+
* current auth marker cover ALL of them?
|
|
172
|
+
*
|
|
173
|
+
* @param {Array<{id: string}>} deferralChanges
|
|
174
|
+
* @returns {{ authorized: boolean, reason: string }}
|
|
175
|
+
*/
|
|
176
|
+
function isAuthorized(deferralChanges) {
|
|
177
|
+
// No-defer pin overrides everything
|
|
178
|
+
const pin = loadNoDeferPin();
|
|
179
|
+
if (pin) {
|
|
180
|
+
return { authorized: false, reason: `no-defer-pin active (pinned at ${pin.pinnedAt}, source: ${pin.source})` };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const auth = loadAuth();
|
|
184
|
+
if (!auth) return { authorized: false, reason: 'no-auth-marker' };
|
|
185
|
+
if (auth.scope === 'all') return { authorized: true, reason: 'auth-scope-all' };
|
|
186
|
+
if (Array.isArray(auth.scope)) {
|
|
187
|
+
const authedSet = new Set(auth.scope);
|
|
188
|
+
const uncovered = deferralChanges.filter(c => !authedSet.has(c.id)).map(c => c.id);
|
|
189
|
+
if (uncovered.length === 0) return { authorized: true, reason: 'auth-covers-all-findings' };
|
|
190
|
+
return { authorized: false, reason: `auth-missing-findings: ${uncovered.join(', ')}` };
|
|
191
|
+
}
|
|
192
|
+
return { authorized: false, reason: 'auth-malformed-scope' };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function consumeAuth(deferralChanges) {
|
|
196
|
+
// Auth is single-use: once a deferral write succeeds, the marker is removed
|
|
197
|
+
// to prevent reuse on subsequent unrelated deferrals.
|
|
198
|
+
clearAuth();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function logBlock({ filePath, changes, reason }) {
|
|
202
|
+
try {
|
|
203
|
+
const logPath = getBlockLogPath();
|
|
204
|
+
const existing = safeJsonParse(logPath, { entries: [] });
|
|
205
|
+
if (!Array.isArray(existing.entries)) existing.entries = [];
|
|
206
|
+
existing.entries.push({
|
|
207
|
+
blockedAt: new Date().toISOString(),
|
|
208
|
+
filePath,
|
|
209
|
+
findingIds: changes.map(c => c.id),
|
|
210
|
+
reason
|
|
211
|
+
});
|
|
212
|
+
// Keep only last 100 entries
|
|
213
|
+
if (existing.entries.length > 100) {
|
|
214
|
+
existing.entries = existing.entries.slice(-100);
|
|
215
|
+
}
|
|
216
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
217
|
+
fs.writeFileSync(logPath, JSON.stringify(existing, null, 2));
|
|
218
|
+
} catch (_err) { /* best effort */ }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isTargetFile(filePath) {
|
|
222
|
+
if (!filePath || typeof filePath !== 'string') return false;
|
|
223
|
+
const base = path.basename(filePath);
|
|
224
|
+
return TARGET_BASENAMES.has(base);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Validate a Write/Edit operation against the deferral gate.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} filePath - path being written/edited
|
|
231
|
+
* @param {string|Object} newContentRaw - new file content (string from Write/Edit, or pre-parsed object)
|
|
232
|
+
* @param {Object} config
|
|
233
|
+
* @returns {{ blocked: boolean, message?: string }}
|
|
234
|
+
*/
|
|
235
|
+
function checkWriteGate(filePath, newContentRaw, config) {
|
|
236
|
+
try {
|
|
237
|
+
if (!isGateEnabled(config)) return { blocked: false };
|
|
238
|
+
if (!isTargetFile(filePath)) return { blocked: false };
|
|
239
|
+
|
|
240
|
+
let newContent;
|
|
241
|
+
if (typeof newContentRaw === 'string') {
|
|
242
|
+
try { newContent = JSON.parse(newContentRaw); } catch (_err) { return { blocked: false }; }
|
|
243
|
+
} else if (newContentRaw && typeof newContentRaw === 'object') {
|
|
244
|
+
newContent = newContentRaw;
|
|
245
|
+
} else {
|
|
246
|
+
return { blocked: false };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Load prior content from disk
|
|
250
|
+
const prevContent = fs.existsSync(filePath) ? safeJsonParse(filePath, null) : null;
|
|
251
|
+
|
|
252
|
+
const changes = detectDeferralChanges(prevContent, newContent);
|
|
253
|
+
if (changes.length === 0) return { blocked: false };
|
|
254
|
+
|
|
255
|
+
const authResult = isAuthorized(changes);
|
|
256
|
+
if (authResult.authorized) {
|
|
257
|
+
consumeAuth(changes);
|
|
258
|
+
return { blocked: false };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
logBlock({ filePath, changes, reason: authResult.reason });
|
|
262
|
+
return {
|
|
263
|
+
blocked: true,
|
|
264
|
+
message: buildBlockMessage(filePath, changes, authResult.reason),
|
|
265
|
+
deferralCount: changes.length
|
|
266
|
+
};
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (process.env.DEBUG) console.error(`[deferral-gate] checkWriteGate error (fail-open): ${err.message}`);
|
|
269
|
+
return { blocked: false };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Validate a Bash command against the deferral gate. Two-stage:
|
|
275
|
+
* Stage 1: does the command mention any target file basename?
|
|
276
|
+
* Stage 2: does the command also mention `deferred` literal substring?
|
|
277
|
+
* If both → fail SAFE (block) unless we can parse the content and prove auth.
|
|
278
|
+
*
|
|
279
|
+
* For v1 we don't deep-parse the bash command; we conservatively block any
|
|
280
|
+
* Bash that touches a target file AND contains a deferral status literal,
|
|
281
|
+
* pointing the AI at the Write/Edit path (which can be properly inspected).
|
|
282
|
+
*/
|
|
283
|
+
function checkBashGate(command, config) {
|
|
284
|
+
try {
|
|
285
|
+
if (!isGateEnabled(config)) return { blocked: false };
|
|
286
|
+
if (typeof command !== 'string' || !command) return { blocked: false };
|
|
287
|
+
|
|
288
|
+
const mentionsTarget = /last-(review|audit)\.json/.test(command);
|
|
289
|
+
if (!mentionsTarget) return { blocked: false };
|
|
290
|
+
|
|
291
|
+
// Heuristic: only block when the command appears to MUTATE the file
|
|
292
|
+
// (writeFileSync, redirection >, sed -i, etc.). Pure reads (cat, jq, grep)
|
|
293
|
+
// are allowed.
|
|
294
|
+
const mutates = /(?:writeFileSync|>\s*[^&|]|>>\s*[^&|]|sed\s+-i|tee\s+|fs\.write|rename(?:Sync)?)/.test(command);
|
|
295
|
+
if (!mutates) return { blocked: false };
|
|
296
|
+
|
|
297
|
+
const mentionsDeferral = /\bdeferred[-_a-zA-Z0-9]*\b|"status"\s*:\s*"(deferred|wont-?fix|skipped|dismissed)/i.test(command);
|
|
298
|
+
if (!mentionsDeferral) return { blocked: false };
|
|
299
|
+
|
|
300
|
+
// We can't easily extract and validate the new content from arbitrary bash.
|
|
301
|
+
// Check auth: if the user has authorized deferrals, allow. Otherwise block.
|
|
302
|
+
const authResult = isAuthorized([{ id: 'unspecified' }]);
|
|
303
|
+
if (authResult.authorized) return { blocked: false };
|
|
304
|
+
|
|
305
|
+
logBlock({ filePath: '(bash)', changes: [{ id: 'unparsed-bash' }], reason: `bash-mutates-target-without-auth: ${authResult.reason}` });
|
|
306
|
+
return {
|
|
307
|
+
blocked: true,
|
|
308
|
+
message:
|
|
309
|
+
`Deferral-gate: this Bash command writes to last-review.json or last-audit.json AND ` +
|
|
310
|
+
`contains a deferral status literal, but no deferral authorization is active.\n\n` +
|
|
311
|
+
`Reason: ${authResult.reason}\n\n` +
|
|
312
|
+
`Options:\n` +
|
|
313
|
+
` 1. Mark findings as 'fixed' instead of 'deferred' (after actually fixing them).\n` +
|
|
314
|
+
` 2. Get explicit user authorization. If the user has just told you to defer, run:\n` +
|
|
315
|
+
` node scripts/flow-defer-auth.js grant --scope=all --reason="<user phrase>"\n` +
|
|
316
|
+
` 3. Use the Write tool with structured JSON content — that path is properly validated\n` +
|
|
317
|
+
` and will allow the write if status changes are not deferrals.\n\n` +
|
|
318
|
+
`Reminder: CLAUDE.md "Review-Findings Anti-Deferral" — the user decides what to defer, not you.`
|
|
319
|
+
};
|
|
320
|
+
} catch (err) {
|
|
321
|
+
if (process.env.DEBUG) console.error(`[deferral-gate] checkBashGate error (fail-open): ${err.message}`);
|
|
322
|
+
return { blocked: false };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function buildBlockMessage(filePath, changes, reason) {
|
|
327
|
+
const findingList = changes.map(c => ` - ${c.id}: ${c.prevStatus || '(new)'} → ${c.newStatus}`).join('\n');
|
|
328
|
+
return (
|
|
329
|
+
`Deferral-gate BLOCKED: write to ${path.basename(filePath)} introduces ${changes.length} ` +
|
|
330
|
+
`deferral${changes.length === 1 ? '' : 's'} without authorization.\n\n` +
|
|
331
|
+
`Findings being deferred:\n${findingList}\n\n` +
|
|
332
|
+
`Reason: ${reason}\n\n` +
|
|
333
|
+
`CLAUDE.md "Review-Findings Anti-Deferral": "Never silently convert a finding to ` +
|
|
334
|
+
`'deferred' without the user explicitly saying 'defer X.'"\n\n` +
|
|
335
|
+
`Options:\n` +
|
|
336
|
+
` 1. Fix the findings instead — mark status: 'fixed' after actually fixing them.\n` +
|
|
337
|
+
` 2. Ask the user explicitly: "Finding X requires ~Y min. Ship / fix / defer? Your call."\n` +
|
|
338
|
+
` 3. If the user already authorized deferrals (e.g., picked option 4 in /wogi-review),\n` +
|
|
339
|
+
` record that explicitly:\n` +
|
|
340
|
+
` node scripts/flow-defer-auth.js grant --scope=all --reason="<verbatim user phrase>"\n` +
|
|
341
|
+
` OR for specific findings:\n` +
|
|
342
|
+
` node scripts/flow-defer-auth.js grant --findings=F5,F6 --reason="..."\n\n` +
|
|
343
|
+
`If a 'no-defer-pin' is active, the user has explicitly forbidden deferrals — fix the ` +
|
|
344
|
+
`findings or surface them as user-decision items.`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports = {
|
|
349
|
+
// Core checks
|
|
350
|
+
checkWriteGate,
|
|
351
|
+
checkBashGate,
|
|
352
|
+
|
|
353
|
+
// Auth API (used by classifier + CLI helper)
|
|
354
|
+
loadAuth,
|
|
355
|
+
loadNoDeferPin,
|
|
356
|
+
writeAuth,
|
|
357
|
+
writeNoDeferPin,
|
|
358
|
+
clearAuth,
|
|
359
|
+
clearNoDeferPin,
|
|
360
|
+
consumeAuth,
|
|
361
|
+
isAuthorized,
|
|
362
|
+
|
|
363
|
+
// Detection helpers
|
|
364
|
+
detectDeferralChanges,
|
|
365
|
+
isDeferralStatus,
|
|
366
|
+
isTargetFile,
|
|
367
|
+
|
|
368
|
+
// Diagnostics
|
|
369
|
+
getAuthPath,
|
|
370
|
+
getNoDeferPinPath,
|
|
371
|
+
getBlockLogPath,
|
|
372
|
+
isGateEnabled,
|
|
373
|
+
getAuthTtlSeconds,
|
|
374
|
+
|
|
375
|
+
// Constants
|
|
376
|
+
TARGET_BASENAMES,
|
|
377
|
+
DEFAULT_TTL_SECONDS,
|
|
378
|
+
DEFERRAL_STATUS_RX
|
|
379
|
+
};
|