@hayasaka7/haya-pet 0.1.0 → 0.2.1

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +59 -17
  3. package/apps/cli/src/haya-pet.js +246 -5
  4. package/apps/cli/test/haya-pet.test.mjs +269 -4
  5. package/apps/companion/package.json +1 -1
  6. package/apps/companion/src/main/index.js +40 -1
  7. package/apps/companion/test/position-store.test.mjs +2 -1
  8. package/docs/architecture.md +84 -7
  9. package/docs/cross-os-qa.md +72 -0
  10. package/docs/known-issues.md +204 -49
  11. package/docs/troubleshooting.md +33 -1
  12. package/package.json +1 -1
  13. package/packages/adapters/src/claude-hooks.js +77 -0
  14. package/packages/adapters/src/claude-transcript.js +74 -0
  15. package/packages/adapters/src/codex-hooks.js +152 -0
  16. package/packages/adapters/src/codex-transcript.js +73 -0
  17. package/packages/adapters/test/claude-hooks.test.mjs +87 -0
  18. package/packages/adapters/test/claude-transcript.test.mjs +70 -0
  19. package/packages/adapters/test/codex-hooks.test.mjs +120 -0
  20. package/packages/adapters/test/codex-transcript.test.mjs +97 -0
  21. package/packages/app-state/src/state.js +21 -1
  22. package/packages/cli-core/src/claude-hook-injection.js +42 -0
  23. package/packages/cli-core/src/claude-transcript-watcher.js +185 -0
  24. package/packages/cli-core/src/codex-hook-injection.js +49 -0
  25. package/packages/cli-core/src/codex-transcript-watcher.js +160 -0
  26. package/packages/cli-core/src/run-command.js +7 -3
  27. package/packages/cli-core/src/run-state.js +87 -0
  28. package/packages/cli-core/test/claude-hook-injection.test.mjs +45 -0
  29. package/packages/cli-core/test/claude-transcript-watcher.test.mjs +121 -0
  30. package/packages/cli-core/test/codex-hook-injection.test.mjs +45 -0
  31. package/packages/cli-core/test/codex-transcript-watcher.test.mjs +108 -0
  32. package/packages/cli-core/test/run-command.test.mjs +20 -0
  33. package/packages/cli-core/test/run-state.test.mjs +113 -0
  34. package/packages/daemon-core/src/approval-process-watcher.js +169 -0
  35. package/packages/daemon-core/test/approval-process-watcher.test.mjs +295 -0
  36. package/packages/platform-core/src/process-snapshot.js +88 -0
  37. package/packages/platform-core/test/process-snapshot.test.mjs +105 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,94 @@
