wotann 0.5.92 → 0.5.94

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.
@@ -7,9 +7,13 @@
7
7
  * escalation, agent config persistence, hardcoded secrets, and invisible text.
8
8
  */
9
9
  import { createHash } from "node:crypto";
10
- import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, } from "node:fs";
10
+ import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
11
11
  import { extname, isAbsolute, join, relative, resolve } from "node:path";
12
- export const TRUSTED_SKILL_REPOS = new Set(["openai/skills", "anthropics/skills", "huggingface/skills"]);
12
+ export const TRUSTED_SKILL_REPOS = new Set([
13
+ "openai/skills",
14
+ "anthropics/skills",
15
+ "huggingface/skills",
16
+ ]);
13
17
  export const INSTALL_POLICY = {
14
18
  builtin: ["allow", "allow", "allow"],
15
19
  trusted: ["allow", "allow", "block"],
@@ -405,7 +409,14 @@ export function contentHash(path) {
405
409
  return `sha256:${h.digest("hex").slice(0, 16)}`;
406
410
  }
407
411
  function p(pattern, patternId, severity, category, description, recommendation) {
408
- return { regex: new RegExp(pattern, "i"), patternId, severity, category, description, recommendation };
412
+ return {
413
+ regex: new RegExp(pattern, "i"),
414
+ patternId,
415
+ severity,
416
+ category,
417
+ description,
418
+ recommendation,
419
+ };
409
420
  }
410
421
  function buildScanResult(input) {
411
422
  const issues = deduplicateIssues(input.findings);
@@ -489,7 +500,17 @@ function checkStructure(skillDir) {
489
500
  return findings;
490
501
  }
491
502
  function structuralIssue(patternId, severity, category, file, match, description, recommendation) {
492
- return { pattern: category, patternId, category, file, line: 0, match, severity, description, recommendation };
503
+ return {
504
+ pattern: category,
505
+ patternId,
506
+ category,
507
+ file,
508
+ line: 0,
509
+ match,
510
+ severity,
511
+ description,
512
+ recommendation,
513
+ };
493
514
  }
494
515
  function listFiles(root) {
495
516
  return listEntries(root).filter((entry) => {
@@ -524,7 +545,7 @@ function isInside(child, parent) {
524
545
  function worstSeverity(issues) {
525
546
  if (issues.length === 0)
526
547
  return "info";
527
- return issues.reduce((worst, issue) => (SEVERITY_ORDER[issue.severity] < SEVERITY_ORDER[worst] ? issue.severity : worst), "info");
548
+ return issues.reduce((worst, issue) => SEVERITY_ORDER[issue.severity] < SEVERITY_ORDER[worst] ? issue.severity : worst, "info");
528
549
  }
529
550
  function compareIssueSeverity(a, b) {
530
551
  return SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
@@ -38,6 +38,20 @@ export interface RawModeStream {
38
38
  * state is preserved when it works) inside a try/catch. If it throws
39
39
  * here, the resolver falls through to `/dev/tty` or refuses cleanly
40
40
  * instead of mounting Ink into the silent loop.
41
+ *
42
+ * Node 25 `_handle` exception (added 2026-05-28 — empirical evidence
43
+ * from user diagnostic): on Node v25.x, the probe call throws
44
+ * "Cannot read properties of undefined (reading '_handle')"
45
+ * because the libuv TTY binding (`this._handle`) isn't initialized at
46
+ * probe time — Node 25 lazily binds it on first I/O. The same
47
+ * `setRawMode` call AT INK'S RENDER COMMIT succeeds because by then
48
+ * Ink has read from the stream and the binding is live. Treating
49
+ * this specific error as "passes" (optimistic) lets the mount
50
+ * proceed; the `mount-interactive-ink` Ink-throw backstop catches
51
+ * any genuine failure at render time and writes the same actionable
52
+ * guidance. False-positive cost: a runtime error message instead of
53
+ * a guard refusal — same end UX. True-positive value: working TUI
54
+ * under Node 25, which is the user's current default.
41
55
  */
42
56
  export declare function isRawModeCapable(stream: unknown): boolean;
43
57
  export interface ResolveStdinOptions {
@@ -46,10 +60,26 @@ export interface ResolveStdinOptions {
46
60
  }
47
61
  /**
48
62
  * Resolve an input stream Ink can drive `useInput()` with:
49
- * 1. `stdin` itself if it is already raw-capable.
50
- * 2. else the controlling terminal (`/dev/tty`) if raw-capable.
63
+ * 1. controlling terminal (`/dev/tty`) if raw-capable — preferred.
64
+ * 2. else `stdin` if it is raw-capable — fallback for CI / Windows /
65
+ * detached processes that have no controlling terminal.
51
66
  * 3. else `null` — caller MUST NOT mount the interactive Ink app
52
67
  * (it would hang); print guidance + exit instead.
68
+ *
69
+ * Why /dev/tty FIRST (changed 2026-05-28 from stdin-first):
70
+ * Under `npx wotann`, `npm exec`, `sudo`, and other launchers, the
71
+ * subprocess's `process.stdin` can pass the static shape check
72
+ * (`isTTY === true`, `setRawMode` is a function) yet behave oddly
73
+ * when Ink actually drives it — the probe's no-op `setRawMode(false)`
74
+ * succeeds while Ink's `setRawMode(true)` then fails, the stream is in
75
+ * flowing mode because some boot import attached a `data` listener,
76
+ * the launcher transformed the descriptor, etc. The controlling
77
+ * terminal is the canonical "user's keyboard" source and is
78
+ * unaffected by any of that — the same pattern `less`, `vim`, `sudo`,
79
+ * and `git rebase -i` use for stdin-needs-tty scenarios. The fallback
80
+ * to `stdin` covers the no-`/dev/tty` cases (CI containers, Windows
81
+ * without a console, detached processes).
82
+ *
53
83
  * Never throws.
54
84
  */
55
85
  export declare function resolveInteractiveStdin(stdin?: unknown, opts?: ResolveStdinOptions): NodeJS.ReadStream | null;
@@ -77,10 +107,14 @@ export interface StdinDiagnosis {
77
107
  readonly ttyProbeResult?: string;
78
108
  }
79
109
  /**
80
- * Walks the same `resolveInteractiveStdin` steps and records what each
81
- * one saw. Use this when the resolver returns `null` to print a
82
- * self-explanatory refusal future bug reports will include the
83
- * diagnosis automatically instead of just "guard refused, why?".
110
+ * Records the full state of BOTH potential input sources — /dev/tty
111
+ * (the preferred source after the 2026-05-28 reorder) and `stdin`
112
+ * (the fallback). Always walks both regardless of whether the
113
+ * resolver would have short-circuited, because the diagnostic is most
114
+ * useful when something looks wrong with one source: the user / next
115
+ * agent gets the full picture, not "stdin worked, didn't look at tty".
116
+ *
117
+ * Never throws.
84
118
  */
85
119
  export declare function diagnoseStdin(stdin?: unknown, opts?: ResolveStdinOptions): StdinDiagnosis;
86
120
  /**
@@ -35,6 +35,20 @@ import { ReadStream } from "node:tty";
35
35
  * state is preserved when it works) inside a try/catch. If it throws
36
36
  * here, the resolver falls through to `/dev/tty` or refuses cleanly
37
37
  * instead of mounting Ink into the silent loop.
38
+ *
39
+ * Node 25 `_handle` exception (added 2026-05-28 — empirical evidence
40
+ * from user diagnostic): on Node v25.x, the probe call throws
41
+ * "Cannot read properties of undefined (reading '_handle')"
42
+ * because the libuv TTY binding (`this._handle`) isn't initialized at
43
+ * probe time — Node 25 lazily binds it on first I/O. The same
44
+ * `setRawMode` call AT INK'S RENDER COMMIT succeeds because by then
45
+ * Ink has read from the stream and the binding is live. Treating
46
+ * this specific error as "passes" (optimistic) lets the mount
47
+ * proceed; the `mount-interactive-ink` Ink-throw backstop catches
48
+ * any genuine failure at render time and writes the same actionable
49
+ * guidance. False-positive cost: a runtime error message instead of
50
+ * a guard refusal — same end UX. True-positive value: working TUI
51
+ * under Node 25, which is the user's current default.
38
52
  */
39
53
  export function isRawModeCapable(stream) {
40
54
  if (stream === null || typeof stream !== "object")
@@ -48,10 +62,22 @@ export function isRawModeCapable(stream) {
48
62
  setRawMode(currentIsRaw);
49
63
  return true;
50
64
  }
51
- catch {
52
- return false;
65
+ catch (e) {
66
+ return isNode25HandleInitDefer(e);
53
67
  }
54
68
  }
69
+ /**
70
+ * Detect the Node 25+ "_handle is undefined at probe time" pattern.
71
+ * Matches both the literal `_handle` reference and the broader libuv
72
+ * binding init-order race that surfaces under the same wording. Kept
73
+ * separate from `isRawModeCapable` so the rationale lives next to
74
+ * the matcher and the same logic can drive `probeStdinDetail`.
75
+ */
76
+ function isNode25HandleInitDefer(e) {
77
+ if (!(e instanceof Error))
78
+ return false;
79
+ return e.message.includes("_handle");
80
+ }
55
81
  /**
56
82
  * Open the controlling terminal as a raw-capable input stream, or
57
83
  * `null` if there is none (Windows without a console, detached
@@ -72,24 +98,42 @@ function defaultOpenControllingTty() {
72
98
  }
73
99
  /**
74
100
  * Resolve an input stream Ink can drive `useInput()` with:
75
- * 1. `stdin` itself if it is already raw-capable.
76
- * 2. else the controlling terminal (`/dev/tty`) if raw-capable.
101
+ * 1. controlling terminal (`/dev/tty`) if raw-capable — preferred.
102
+ * 2. else `stdin` if it is raw-capable — fallback for CI / Windows /
103
+ * detached processes that have no controlling terminal.
77
104
  * 3. else `null` — caller MUST NOT mount the interactive Ink app
78
105
  * (it would hang); print guidance + exit instead.
106
+ *
107
+ * Why /dev/tty FIRST (changed 2026-05-28 from stdin-first):
108
+ * Under `npx wotann`, `npm exec`, `sudo`, and other launchers, the
109
+ * subprocess's `process.stdin` can pass the static shape check
110
+ * (`isTTY === true`, `setRawMode` is a function) yet behave oddly
111
+ * when Ink actually drives it — the probe's no-op `setRawMode(false)`
112
+ * succeeds while Ink's `setRawMode(true)` then fails, the stream is in
113
+ * flowing mode because some boot import attached a `data` listener,
114
+ * the launcher transformed the descriptor, etc. The controlling
115
+ * terminal is the canonical "user's keyboard" source and is
116
+ * unaffected by any of that — the same pattern `less`, `vim`, `sudo`,
117
+ * and `git rebase -i` use for stdin-needs-tty scenarios. The fallback
118
+ * to `stdin` covers the no-`/dev/tty` cases (CI containers, Windows
119
+ * without a console, detached processes).
120
+ *
79
121
  * Never throws.
80
122
  */
81
123
  export function resolveInteractiveStdin(stdin = process.stdin, opts = {}) {
82
- if (isRawModeCapable(stdin))
83
- return stdin;
84
124
  const open = opts.openControllingTty ?? defaultOpenControllingTty;
85
125
  let tty = null;
86
126
  try {
87
127
  tty = open();
88
128
  }
89
129
  catch {
90
- return null;
130
+ tty = null;
91
131
  }
92
- return isRawModeCapable(tty) ? tty : null;
132
+ if (tty !== null && isRawModeCapable(tty))
133
+ return tty;
134
+ if (isRawModeCapable(stdin))
135
+ return stdin;
136
+ return null;
93
137
  }
94
138
  /**
95
139
  * Same observation pattern as `isRawModeCapable` but captures the
@@ -123,21 +167,16 @@ function probeStdinDetail(stream) {
123
167
  }
124
168
  }
125
169
  /**
126
- * Walks the same `resolveInteractiveStdin` steps and records what each
127
- * one saw. Use this when the resolver returns `null` to print a
128
- * self-explanatory refusal future bug reports will include the
129
- * diagnosis automatically instead of just "guard refused, why?".
170
+ * Records the full state of BOTH potential input sources — /dev/tty
171
+ * (the preferred source after the 2026-05-28 reorder) and `stdin`
172
+ * (the fallback). Always walks both regardless of whether the
173
+ * resolver would have short-circuited, because the diagnostic is most
174
+ * useful when something looks wrong with one source: the user / next
175
+ * agent gets the full picture, not "stdin worked, didn't look at tty".
176
+ *
177
+ * Never throws.
130
178
  */
131
179
  export function diagnoseStdin(stdin = process.stdin, opts = {}) {
132
- const stdinProbe = probeStdinDetail(stdin);
133
- if (stdinProbe.probeResult === "ok") {
134
- return {
135
- stdinIsTTY: stdinProbe.isTTY,
136
- stdinHasSetRawMode: stdinProbe.hasSetRawMode,
137
- stdinProbeResult: stdinProbe.probeResult,
138
- ttyOpenResult: "not-attempted",
139
- };
140
- }
141
180
  const open = opts.openControllingTty ?? defaultOpenControllingTty;
142
181
  let tty = null;
143
182
  let ttyOpenResult;
@@ -148,6 +187,7 @@ export function diagnoseStdin(stdin = process.stdin, opts = {}) {
148
187
  catch (e) {
149
188
  ttyOpenResult = e instanceof Error ? e.message : String(e);
150
189
  }
190
+ const stdinProbe = probeStdinDetail(stdin);
151
191
  if (tty === null) {
152
192
  return {
153
193
  stdinIsTTY: stdinProbe.isTTY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wotann",
3
- "version": "0.5.92",
3
+ "version": "0.5.94",
4
4
  "description": "WOTANN — The All-Father of AI Agent Harnesses",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",