@stravica-ai/digital-operator-pa 0.1.57 → 0.1.59

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.
@@ -4,10 +4,10 @@
4
4
  //
5
5
  // `pa-operator-attendance session-start`
6
6
  // Wired into Claude Code's SessionStart hook. Computes the attendance
7
- // stamp (TTY + env + no grant), persists it to a per-session cache,
8
- // and emits a one-line `additionalContext` block carrying
9
- // `operator-present: true|false[...]` for the loaded agent to read as
10
- // the first line of its workState context.
7
+ // stamp (env + parent-process TTY + no grant), persists it to a
8
+ // per-session cache, and emits a one-line `additionalContext` block
9
+ // carrying `operator-present: true|false[...]` for the loaded agent
10
+ // to read as the first line of its workState context.
11
11
  //
12
12
  // `pa-operator-attendance pre-tool-use`
13
13
  // Wired into Claude Code's PreToolUse hook, matcher Bash + Edit +
@@ -31,10 +31,33 @@
31
31
  //
32
32
  // Zero dependencies beyond node stdlib and the shared grant module in
33
33
  // `@stravica-ai/engineering-pa-core/src/attendance/grant.mjs`.
34
+ //
35
+ // Attendance detection (0.1.58 fix for w-2026-10-02-dave-044, from Dex's
36
+ // 0.1.57 review): the hook's own stdout is always a pipe (the client
37
+ // reads the JSON envelope), so stdout.isTTY cannot discriminate. The
38
+ // discriminator is the hook's PARENT process: in an attended session
39
+ // the parent is the claude client attached to the operator's terminal
40
+ // (ps reports a real tty, e.g. `ttys000`); in a `claude -p` worker or a
41
+ // `claude --print` child the parent is a bash shell under a Bash tool
42
+ // with no controlling tty (ps reports `??`). We therefore probe
43
+ // `ps -o tty= -p <ppid>` and treat a non-empty, non-`?`/non-`??` result
44
+ // as a real tty. On win32 ps is absent, so we fall back to the env var
45
+ // alone and record `ttyProbe: 'unavailable'` on the stamp so the
46
+ // limitation is visible in the decision log.
47
+ //
48
+ // Pattern un-anchoring and log redaction (0.1.58 fixes B and C): the
49
+ // raw-claude and unattended-mutate regexes tolerate any number of
50
+ // leading `NAME=value` (and `env NAME=value`) prefixes so a line like
51
+ // `GITHUB_TOKEN=x gh pr create` still matches, and the Bash command is
52
+ // run through `redactBashCommand` before it lands in the decision log,
53
+ // stripping values of `NAME=value` prefixes, `Authorization:` header
54
+ // values, `--token`/`token=` style arguments, and
55
+ // `https://user:secret@` URL credentials.
34
56
 
35
57
  import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
36
58
  import { dirname, join } from 'node:path';
37
59
  import { tmpdir } from 'node:os';
