@hayasaka7/haya-pet 0.3.9 → 0.3.11
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/CHANGELOG.md +29 -0
- package/README.md +6 -0
- package/apps/cli/src/haya-pet.js +190 -155
- package/apps/cli/test/haya-pet.test.mjs +33 -7
- package/docs/architecture.md +4 -5
- package/docs/known-issues.md +74 -32
- package/docs/troubleshooting.md +2 -2
- package/package.json +1 -1
- package/packages/adapters/src/codex-hooks.js +6 -8
- package/packages/cli-core/src/background-tasks.js +63 -0
- package/packages/cli-core/src/codex-hook-injection.js +99 -54
- package/packages/cli-core/src/run-state.js +83 -17
- package/packages/cli-core/test/background-tasks.test.mjs +115 -0
- package/packages/cli-core/test/codex-hook-injection.test.mjs +29 -26
- package/packages/cli-core/test/run-state.test.mjs +68 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ All notable changes to HAYA Pet are documented here. This project adheres to
|
|
|
7
7
|
> 0.2.0 npm publish; they are listed under 0.2.1, which is the first version that
|
|
8
8
|
> ships them.
|
|
9
9
|
|
|
10
|
+
## [0.3.11]
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Codex live-status hooks now work with custom profiles.** HAYA Pet no longer
|
|
14
|
+
injects its own `-p haya-pet` profile or skips hooks when the wrapped Codex
|
|
15
|
+
command already has `-p` / `--profile`. Instead it merges stable HAYA-managed
|
|
16
|
+
hook entries into `$CODEX_HOME/hooks.json`, preserving any existing user hooks
|
|
17
|
+
and leaving profile args such as `--profile fugu` untouched. The wrapper still
|
|
18
|
+
resolves profile-specific `approvals_reviewer` settings so approval status
|
|
19
|
+
matches the selected Codex profile.
|
|
20
|
+
|
|
21
|
+
## [0.3.10]
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **A running Claude Code subagent no longer drives the main session's status.**
|
|
25
|
+
With hooks enabled and a multi-agent run, once the main agent had stopped but a
|
|
26
|
+
subagent was still working, two things went wrong: the pet dropped to *idle*
|
|
27
|
+
(even though work was ongoing), and while the subagent ran its tool calls flipped
|
|
28
|
+
the pet between *running tools* / *editing files* / *thinking*. Fix, checked
|
|
29
|
+
**only at the main agent's `Stop`** (no timers, no persisted state): (1) Claude's
|
|
30
|
+
`Stop` payload carries a live `background_tasks` snapshot — when it still lists a
|
|
31
|
+
running **subagent**, the pet shows *running tools* with the message **"Subagent
|
|
32
|
+
running"**, and the follow-up `Stop` (empty `background_tasks`) clears it back to
|
|
33
|
+
*idle*; (2) every subagent-originated hook event carries an `agent_id`, so the
|
|
34
|
+
reporter now **drops any event with an `agent_id`**, and a subagent's activity can
|
|
35
|
+
no longer overwrite the main session's status. Background **shells** are
|
|
36
|
+
deliberately not surfaced (their completion isn't reliably observable). See
|
|
37
|
+
`docs/known-issues.md`.
|
|
38
|
+
|
|
10
39
|
## [0.3.9]
|
|
11
40
|
|
|
12
41
|
### Fixed
|
package/README.md
CHANGED
|
@@ -193,6 +193,12 @@ resolved `approvals_reviewer` setting, and transcript watchers report tool/file
|
|
|
193
193
|
activity plus guardian-review outcomes. With **"Approve for me"** the pet shows
|
|
194
194
|
*reviewing* immediately; *waiting for approval* is reserved for Codex's manual
|
|
195
195
|
"Ask for approval" mode.
|
|
196
|
+
|
|
197
|
+
Codex custom profiles are preserved. HAYA Pet installs its live-status hooks in
|
|
198
|
+
`$CODEX_HOME/hooks.json` instead of passing its own `-p` profile, so launches such
|
|
199
|
+
as `haya-pet run --client codex -- codex --profile fugu` keep both the selected
|
|
200
|
+
Codex profile and HAYA live status.
|
|
201
|
+
|
|
196
202
|
Per-tool `PreToolUse` hooks still depend on an upstream Codex gap
|
|
197
203
|
([openai/codex#16732](https://github.com/openai/codex/issues/16732)); the
|
|
198
204
|
transcript watcher covers that in the meantime.
|
package/apps/cli/src/haya-pet.js
CHANGED
|
@@ -5,7 +5,7 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { runGenericCommand as defaultRunGenericCommand } from "../../../packages/cli-core/src/run-command.js";
|
|
8
|
-
import { parseStateArgs, runStateCommand,
|
|
8
|
+
import { parseStateArgs, runStateCommand, readHookPayloadFromStdin } from "../../../packages/cli-core/src/run-state.js";
|
|
9
9
|
import { removeSessionTranscriptLink } from "../../../packages/cli-core/src/session-transcript-link.js";
|
|
10
10
|
import { injectClaudeHooks as defaultInjectClaudeHooks } from "../../../packages/cli-core/src/claude-hook-injection.js";
|
|
11
11
|
import { injectCodexHooks as defaultInjectCodexHooks } from "../../../packages/cli-core/src/codex-hook-injection.js";
|
|
@@ -219,8 +219,10 @@ function resolveCodexApprovalsReviewer(options = {}) {
|
|
|
219
219
|
|
|
220
220
|
const home = options.codexHome ?? env.CODEX_HOME ?? resolveHomeCodexDir(options.homeDir, env);
|
|
221
221
|
const readFile = options.readFile ?? readFileSync;
|
|
222
|
+
const profileName = findCodexProfileInArgs(options.childArgs ?? []);
|
|
223
|
+
const fromProfile = profileName ? readCodexApprovalsReviewerFromProfile(home, profileName, readFile) : undefined;
|
|
222
224
|
const fromConfig = readCodexApprovalsReviewerFromConfig(home, readFile);
|
|
223
|
-
return fromConfig ?? "user";
|
|
225
|
+
return fromProfile ?? fromConfig ?? "user";
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
function resolveHomeCodexDir(homeDir, env) {
|
|
@@ -250,12 +252,49 @@ function findCodexApprovalsReviewerInArgs(args) {
|
|
|
250
252
|
return reviewer;
|
|
251
253
|
}
|
|
252
254
|
|
|
255
|
+
function findCodexProfileInArgs(args) {
|
|
256
|
+
let profileName;
|
|
257
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
258
|
+
const arg = args[index];
|
|
259
|
+
let value;
|
|
260
|
+
if (arg === "-p" || arg === "--profile") {
|
|
261
|
+
value = args[index + 1];
|
|
262
|
+
index += 1;
|
|
263
|
+
} else if (arg.startsWith("--profile=")) {
|
|
264
|
+
value = arg.slice("--profile=".length);
|
|
265
|
+
} else if (arg.startsWith("-p=")) {
|
|
266
|
+
value = arg.slice("-p=".length);
|
|
267
|
+
} else if (arg.startsWith("-p") && arg.length > 2) {
|
|
268
|
+
value = arg.slice(2);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (isCodexProfileName(value)) {
|
|
272
|
+
profileName = value;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return profileName;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isCodexProfileName(value) {
|
|
279
|
+
return typeof value === "string" && /^[A-Za-z0-9_-]+$/.test(value);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function readCodexApprovalsReviewerFromProfile(codexHome, profileName, readFile) {
|
|
283
|
+
if (!codexHome || !isCodexProfileName(profileName)) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
return readCodexApprovalsReviewerFromFile(join(codexHome, `${profileName}.config.toml`), readFile);
|
|
287
|
+
}
|
|
253
288
|
function readCodexApprovalsReviewerFromConfig(codexHome, readFile) {
|
|
254
289
|
if (!codexHome) {
|
|
255
290
|
return undefined;
|
|
256
291
|
}
|
|
292
|
+
return readCodexApprovalsReviewerFromFile(join(codexHome, "config.toml"), readFile);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function readCodexApprovalsReviewerFromFile(path, readFile) {
|
|
257
296
|
try {
|
|
258
|
-
return parseTopLevelApprovalsReviewer(readFile(
|
|
297
|
+
return parseTopLevelApprovalsReviewer(readFile(path, "utf8"));
|
|
259
298
|
} catch {
|
|
260
299
|
return undefined;
|
|
261
300
|
}
|
|
@@ -407,162 +446,154 @@ async function runRunCommand(parsed, dependencies) {
|
|
|
407
446
|
};
|
|
408
447
|
}
|
|
409
448
|
|
|
410
|
-
// Codex: no `--settings` equivalent, so
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
// we skip injection and say so rather than clobber their choice. Codex
|
|
449
|
+
// Codex: no `--settings` equivalent, so install stable user-level hooks in
|
|
450
|
+
// CODEX_HOME/hooks.json. Codex loads user hooks alongside any selected
|
|
451
|
+
// -p/--profile, so the wrapper never consumes the user's single profile slot.
|
|
414
452
|
// PreToolUse is not reliable, so a transcript watcher supplies tool activity.
|
|
415
453
|
const codexHooksOn = hooksOn && parsed.clientId === "codex";
|
|
416
454
|
if (codexHooksOn) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
childEnv = {
|
|
425
|
-
...env,
|
|
426
|
-
HAYA_PET_SESSION_ID: sessionId,
|
|
427
|
-
HAYA_PET_CODEX_APPROVAL_REVIEWER: resolveCodexApprovalsReviewer({
|
|
428
|
-
childArgs: parsed.childArgs,
|
|
429
|
-
env,
|
|
430
|
-
homeDir: dependencies.homeDir,
|
|
431
|
-
codexHome: dependencies.codexHome,
|
|
432
|
-
readFile: dependencies.readFile
|
|
433
|
-
})
|
|
434
|
-
};
|
|
435
|
-
cleanup = injected.cleanup;
|
|
436
|
-
|
|
437
|
-
// Pin both Codex watchers to THIS session's rollout via the
|
|
438
|
-
// session->transcript link the `haya-pet state` reporter records from the
|
|
439
|
-
// hook payload's transcript_path, instead of guessing newest-by-mtime (which
|
|
440
|
-
// leaks a concurrent same-cwd session's activity/interrupts).
|
|
441
|
-
const sessionDir = resolveSessionDir(dependencies, env);
|
|
442
|
-
|
|
443
|
-
const activeToolCalls = new Set();
|
|
444
|
-
const watcher = watchCodexTranscript({
|
|
455
|
+
const injected = injectCodexHooks();
|
|
456
|
+
childEnv = {
|
|
457
|
+
...env,
|
|
458
|
+
HAYA_PET_SESSION_ID: sessionId,
|
|
459
|
+
HAYA_PET_CODEX_APPROVAL_REVIEWER: resolveCodexApprovalsReviewer({
|
|
460
|
+
childArgs: parsed.childArgs,
|
|
461
|
+
env,
|
|
445
462
|
homeDir: dependencies.homeDir,
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
onToolEvent: (event) => {
|
|
452
|
-
hookDebugLog(env, now, {
|
|
453
|
-
source: "codex_transcript",
|
|
454
|
-
event: event.type,
|
|
455
|
-
toolCallId: event.toolCallId,
|
|
456
|
-
toolName: event.toolName,
|
|
457
|
-
state: event.state
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
// Esc-interrupt fires no Stop hook, so without this the pet stays stuck
|
|
461
|
-
// on "thinking"/"running" until the stale sweep.
|
|
462
|
-
if (event.type === "turn_aborted") {
|
|
463
|
-
activeToolCalls.clear();
|
|
464
|
-
messageSender
|
|
465
|
-
.send({
|
|
466
|
-
type: "state",
|
|
467
|
-
sessionId,
|
|
468
|
-
state: "interrupted",
|
|
469
|
-
summary: "interrupted",
|
|
470
|
-
confidence: 0.9,
|
|
471
|
-
source: "client_log",
|
|
472
|
-
updatedAt: now()
|
|
473
|
-
})
|
|
474
|
-
.catch(() => {});
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
463
|
+
codexHome: dependencies.codexHome,
|
|
464
|
+
readFile: dependencies.readFile
|
|
465
|
+
})
|
|
466
|
+
};
|
|
467
|
+
cleanup = injected.cleanup;
|
|
477
468
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
sessionId,
|
|
484
|
-
state: event.state,
|
|
485
|
-
summary: event.toolName,
|
|
486
|
-
confidence: 0.85,
|
|
487
|
-
source: "client_log",
|
|
488
|
-
updatedAt: now()
|
|
489
|
-
})
|
|
490
|
-
.catch(() => {});
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
469
|
+
// Pin both Codex watchers to THIS session's rollout via the
|
|
470
|
+
// session->transcript link the `haya-pet state` reporter records from the
|
|
471
|
+
// hook payload's transcript_path, instead of guessing newest-by-mtime (which
|
|
472
|
+
// leaks a concurrent same-cwd session's activity/interrupts).
|
|
473
|
+
const sessionDir = resolveSessionDir(dependencies, env);
|
|
493
474
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
475
|
+
const activeToolCalls = new Set();
|
|
476
|
+
const watcher = watchCodexTranscript({
|
|
477
|
+
homeDir: dependencies.homeDir,
|
|
478
|
+
sessionsRoot: dependencies.codexSessionsRoot,
|
|
479
|
+
cwd,
|
|
480
|
+
startedAt: now(),
|
|
481
|
+
sessionId,
|
|
482
|
+
sessionDir,
|
|
483
|
+
onToolEvent: (event) => {
|
|
484
|
+
hookDebugLog(env, now, {
|
|
485
|
+
source: "codex_transcript",
|
|
486
|
+
event: event.type,
|
|
487
|
+
toolCallId: event.toolCallId,
|
|
488
|
+
toolName: event.toolName,
|
|
489
|
+
state: event.state
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Esc-interrupt fires no Stop hook, so without this the pet stays stuck
|
|
493
|
+
// on "thinking"/"running" until the stale sweep.
|
|
494
|
+
if (event.type === "turn_aborted") {
|
|
495
|
+
activeToolCalls.clear();
|
|
496
|
+
messageSender
|
|
497
|
+
.send({
|
|
498
|
+
type: "state",
|
|
499
|
+
sessionId,
|
|
500
|
+
state: "interrupted",
|
|
501
|
+
summary: "interrupted",
|
|
502
|
+
confidence: 0.9,
|
|
503
|
+
source: "client_log",
|
|
504
|
+
updatedAt: now()
|
|
505
|
+
})
|
|
506
|
+
.catch(() => {});
|
|
507
|
+
return;
|
|
509
508
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
watcher.stop();
|
|
514
|
-
previousStopWatcher();
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
// With "Approve for me" (approvals_reviewer=auto_review, legacy alias
|
|
518
|
-
// guardian_subagent), Codex routes approval requests to a guardian
|
|
519
|
-
// subagent and never shows the human approval UI — yet the
|
|
520
|
-
// PermissionRequest hook still fires at request creation. The hook's
|
|
521
|
-
// reporter uses the resolved approvals reviewer config: auto-review
|
|
522
|
-
// reports reviewing immediately, while manual review reports waiting.
|
|
523
|
-
// The guardian's own rollout is the only observable record of the
|
|
524
|
-
// review, so tail it: a review turn starting proves the agent is
|
|
525
|
-
// reviewing; an "allow" verdict proves the action proceeds; a "deny"
|
|
526
|
-
// verdict goes back to the model, which keeps working. An unreadable
|
|
527
|
-
// verdict reports nothing — a pending cue is never cleared on a guess.
|
|
528
|
-
const guardianWatcher = watchCodexGuardianReviews({
|
|
529
|
-
homeDir: dependencies.homeDir,
|
|
530
|
-
sessionsRoot: dependencies.codexSessionsRoot,
|
|
531
|
-
cwd,
|
|
532
|
-
startedAt: now(),
|
|
533
|
-
sessionId,
|
|
534
|
-
sessionDir,
|
|
535
|
-
onReviewEvent: (event) => {
|
|
536
|
-
hookDebugLog(env, now, {
|
|
537
|
-
source: "codex_guardian",
|
|
538
|
-
event: event.type,
|
|
539
|
-
outcome: event.outcome
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
const report = resolveGuardianStateReport(event);
|
|
543
|
-
if (!report) {
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
509
|
+
|
|
510
|
+
if (event.type === "tool_started") {
|
|
511
|
+
activeToolCalls.add(event.toolCallId);
|
|
546
512
|
messageSender
|
|
547
513
|
.send({
|
|
548
514
|
type: "state",
|
|
549
515
|
sessionId,
|
|
550
|
-
state:
|
|
551
|
-
summary:
|
|
516
|
+
state: event.state,
|
|
517
|
+
summary: event.toolName,
|
|
552
518
|
confidence: 0.85,
|
|
553
519
|
source: "client_log",
|
|
554
520
|
updatedAt: now()
|
|
555
521
|
})
|
|
556
522
|
.catch(() => {});
|
|
523
|
+
return;
|
|
557
524
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
525
|
+
|
|
526
|
+
if (event.type === "tool_finished") {
|
|
527
|
+
activeToolCalls.delete(event.toolCallId);
|
|
528
|
+
if (activeToolCalls.size === 0) {
|
|
529
|
+
messageSender
|
|
530
|
+
.send({
|
|
531
|
+
type: "state",
|
|
532
|
+
sessionId,
|
|
533
|
+
state: "thinking",
|
|
534
|
+
confidence: 0.85,
|
|
535
|
+
source: "client_log",
|
|
536
|
+
updatedAt: now()
|
|
537
|
+
})
|
|
538
|
+
.catch(() => {});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
const previousStopWatcher = stopWatcher;
|
|
544
|
+
stopWatcher = () => {
|
|
545
|
+
watcher.stop();
|
|
546
|
+
previousStopWatcher();
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// With "Approve for me" (approvals_reviewer=auto_review, legacy alias
|
|
550
|
+
// guardian_subagent), Codex routes approval requests to a guardian
|
|
551
|
+
// subagent and never shows the human approval UI — yet the
|
|
552
|
+
// PermissionRequest hook still fires at request creation. The hook's
|
|
553
|
+
// reporter uses the resolved approvals reviewer config: auto-review
|
|
554
|
+
// reports reviewing immediately, while manual review reports waiting.
|
|
555
|
+
// The guardian's own rollout is the only observable record of the
|
|
556
|
+
// review, so tail it: a review turn starting proves the agent is
|
|
557
|
+
// reviewing; an "allow" verdict proves the action proceeds; a "deny"
|
|
558
|
+
// verdict goes back to the model, which keeps working. An unreadable
|
|
559
|
+
// verdict reports nothing — a pending cue is never cleared on a guess.
|
|
560
|
+
const guardianWatcher = watchCodexGuardianReviews({
|
|
561
|
+
homeDir: dependencies.homeDir,
|
|
562
|
+
sessionsRoot: dependencies.codexSessionsRoot,
|
|
563
|
+
cwd,
|
|
564
|
+
startedAt: now(),
|
|
565
|
+
sessionId,
|
|
566
|
+
sessionDir,
|
|
567
|
+
onReviewEvent: (event) => {
|
|
568
|
+
hookDebugLog(env, now, {
|
|
569
|
+
source: "codex_guardian",
|
|
570
|
+
event: event.type,
|
|
571
|
+
outcome: event.outcome
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const report = resolveGuardianStateReport(event);
|
|
575
|
+
if (!report) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
messageSender
|
|
579
|
+
.send({
|
|
580
|
+
type: "state",
|
|
581
|
+
sessionId,
|
|
582
|
+
state: report.state,
|
|
583
|
+
summary: report.summary,
|
|
584
|
+
confidence: 0.85,
|
|
585
|
+
source: "client_log",
|
|
586
|
+
updatedAt: now()
|
|
587
|
+
})
|
|
588
|
+
.catch(() => {});
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
const stopWithoutGuardian = stopWatcher;
|
|
592
|
+
stopWatcher = () => {
|
|
593
|
+
guardianWatcher.stop();
|
|
594
|
+
stopWithoutGuardian();
|
|
595
|
+
removeSessionTranscriptLink({ sessionDir, sessionId });
|
|
596
|
+
};
|
|
566
597
|
}
|
|
567
598
|
|
|
568
599
|
try {
|
|
@@ -629,13 +660,6 @@ function isTruthyFlag(value) {
|
|
|
629
660
|
return value === "1" || value === "true";
|
|
630
661
|
}
|
|
631
662
|
|
|
632
|
-
// Detect a user-supplied Codex profile flag so we don't clobber it: -p, --profile,
|
|
633
|
-
// or the `--profile=foo` / `-p=foo` forms.
|
|
634
|
-
function hasProfileArg(args) {
|
|
635
|
-
return args.some(
|
|
636
|
-
(arg) => arg === "-p" || arg === "--profile" || arg.startsWith("--profile=") || arg.startsWith("-p=")
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
663
|
|
|
640
664
|
function createConfigStateFile(dependencies) {
|
|
641
665
|
const paths = getDefaultPaths({
|
|
@@ -969,20 +993,31 @@ if (isDirectRun(import.meta.url, process.argv[1])) {
|
|
|
969
993
|
}
|
|
970
994
|
|
|
971
995
|
// Real-process entry. For a `haya-pet state` invocation — which is ALWAYS a client
|
|
972
|
-
// hook child — read the hook payload from stdin once to learn this session's real
|
|
973
|
-
// transcript path
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
-
//
|
|
996
|
+
// hook child — read the hook payload from stdin once to learn: this session's real
|
|
997
|
+
// transcript path (for the session->transcript link); the live background_tasks
|
|
998
|
+
// snapshot (so a Stop with a still-running subagent keeps a working cue instead of
|
|
999
|
+
// idle); and the agent_id (present only for subagent-originated events, which the
|
|
1000
|
+
// reporter drops so a subagent's activity never overwrites the main status). All
|
|
1001
|
+
// are handed to the reporter via dependencies. Done here (not inside
|
|
1002
|
+
// main/runStateCommand) so unit tests, and every other command that needs stdin
|
|
1003
|
+
// passed through to its child (e.g. `run`), never touch stdin.
|
|
977
1004
|
async function bootstrap() {
|
|
978
1005
|
const argv = process.argv.slice(2);
|
|
979
1006
|
const dependencies = {};
|
|
980
1007
|
if (argv[0] === "state") {
|
|
981
1008
|
try {
|
|
982
|
-
const transcriptPath = await
|
|
1009
|
+
const { transcriptPath, backgroundTasks, agentId } = await readHookPayloadFromStdin();
|
|
983
1010
|
if (transcriptPath) {
|
|
984
1011
|
dependencies.transcriptPath = transcriptPath;
|
|
985
1012
|
}
|
|
1013
|
+
if (Array.isArray(backgroundTasks) && backgroundTasks.length > 0) {
|
|
1014
|
+
dependencies.backgroundTasks = backgroundTasks;
|
|
1015
|
+
}
|
|
1016
|
+
// Present only for subagent-originated events — the reporter drops those so a
|
|
1017
|
+
// subagent's tool use never overwrites the main session's status.
|
|
1018
|
+
if (agentId) {
|
|
1019
|
+
dependencies.agentId = agentId;
|
|
1020
|
+
}
|
|
986
1021
|
} catch {
|
|
987
1022
|
// a missing/garbled payload just means no binding this time — never fatal
|
|
988
1023
|
}
|
|
@@ -537,7 +537,7 @@ test("hooks command parses and persists the toggle", async () => {
|
|
|
537
537
|
assert.ok(lines.some((l) => l.includes("on")));
|
|
538
538
|
});
|
|
539
539
|
|
|
540
|
-
test("persisted `hooks on`
|
|
540
|
+
test("persisted `hooks on` installs Codex hooks without consuming the profile slot", async () => {
|
|
541
541
|
const calls = [];
|
|
542
542
|
let injected = 0;
|
|
543
543
|
await runAiPet(["run", "--client", "codex", "--", "codex"], {
|
|
@@ -546,7 +546,7 @@ test("persisted `hooks on` injects a Codex profile via -p at the front of args",
|
|
|
546
546
|
heartbeatIntervalMs: 10,
|
|
547
547
|
send: async () => {},
|
|
548
548
|
createStateFile: hooksStateFile(true),
|
|
549
|
-
injectCodexHooks: () => { injected += 1; return {
|
|
549
|
+
injectCodexHooks: () => { injected += 1; return { hooksPath: "C:\\Users\\A\\.codex\\hooks.json", cleanup: () => {} }; },
|
|
550
550
|
runGenericCommand: async (options) => {
|
|
551
551
|
calls.push(options);
|
|
552
552
|
return { sessionId: options.sessionId, pid: 1, exitCode: 0 };
|
|
@@ -554,7 +554,8 @@ test("persisted `hooks on` injects a Codex profile via -p at the front of args",
|
|
|
554
554
|
});
|
|
555
555
|
|
|
556
556
|
assert.equal(injected, 1, "config preference enables Codex hooks");
|
|
557
|
-
assert.deepEqual(calls[0].args, [
|
|
557
|
+
assert.deepEqual(calls[0].args, [], "Codex args stay untouched");
|
|
558
|
+
assert.equal(calls[0].env.HAYA_PET_SESSION_ID, calls[0].sessionId);
|
|
558
559
|
assert.equal(calls[0].env.HAYA_PET_CODEX_APPROVAL_REVIEWER, "user");
|
|
559
560
|
});
|
|
560
561
|
|
|
@@ -577,6 +578,27 @@ test("codex hooks pass auto-review config to the PermissionRequest reporter", as
|
|
|
577
578
|
assert.equal(calls[0].env.HAYA_PET_CODEX_APPROVAL_REVIEWER, "auto_review");
|
|
578
579
|
});
|
|
579
580
|
|
|
581
|
+
test("codex hooks read approvals reviewer from the selected profile config", async () => {
|
|
582
|
+
const calls = [];
|
|
583
|
+
await runAiPet(["run", "--client", "codex", "--", "codex", "--profile=fugu"], {
|
|
584
|
+
cwd: process.cwd(),
|
|
585
|
+
env: { USERPROFILE: "C:\\Users\\A" },
|
|
586
|
+
heartbeatIntervalMs: 10,
|
|
587
|
+
send: async () => {},
|
|
588
|
+
createStateFile: hooksStateFile(true),
|
|
589
|
+
injectCodexHooks: () => ({ hooksPath: "C:\\Users\\A\\.codex\\hooks.json", cleanup: () => {} }),
|
|
590
|
+
readFile: (path) => String(path).endsWith("fugu.config.toml")
|
|
591
|
+
? 'approvals_reviewer = "auto_review"\n'
|
|
592
|
+
: 'approvals_reviewer = "user"\n',
|
|
593
|
+
runGenericCommand: async (options) => {
|
|
594
|
+
calls.push(options);
|
|
595
|
+
return { sessionId: options.sessionId, pid: 1, exitCode: 0 };
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
assert.deepEqual(calls[0].args, ["--profile=fugu"], "profile arg is untouched");
|
|
600
|
+
assert.equal(calls[0].env.HAYA_PET_CODEX_APPROVAL_REVIEWER, "auto_review");
|
|
601
|
+
});
|
|
580
602
|
test("codex hooks also start a transcript watcher for tool activity", async () => {
|
|
581
603
|
const sent = [];
|
|
582
604
|
let fireToolEvent;
|
|
@@ -658,9 +680,10 @@ test("codex hooks also start a guardian-review watcher that reports review state
|
|
|
658
680
|
]);
|
|
659
681
|
});
|
|
660
682
|
|
|
661
|
-
test("codex hooks
|
|
683
|
+
test("codex hooks preserve user profile args and still wire live status", async () => {
|
|
662
684
|
const calls = [];
|
|
663
685
|
let injected = 0;
|
|
686
|
+
let watched = 0;
|
|
664
687
|
const lines = [];
|
|
665
688
|
await runAiPet(["run", "--client", "codex", "--", "codex", "-p", "mine"], {
|
|
666
689
|
cwd: process.cwd(),
|
|
@@ -669,16 +692,19 @@ test("codex hooks are skipped (with a notice) when the user passes their own -p"
|
|
|
669
692
|
send: async () => {},
|
|
670
693
|
createStateFile: hooksStateFile(true),
|
|
671
694
|
print: (line) => lines.push(line),
|
|
672
|
-
injectCodexHooks: () => { injected += 1; return {
|
|
695
|
+
injectCodexHooks: () => { injected += 1; return { hooksPath: "C:\\Users\\A\\.codex\\hooks.json", cleanup: () => {} }; },
|
|
696
|
+
watchCodexTranscript: () => { watched += 1; return { stop: () => {} }; },
|
|
673
697
|
runGenericCommand: async (options) => {
|
|
674
698
|
calls.push(options);
|
|
675
699
|
return { sessionId: options.sessionId, pid: 1, exitCode: 0 };
|
|
676
700
|
}
|
|
677
701
|
});
|
|
678
702
|
|
|
679
|
-
assert.equal(injected,
|
|
703
|
+
assert.equal(injected, 1, "hooks are installed even when Codex gets a user profile");
|
|
704
|
+
assert.equal(watched, 1, "transcript watcher still starts for profiled runs");
|
|
680
705
|
assert.deepEqual(calls[0].args, ["-p", "mine"], "user args untouched");
|
|
681
|
-
assert.
|
|
706
|
+
assert.equal(calls[0].env.HAYA_PET_SESSION_ID, calls[0].sessionId);
|
|
707
|
+
assert.ok(!lines.some((l) => /skipped/i.test(l)), "profile runs no longer skip hooks");
|
|
682
708
|
});
|
|
683
709
|
|
|
684
710
|
test("codex does NOT inject hooks by default (safe out-of-box)", async () => {
|
package/docs/architecture.md
CHANGED
|
@@ -63,11 +63,10 @@ in-session activity through the shared, client-agnostic `haya-pet state` command
|
|
|
63
63
|
|
|
64
64
|
The injection mechanism differs per client. **Claude Code** takes a stable
|
|
65
65
|
`claude --settings <file>`. **Codex** has no per-invocation settings flag, so the
|
|
66
|
-
wrapper
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
notice. Codex's hook command must be unquoted at the program position (it runs via
|
|
66
|
+
wrapper merges stable user-level hooks into `$CODEX_HOME/hooks.json`. Codex loads
|
|
67
|
+
that hook source alongside any selected `-p/--profile`, so custom profiles remain
|
|
68
|
+
untouched while HAYA Pet still sets the per-session environment and watchers.
|
|
69
|
+
Codex's hook command must be unquoted at the program position (it runs via
|
|
71
70
|
`cmd /c`, which strips a leading quote) and its matchers can't use look-around
|
|
72
71
|
(Rust regex) — see [known-issues.md](known-issues.md). Codex's L4 is **partial**:
|
|
73
72
|
`PreToolUse` doesn't fire upstream yet, so tool activity comes from an L3
|