throughline 0.8.7 → 0.8.9
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 +36 -1
- package/README.ja.md +10 -0
- package/README.md +10 -0
- package/docs/04_public_release_plan.md +3 -1
- package/docs/05_codex_first_roadmap.md +2 -2
- package/package.json +9 -2
- package/src/cli/doctor.mjs +53 -39
- package/src/cli/doctor.test.mjs +37 -3
- package/src/cli/factory-diagnostics.mjs +7 -6
- package/src/cli/factory-diagnostics.test.mjs +73 -7
- package/src/cli/install.mjs +59 -3
- package/src/cli/install.test.mjs +75 -9
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,39 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.8.9] — 2026-08-02
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Codex hook diagnosis no longer depends on the caller's `PATH`. The expected
|
|
18
|
+
hook command is rebuilt per invocation, and `resolveCodexHookNodePath` returns
|
|
19
|
+
the PATH form of Node when one is on `PATH` and `process.execPath` otherwise.
|
|
20
|
+
Comparing that string against the registered command classified a correctly
|
|
21
|
+
installed hook as a legacy command whenever the two representations differed,
|
|
22
|
+
so `doctor --codex` reported "legacy command needs reinstall" and
|
|
23
|
+
`factory-diagnostics` reported `codex_hooks` as `not_ready` for every scheduled
|
|
24
|
+
run started from a minimal environment. Hook commands are now compared by
|
|
25
|
+
parsed identity — same Node executable by realpath, same CLI script by
|
|
26
|
+
realpath, same event — so a hook registered as `/opt/homebrew/bin/node` still
|
|
27
|
+
matches an expectation resolved to `/opt/homebrew/Cellar/node/<ver>/bin/node`.
|
|
28
|
+
Hooks pointing at a different Throughline installation, a different event, or
|
|
29
|
+
the legacy PATH-resolved form remain flagged for reinstall, and paths whose
|
|
30
|
+
realpath cannot be resolved are never treated as equivalent.
|
|
31
|
+
|
|
32
|
+
## [0.8.8] — 2026-08-02
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Codex hook installation now writes the supported `timeout` field in seconds
|
|
37
|
+
for UserPromptSubmit, PostToolUse, and Stop. The previously emitted
|
|
38
|
+
`timeoutSec` field was ignored by Codex, leaving all three hooks at the
|
|
39
|
+
600-second default.
|
|
40
|
+
- Reinstall canonicalizes existing Throughline-managed hooks by command
|
|
41
|
+
identity, replacing legacy `timeoutSec` entries while preserving unrelated
|
|
42
|
+
Codex hooks. Doctor reports the effective `timeout` field and explicitly
|
|
43
|
+
flags the legacy key for reinstall; factory diagnostics no longer classify
|
|
44
|
+
the ignored key as ready.
|
|
45
|
+
|
|
13
46
|
## [0.8.7] — 2026-07-20
|
|
14
47
|
|
|
15
48
|
### Fixed
|
|
@@ -1177,7 +1210,9 @@ two attempts, instrument first instead of patching again.
|
|
|
1177
1210
|
|
|
1178
1211
|
---
|
|
1179
1212
|
|
|
1180
|
-
[Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.
|
|
1213
|
+
[Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.9...HEAD
|
|
1214
|
+
[0.8.9]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.8...v0.8.9
|
|
1215
|
+
[0.8.8]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.7...v0.8.8
|
|
1181
1216
|
[0.8.7]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.6...v0.8.7
|
|
1182
1217
|
[0.8.6]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.5...v0.8.6
|
|
1183
1218
|
[0.8.5]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.4...v0.8.5
|
package/README.ja.md
CHANGED
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
> **Claude Code のコンテキスト消費を約 90% 削減しつつ、記憶はほぼそのまま残す。**
|
|
15
15
|
> 「時間の新旧」ではなく **「コンテンツの種類」** で会話を分離する。人間が読みたいテキストは残し、機械が出力したツール I/O は SQLite に退避する。同じ判断、同じ文脈、9 割軽量。
|
|
16
16
|
|
|
17
|
+
[kitepon.dev](https://kitepon.dev/)を運営する[クオ(@QLyun35332)](https://x.com/QLyun35332)が
|
|
18
|
+
開発・メンテナンスしています。
|
|
19
|
+
|
|
20
|
+
## 所有境界
|
|
21
|
+
|
|
22
|
+
本repositoryはdatabase、migration、capture契約、release、diagnosticsを所有します。
|
|
23
|
+
製品横断の導入とhost統合は、kitepon.devの製品開発を支える内部基盤
|
|
24
|
+
[dotagents](https://github.com/kitepon-rgb/dotagents)が担当します。
|
|
25
|
+
MarkItDownは別区分の第三者CLIです。
|
|
26
|
+
|
|
17
27
|
## 30 秒で始める
|
|
18
28
|
|
|
19
29
|
```bash
|
package/README.md
CHANGED
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
> **Cut ~90% of Claude Code's context usage while keeping nearly all the memory.**
|
|
15
15
|
> Throughline separates conversation by **type, not time** — humans-readable text stays, machine-generated tool output retires to SQLite. Same decisions, same context, 90% lighter.
|
|
16
16
|
|
|
17
|
+
Built and maintained by [Quo](https://x.com/QLyun35332) at [kitepon.dev](https://kitepon.dev/en).
|
|
18
|
+
|
|
19
|
+
## Ownership boundary
|
|
20
|
+
|
|
21
|
+
This repository owns the database, migrations, capture contracts, release, and
|
|
22
|
+
diagnostics. Cross-product installation and host integration are handled by
|
|
23
|
+
[dotagents](https://github.com/kitepon-rgb/dotagents), the internal development
|
|
24
|
+
toolchain behind kitepon.dev's products.
|
|
25
|
+
MarkItDown is a separately managed third-party CLI.
|
|
26
|
+
|
|
17
27
|
## In 30 seconds
|
|
18
28
|
|
|
19
29
|
```bash
|
|
@@ -142,7 +142,9 @@ schema v4 で PostToolUse (`capture-tool`) は廃止、L2/L3 は Stop 内で一
|
|
|
142
142
|
| **npm 公開 (v0.6.3): Codex diagnostics ready 集約 + Windows bounded observer** | 3 managed Codex hooks が canonical `ready` のとき hook summary / Codex connector を `ready` とし、未検査 Claude connector は明示 `unverified` のまま Codex-only overall から除外する。Windows runtime-error observer は、既存 state の使用前検証と新規 file の ACL 付与・read-back を維持しつつ、同一 mutation 内の重複 PowerShell ACL 検証を除いた。公開commit `fc83ddf`、CI `29284655280`(9/9 green)、npm `throughline@0.6.3`、tag / GitHub Release、npm shasum `4f3fcd2598a75f026358dae7f3eb3165242b580b` を2026-07-14に確認した |
|
|
143
143
|
| **npm 公開 (v0.8.4): Codex handoff host境界の明示化** | `$throughline` skillは現在のCodex UI surfaceからDesktop/VS Code/CLIを選び、`--open-host`を明示する。shell/永続PTYの継承環境はsurface判定に使わない。`codex-handoff-start`は互換の`openHost`を残し、requested / resolved hostをJSONとtextで報告する。古いVS Code由来PTY事故形と配布skillをfocused testで固定し、Claude-facing hook/`/tl`/baton/resume契約は変更しない。公開commit `5b840b6`、CI run `29721583754` 9/9 green、npm `latest` 0.8.4、shasum `1f2c39a22e45f3e02e8739ee5fd6ceefc6a71034`、tag / GitHub Release、registry由来global install、配置skill一致、`doctor --codex` exit 0を2026-07-20に確認した。判断証拠は [ADR 0017](adr/0017-codex-handoff-host-boundary.md) |
|
|
144
144
|
| **npm公開 (v0.8.6): 製品所有DB migration入口** | `throughline migrate --json` は既存DBだけをproduction migrationで現行schemaへ移行し、versioned bounded JSONを返す。DB不在は作らず`not_applicable`、現行は`already_current`、future schemaと失敗は非0。`factory-diagnostics`のread-only契約は維持する。0.8.5はdry-run後に別作業の未コミット文書が混入したためdeprecateし、同一runtimeをclean worktreeから0.8.6として再公開した。設計判断は [ADR 0018](adr/0018-product-owned-database-migration.md) |
|
|
145
|
-
|
|
|
145
|
+
| **公開済み (v0.8.7): Windows CI release latency** | completed-turn receiptの256件境界fixtureと実ACL検証を分離し、production mutationのPowerShellはdistinct state transitionだけに限定する。temporary ACL失敗時の旧store保持とdirectory/lock/final storeのnative ACL testを維持する。CIは9 matrixのままevent/ref単位concurrency、unit test 5分SLO/8分timeoutとする。設計判断は [ADR 0020](adr/0020-windows-ci-release-latency.md) |
|
|
146
|
+
| **npm公開 (v0.8.8): Codex hook timeout key修理** | Codex `hooks.json` の3 managed hookは秒単位の正規key `timeout`を30/30/300で書く。無視される旧`timeoutSec` entryは次回global installでcommand identityにより置換し、doctorは旧形式を要再install、factory diagnosticsはnot readyとする。Claude hooksとObserver wait deadline APIは変更しない。npm `latest` として2026-08-02T00:43Zに公開済み。tag / GitHub Releaseは未作成(要作成) |
|
|
147
|
+
| **release準備 (v0.8.9): Codex hook診断のPATH非依存化** | 期待hook commandのnode表記は呼び出し元の`PATH`で変わるため、登録済みcommandとの文字列比較では最小PATH(launchd factory reporter等)から正規登録が「legacy command」と誤判定され、`codex_hooks`が恒常的に`not_ready`になっていた。比較を解析済みidentity(node実体realpath+CLI script realpath+event)へ変更する。別install/別event/旧PATH解決型/realpath未解決は従来どおり要再install扱い。hooks.json の書式と Claude-facing hook は変更しない |
|
|
146
148
|
| **未リリース: Codex current-session 75% trigger** | Codex 自動発火を token-monitor に依存させず、global install が Codex `UserPromptSubmit` / `PostToolUse` hooks も登録する。hook は当該 Codex session の rollout `token_count` を直接読み、verified 75% 以上なら同じ user turn または tool loop 継続前に `$throughline` workflow 実行指示を `additionalContext` で注入する。Stop hook の guarded auto-refresh は残す。`~/.codex/config.toml` は旧 `codex_hooks = true` に加えて現行 `hooks = true` も有効化する |
|
|
147
149
|
| **グローバル E2E 検証** | 2026-04-17 別ディレクトリから `throughline doctor` 全緑を確認 |
|
|
148
150
|
|
|
@@ -456,9 +456,9 @@ Phase 5 implementation status (2026-05-06):
|
|
|
456
456
|
- 修正: Caveat の Codex Stop hook は `async: false` で登録され、nested `codex exec` smoke で Stop -> 次 turn 注入まで確認済みだった。Throughline は Claude Stop と同じ発想で Codex Stop も `async: true` にしていたため、Codex 側だけ Caveat と同じ同期 hook に寄せた。既に `async: true` で登録済みの Throughline Codex Stop hook も次回 `throughline install` で `async: false` に更新される。
|
|
457
457
|
- 再実測: `npm install -g .` -> `throughline install` 後、`~/.codex/hooks.json` の Throughline Codex Stop は `async: false` になり、Caveat / Spotter hooks は保持された。`codex exec --json -C /home/kite/projects/Throughline "Reply exactly: TL_CODEX_SYNC_STOP_SMOKE_20260506"` で child thread `019dfd4f-93ff-7522-8f89-bd1e1996c8d7` が作られ、直後の `throughline doctor --codex` で latest DB session が `codex:019dfd4f-93ff-7522-8f89-bd1e1996c8d7` に進んだ。Codex Stop hook の自然 capture は同期登録で確認済み。
|
|
458
458
|
- 追加修正: Caveat の Codex hook は bare `caveat` ではなく絶対 node + installed CLI script path で登録されていた。Throughline も Codex App Server / VSCode host の PATH 差分を避けるため、旧 bare `throughline codex-hook stop` を次回 install で絶対 node + `bin/throughline.mjs codex-hook stop` に置換する。`doctor --codex` は Codex hook feature / Throughline Stop hook command / legacy bare command を診断表示する。
|
|
459
|
-
- 絶対パス型の再実測: `npm install -g .` -> `throughline install` 後、`~/.codex/hooks.json` の Stop 先頭は `/usr/bin/node /home/kite/projects/Throughline/bin/throughline.mjs codex-hook stop`、`async: false
|
|
459
|
+
- 絶対パス型の再実測: `npm install -g .` -> `throughline install` 後、`~/.codex/hooks.json` の Stop 先頭は `/usr/bin/node /home/kite/projects/Throughline/bin/throughline.mjs codex-hook stop`、`async: false`になり、Caveat / Spotter hooks は 2 番目以降に保持された。2026-08-02 correction: 当時記録した`timeoutSec: 300`はCodexに無視される誤keyで、v0.8.8から正規の`timeout: 300`へ修正した。`doctor --codex` は `Codex hooks feature: enabled` / `Codex Stop hook: registered` を表示した。`codex exec --json -C /home/kite/projects/Throughline "Reply exactly: TL_CODEX_ABSOLUTE_STOP_SMOKE_20260506"` で child thread `019dfd5e-1248-7c11-8ddc-97e1b0701e10` が作られ、直後の `throughline doctor --codex` で latest DB session が `codex:019dfd5e-1248-7c11-8ddc-97e1b0701e10` に進んだ。
|
|
460
460
|
- 追加確認: current VSCode-origin parent thread `019dfd38-c530-71c3-b7b8-180bdd3054bc` は hook shape 変更前に開始していたため、変更後の自然 Stop smoke としては不適格。assistant final 後に rollout は `task_complete` まで進んだが latest DB session は exec child のままだった。次に VSCode-origin を見る場合は、hook shape 変更後に新しく開始した Codex session で確認する。
|
|
461
|
-
- 最終確認: hook shape 変更後に新しく開始した VSCode-origin Codex session で 1 turn 完了後、`throughline doctor --codex` を実行した。`current Codex thread` は `019dfd62-9a9d-7211-bf91-89d8e3fc908e`、`latest DB session` は `codex:019dfd62-9a9d-7211-bf91-89d8e3fc908e` で一致し、`Codex hooks feature: enabled`、`Codex Stop hook: registered`、command は `/usr/bin/node /home/kite/projects/Throughline/bin/throughline.mjs codex-hook stop`、`async: false
|
|
461
|
+
- 最終確認: hook shape 変更後に新しく開始した VSCode-origin Codex session で 1 turn 完了後、`throughline doctor --codex` を実行した。`current Codex thread` は `019dfd62-9a9d-7211-bf91-89d8e3fc908e`、`latest DB session` は `codex:019dfd62-9a9d-7211-bf91-89d8e3fc908e` で一致し、`Codex hooks feature: enabled`、`Codex Stop hook: registered`、command は `/usr/bin/node /home/kite/projects/Throughline/bin/throughline.mjs codex-hook stop`、`async: false`だった。2026-08-02 correction: 当時の`timeoutSec: 300`は無効で、v0.8.8の再install後は`timeout: 300`を正とする。VSCode-origin の自然 Stop hookによるDB captureの証拠自体は維持する。
|
|
462
462
|
- historical 2026-05-07 correction: 以下の Codex trim smoke は live app-server primitive の履歴として残す。ただし 2026-05-06 incident 後、restart / reconnect 越しの durable context trim 成功とは一時的に扱わず、`$throughline` / Codex Stop hook の automatic mutation を止めていた。
|
|
463
463
|
- 2026-05-10 UX 修正: bare `$throughline` は `doctor --codex` / `trim --dry-run --all` / `trim --preflight --all` を AI に順番実行させる説明 surface でも、`trim --execute --host codex --all` を直接実行する current-thread refresh でもなく、`throughline codex-handoff-start --execute --open-host <current-codex-surface>` による app-server 新スレッド handoff とする。current surfaceはCodex UI contextから明示し、current-thread rollback / inject は明示要求時だけ使う。
|
|
464
464
|
- memory contract 修正: Codex guarded trim でも注入 memory は元の `/tl` 思想を正とする。古い turn は L1 summaries、直近 20 turn は L2 full bodies、L3 は reference only で、L3 bodies / tool payloads は注入しない。rollout source は rollback candidate と app-server turn-count 補正の根拠であり、Throughline DB memory がある場合に rollout active work preview を注入 memory として使わない。DB memory が無い execute は rollout preview を注入せず、mutation 前に拒否する。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "throughline",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
|
|
6
6
|
"keywords": [
|
|
@@ -9,12 +9,19 @@
|
|
|
9
9
|
"context-compression",
|
|
10
10
|
"llm"
|
|
11
11
|
],
|
|
12
|
-
"author":
|
|
12
|
+
"author": {
|
|
13
|
+
"name": "Quo / クオ at kitepon.dev",
|
|
14
|
+
"url": "https://kitepon.dev/"
|
|
15
|
+
},
|
|
13
16
|
"license": "MIT",
|
|
14
17
|
"repository": {
|
|
15
18
|
"type": "git",
|
|
16
19
|
"url": "git+https://github.com/kitepon-rgb/Throughline.git"
|
|
17
20
|
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/kitepon-rgb/Throughline/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/kitepon-rgb/Throughline#readme",
|
|
18
25
|
"bin": {
|
|
19
26
|
"throughline": "bin/throughline.mjs"
|
|
20
27
|
},
|
package/src/cli/doctor.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
buildCodexPostToolUseHookCommand,
|
|
33
33
|
buildCodexStopHookCommand,
|
|
34
34
|
buildCodexUserPromptSubmitHookCommand,
|
|
35
|
+
isEquivalentCodexHookCommand,
|
|
35
36
|
isThroughlineCodexHookCommand,
|
|
36
37
|
isThroughlineCodexPostToolUseCommand,
|
|
37
38
|
isThroughlineCodexStopCommand,
|
|
@@ -480,6 +481,31 @@ function summarizeHookTrust(hooks, { hooksFeatureEnabled, codexHooksFeatureEnabl
|
|
|
480
481
|
};
|
|
481
482
|
}
|
|
482
483
|
|
|
484
|
+
function hasLegacyCodexHookTimeout(hook) {
|
|
485
|
+
return Object.hasOwn(hook, 'timeoutSec');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function describeCodexHookRegistration(hooks, legacyCommandHooks, expectedTimeout) {
|
|
489
|
+
if (hooks.length === 0) return 'not registered';
|
|
490
|
+
if (legacyCommandHooks.length > 0) return 'legacy command needs reinstall';
|
|
491
|
+
if (hooks.some(hasLegacyCodexHookTimeout)) return 'legacy timeout key needs reinstall';
|
|
492
|
+
if (hooks.length !== 1) return 'configuration needs reinstall';
|
|
493
|
+
const hook = hooks[0];
|
|
494
|
+
return hook.type === 'command' && hook.timeout === expectedTimeout && hook.async === false
|
|
495
|
+
? 'registered'
|
|
496
|
+
: 'configuration needs reinstall';
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function printCodexHookDetails(hook) {
|
|
500
|
+
console.log(` command: ${hook.command}`);
|
|
501
|
+
console.log(` async: ${hook.async === false ? 'false' : String(hook.async)}`);
|
|
502
|
+
console.log(` timeout: ${hook.timeout ?? '(default: 600)'}`);
|
|
503
|
+
if (hasLegacyCodexHookTimeout(hook)) {
|
|
504
|
+
console.log(` legacy timeoutSec: ${hook.timeoutSec} (ignored by Codex; reinstall required)`);
|
|
505
|
+
}
|
|
506
|
+
console.log(` trusted: ${hook.throughlineDoctorTrusted ? 'yes' : 'no'}`);
|
|
507
|
+
}
|
|
508
|
+
|
|
483
509
|
function readCodexHookDiagnosis(codexHome) {
|
|
484
510
|
const hooksPath = join(codexHome, 'hooks.json');
|
|
485
511
|
const configPath = join(codexHome, 'config.toml');
|
|
@@ -551,13 +577,17 @@ function readCodexHookDiagnosis(codexHome) {
|
|
|
551
577
|
);
|
|
552
578
|
const stopHooks = listHooksWithTrust(parsed, 'Stop', 'stop', hooksPath, out.trustedStateKeys);
|
|
553
579
|
out.managedPromptHooks = promptHooks.filter(h => isThroughlineCodexHookCommand(h.command));
|
|
554
|
-
out.legacyManagedPromptHooks = out.managedPromptHooks.filter(
|
|
580
|
+
out.legacyManagedPromptHooks = out.managedPromptHooks.filter(
|
|
581
|
+
h => !isEquivalentCodexHookCommand(h.command, expectedPromptCommand),
|
|
582
|
+
);
|
|
555
583
|
out.managedPostToolUseHooks = postToolUseHooks.filter(h => isThroughlineCodexPostToolUseCommand(h.command));
|
|
556
584
|
out.legacyManagedPostToolUseHooks = out.managedPostToolUseHooks.filter(
|
|
557
|
-
h => h.command
|
|
585
|
+
h => !isEquivalentCodexHookCommand(h.command, expectedPostToolUseCommand),
|
|
558
586
|
);
|
|
559
587
|
out.managedStopHooks = stopHooks.filter(h => isThroughlineCodexStopCommand(h.command));
|
|
560
|
-
out.legacyManagedStopHooks = out.managedStopHooks.filter(
|
|
588
|
+
out.legacyManagedStopHooks = out.managedStopHooks.filter(
|
|
589
|
+
h => !isEquivalentCodexHookCommand(h.command, expectedStopCommand),
|
|
590
|
+
);
|
|
561
591
|
out.managedHookTrust = summarizeHookTrust(
|
|
562
592
|
[
|
|
563
593
|
...out.managedPromptHooks,
|
|
@@ -599,47 +629,29 @@ function runCodexDiagnosis({
|
|
|
599
629
|
console.log(` CODEX_HOME: ${codexHome}`);
|
|
600
630
|
console.log(` Codex hooks feature: ${hookDiagnosis.featureEnabled ? 'enabled' : 'not enabled'}`);
|
|
601
631
|
console.log(` Codex hook trust: ${hookDiagnosis.managedHookTrust.status}`);
|
|
602
|
-
console.log(` Codex UserPrompt hook: ${
|
|
603
|
-
hookDiagnosis.managedPromptHooks
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
: 'registered'
|
|
608
|
-
}`);
|
|
632
|
+
console.log(` Codex UserPrompt hook: ${describeCodexHookRegistration(
|
|
633
|
+
hookDiagnosis.managedPromptHooks,
|
|
634
|
+
hookDiagnosis.legacyManagedPromptHooks,
|
|
635
|
+
30,
|
|
636
|
+
)}`);
|
|
609
637
|
if (hookDiagnosis.managedPromptHooks.length > 0) {
|
|
610
|
-
|
|
611
|
-
console.log(` command: ${h.command}`);
|
|
612
|
-
console.log(` async: ${h.async === false ? 'false' : String(h.async)}`);
|
|
613
|
-
console.log(` timeoutSec: ${h.timeoutSec ?? '(default)'}`);
|
|
614
|
-
console.log(` trusted: ${h.throughlineDoctorTrusted ? 'yes' : 'no'}`);
|
|
638
|
+
printCodexHookDetails(hookDiagnosis.managedPromptHooks[0]);
|
|
615
639
|
}
|
|
616
|
-
console.log(` Codex PostTool hook: ${
|
|
617
|
-
hookDiagnosis.managedPostToolUseHooks
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
: 'registered'
|
|
622
|
-
}`);
|
|
640
|
+
console.log(` Codex PostTool hook: ${describeCodexHookRegistration(
|
|
641
|
+
hookDiagnosis.managedPostToolUseHooks,
|
|
642
|
+
hookDiagnosis.legacyManagedPostToolUseHooks,
|
|
643
|
+
30,
|
|
644
|
+
)}`);
|
|
623
645
|
if (hookDiagnosis.managedPostToolUseHooks.length > 0) {
|
|
624
|
-
|
|
625
|
-
console.log(` command: ${h.command}`);
|
|
626
|
-
console.log(` async: ${h.async === false ? 'false' : String(h.async)}`);
|
|
627
|
-
console.log(` timeoutSec: ${h.timeoutSec ?? '(default)'}`);
|
|
628
|
-
console.log(` trusted: ${h.throughlineDoctorTrusted ? 'yes' : 'no'}`);
|
|
646
|
+
printCodexHookDetails(hookDiagnosis.managedPostToolUseHooks[0]);
|
|
629
647
|
}
|
|
630
|
-
console.log(` Codex Stop hook: ${
|
|
631
|
-
hookDiagnosis.managedStopHooks
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
: 'registered'
|
|
636
|
-
}`);
|
|
648
|
+
console.log(` Codex Stop hook: ${describeCodexHookRegistration(
|
|
649
|
+
hookDiagnosis.managedStopHooks,
|
|
650
|
+
hookDiagnosis.legacyManagedStopHooks,
|
|
651
|
+
300,
|
|
652
|
+
)}`);
|
|
637
653
|
if (hookDiagnosis.managedStopHooks.length > 0) {
|
|
638
|
-
|
|
639
|
-
console.log(` command: ${h.command}`);
|
|
640
|
-
console.log(` async: ${h.async === false ? 'false' : String(h.async)}`);
|
|
641
|
-
console.log(` timeoutSec: ${h.timeoutSec ?? '(default)'}`);
|
|
642
|
-
console.log(` trusted: ${h.throughlineDoctorTrusted ? 'yes' : 'no'}`);
|
|
654
|
+
printCodexHookDetails(hookDiagnosis.managedStopHooks[0]);
|
|
643
655
|
}
|
|
644
656
|
console.log(` VSCode monitor task: ${monitorTaskDiagnosis.status}`);
|
|
645
657
|
if (monitorTaskDiagnosis.path) {
|
|
@@ -1024,6 +1036,8 @@ export const _internal = {
|
|
|
1024
1036
|
buildCodexContextRefreshDiagnosis,
|
|
1025
1037
|
readCodexHostPrimitiveDiagnosis,
|
|
1026
1038
|
readCodexHookDiagnosis,
|
|
1039
|
+
describeCodexHookRegistration,
|
|
1040
|
+
printCodexHookDetails,
|
|
1027
1041
|
readVsCodeMonitorTaskDiagnosis,
|
|
1028
1042
|
isPidAlive,
|
|
1029
1043
|
findLatestJsonlInSameDir,
|
package/src/cli/doctor.test.mjs
CHANGED
|
@@ -19,8 +19,42 @@ const {
|
|
|
19
19
|
runTrimDiagnosis,
|
|
20
20
|
buildCodexContextRefreshDiagnosis,
|
|
21
21
|
readCodexHostPrimitiveDiagnosis,
|
|
22
|
+
describeCodexHookRegistration,
|
|
23
|
+
printCodexHookDetails,
|
|
22
24
|
} = _internal;
|
|
23
25
|
|
|
26
|
+
test('Codex hook diagnosis requires reinstall for legacy timeoutSec key', () => {
|
|
27
|
+
const legacy = { type: 'command', command: 'canonical', timeoutSec: 300, async: false };
|
|
28
|
+
assert.equal(
|
|
29
|
+
describeCodexHookRegistration([legacy], [], 300),
|
|
30
|
+
'legacy timeout key needs reinstall',
|
|
31
|
+
);
|
|
32
|
+
assert.equal(
|
|
33
|
+
describeCodexHookRegistration([{ type: 'command', command: 'canonical', timeout: 300, async: false }], [], 300),
|
|
34
|
+
'registered',
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('Codex hook diagnosis prints timeout and exposes ignored legacy key', () => {
|
|
39
|
+
const lines = [];
|
|
40
|
+
const originalLog = console.log;
|
|
41
|
+
console.log = (line) => lines.push(line);
|
|
42
|
+
try {
|
|
43
|
+
printCodexHookDetails({
|
|
44
|
+
command: 'canonical',
|
|
45
|
+
timeout: 300,
|
|
46
|
+
timeoutSec: 30,
|
|
47
|
+
async: false,
|
|
48
|
+
throughlineDoctorTrusted: true,
|
|
49
|
+
});
|
|
50
|
+
} finally {
|
|
51
|
+
console.log = originalLog;
|
|
52
|
+
}
|
|
53
|
+
const output = lines.join('\n');
|
|
54
|
+
assert.match(output, /timeout:\s+300/);
|
|
55
|
+
assert.match(output, /legacy timeoutSec:\s+30 \(ignored by Codex; reinstall required\)/);
|
|
56
|
+
});
|
|
57
|
+
|
|
24
58
|
// ─── parseArgs ──────────────────────────────────────────────────────
|
|
25
59
|
|
|
26
60
|
test('parseArgs: 引数なしは session null', () => {
|
|
@@ -345,7 +379,7 @@ test('readCodexHookDiagnosis detects Codex prompt and Stop hooks', () => {
|
|
|
345
379
|
{
|
|
346
380
|
type: 'command',
|
|
347
381
|
command: '/usr/bin/node /pkg/bin/throughline.mjs codex-hook user-prompt-submit',
|
|
348
|
-
|
|
382
|
+
timeout: 30,
|
|
349
383
|
async: false,
|
|
350
384
|
},
|
|
351
385
|
],
|
|
@@ -357,7 +391,7 @@ test('readCodexHookDiagnosis detects Codex prompt and Stop hooks', () => {
|
|
|
357
391
|
{
|
|
358
392
|
type: 'command',
|
|
359
393
|
command: '/usr/bin/node /pkg/bin/throughline.mjs codex-hook post-tool-use',
|
|
360
|
-
|
|
394
|
+
timeout: 30,
|
|
361
395
|
async: false,
|
|
362
396
|
},
|
|
363
397
|
],
|
|
@@ -369,7 +403,7 @@ test('readCodexHookDiagnosis detects Codex prompt and Stop hooks', () => {
|
|
|
369
403
|
{
|
|
370
404
|
type: 'command',
|
|
371
405
|
command: 'throughline codex-hook stop',
|
|
372
|
-
|
|
406
|
+
timeout: 300,
|
|
373
407
|
async: true,
|
|
374
408
|
},
|
|
375
409
|
],
|
|
@@ -7,6 +7,7 @@ import { buildFactoryDiagnostics } from '../factory-diagnostics.mjs';
|
|
|
7
7
|
import { findCodexThreadCandidate, defaultCodexHome } from '../codex-thread-index.mjs';
|
|
8
8
|
import { resolveCodexThreadIdentity } from '../codex-thread-identity.mjs';
|
|
9
9
|
import { CURRENT_VERSION } from '../db.mjs';
|
|
10
|
+
import { isEquivalentCodexHookCommand } from './install.mjs';
|
|
10
11
|
import { _internal as doctorInternal } from './doctor.mjs';
|
|
11
12
|
|
|
12
13
|
const require = createRequire(import.meta.url);
|
|
@@ -122,12 +123,12 @@ function hasFactoryDatabaseShape(db) {
|
|
|
122
123
|
return bodiesUnique;
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
function eventStatus({ hooks, legacyHooks, featureEnabled, expectedCommand,
|
|
126
|
+
function eventStatus({ hooks, legacyHooks, featureEnabled, expectedCommand, timeout }) {
|
|
126
127
|
if (hooks.length === 0) return featureEnabled ? 'not_ready' : 'not_applicable';
|
|
127
128
|
if (!featureEnabled || legacyHooks.length > 0 || hooks.length !== 1) return 'not_ready';
|
|
128
129
|
const hook = hooks[0];
|
|
129
|
-
return hook.type === 'command' && hook.command
|
|
130
|
-
hook.async === false ? 'ready' : 'not_ready';
|
|
130
|
+
return hook.type === 'command' && isEquivalentCodexHookCommand(hook.command, expectedCommand) &&
|
|
131
|
+
hook.timeout === timeout && hook.async === false ? 'ready' : 'not_ready';
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
export function inspectFactoryHooks({ codexHome = defaultCodexHome(), readHooks = doctorInternal.readCodexHookDiagnosis } = {}) {
|
|
@@ -143,21 +144,21 @@ export function inspectFactoryHooks({ codexHome = defaultCodexHome(), readHooks
|
|
|
143
144
|
legacyHooks: diagnosis.legacyManagedPromptHooks,
|
|
144
145
|
featureEnabled: diagnosis.featureEnabled,
|
|
145
146
|
expectedCommand: diagnosis.expectedPromptCommand,
|
|
146
|
-
|
|
147
|
+
timeout: 30,
|
|
147
148
|
}),
|
|
148
149
|
postToolUse: eventStatus({
|
|
149
150
|
hooks: diagnosis.managedPostToolUseHooks,
|
|
150
151
|
legacyHooks: diagnosis.legacyManagedPostToolUseHooks,
|
|
151
152
|
featureEnabled: diagnosis.featureEnabled,
|
|
152
153
|
expectedCommand: diagnosis.expectedPostToolUseCommand,
|
|
153
|
-
|
|
154
|
+
timeout: 30,
|
|
154
155
|
}),
|
|
155
156
|
stop: eventStatus({
|
|
156
157
|
hooks: diagnosis.managedStopHooks,
|
|
157
158
|
legacyHooks: diagnosis.legacyManagedStopHooks,
|
|
158
159
|
featureEnabled: diagnosis.featureEnabled,
|
|
159
160
|
expectedCommand: diagnosis.expectedStopCommand,
|
|
160
|
-
|
|
161
|
+
timeout: 300,
|
|
161
162
|
}),
|
|
162
163
|
};
|
|
163
164
|
const values = Object.values(events);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
-
import { existsSync, mkdtempSync, rmSync, statSync } from 'node:fs';
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
|
|
@@ -166,11 +166,11 @@ test('factory-diagnostics hook inspection rejects corrupt and false-ready shapes
|
|
|
166
166
|
expectedPromptCommand: 'prompt',
|
|
167
167
|
expectedPostToolUseCommand: 'post',
|
|
168
168
|
expectedStopCommand: 'stop',
|
|
169
|
-
managedPromptHooks: [{ type: 'command', command: 'prompt',
|
|
169
|
+
managedPromptHooks: [{ type: 'command', command: 'prompt', timeout: 30, async: true }],
|
|
170
170
|
legacyManagedPromptHooks: [],
|
|
171
|
-
managedPostToolUseHooks: [{ type: 'command', command: 'post',
|
|
171
|
+
managedPostToolUseHooks: [{ type: 'command', command: 'post', timeout: 30, async: false }],
|
|
172
172
|
legacyManagedPostToolUseHooks: [],
|
|
173
|
-
managedStopHooks: [{ type: 'command', command: 'stop',
|
|
173
|
+
managedStopHooks: [{ type: 'command', command: 'stop', timeout: 300, async: false }],
|
|
174
174
|
legacyManagedStopHooks: [],
|
|
175
175
|
}),
|
|
176
176
|
});
|
|
@@ -188,11 +188,11 @@ test('factory-diagnostics hook inspection summarizes every canonical ready event
|
|
|
188
188
|
expectedPromptCommand: 'prompt',
|
|
189
189
|
expectedPostToolUseCommand: 'post',
|
|
190
190
|
expectedStopCommand: 'stop',
|
|
191
|
-
managedPromptHooks: [{ type: 'command', command: 'prompt',
|
|
191
|
+
managedPromptHooks: [{ type: 'command', command: 'prompt', timeout: 30, async: false }],
|
|
192
192
|
legacyManagedPromptHooks: [],
|
|
193
|
-
managedPostToolUseHooks: [{ type: 'command', command: 'post',
|
|
193
|
+
managedPostToolUseHooks: [{ type: 'command', command: 'post', timeout: 30, async: false }],
|
|
194
194
|
legacyManagedPostToolUseHooks: [],
|
|
195
|
-
managedStopHooks: [{ type: 'command', command: 'stop',
|
|
195
|
+
managedStopHooks: [{ type: 'command', command: 'stop', timeout: 300, async: false }],
|
|
196
196
|
legacyManagedStopHooks: [],
|
|
197
197
|
}),
|
|
198
198
|
});
|
|
@@ -202,6 +202,72 @@ test('factory-diagnostics hook inspection summarizes every canonical ready event
|
|
|
202
202
|
assert.equal(result.reason, 'hooks_inspected');
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
test('factory-diagnostics hook inspection: 最小 PATH 由来の node 別表記を not_ready にしない', () => {
|
|
206
|
+
// launchd の factory reporter は PATH に /opt/homebrew/bin を持たないため、期待値の
|
|
207
|
+
// node が PATH symlink ではなく実体表記になる。同一 node を指す限り ready を維持する。
|
|
208
|
+
const dir = mkdtempSync(join(tmpdir(), 'tl-factory-hook-'));
|
|
209
|
+
try {
|
|
210
|
+
mkdirSync(join(dir, 'bin'));
|
|
211
|
+
mkdirSync(join(dir, 'cellar'));
|
|
212
|
+
const realNode = join(dir, 'cellar', 'node');
|
|
213
|
+
const pathNode = join(dir, 'bin', 'node');
|
|
214
|
+
const script = join(dir, 'throughline.mjs');
|
|
215
|
+
writeFileSync(realNode, '');
|
|
216
|
+
writeFileSync(script, '');
|
|
217
|
+
symlinkSync(realNode, pathNode);
|
|
218
|
+
const registered = (event) => `${pathNode} ${script} codex-hook ${event}`;
|
|
219
|
+
const expected = (event) => `${realNode} ${script} codex-hook ${event}`;
|
|
220
|
+
|
|
221
|
+
const result = inspectFactoryHooks({
|
|
222
|
+
readHooks: () => ({
|
|
223
|
+
configExists: true,
|
|
224
|
+
configReadable: true,
|
|
225
|
+
hooksExists: true,
|
|
226
|
+
hooksReadable: true,
|
|
227
|
+
featureEnabled: true,
|
|
228
|
+
expectedPromptCommand: expected('user-prompt-submit'),
|
|
229
|
+
expectedPostToolUseCommand: expected('post-tool-use'),
|
|
230
|
+
expectedStopCommand: expected('stop'),
|
|
231
|
+
managedPromptHooks: [{ type: 'command', command: registered('user-prompt-submit'), timeout: 30, async: false }],
|
|
232
|
+
legacyManagedPromptHooks: [],
|
|
233
|
+
managedPostToolUseHooks: [{ type: 'command', command: registered('post-tool-use'), timeout: 30, async: false }],
|
|
234
|
+
legacyManagedPostToolUseHooks: [],
|
|
235
|
+
managedStopHooks: [{ type: 'command', command: registered('stop'), timeout: 300, async: false }],
|
|
236
|
+
legacyManagedStopHooks: [],
|
|
237
|
+
}),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
assert.deepEqual(result.events, { userPromptSubmit: 'ready', postToolUse: 'ready', stop: 'ready' });
|
|
241
|
+
assert.equal(result.status, 'ready');
|
|
242
|
+
} finally {
|
|
243
|
+
rmSync(dir, { recursive: true, force: true });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('factory-diagnostics hook inspection rejects legacy timeoutSec keys', () => {
|
|
248
|
+
const result = inspectFactoryHooks({
|
|
249
|
+
readHooks: () => ({
|
|
250
|
+
configExists: true,
|
|
251
|
+
configReadable: true,
|
|
252
|
+
hooksExists: true,
|
|
253
|
+
hooksReadable: true,
|
|
254
|
+
featureEnabled: true,
|
|
255
|
+
expectedPromptCommand: 'prompt',
|
|
256
|
+
expectedPostToolUseCommand: 'post',
|
|
257
|
+
expectedStopCommand: 'stop',
|
|
258
|
+
managedPromptHooks: [{ type: 'command', command: 'prompt', timeoutSec: 30, async: false }],
|
|
259
|
+
legacyManagedPromptHooks: [],
|
|
260
|
+
managedPostToolUseHooks: [{ type: 'command', command: 'post', timeoutSec: 30, async: false }],
|
|
261
|
+
legacyManagedPostToolUseHooks: [],
|
|
262
|
+
managedStopHooks: [{ type: 'command', command: 'stop', timeoutSec: 300, async: false }],
|
|
263
|
+
legacyManagedStopHooks: [],
|
|
264
|
+
}),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
assert.deepEqual(result.events, { userPromptSubmit: 'not_ready', postToolUse: 'not_ready', stop: 'not_ready' });
|
|
268
|
+
assert.equal(result.status, 'not_ready');
|
|
269
|
+
});
|
|
270
|
+
|
|
205
271
|
function createFactorySchema(db) {
|
|
206
272
|
db.exec(`
|
|
207
273
|
PRAGMA user_version = 9;
|
package/src/cli/install.mjs
CHANGED
|
@@ -163,6 +163,62 @@ export function isThroughlineCodexPostToolUseCommand(command) {
|
|
|
163
163
|
);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
const CODEX_HOOK_EVENTS = new Set(['stop', 'user-prompt-submit', 'post-tool-use']);
|
|
167
|
+
|
|
168
|
+
function tokenizeCommand(command) {
|
|
169
|
+
const tokens = [];
|
|
170
|
+
const pattern = /"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g;
|
|
171
|
+
let match;
|
|
172
|
+
while ((match = pattern.exec(command)) !== null) {
|
|
173
|
+
// quoteCommandPath は `"` だけを escape する。Windows path の `\` は温存する。
|
|
174
|
+
if (match[1] !== undefined) tokens.push(match[1].replace(/\\"/g, '"'));
|
|
175
|
+
else if (match[2] !== undefined) tokens.push(match[2]);
|
|
176
|
+
else tokens.push(match[3]);
|
|
177
|
+
}
|
|
178
|
+
return tokens;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 絶対パス型の Codex hook command を {nodePath, scriptPath, event} へ分解する。
|
|
183
|
+
* 旧 PATH 解決型 (`throughline codex-hook stop`) など、この形でないものは null。
|
|
184
|
+
*/
|
|
185
|
+
export function parseCodexHookCommand(command) {
|
|
186
|
+
if (typeof command !== 'string') return null;
|
|
187
|
+
const tokens = tokenizeCommand(command.replace(/^&\s+/, ''));
|
|
188
|
+
if (tokens.length !== 4) return null;
|
|
189
|
+
const [nodePath, scriptPath, subcommand, event] = tokens;
|
|
190
|
+
if (subcommand !== 'codex-hook' || !CODEX_HOOK_EVENTS.has(event)) return null;
|
|
191
|
+
if (!scriptPath.endsWith('throughline.mjs')) return null;
|
|
192
|
+
return { nodePath, scriptPath, event };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 登録済み hook command が期待値と同じ実行体を指すかを判定する。
|
|
197
|
+
*
|
|
198
|
+
* node の表記は呼び出し元の PATH で変わる (`resolveCodexHookNodePath` は PATH 上に
|
|
199
|
+
* 同一 node があればその表記を、無ければ `process.execPath` を返す)。launchd の
|
|
200
|
+
* ような最小 PATH から診断すると `/opt/homebrew/bin/node` で登録された正規 hook が
|
|
201
|
+
* `/opt/homebrew/Cellar/node/<ver>/bin/node` と文字列比較され、正しい登録が legacy
|
|
202
|
+
* と誤判定される。実体 (realpath) の同一性で比較してこれを防ぐ。realpath を解決
|
|
203
|
+
* できない場合は同一とみなさない。
|
|
204
|
+
*/
|
|
205
|
+
export function isEquivalentCodexHookCommand(actual, expected, { realpath = realpathSync.native } = {}) {
|
|
206
|
+
if (typeof actual !== 'string' || typeof expected !== 'string') return false;
|
|
207
|
+
if (actual === expected) return true;
|
|
208
|
+
const left = parseCodexHookCommand(actual);
|
|
209
|
+
const right = parseCodexHookCommand(expected);
|
|
210
|
+
if (!left || !right || left.event !== right.event) return false;
|
|
211
|
+
return isSameExecutablePath(left.scriptPath, right.scriptPath, realpath) &&
|
|
212
|
+
isSameExecutablePath(left.nodePath, right.nodePath, realpath);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isSameExecutablePath(a, b, realpath) {
|
|
216
|
+
if (a === b) return true;
|
|
217
|
+
const left = safeRealpath(a, realpath);
|
|
218
|
+
const right = safeRealpath(b, realpath);
|
|
219
|
+
return left !== null && right !== null && left === right;
|
|
220
|
+
}
|
|
221
|
+
|
|
166
222
|
function createCodexHooks() {
|
|
167
223
|
return {
|
|
168
224
|
UserPromptSubmit: {
|
|
@@ -170,7 +226,7 @@ function createCodexHooks() {
|
|
|
170
226
|
{
|
|
171
227
|
type: 'command',
|
|
172
228
|
command: buildCodexUserPromptSubmitHookCommand(),
|
|
173
|
-
|
|
229
|
+
timeout: 30,
|
|
174
230
|
async: false,
|
|
175
231
|
statusMessage: null,
|
|
176
232
|
},
|
|
@@ -181,7 +237,7 @@ function createCodexHooks() {
|
|
|
181
237
|
{
|
|
182
238
|
type: 'command',
|
|
183
239
|
command: buildCodexPostToolUseHookCommand(),
|
|
184
|
-
|
|
240
|
+
timeout: 30,
|
|
185
241
|
async: false,
|
|
186
242
|
statusMessage: null,
|
|
187
243
|
},
|
|
@@ -192,7 +248,7 @@ function createCodexHooks() {
|
|
|
192
248
|
{
|
|
193
249
|
type: 'command',
|
|
194
250
|
command: buildCodexStopHookCommand(),
|
|
195
|
-
|
|
251
|
+
timeout: 300,
|
|
196
252
|
async: false,
|
|
197
253
|
statusMessage: null,
|
|
198
254
|
},
|
package/src/cli/install.test.mjs
CHANGED
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
buildCodexPostToolUseHookCommand,
|
|
9
9
|
buildCodexStopHookCommand,
|
|
10
10
|
buildCodexUserPromptSubmitHookCommand,
|
|
11
|
+
isEquivalentCodexHookCommand,
|
|
11
12
|
isThroughlineCodexHookCommand,
|
|
13
|
+
parseCodexHookCommand,
|
|
12
14
|
resolveCodexHookNodePath,
|
|
13
15
|
run,
|
|
14
16
|
resolveThroughlineOnPath,
|
|
@@ -120,19 +122,21 @@ test('global install registers Codex session hooks and enables hooks features',
|
|
|
120
122
|
.find(h => h.command === expectedStopCommand);
|
|
121
123
|
assert.ok(codexHook, 'Codex Stop should have absolute throughline.mjs codex-hook stop');
|
|
122
124
|
assert.equal(codexHook.async, false, 'Codex Stop hook should be synchronous for Codex');
|
|
123
|
-
assert.equal(codexHook.
|
|
125
|
+
assert.equal(codexHook.timeout, 300, 'Codex Stop hook should allow summarizer time');
|
|
124
126
|
const promptHook = hooks.hooks.UserPromptSubmit
|
|
125
127
|
.flatMap(g => g.hooks ?? [])
|
|
126
128
|
.find(h => h.command === expectedPromptCommand);
|
|
127
129
|
assert.ok(promptHook, 'Codex UserPromptSubmit should have absolute throughline.mjs codex-hook user-prompt-submit');
|
|
128
130
|
assert.equal(promptHook.async, false, 'Codex UserPromptSubmit hook should be synchronous for capture/monitor state');
|
|
129
|
-
assert.equal(promptHook.
|
|
131
|
+
assert.equal(promptHook.timeout, 30, 'Codex UserPromptSubmit hook should be short');
|
|
130
132
|
const postToolUseHook = hooks.hooks.PostToolUse
|
|
131
133
|
.flatMap(g => g.hooks ?? [])
|
|
132
134
|
.find(h => h.command === expectedPostToolUseCommand);
|
|
133
135
|
assert.ok(postToolUseHook, 'Codex PostToolUse should have absolute throughline.mjs codex-hook post-tool-use');
|
|
134
136
|
assert.equal(postToolUseHook.async, false, 'Codex PostToolUse hook should be synchronous for capture/monitor state');
|
|
135
|
-
assert.equal(postToolUseHook.
|
|
137
|
+
assert.equal(postToolUseHook.timeout, 30, 'Codex PostToolUse hook should be short');
|
|
138
|
+
const managedHooks = [codexHook, promptHook, postToolUseHook];
|
|
139
|
+
assert.doesNotMatch(JSON.stringify(managedHooks), /timeoutSec/);
|
|
136
140
|
const config = readFileSync(join(home.dir, '.codex', 'config.toml'), 'utf8');
|
|
137
141
|
assert.match(config, /^\[features\]\ncodex_hooks = true/m);
|
|
138
142
|
assert.match(config, /^hooks = true/m);
|
|
@@ -245,7 +249,7 @@ test('global install preserves existing Codex hooks and is idempotent', async ()
|
|
|
245
249
|
{
|
|
246
250
|
type: 'command',
|
|
247
251
|
command: '/usr/bin/node /home/kite/.npm-global/bin/caveat codex-hook stop',
|
|
248
|
-
|
|
252
|
+
timeout: 5,
|
|
249
253
|
async: false,
|
|
250
254
|
statusMessage: null,
|
|
251
255
|
},
|
|
@@ -287,7 +291,7 @@ test('global install preserves existing Codex hooks and is idempotent', async ()
|
|
|
287
291
|
}
|
|
288
292
|
});
|
|
289
293
|
|
|
290
|
-
test('global install
|
|
294
|
+
test('global install replaces legacy timeoutSec Throughline Codex hook shapes', async () => {
|
|
291
295
|
const home = makeTempHome();
|
|
292
296
|
if (home.resolved !== home.dir) {
|
|
293
297
|
home.restore();
|
|
@@ -356,19 +360,22 @@ test('global install updates existing Throughline Codex hook shapes', async () =
|
|
|
356
360
|
.filter(h => h.command === expectedStopCommand);
|
|
357
361
|
assert.equal(codexHooks.length, 1);
|
|
358
362
|
assert.equal(codexHooks[0].async, false);
|
|
359
|
-
assert.equal(codexHooks[0].
|
|
363
|
+
assert.equal(codexHooks[0].timeout, 300);
|
|
364
|
+
assert.equal(Object.hasOwn(codexHooks[0], 'timeoutSec'), false);
|
|
360
365
|
const promptHooks = hooks.hooks.UserPromptSubmit
|
|
361
366
|
.flatMap(g => g.hooks ?? [])
|
|
362
367
|
.filter(h => h.command === expectedPromptCommand);
|
|
363
368
|
assert.equal(promptHooks.length, 1);
|
|
364
369
|
assert.equal(promptHooks[0].async, false);
|
|
365
|
-
assert.equal(promptHooks[0].
|
|
370
|
+
assert.equal(promptHooks[0].timeout, 30);
|
|
371
|
+
assert.equal(Object.hasOwn(promptHooks[0], 'timeoutSec'), false);
|
|
366
372
|
const postToolUseHooks = hooks.hooks.PostToolUse
|
|
367
373
|
.flatMap(g => g.hooks ?? [])
|
|
368
374
|
.filter(h => h.command === expectedPostToolUseCommand);
|
|
369
375
|
assert.equal(postToolUseHooks.length, 1);
|
|
370
376
|
assert.equal(postToolUseHooks[0].async, false);
|
|
371
|
-
assert.equal(postToolUseHooks[0].
|
|
377
|
+
assert.equal(postToolUseHooks[0].timeout, 30);
|
|
378
|
+
assert.equal(Object.hasOwn(postToolUseHooks[0], 'timeoutSec'), false);
|
|
372
379
|
assert.equal(
|
|
373
380
|
hooks.hooks.Stop.flatMap(g => g.hooks ?? []).filter(h => h.command === 'throughline codex-hook stop').length,
|
|
374
381
|
0,
|
|
@@ -426,7 +433,7 @@ test('global uninstall removes only Throughline-managed Codex hook', async () =>
|
|
|
426
433
|
{
|
|
427
434
|
type: 'command',
|
|
428
435
|
command: '/usr/bin/node /home/kite/.npm-global/bin/caveat codex-hook stop',
|
|
429
|
-
|
|
436
|
+
timeout: 5,
|
|
430
437
|
async: false,
|
|
431
438
|
statusMessage: null,
|
|
432
439
|
},
|
|
@@ -575,6 +582,65 @@ test('resolveCodexHookNodePath: falls back to process execPath when PATH node is
|
|
|
575
582
|
assert.equal(result, execPath);
|
|
576
583
|
});
|
|
577
584
|
|
|
585
|
+
test('parseCodexHookCommand: 絶対パス型を分解し、旧 PATH 解決型と別コマンドは null', () => {
|
|
586
|
+
assert.deepEqual(
|
|
587
|
+
parseCodexHookCommand('/opt/homebrew/bin/node /pkg/bin/throughline.mjs codex-hook stop'),
|
|
588
|
+
{ nodePath: '/opt/homebrew/bin/node', scriptPath: '/pkg/bin/throughline.mjs', event: 'stop' },
|
|
589
|
+
);
|
|
590
|
+
assert.deepEqual(
|
|
591
|
+
parseCodexHookCommand('& "C:\\node.exe" "C:\\pkg\\bin\\throughline.mjs" codex-hook post-tool-use'),
|
|
592
|
+
{ nodePath: 'C:\\node.exe', scriptPath: 'C:\\pkg\\bin\\throughline.mjs', event: 'post-tool-use' },
|
|
593
|
+
);
|
|
594
|
+
assert.equal(parseCodexHookCommand('throughline codex-hook stop'), null);
|
|
595
|
+
assert.equal(parseCodexHookCommand('/bin/node /pkg/bin/throughline.mjs codex-hook rewind'), null);
|
|
596
|
+
assert.equal(parseCodexHookCommand('/bin/node /pkg/bin/other.mjs codex-hook stop'), null);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test('isEquivalentCodexHookCommand: 同一 node 実体を指す別表記を legacy と誤判定しない', () => {
|
|
600
|
+
// launchd の最小 PATH から診断すると expected 側だけ Cellar 実体表記になる。
|
|
601
|
+
const registered = '/opt/homebrew/bin/node /pkg/bin/throughline.mjs codex-hook user-prompt-submit';
|
|
602
|
+
const expected = '/opt/homebrew/Cellar/node/26.5.0/bin/node /pkg/bin/throughline.mjs codex-hook user-prompt-submit';
|
|
603
|
+
const realpath = (p) => (p.endsWith('node') ? '/real/node' : p);
|
|
604
|
+
assert.equal(isEquivalentCodexHookCommand(registered, expected, { realpath }), true);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test('isEquivalentCodexHookCommand: 別 install / 別 event / 旧形式は同一とみなさない', () => {
|
|
608
|
+
const realpath = (p) => p;
|
|
609
|
+
assert.equal(
|
|
610
|
+
isEquivalentCodexHookCommand(
|
|
611
|
+
'/bin/node /old/bin/throughline.mjs codex-hook stop',
|
|
612
|
+
'/bin/node /new/bin/throughline.mjs codex-hook stop',
|
|
613
|
+
{ realpath },
|
|
614
|
+
),
|
|
615
|
+
false,
|
|
616
|
+
);
|
|
617
|
+
assert.equal(
|
|
618
|
+
isEquivalentCodexHookCommand(
|
|
619
|
+
'/bin/node /pkg/bin/throughline.mjs codex-hook stop',
|
|
620
|
+
'/bin/node /pkg/bin/throughline.mjs codex-hook post-tool-use',
|
|
621
|
+
{ realpath },
|
|
622
|
+
),
|
|
623
|
+
false,
|
|
624
|
+
);
|
|
625
|
+
assert.equal(
|
|
626
|
+
isEquivalentCodexHookCommand(
|
|
627
|
+
'throughline codex-hook stop',
|
|
628
|
+
'/bin/node /pkg/bin/throughline.mjs codex-hook stop',
|
|
629
|
+
{ realpath },
|
|
630
|
+
),
|
|
631
|
+
false,
|
|
632
|
+
);
|
|
633
|
+
// realpath を解決できない時は同一とみなさない(暗黙の緩和をしない)
|
|
634
|
+
assert.equal(
|
|
635
|
+
isEquivalentCodexHookCommand(
|
|
636
|
+
'/a/node /pkg/bin/throughline.mjs codex-hook stop',
|
|
637
|
+
'/b/node /pkg/bin/throughline.mjs codex-hook stop',
|
|
638
|
+
{ realpath: (p) => { throw new Error(`missing: ${p}`); } },
|
|
639
|
+
),
|
|
640
|
+
false,
|
|
641
|
+
);
|
|
642
|
+
});
|
|
643
|
+
|
|
578
644
|
test('install is idempotent: second run keeps exactly one tl.md and one hook entry', async () => {
|
|
579
645
|
const home = makeTempHome();
|
|
580
646
|
if (home.resolved !== home.dir) {
|