60
+ import { spawnSync } from 'node:child_process';
38
61
  import {
39
62
  loadOrMintHostKey,
40
63
  verifyGrant,
@@ -60,8 +83,8 @@ Environment:
60
83
  PA_ATTENDANCE_LOG_DIR=/path/to/log/dir override decision-log dir
61
84
 
62
85
  Decision log (observe mode): one JSONL line per would-deny at
63
- <logDir>/attendance-decisions.jsonl. Rejection log: a hostile grant lands
64
- at <logDir>/attendance-rejects.log.
86
+ <logDir>/attendance-decisions.jsonl, with Bash commands redacted. A
87
+ hostile grant lands at <logDir>/attendance-rejects.log.
65
88
 
66
89
  Usage:
67
90
  pa-operator-attendance session-start
@@ -69,37 +92,128 @@ Usage:
69
92
  pa-operator-attendance --help
70
93
  `;
71
94
 
72
- const RAW_CLAUDE_PATTERN = /^\s*claude(\s+[^-\s]|\s*$)/;
95
+ // Env-prefix tolerance: zero or more `NAME=value` tokens (optionally
96
+ // preceded by `env`) before the command word.
97
+ const ENV_PREFIX = String.raw`(?:(?:env\s+)?[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*`;
98
+
99
+ const RAW_CLAUDE_PATTERN = new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`claude(\s+[^-\s]|\s*$)`);
73
100
 
74
101
  // Bash commands denied for an unattended + no-grant session. Any line
75
102
  // matching one of these is a durable estate side effect: a commit, a
76
103
  // push, a write through the pa-family CLI, a redirect, a tee, an rm.
77
104
  // Read-only verbs (git status, git log, ls, cat, grep, etc.) are not
78
- // listed and pass through.
105
+ // listed and pass through. Each pattern begins with `^\s*${ENV_PREFIX}`
106
+ // so a leading `GITHUB_TOKEN=x` or `env NAME=value` does not bypass the
107
+ // rule.
79
108
  const UNATTENDED_MUTATE_PATTERNS = [
80
- /^\s*git\s+(commit|push|merge|rebase|cherry-pick|reset|checkout|branch|tag|stash|am|revert|pull)\b/,
81
- /^\s*gh\s+(pr\s+(create|edit|merge|close|comment|review)|repo\s+(create|edit|delete|archive)|issue\s+(create|edit|close|comment)|release\s+(create|edit|delete)|workflow\s+(enable|disable|run)|secret\s+(set|remove))\b/,
82
- /^\s*(pnpm\s+exec\s+)?pa\s+(work|project|library|relay|autonomy|content-pub|secrets|scheduler)\s+(?!(?:help|list|ls|get|show|view|status|tail|find|search|export|path|inspect|whoami|context|overview|today|related|domain|orphans|read|print))/,
83
- /^\s*(pnpm\s+exec\s+)?library\s+(put|flush|refresh-clones|up-merge)\b/,
84
- /^\s*(pnpm\s+exec\s+)?claude-relay\s+(send|publish|post)\b/,
85
- /^\s*rm\b/,
86
- /^\s*mv\b/,
87
- /^\s*cp\b/,
88
- /^\s*mkdir\b/,
89
- /^\s*touch\b/,
90
- /^\s*(ln|chmod|chown)\b/,
91
- /^\s*(tee|dd)\b/,
92
- /^\s*(apt|apt-get|brew|npm|pnpm|yarn)\s+(install|add|remove|uninstall|upgrade|update|publish)\b/,
109
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`git\s+(commit|push|merge|rebase|cherry-pick|reset|checkout|branch|tag|stash|am|revert|pull)\b`),
110
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`gh\s+(pr\s+(create|edit|merge|close|comment|review)|repo\s+(create|edit|delete|archive)|issue\s+(create|edit|close|comment)|release\s+(create|edit|delete)|workflow\s+(enable|disable|run)|secret\s+(set|remove))\b`),
111
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`(pnpm\s+exec\s+)?pa\s+(work|project|library|relay|autonomy|content-pub|secrets|scheduler)\s+(?!(?:help|list|ls|get|show|view|status|tail|find|search|export|path|inspect|whoami|context|overview|today|related|domain|orphans|read|print))`),
112
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`(pnpm\s+exec\s+)?library\s+(put|flush|refresh-clones|up-merge)\b`),
113
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`(pnpm\s+exec\s+)?claude-relay\s+(send|publish|post)\b`),
114
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`rm\b`),
115
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`mv\b`),
116
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`cp\b`),
117
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`mkdir\b`),
118
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`touch\b`),
119
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`(ln|chmod|chown)\b`),
120
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`(tee|dd)\b`),
121
+ new RegExp(String.raw`^\s*` + ENV_PREFIX + String.raw`(apt|apt-get|brew|npm|pnpm|yarn)\s+(install|add|remove|uninstall|upgrade|update|publish)\b`),
93
122
  /[^<>]>\s*\S/, // `foo > bar`, `foo 2> bar`: any single-redirect write
94
123
  />>\s*\S/, // append redirect
95
124
  ];
96
125
 
126
+ // Default TTY probe: ask `ps` for the controlling TTY of the hook's
127
+ // parent process. Returns `null` on no-tty (`??` / `?` / empty) or
128
+ // probe error, a non-empty TTY name (e.g. `ttys000`, `pts/0`) when the
129
+ // parent has one, and `'unavailable'` on win32 (where `ps` is absent).
130
+ // Fail-safe: any error path returns `null` so the detection drops to
131
+ // "unattended", which never grants writes it should not.
132
+ function defaultTtyProbe({ ppid, platform }) {
133
+ if (platform === 'win32') return 'unavailable';
134
+ if (!Number.isInteger(ppid) || ppid <= 0) return null;
135
+ try {
136
+ const result = spawnSync('ps', ['-o', 'tty=', '-p', String(ppid)], {
137
+ encoding: 'utf8',
138
+ timeout: 2000,
139
+ });
140
+ if (result.error || result.status !== 0) return null;
141
+ const tty = (result.stdout || '').trim();
142
+ if (!tty || tty === '?' || tty === '??') return null;
143
+ return tty;
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ // Resolve the detection: returns { attended, parentTty, envAttended,
150
+ // grantPresent }. The attended gate is positive-proof: env === '1' AND
151
+ // a real controlling TTY on the parent process AND no grant. On a
152
+ // platform where the TTY probe is unavailable (win32), fall back to the
153
+ // env var alone so a Windows operator can still mark the session
154
+ // attended; the stamp records `ttyProbe: 'unavailable'` so the
155
+ // limitation is visible in the decision log.
156
+ function resolveAttendance({ env, ttyProbe, platform, ppid }) {
157
+ const envAttended = env.CLAUDE_CODE_SESSION_ATTENDED === '1';
158
+ const grantPresent = typeof env.PA_WORKER_GRANT === 'string' && env.PA_WORKER_GRANT.length > 0;
159
+ const parentTty = ttyProbe({ ppid, platform, env });
160
+ let attended;
161
+ if (parentTty === 'unavailable') {
162
+ attended = envAttended && !grantPresent;
163
+ } else {
164
+ attended = envAttended && typeof parentTty === 'string' && parentTty.length > 0 && !grantPresent;
165
+ }
166
+ return { attended, parentTty, envAttended, grantPresent };
167
+ }
168
+
169
+ // Redact a Bash command before writing it into the decision log or any
170
+ // stderr trace line. Values redacted:
171
+ // 1. `NAME=value` and `env NAME=value` prefixes (and inline forms):
172
+ // the key is kept, the value is replaced with `<redacted>`.
173
+ // 2. `Authorization: Bearer ...` and `Authorization: ...` header
174
+ // values.
175
+ // 3. `--token <x>`, `--token=<x>`, `token=<x>` argument-borne tokens.
176
+ // 4. `https://user:secret@host` URL credentials: user kept, secret
177
+ // replaced.
178
+ export function redactBashCommand(cmd) {
179
+ if (typeof cmd !== 'string') return cmd;
180
+ let out = cmd;
181
+ // user:secret@host URL credential (do this BEFORE the KEY=VALUE pass
182
+ // so the scheme:// does not accidentally feed the key regex).
183
+ out = out.replace(/(https?:\/\/[^:/\s@]+):([^@\s'"`]+)@/gi, '$1:<redacted>@');
184
+ // Authorization header value (any scheme: Bearer, token, Basic,
185
+ // Digest, or an unknown word; and the raw-value no-scheme form). Three
186
+ // passes handle the single-quoted, double-quoted, and unquoted header
187
+ // forms. The quoted passes consume up to the closing quote so secrets
188
+ // that contain whitespace or inner punctuation (token schemes, Basic
189
+ // base64, Digest comma-separated field lists with inner "quoted"
190
+ // fields) never leak past the scheme word. 0.1.58 only redacted the
191
+ // scheme word for non-Bearer schemes, so `Authorization: token <x>`
192
+ // landed in the log as `Authorization: <redacted> <x>` (Dex's 0.1.58
193
+ // verification). 0.1.59 extends the redaction to the full header
194
+ // value.
195
+ out = out.replace(/'([^'\n]*?Authorization\s*:\s*)[^'\n]+(?=')/gi, (_m, pre) => `'${pre}<redacted>`);
196
+ out = out.replace(/"([^"\n]*?Authorization\s*:\s*)[^"\n]+(?=")/gi, (_m, pre) => `"${pre}<redacted>`);
197
+ out = out.replace(/(Authorization\s*:\s*)[^\s'"`,;)]+(?:\s+[^\s'"`,;)]+)?/gi, '$1<redacted>');
198
+ // --token / --token= and token=
199
+ out = out.replace(/(--?token[\s=]+)([^\s'"`;)]+)/gi, '$1<redacted>');
200
+ // NAME=value (env-style). Match a word boundary that is not preceded
201
+ // by `-` or `=` so `--foo=bar` and `=key=val` do not re-match the
202
+ // trailing key.
203
+ out = out.replace(/(^|[\s;&|`(])((?:env\s+)?)([A-Za-z_][A-Za-z0-9_]*)=([^\s'"`]+)/g,
204
+ (_m, lead, envWord, key, _val) => `${lead}${envWord}${key}=<redacted>`);
205
+ return out;
206
+ }
207
+
97
208
  export async function main(argv = [], {
98
209
  stdin = process.stdin,
99
210
  stdout = process.stdout,
100
211
  stderr = process.stderr,
101
212
  env = process.env,
102
213
  now = () => new Date(),
214
+ ttyProbe = defaultTtyProbe,
215
+ platform = process.platform,
216
+ ppid = process.ppid,
103
217
  } = {}) {
104
218
  if (argv.includes('-h') || argv.includes('--help')) {
105
219
  stderr.write(USAGE);
@@ -112,10 +226,10 @@ export async function main(argv = [], {
112
226
  }
113
227
  try {
114
228
  if (sub === 'session-start') {
115
- return await runSessionStart({ stdin, stdout, stderr, env, now });
229
+ return await runSessionStart({ stdin, stdout, stderr, env, now, ttyProbe, platform, ppid });
116
230
  }
117
231
  if (sub === 'pre-tool-use') {
118
- return await runPreToolUse({ stdin, stdout, stderr, env, now });
232
+ return await runPreToolUse({ stdin, stdout, stderr, env, now, ttyProbe, platform, ppid });
119
233
  }
120
234
  stderr.write(`pa-operator-attendance: unknown subcommand "${sub}"\n`);
121
235
  return 1;
@@ -131,25 +245,13 @@ export async function main(argv = [], {
131
245
 
132
246
  // --- session-start ---------------------------------------------------------
133
247
 
134
- async function runSessionStart({ stdin, stdout, stderr, env, now }) {
248
+ async function runSessionStart({ stdin, stdout, stderr, env, now, ttyProbe, platform, ppid }) {
135
249
  const payload = await readPayload(stdin);
136
250
  // SessionStart has no required fields we gate on; we still parse it so
137
251
  // a payload with `session_id` can key the cache properly.
138
252
  const sessionId = (payload && payload.session_id) || env.CLAUDE_CODE_SESSION_ID || 'unknown';
139
253
 
140
- // Attendance detection. The empirical probe for w-2026-10-02-dave-018
141
- // (session-start leg) proved against client 2.1.287 that:
142
- // - a `claude -p` child always sees CLAUDE_CODE_SESSION_ATTENDED=0
143
- // (not unset; the client zeroes it explicitly)
144
- // - a SessionStart hook fired by a `claude -p` child sees both
145
- // stdin and stdout as non-TTY
146
- // So the positive-proof gate is: stdout.isTTY AND env === '1' AND no
147
- // grant. Any other combination is unattended, which is the fail-safe
148
- // direction (a client bump that strips the var defaults to absent).
149
- const grantPresent = typeof env.PA_WORKER_GRANT === 'string' && env.PA_WORKER_GRANT.length > 0;
150
- const envAttended = env.CLAUDE_CODE_SESSION_ATTENDED === '1';
151
- const stdoutIsTty = Boolean(stdout.isTTY);
152
- const attended = stdoutIsTty && envAttended && !grantPresent;
254
+ const { attended, parentTty, envAttended, grantPresent } = resolveAttendance({ env, ttyProbe, platform, ppid });
153
255
 
154
256
  let stamp;
155
257
  if (attended) {
@@ -209,7 +311,7 @@ async function runSessionStart({ stdin, stdout, stderr, env, now }) {
209
311
  decision: 'stamp',
210
312
  stamp,
211
313
  sessionId,
212
- attendedSignals: { stdoutIsTty, envAttended, grantPresent },
314
+ attendedSignals: { parentTty, envAttended, grantPresent, platform },
213
315
  });
214
316
  return 0;
215
317
  }
@@ -224,7 +326,7 @@ function formatStampLine(stamp) {
224
326
 
225
327
  // --- pre-tool-use ----------------------------------------------------------
226
328
 
227
- async function runPreToolUse({ stdin, stdout, stderr, env, now }) {
329
+ async function runPreToolUse({ stdin, stdout, stderr, env, now, ttyProbe, platform, ppid }) {
228
330
  const mode = env.PA_OPERATOR_ATTENDANCE_MODE === 'enforce' ? 'enforce' : 'observe';
229
331
  let payload;
230
332
  try {
@@ -240,7 +342,7 @@ async function runPreToolUse({ stdin, stdout, stderr, env, now }) {
240
342
  return 1;
241
343
  }
242
344
  const sessionId = (payload && payload.session_id) || env.CLAUDE_CODE_SESSION_ID || 'unknown';
243
- const stamp = await readStamp({ env, sessionId }) || recomputeStamp(env);
345
+ const stamp = await readStamp({ env, sessionId }) || recomputeStamp({ env, ttyProbe, platform, ppid });
244
346
 
245
347
  const bashCommand = toolName === 'Bash' && typeof toolInput.command === 'string'
246
348
  ? toolInput.command
@@ -250,7 +352,8 @@ async function runPreToolUse({ stdin, stdout, stderr, env, now }) {
250
352
  // The flag-only forms (`claude --help`, `claude --version`, `claude -v`,
251
353
  // `claude -h`) pass; everything else (`claude help foo`, `claude -p`,
252
354
  // `claude mcp list`, bare `claude`) is a session launch and must go
253
- // through the launcher.
355
+ // through the launcher. Leading `NAME=value` prefixes do not bypass
356
+ // the rule (0.1.58 fix B).
254
357
  if (bashCommand) {
255
358
  for (const line of bashCommand.split('\n')) {
256
359
  if (RAW_CLAUDE_PATTERN.test(line)) {
@@ -299,7 +402,7 @@ async function runPreToolUse({ stdin, stdout, stderr, env, now }) {
299
402
  return 0;
300
403
  }
301
404
 
302
- function recomputeStamp(env) {
405
+ function recomputeStamp({ env, ttyProbe, platform, ppid }) {
303
406
  // Fallback when the session-start cache is missing (hook ordering, cold
304
407
  // start). Mirror the session-start detection without the key material,
305
408
  // which means a grant that LOOKS valid shape-wise is treated as
@@ -307,24 +410,24 @@ function recomputeStamp(env) {
307
410
  // closed if session-start's richer stamp was lost, but it does not
308
411
  // fabricate a verdict it does not have evidence for.
309
412
  const grantPresent = typeof env.PA_WORKER_GRANT === 'string' && env.PA_WORKER_GRANT.length > 0;
310
- const envAttended = env.CLAUDE_CODE_SESSION_ATTENDED === '1';
311
- // In the pre-tool-use leg, stdout is always the hook pipe, so TTY check
312
- // alone is unreliable. Trust the env signal only when there is no grant.
313
413
  if (grantPresent) return { operatorPresent: false, grant: 'present', scope: 'dispatch-only' };
314
- if (envAttended) return { operatorPresent: true, grant: 'none', scope: 'attended' };
315
- return { operatorPresent: false, grant: 'none', scope: 'refusal', refusal: 'no stamp cache, env signals absent' };
414
+ const { attended } = resolveAttendance({ env, ttyProbe, platform, ppid });
415
+ if (attended) return { operatorPresent: true, grant: 'none', scope: 'attended' };
416
+ return { operatorPresent: false, grant: 'none', scope: 'refusal', refusal: 'no stamp cache, env/parent-tty signals absent' };
316
417
  }
317
418
 
318
419
  async function denyOrObserve({ env, now, mode, stdout, stderr, sessionId, toolName, bashCommand, stamp, rule, matched, message }) {
420
+ const safeBashCommand = bashCommand == null ? null : redactBashCommand(bashCommand);
421
+ const safeMatched = typeof matched === 'string' ? redactBashCommand(matched) : matched;
319
422
  await appendDecisionLog({
320
423
  env,
321
424
  now: now(),
322
425
  leg: 'pre-tool-use',
323
426
  decision: mode === 'enforce' ? 'deny' : 'observe',
324
427
  rule,
325
- matched,
428
+ matched: safeMatched,
326
429
  toolName,
327
- bashCommand,
430
+ bashCommand: safeBashCommand,
328
431
  stamp,
329
432
  sessionId,
330
433
  mode,
@@ -335,7 +438,9 @@ async function denyOrObserve({ env, now, mode, stdout, stderr, sessionId, toolNa
335
438
  }
336
439
  // Observe mode: write a one-line stderr note so the operator can see
337
440
  // what would have been denied without the tool call being blocked.
338
- stderr.write(`pa-operator-attendance: OBSERVE would deny (${rule}: ${matched})\n`);
441
+ // The matched text is redacted so a secret-bearing command does not
442
+ // leak into stderr either.
443
+ stderr.write(`pa-operator-attendance: OBSERVE would deny (${rule}: ${safeMatched})\n`);
339
444
  return 0;
340
445
  }
341
446
 
@@ -212,9 +212,9 @@ The stamp is produced by `pa-operator-attendance session-start` (a Claude Code S
212
212
 
213
213
  A dispatch that genuinely needs its own child session goes through `pa-worker-launcher`, which mints the signed grant, strips the attended envelope, and spawns `claude --print` with `--setting-sources user` from a neutral cwd. Workers never invoke `claude` directly from Bash: the coordinator-hook deny plus the attendance pre-tool-use leg catch both.
214
214
 
215
- Rollout is observe-mode by default for one point release: every would-deny lands as a JSONL line under `~/.local/share/pa/attendance-decisions.jsonl` but the tool call proceeds, so operators can inspect the gate's behaviour on real traffic before `PA_OPERATOR_ATTENDANCE_MODE=enforce` becomes the shipped default. Attendance detection is positive-proof: `stdout.isTTY && CLAUDE_CODE_SESSION_ATTENDED === '1' && !PA_WORKER_GRANT`. Any other combination is unattended, which is the fail-safe direction. A client bump that strips the env var defaults the gate to "unattended" and surfaces loudly rather than silently failing open.
215
+ Rollout is observe-mode by default for one point release: every would-deny lands as a JSONL line under `~/.local/share/pa/attendance-decisions.jsonl` (with Bash commands redacted so argument-borne tokens, Authorization headers and URL credentials never land in the log) but the tool call proceeds, so operators can inspect the gate's behaviour on real traffic before `PA_OPERATOR_ATTENDANCE_MODE=enforce` becomes the shipped default. Attendance detection is positive-proof: `CLAUDE_CODE_SESSION_ATTENDED === '1' && parentHasControllingTty && !PA_WORKER_GRANT`. The hook's own stdout is always a pipe (the client reads the JSON envelope), so the discriminator is the hook's PARENT process: in an attended session the parent is the claude client attached to the operator's terminal; in a `claude -p` worker or any Bash-tool child the parent is a bash shell with no controlling tty. We probe `ps -o tty= -p <ppid>` on POSIX and treat a non-empty, non-`?`/non-`??` result as a real tty; on win32 we fall back to the env var alone and record `parentTty: 'unavailable'` on the stamp. Any other combination is unattended, which is the fail-safe direction. A client bump that strips the env var defaults the gate to "unattended" and surfaces loudly rather than silently failing open.
216
216
 
217
- Reference: `packages/cli/src/operator-attendance/cli.mjs`, `packages/cli/src/worker-launcher/cli.mjs`, `packages/core/src/attendance/grant.mjs`. Rationale and incident: `w-2026-10-02-dave-016` (review), `w-2026-10-02-dave-018` (build).
217
+ Reference: `packages/cli/src/operator-attendance/cli.mjs`, `packages/cli/src/worker-launcher/cli.mjs`, `packages/core/src/attendance/grant.mjs`. Rationale and incidents: `w-2026-10-02-dave-016` (review), `w-2026-10-02-dave-018` (build), `w-2026-10-02-dave-044` (0.1.58 detection + pattern + redaction fix, from Dex's 0.1.57 review).
218
218
 
219
219
  ---
220
220
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stravica-ai/digital-operator-pa",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "description": "Digital Operator PA: single-bundled public package surface (bins + library) for the platform.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",