1
+ # Changelog
2
+
3
+ All notable changes to Haya Pet are documented here. This project adheres to
4
+ [Semantic Versioning](https://semver.org/).
5
+
6
+ > Note: some entries originally drafted under 0.2.0 actually landed *after* the
7
+ > 0.2.0 npm publish; they are listed under 0.2.1, which is the first version that
8
+ > ships them.
9
+
10
+ ## [0.2.1]
11
+
12
+ ### Added
13
+ - **Approval-accept detection** — when you **approve** a permission prompt for a
14
+ command, the pet now flips from *waiting for approval* to *working* a couple of
15
+ seconds after the command actually starts, instead of showing "waiting" for the
16
+ tool's whole run. Clients emit **no event at the accept moment** (verified for
17
+ Claude Code: no hook, no transcript record; `PostToolUse` only fires when the
18
+ tool *finishes*) — so for a long approved build/test the pet used to sit on
19
+ "waiting" for minutes. Detection is **event-based, never a timer**: while a
20
+ session waits, the companion watches the client's **process tree**, and only a
21
+ new process that verifiably starts under the client (and survives two
22
+ consecutive polls, filtering hook blips) counts as an approval. An unanswered
23
+ prompt spawns nothing, so the warning stays up until you actually decide.
24
+ In-process approvals (file edits) aren't detected but complete in milliseconds
25
+ after approval anyway. Windows verified live; macOS (`ps`) and Linux (`/proc`)
26
+ listers are included pending live hardware verification. Details in
27
+ `docs/known-issues.md`.
28
+ - **`haya-pet hooks on` / `off` / `status`** — persists the live-status preference,
29
+ so you enable it once instead of setting an env var every shell. The toggle is
30
+ **global**: it covers every hook-capable client (Claude Code and Codex).
31
+ `HAYA_PET_HOOKS=1` (on) / `HAYA_PET_NO_HOOKS=1` (off) still work as per-run overrides.
32
+ - **Codex live status via per-session hooks** (opt-in: `haya-pet hooks on`). haya-pet
33
+ injects a stable `~/.codex/haya-pet.config.toml` profile and launches
34
+ `codex -p haya-pet`, layering the hooks on top of your base config (auth/model/MCP
35
+ untouched). The hooks report through the same `haya-pet state` reporter, with full
36
+ terminal fidelity. First run shows Codex's one-time *review hooks* prompt; approve
37
+ it once. If you already pass your own `-p/--profile`, haya-pet skips injection and
38
+ says so (Codex allows only one profile). Hooks cover `thinking` (turn start /
39
+ after tools) and `idle` (turn end); a **Codex transcript watcher** fills in tool
40
+ activity (`running_tool` / `editing_files`) by tailing the session JSONL, since
41
+ Codex's `PreToolUse` hook doesn't fire upstream yet
42
+ ([openai/codex#16732](https://github.com/openai/codex/issues/16732)).
43
+ *Waiting for approval* stays unavailable for Codex until that lands.
44
+ - **L3 transcript watcher (Claude Code)** — tails Claude's session JSONL to reliably
45
+ clear *waiting for approval* when a permission is **denied** (Claude fires no hook
46
+ on a manual denial). Ground-truth based, never a timer, so a genuinely-pending
47
+ approval keeps alerting until you actually decide.
48
+ - **`PermissionRequest` hook** for a snappier *waiting for approval* cue (fires the
49
+ instant the dialog appears, ahead of the notification).
50
+
51
+ ### Fixed
52
+ - Pet stuck on *waiting for approval* after a manual **denial** (see the Claude
53
+ transcript watcher above).
54
+ - Pet stuck on *waiting for approval* after an **accept**, for as long as the
55
+ approved tool kept running (see approval-accept detection above).
56
+ - `Notification` events other than permission prompts (e.g. `idle_prompt`) were
57
+ mislabeled as *waiting for approval*; they are now mapped correctly.
58
+
59
+ ## [0.2.0]
60
+
61
+ ### Changed
62
+ - **`haya-pet run` now defaults to native passthrough** (`stdio: "inherit"`). The
63
+ wrapped CLI talks directly to your terminal, so **Shift+Tab**, mouse-wheel
64
+ scroll, and word-edit all work normally. PTY observation is now opt-in via
65
+ `--observe` (it routes input through ConPTY on Windows, which can mangle special
66
+ keys — use it only for non-interactive runs).
67
+
68
+ ### Added
69
+ - **Claude Code live status via per-session hooks** (opt-in: `HAYA_PET_HOOKS=1` in
70
+ this release; 0.2.1 adds the persisted `haya-pet hooks on` toggle).
71
+ Injects a stable settings file through `claude --settings <file>` — no change to
72
+ your global config — wiring Claude's events to a new `haya-pet state` reporter so
73
+ the pet shows thinking / running tools / editing files / waiting for approval,
74
+ with full terminal fidelity (no PTY). First run shows Claude's one-time
75
+ *review hooks* prompt; approve it once.
76
+ - **`haya-pet state <state>` command** — reporter used by client hooks to push live
77
+ status to the daemon over IPC.
78
+ - **`HAYA_PET_HOOK_DEBUG=<file>`** — append one JSONL line per status event
79
+ (hook- and transcript-sourced) for diagnostics.
80
+
81
+ ### Fixed
82
+ - Claude Code TUI accepted no keyboard input when hooks were injected — caused by a
83
+ volatile per-session argument and temp path that re-triggered Claude's hook-trust
84
+ review every launch. Hook commands and the settings path are now stable; the
85
+ session id is passed via the `HAYA_PET_SESSION_ID` env var.
86
+
87
+ ### Notes
88
+ - In this release Codex and Antigravity had no hook adapter — native passthrough
89
+ with lifecycle status, or `--observe` for coarse PTY activity. 0.2.1 adds the
90
+ Codex adapter; Antigravity remains a planned follow-up.
91
+
92
+ ## [0.1.0]
93
+ - Initial generic AI CLI pet runtime: overlay companion, session bubbles, daemon
94
+ IPC, client adapters, pet asset pipeline, cross-OS paths.
package/README.md CHANGED
@@ -51,7 +51,9 @@ Haya Pet watches all of them and presents one ambient interface:
51
51
  - 🧠 **Normalized state model** — every client maps to a shared state vocabulary
52
52
  (`thinking`, `running_tool`, `waiting_approval`, `reviewing`, `failed`, …).
53
53
  - 🧩 **Client adapters** with tiered support (process wrapper → PTY observer →
54
- log/state → official plugin) so the daemon never bakes in client-specific logic.
54
+ client hooks) so the daemon never bakes in client-specific logic. Default is
55
+ lifecycle status; richer status is opt-in (Claude Code / Codex hooks via
56
+ `haya-pet hooks on`, or PTY `--observe` for any client).
55
57
  - 🚀 **Zero-setup launch** — `haya-pet run …` auto-starts the overlay; no separate
56
58
  daemon to manage.
57
59
  - 🖼️ **Codex-compatible pet assets** (1536×1872 sprite atlas, 9 actions).
@@ -92,8 +94,10 @@ Haya Pet watches all of them and presents one ambient interface:
92
94
  | **Node ≥ 18** | Runtime + companion (Electron) |
93
95
  | **npm** | Install + scripts |
94
96
 
95
- > Live activity status uses the optional `node-pty` (installed automatically when
96
- > it can build; the pet degrades to lifecycle-only tracking without it).
97
+ > Default status is lifecycle-only and needs no extra modules. Opt-in Claude Code /
98
+ > Codex hooks (`haya-pet hooks on`) also need none. The opt-in `--observe` PTY mode uses
99
+ > `node-pty` (installed automatically when it can build; without it, `--observe`
100
+ > degrades to lifecycle-only tracking).
97
101
 
98
102
  ## Install
99
103
 
@@ -138,19 +142,57 @@ shows success (a green check) or failure (a red cross), then fades.
138
142
 
139
143
  ### Live activity status
140
144
 
141
- By default the wrapper runs the CLI through a pseudo-terminal and shows *working*
142
- while the AI produces output, returning to *idle* after a short quiet window.
143
- Success/failure come from the real exit code never from scraping the word
144
- "error" out of output. Your terminal stays fully interactive.
145
+ `haya-pet run` uses **native passthrough by default** the CLI talks directly to
146
+ your terminal, so every input mode (Shift+Tab, mouse wheel, word-edit) works
147
+ exactly as it does without the wrapper. Out of the box, every client shows
148
+ **lifecycle status** (a session bubble while it runs; success/failure from the
149
+ real exit code, never from scraping "error" out of output).
145
150
 
146
151
  ```bash
147
- haya-pet run -- claude # live status (default)
148
- haya-pet run --no-observe -- claude # lifecycle only (opt out of PTY observation)
152
+ haya-pet run --client claude-code -- claude # full fidelity, lifecycle status
153
+ haya-pet run --client codex -- codex # full fidelity, lifecycle status
149
154
  ```
150
155
 
151
- > ⚠️ Running a CLI through the default PTY observation currently affects terminal
152
- > scrolling and backspace in some setups — see
153
- > [docs/known-issues.md](docs/known-issues.md). `--no-observe` avoids it.
156
+ Two **opt-in** ways to get richer *in-session* status (thinking / running tools /
157
+ editing files / waiting for approval):
158
+
159
+ ```bash
160
+ # Claude Code AND Codex — live status via per-session hooks, NO terminal-fidelity
161
+ # tradeoff. Enable once (persisted, global); the first run for each client shows a
162
+ # one-time "review hooks" prompt you approve once.
163
+ haya-pet hooks on
164
+ haya-pet run --client claude-code -- claude
165
+ haya-pet run --client codex -- codex
166
+ # (per-run override without persisting: HAYA_PET_HOOKS=1 …, or $env:HAYA_PET_HOOKS=1 in PowerShell)
167
+ # (turn back off: haya-pet hooks off · check: haya-pet hooks status)
168
+
169
+ # Any client — coarse live status by watching output through a PTY.
170
+ haya-pet run --observe --client codex -- codex
171
+ ```
172
+
173
+ > **Codex coverage.** Codex shows `thinking` (working) and `idle` (done) via hooks,
174
+ > plus `running_tool` / `editing_files` via a session-transcript watcher.
175
+ > *Waiting for approval* doesn't arrive yet because of an upstream gap where
176
+ > Codex's `PermissionRequest` hook doesn't fire
177
+ > ([openai/codex#16732](https://github.com/openai/codex/issues/16732)); it'll start
178
+ > working automatically once Codex fixes it. Also: if you pass your own
179
+ > `-p/--profile` to codex, haya-pet skips hook injection (Codex allows one
180
+ > profile) and tells you. Claude Code has full coverage.
181
+
182
+ > **Approval prompts resolve correctly** (Claude Code): deny → the pet returns to
183
+ > idle the moment the denial lands in the session transcript; accept a command →
184
+ > the pet flips to *working* a couple of seconds after the approved command
185
+ > actually starts running (detected from the client's process tree — a real
186
+ > event, never a timeout, so an unanswered prompt keeps warning until you decide).
187
+
188
+ > **Why opt-in?**
189
+ > - **Hooks (Claude Code / Codex):** injecting hooks makes the client show a
190
+ > one-time *review hooks* trust prompt. We don't disrupt your session by default;
191
+ > turn it on once with `haya-pet hooks on` when you're happy to approve the hooks.
192
+ > - **`--observe` (any client):** PTY observation infers status from output, but on
193
+ > Windows it routes input through ConPTY, which can break **Shift+Tab**, mouse
194
+ > scroll, and word-edit. Use it only for non-interactive runs. See
195
+ > [docs/known-issues.md](docs/known-issues.md).
154
196
 
155
197
  ## Add and choose a pet
156
198
 
@@ -219,14 +261,14 @@ Full list (incl. repairing a broken Electron install): [docs/troubleshooting.md]
219
261
 
220
262
  | Client | Status | Support level |
221
263
  |---|---|---|
222
- | Generic CLI | ✅ | L1 process wrapper |
223
- | Codex | ✅ | L1 + L2 PTY observation |
224
- | Claude Code | ✅ | L1 + L2 PTY observation |
225
- | Antigravity | ✅ | L1 wrapper |
264
+ | Generic CLI | ✅ | L1 process wrapper (+ L2 PTY via `--observe`) |
265
+ | Codex | ✅ | L1 wrapper + **L4 live-status hooks** (opt-in `haya-pet hooks on`; partial — see note) |
266
+ | Claude Code | ✅ | L1 wrapper + **L4 live-status hooks** (opt-in `haya-pet hooks on`) |
267
+ | Antigravity | ✅ | L1 wrapper (+ L2 PTY via `--observe`) |
226
268
  | Gemini CLI / Aider / others | 🔜 | via the generic adapter |
227
269
 
228
270
  (See [docs/architecture.md](docs/architecture.md) for the support tiers and the
229
- platform matrix.)
271
+ platform matrix, and [CHANGELOG.md](CHANGELOG.md) for release notes.)
230
272
 
231
273
  ## Privacy
232
274
 
@@ -1,14 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import { realpathSync } from "node:fs";
2
+ import { realpathSync, appendFileSync } from "node:fs";
3
3
  import { spawn } from "node:child_process";
4
+ import { randomUUID } from "node:crypto";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { runGenericCommand as defaultRunGenericCommand } from "../../../packages/cli-core/src/run-command.js";
7
+ import { parseStateArgs, runStateCommand } from "../../../packages/cli-core/src/run-state.js";
8
+ import { injectClaudeHooks as defaultInjectClaudeHooks } from "../../../packages/cli-core/src/claude-hook-injection.js";
9
+ import { injectCodexHooks as defaultInjectCodexHooks } from "../../../packages/cli-core/src/codex-hook-injection.js";
10
+ import { watchClaudeTranscript as defaultWatchClaudeTranscript } from "../../../packages/cli-core/src/claude-transcript-watcher.js";
11
+ import { watchCodexTranscript as defaultWatchCodexTranscript } from "../../../packages/cli-core/src/codex-transcript-watcher.js";
6
12
  import { ensureCompanionConnection } from "../../../packages/cli-core/src/companion-launcher.js";
7
13
  import { createIpcClient as defaultCreateIpcClient } from "../../../packages/daemon-core/src/ipc-server.js";
8
14
  import { getDefaultPaths } from "../../../packages/platform-core/src/paths.js";
9
15
  import { discoverPets as defaultDiscoverPets } from "../../../packages/pet-core/src/discovery.js";
10
16
  import { createStateFile as defaultCreateStateFile } from "../../../packages/app-state/src/state-file.js";
11
- import { getSelectedPetId, setSelectedPet } from "../../../packages/app-state/src/state.js";
17
+ import { getSelectedPetId, setSelectedPet, getHooksEnabled, setHooksEnabled } from "../../../packages/app-state/src/state.js";
12
18
  import { getAdapterInfo } from "../../../packages/adapters/src/adapter-info.js";
13
19
 
14
20
  const CLIENT_DISPLAY_NAMES = Object.freeze({
@@ -41,6 +47,14 @@ export function parseAiPetArgs(argv) {
41
47
  return { command: "stop" };
42
48
  }
43
49
 
50
+ if (command === "state") {
51
+ return parseStateArgs(rest);
52
+ }
53
+
54
+ if (command === "hooks") {
55
+ return parseHooksArgs(rest);
56
+ }
57
+
44
58
  throw new Error(`Unsupported haya-pet command: ${command}`);
45
59
  }
46
60
 
@@ -59,6 +73,14 @@ export async function runAiPet(argv, dependencies = {}) {
59
73
  return runStopCommand(parsed, dependencies);
60
74
  }
61
75
 
76
+ if (parsed.command === "state") {
77
+ return runStateCommand(parsed, dependencies);
78
+ }
79
+
80
+ if (parsed.command === "hooks") {
81
+ return runHooksCommand(parsed, dependencies);
82
+ }
83
+
62
84
  return runRunCommand(parsed, dependencies);
63
85
  }
64
86
 
@@ -102,26 +124,213 @@ export async function runStartCommand(_parsed, dependencies = {}) {
102
124
 
103
125
  async function runRunCommand(parsed, dependencies) {
104
126
  const runGenericCommand = dependencies.runGenericCommand ?? defaultRunGenericCommand;
127
+ const injectClaudeHooks = dependencies.injectClaudeHooks ?? defaultInjectClaudeHooks;
128
+ const injectCodexHooks = dependencies.injectCodexHooks ?? defaultInjectCodexHooks;
129
+ const watchClaudeTranscript = dependencies.watchClaudeTranscript ?? defaultWatchClaudeTranscript;
130
+ const watchCodexTranscript = dependencies.watchCodexTranscript ?? defaultWatchCodexTranscript;
131
+ const print = dependencies.print ?? defaultPrint;
132
+ const env = dependencies.env ?? process.env;
133
+ const now = dependencies.now ?? Date.now;
134
+ const cwd = dependencies.cwd ?? process.cwd();
105
135
  const messageSender = await createMessageSender(dependencies);
106
136
 
137
+ const sessionId = dependencies.sessionId ?? `sess_${randomUUID()}`;
138
+ let childArgs = parsed.childArgs;
139
+ let childEnv = env;
140
+ let cleanup = () => {};
141
+ let stopWatcher = () => {};
142
+
143
+ // Native passthrough is always the default (full terminal fidelity). Live-status
144
+ // hooks are OPT-IN — persisted via `haya-pet hooks on`, or per-run via
145
+ // HAYA_PET_HOOKS=1 — because injecting hooks makes the client show a one-time
146
+ // "review hooks" trust prompt; we never disrupt the user's session uninvited.
147
+ // Both clients report live status via the `haya-pet state` reporter (no PTY, so
148
+ // Shift+Tab works); the session id rides in via HAYA_PET_SESSION_ID.
149
+ const hooksOn = await resolveHooksEnabled(env, dependencies);
150
+
151
+ // Claude Code: inject a stable `--settings` file.
152
+ const claudeHooksOn = hooksOn && parsed.clientId === "claude-code";
153
+ if (claudeHooksOn) {
154
+ const injected = injectClaudeHooks();
155
+ childArgs = [...parsed.childArgs, "--settings", injected.settingsPath];
156
+ childEnv = { ...env, HAYA_PET_SESSION_ID: sessionId };
157
+ cleanup = injected.cleanup;
158
+
159
+ // Claude fires NO hook when the user manually denies a permission, so the
160
+ // pet would stay stuck on "waiting for approval". Tail the session transcript
161
+ // (ground truth) and clear to idle the moment a denial is recorded — never on
162
+ // a timer, so a genuinely-pending approval keeps alerting until it's resolved.
163
+ const watcher = watchClaudeTranscript({
164
+ cwd,
165
+ homeDir: dependencies.homeDir,
166
+ startedAt: now(),
167
+ onDenial: (event) => {
168
+ hookDebugLog(env, now, { source: "transcript", event: "denied", state: "idle", toolUseId: event?.toolUseId });
169
+ messageSender
170
+ .send({
171
+ type: "state",
172
+ sessionId,
173
+ state: "idle",
174
+ summary: "approval denied",
175
+ confidence: 0.9,
176
+ source: "client_log",
177
+ updatedAt: now()
178
+ })
179
+ .catch(() => {});
180
+ }
181
+ });
182
+ stopWatcher = watcher.stop;
183
+ }
184
+
185
+ // Codex: no `--settings` equivalent, so inject a stable profile and add
186
+ // `-p <name>` at the FRONT (a global flag must precede any subcommand). Codex
187
+ // takes only one profile, so if the user already passes their own -p/--profile
188
+ // we skip injection and say so rather than clobber their choice. Codex
189
+ // PreToolUse is not reliable, so a transcript watcher supplies tool activity.
190
+ const codexHooksOn = hooksOn && parsed.clientId === "codex";
191
+ if (codexHooksOn) {
192
+ if (hasProfileArg(parsed.childArgs)) {
193
+ print(
194
+ "haya-pet: Codex live-status hooks skipped — you passed your own -p/--profile (Codex allows only one)."
195
+ );
196
+ } else {
197
+ const injected = injectCodexHooks();
198
+ childArgs = ["-p", injected.profileName, ...parsed.childArgs];
199
+ childEnv = { ...env, HAYA_PET_SESSION_ID: sessionId };
200
+ cleanup = injected.cleanup;
201
+
202
+ const activeToolCalls = new Set();
203
+ const watcher = watchCodexTranscript({
204
+ homeDir: dependencies.homeDir,
205
+ sessionsRoot: dependencies.codexSessionsRoot,
206
+ startedAt: now(),
207
+ onToolEvent: (event) => {
208
+ hookDebugLog(env, now, {
209
+ source: "codex_transcript",
210
+ event: event.type,
211
+ toolCallId: event.toolCallId,
212
+ toolName: event.toolName,
213
+ state: event.state
214
+ });
215
+
216
+ if (event.type === "tool_started") {
217
+ activeToolCalls.add(event.toolCallId);
218
+ messageSender
219
+ .send({
220
+ type: "state",
221
+ sessionId,
222
+ state: event.state,
223
+ summary: event.toolName,
224
+ confidence: 0.85,
225
+ source: "client_log",
226
+ updatedAt: now()
227
+ })
228
+ .catch(() => {});
229
+ return;
230
+ }
231
+
232
+ if (event.type === "tool_finished") {
233
+ activeToolCalls.delete(event.toolCallId);
234
+ if (activeToolCalls.size === 0) {
235
+ messageSender
236
+ .send({
237
+ type: "state",
238
+ sessionId,
239
+ state: "thinking",
240
+ confidence: 0.85,
241
+ source: "client_log",
242
+ updatedAt: now()
243
+ })
244
+ .catch(() => {});
245
+ }
246
+ }
247
+ }
248
+ });
249
+ const previousStopWatcher = stopWatcher;
250
+ stopWatcher = () => {
251
+ watcher.stop();
252
+ previousStopWatcher();
253
+ };
254
+ }
255
+ }
256
+
107
257
  try {
108
258
  return await runGenericCommand({
109
259
  command: parsed.childCommand,
110
- args: parsed.childArgs,
111
- cwd: dependencies.cwd ?? process.cwd(),
260
+ args: childArgs,
261
+ cwd,
112
262
  clientId: parsed.clientId,
113
263
  clientDisplayName: CLIENT_DISPLAY_NAMES[parsed.clientId] ?? parsed.clientId,
114
264
  observe: parsed.observe,
265
+ sessionId,
266
+ env: childEnv,
115
267
  heartbeatIntervalMs: dependencies.heartbeatIntervalMs,
116
268
  now: dependencies.now,
117
269
  stdio: dependencies.stdio,
118
270
  send: messageSender.send
119
271
  });
120
272
  } finally {
273
+ stopWatcher();
274
+ cleanup();
121
275
  await messageSender.close();
122
276
  }
123
277
  }
124
278
 
279
+ // Resolve whether live-status hooks should be injected for this run (any
280
+ // hook-capable client). Precedence: HAYA_PET_NO_HOOKS forces off, HAYA_PET_HOOKS
281
+ // forces on (per-run overrides), otherwise the persisted `haya-pet hooks on/off`
282
+ // preference.
283
+ async function resolveHooksEnabled(env, dependencies) {
284
+ if (isTruthyFlag(env.HAYA_PET_NO_HOOKS)) {
285
+ return false;
286
+ }
287
+ if (isTruthyFlag(env.HAYA_PET_HOOKS)) {
288
+ return true;
289
+ }
290
+ try {
291
+ const state = await createConfigStateFile(dependencies).load();
292
+ return getHooksEnabled(state);
293
+ } catch {
294
+ return false;
295
+ }
296
+ }
297
+
298
+ function isTruthyFlag(value) {
299
+ return value === "1" || value === "true";
300
+ }
301
+
302
+ // Detect a user-supplied Codex profile flag so we don't clobber it: -p, --profile,
303
+ // or the `--profile=foo` / `-p=foo` forms.
304
+ function hasProfileArg(args) {
305
+ return args.some(
306
+ (arg) => arg === "-p" || arg === "--profile" || arg.startsWith("--profile=") || arg.startsWith("-p=")
307
+ );
308
+ }
309
+
310
+ function createConfigStateFile(dependencies) {
311
+ const paths = getDefaultPaths({
312
+ platform: dependencies.platform,
313
+ env: dependencies.env,
314
+ homeDir: dependencies.homeDir
315
+ });
316
+ const createStateFile = dependencies.createStateFile ?? defaultCreateStateFile;
317
+ return createStateFile({ statePath: paths.statePath });
318
+ }
319
+
320
+ // Best-effort: mirror the reporter's HAYA_PET_HOOK_DEBUG log so transcript-driven
321
+ // events (which don't go through `haya-pet state`) show up in the same trace.
322
+ function hookDebugLog(env, now, entry) {
323
+ const target = env.HAYA_PET_HOOK_DEBUG;
324
+ if (!target) {
325
+ return;
326
+ }
327
+ try {
328
+ appendFileSync(target, `${JSON.stringify({ ts: now(), ...entry })}\n`);
329
+ } catch {
330
+ // diagnostics must never break the run
331
+ }
332
+ }
333
+
125
334
  export async function runPetsCommand(parsed, dependencies = {}) {
126
335
  const paths = getDefaultPaths({
127
336
  platform: dependencies.platform,
@@ -181,6 +390,38 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
181
390
  return result;
182
391
  }
183
392
 
393
+ function parseHooksArgs(args) {
394
+ const [action = "status"] = args;
395
+ if (action === "on" || action === "off" || action === "status") {
396
+ return { command: "hooks", action };
397
+ }
398
+ throw new Error(`Unknown hooks action: ${action} (use on, off, or status)`);
399
+ }
400
+
401
+ // Persisted GLOBAL toggle for live-status hooks (the convenient alternative to
402
+ // setting HAYA_PET_HOOKS every shell). Covers every hook-capable client — Claude
403
+ // Code and Codex today.
404
+ export async function runHooksCommand(parsed, dependencies = {}) {
405
+ const print = dependencies.print ?? defaultPrint;
406
+ const stateFile = createConfigStateFile(dependencies);
407
+ const state = await stateFile.load();
408
+
409
+ if (parsed.action === "status") {
410
+ const enabled = getHooksEnabled(state);
411
+ print(`Live-status hooks: ${enabled ? "on" : "off"}`);
412
+ return { command: "hooks", action: "status", enabled };
413
+ }
414
+
415
+ const enabled = parsed.action === "on";
416
+ await stateFile.save(setHooksEnabled(state, enabled));
417
+ print(
418
+ enabled
419
+ ? "Live-status hooks: on. The first `haya-pet run` for Claude Code or Codex asks the client to review the hooks once — approve it."
420
+ : "Live-status hooks: off."
421
+ );
422
+ return { command: "hooks", action: parsed.action, enabled };
423
+ }
424
+
184
425
  function parsePetsArgs(args) {
185
426
  if (args.length === 0) {
186
427
  return { command: "pets", action: "list" };
@@ -205,7 +446,7 @@ function parsePetsArgs(args) {
205
446
 
206
447
  function parseRunArgs(args) {
207
448
  let clientId = "generic";
208
- let observe = true; // live PTY observation is on by default; --no-observe opts out
449
+ let observe = false; // native passthrough by default (full terminal fidelity); --observe opts in
209
450
  let childStart = -1;
210
451
 
211
452
  for (let index = 0; index < args.length; index += 1) {