throughline 0.6.1 → 0.6.2
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 +20 -0
- package/README.md +14 -2
- package/bin/throughline.mjs +31 -0
- package/docs/00_overview.md +2 -0
- package/docs/04_public_release_plan.md +2 -0
- package/docs/13_native_factory_diagnostics_plan.md +46 -0
- package/docs/BUGHUB_RUNTIME_ERROR_STORE_PLAN.md +52 -0
- package/package.json +2 -2
- package/src/auditor-context.test.mjs +8 -1
- package/src/cli/codex-hook.mjs +7 -4
- package/src/cli/codex-hook.test.mjs +4 -0
- package/src/cli/codex-restore-smoke.mjs +2 -1
- package/src/cli/codex-restore-source-audit.mjs +1 -1
- package/src/cli/doctor.mjs +5 -1
- package/src/cli/factory-diagnostics.mjs +246 -0
- package/src/cli/factory-diagnostics.test.mjs +201 -0
- package/src/cli/runtime-errors.mjs +85 -0
- package/src/cli/runtime-errors.test.mjs +75 -0
- package/src/cli/trim.mjs +4 -4
- package/src/codex-handoff-model-smoke.mjs +2 -3
- package/src/codex-sidecar-cli.test.mjs +19 -8
- package/src/codex-sidecar.mjs +2 -5
- package/src/codex-sidecar.test.mjs +17 -9
- package/src/codex-thread-index.mjs +7 -1
- package/src/db.mjs +1 -1
- package/src/factory-diagnostics.mjs +118 -0
- package/src/factory-diagnostics.test.mjs +97 -0
- package/src/haiku-summarizer.mjs +3 -5
- package/src/haiku-summarizer.test.mjs +52 -47
- package/src/hook-entrypoints.test.mjs +3 -1
- package/src/phase0-spotter-contract.test.mjs +6 -7
- package/src/portable-spawn-sync.mjs +58 -0
- package/src/portable-spawn-sync.test.mjs +40 -0
- package/src/prompt-submit.mjs +2 -0
- package/src/runtime-error-hook.test.mjs +106 -0
- package/src/runtime-error-observer.mjs +8 -0
- package/src/runtime-error-store.mjs +595 -0
- package/src/runtime-error-store.test.mjs +307 -0
- package/src/session-start.mjs +2 -0
- package/src/test-env.mjs +59 -2
- package/src/turn-processor.mjs +2 -0
- package/src/windows-acl-test-helper.mjs +29 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.6.2] — release candidate (pending)
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Added an opt-in, local-only runtime error aggregate for BugHub factory
|
|
18
|
+
reporting. Collection requires the canonical dotagents config boolean
|
|
19
|
+
`collection.enabled: true`; it never performs network I/O or accepts raw
|
|
20
|
+
exceptions, stderr, stacks, prompts, sessions, paths, or arbitrary context.
|
|
21
|
+
Fixed hook error codes/templates are aggregated by SHA-256 fingerprint in an
|
|
22
|
+
owner-private atomic store with count, first/last seen, resolve/reopen,
|
|
23
|
+
monotonic cursor/ack, retention that preserves unacknowledged records, and
|
|
24
|
+
bounded `throughline runtime-errors ... --json` snapshot/diagnostics APIs.
|
|
25
|
+
- Release candidate only: collection remains disabled by default and the local
|
|
26
|
+
store sends no network traffic. Publication, tag, CI, and registry-install
|
|
27
|
+
verification are pending.
|
|
28
|
+
- Raised the Node.js floor to 22.13, where `node:sqlite` is available without
|
|
29
|
+
an experimental command-line flag; CI now exercises that exact minimum.
|
|
30
|
+
|
|
13
31
|
## [0.6.1] — 2026-07-13
|
|
14
32
|
|
|
15
33
|
### Added
|
|
@@ -970,6 +988,8 @@ two attempts, instrument first instead of patching again.
|
|
|
970
988
|
|
|
971
989
|
---
|
|
972
990
|
|
|
991
|
+
[Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.6.2...HEAD
|
|
992
|
+
[0.6.2]: https://github.com/kitepon-rgb/Throughline/compare/v0.6.1...v0.6.2
|
|
973
993
|
[0.3.22]: https://github.com/kitepon-rgb/Throughline/releases/tag/v0.3.22
|
|
974
994
|
[0.3.21]: https://github.com/kitepon-rgb/Throughline/compare/v0.3.19...v0.3.21
|
|
975
995
|
[0.3.20]: https://github.com/kitepon-rgb/Throughline/compare/v0.3.19...v0.3.20
|
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ guarded `trim --execute --host codex` surface.
|
|
|
58
58
|
| **Tool I/O handling** | retired to L3, retrievable by `/sc-detail HH:MM:SS` | gone | folded into summary, unreadable | folded into summary |
|
|
59
59
|
| **Coding-assistant fit** | high — tool I/O is the heavy 80% | low — you lose the thread | medium — but irreversible | medium |
|
|
60
60
|
| **Auto-inheritance risk** | low (typed `/clear` / `/tl` names the predecessor) | n/a | n/a | high |
|
|
61
|
-
| **Runtime deps** | **zero** (Node 22.
|
|
61
|
+
| **Runtime deps** | **zero** (Node 22.13+ built-in `node:sqlite`) | n/a | n/a | many |
|
|
62
62
|
| **Multi-session token monitor** | ✅ real `message.usage` / Codex rollout `token_count` | — | — | — |
|
|
63
63
|
|
|
64
64
|
**Short version**: `/clear` throws everything away, `/compact` blurs everything together, Throughline keeps the *text* you wrote verbatim and only retires the *tool output* — which is where 80% of the bloat lives.
|
|
@@ -716,6 +716,11 @@ entry to the `tasks` array yourself:
|
|
|
716
716
|
|
|
717
717
|
## Commands
|
|
718
718
|
|
|
719
|
+
**v0.6.2 は release candidate(公開待ち)です。** factory diagnostics と local
|
|
720
|
+
runtime error aggregate は collection が既定OFFで、canonical dotagents config の
|
|
721
|
+
`collection.enabled: true` を明示した場合だけ収集します。いずれも network I/O は
|
|
722
|
+
行いません。npm公開、tag、CI、registry由来installの確認は未完了です。
|
|
723
|
+
|
|
719
724
|
| Command | What it does |
|
|
720
725
|
| ---------------------------------------------- | ------------------------------------------------------------ |
|
|
721
726
|
| `throughline install` | Register Claude user hooks/slash commands, the global Codex UserPromptSubmit/PostToolUse/Stop hooks, the global `$throughline` Codex skill, and the current VS Code monitor task when applicable |
|
|
@@ -728,6 +733,13 @@ entry to the `tasks` array yourself:
|
|
|
728
733
|
| `throughline doctor --session <id-prefix>` | Diagnose a specific session — detect state/transcript drift, idle vs. stuck |
|
|
729
734
|
| `throughline doctor --trim --host claude\|codex` | Diagnose trim host boundaries, manual procedure, and Codex host primitive blockage |
|
|
730
735
|
| `throughline doctor --codex` | Diagnose Codex primary entry state, captured DB sessions, context-refresh memory contract, new-thread handoff readiness, safe continuation status, and host primitive audit |
|
|
736
|
+
| `throughline factory-diagnostics --json` | Versioned read-only native factory readiness JSON for database schema/migration, connector hooks, and representative capture/restore/handoff; does not emit bodies, secrets, absolute paths, or raw state |
|
|
737
|
+
| `throughline runtime-errors snapshot --json` | Read the bounded product-owned runtime error aggregate. Collection occurs only when canonical dotagents config explicitly sets `collection.enabled: true`; this command performs no network I/O |
|
|
738
|
+
| `throughline runtime-errors diagnostics --json` | Read bounded collection/store status without exposing the state path or raw errors |
|
|
739
|
+
| `throughline runtime-errors ack <cursor> --json` | Explicitly acknowledge records through a monotonic cursor; unacknowledged records are never compacted |
|
|
740
|
+
| `throughline runtime-errors resolve <fingerprint> --json` | Explicitly resolve an aggregate; observing the same fingerprint again reopens it |
|
|
741
|
+
| `throughline runtime-errors reopen <fingerprint> --json` | Explicitly reopen a resolved aggregate without fabricating a new occurrence |
|
|
742
|
+
| `throughline runtime-errors compact --json` | Remove only acknowledged, resolved aggregates after retention; open or unacknowledged records remain |
|
|
731
743
|
| `throughline handoff-preview --session <id>` | Print a Codex-facing `throughline_handoff` JSON projection |
|
|
732
744
|
| `throughline codex-capture --codex-thread-id <id>` | Capture active Codex rollout turns into a `codex:<thread_id>` DB session |
|
|
733
745
|
| `throughline codex-summarize --session codex:<id>` | Summarize captured Codex L2 into L1 with the Codex CLI backend |
|
|
@@ -820,7 +832,7 @@ throughline auditor-context --session claude-session-id --project "$PWD" \
|
|
|
820
832
|
|
|
821
833
|
## Requirements
|
|
822
834
|
|
|
823
|
-
- **Node.js >= 22.
|
|
835
|
+
- **Node.js >= 22.13** (for the stable, flag-free built-in `node:sqlite` module — no native build
|
|
824
836
|
required, no `npm install` of SQLite bindings)
|
|
825
837
|
- **Claude Code** with hooks support (`SessionStart`, `Stop`)
|
|
826
838
|
- **Claude Max subscription** (for Haiku-based L1 summarization via `claude -p`)
|
package/bin/throughline.mjs
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* throughline detail <時刻> # L2+L3 詳細取得 (Claude が Bash 経由で呼ぶ想定)
|
|
12
12
|
* throughline handoff-preview # Codex-facing throughline_handoff JSON preview
|
|
13
13
|
* throughline auditor-context --json # Read-only bounded auditor context JSON
|
|
14
|
+
* throughline factory-diagnostics --json # Native factory read-only readiness JSON
|
|
15
|
+
* throughline runtime-errors snapshot --json # Product-owned runtime error aggregates
|
|
14
16
|
* throughline codex-capture # Capture active Codex rollout turns into Throughline DB
|
|
15
17
|
* throughline codex-hook user-prompt-submit # Codex current-session auto-refresh prompt hook
|
|
16
18
|
* throughline codex-hook post-tool-use # Codex current-session auto-refresh tool-loop hook
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
|
|
42
44
|
const [, , cmd, ...rest] = process.argv;
|
|
43
45
|
|
|
46
|
+
try {
|
|
44
47
|
switch (cmd) {
|
|
45
48
|
case 'install':
|
|
46
49
|
await (await import('../src/cli/install.mjs')).run(rest);
|
|
@@ -71,6 +74,16 @@ switch (cmd) {
|
|
|
71
74
|
if (exitCode !== 0) process.exitCode = exitCode;
|
|
72
75
|
break;
|
|
73
76
|
}
|
|
77
|
+
case 'factory-diagnostics': {
|
|
78
|
+
const exitCode = (await import('../src/cli/factory-diagnostics.mjs')).run(rest);
|
|
79
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'runtime-errors': {
|
|
83
|
+
const exitCode = (await import('../src/cli/runtime-errors.mjs')).run(rest);
|
|
84
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
74
87
|
case 'codex-capture':
|
|
75
88
|
await (await import('../src/cli/codex-capture.mjs')).run(rest);
|
|
76
89
|
break;
|
|
@@ -142,6 +155,18 @@ switch (cmd) {
|
|
|
142
155
|
default:
|
|
143
156
|
await showHelp();
|
|
144
157
|
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const code = {
|
|
160
|
+
'session-start': 'HOOK_SESSION_START_FAILED',
|
|
161
|
+
'prompt-submit': 'HOOK_PROMPT_SUBMIT_FAILED',
|
|
162
|
+
'process-turn': 'HOOK_PROCESS_TURN_FAILED',
|
|
163
|
+
}[cmd];
|
|
164
|
+
if (code) {
|
|
165
|
+
const { recordRuntimeErrorBestEffort } = await import('../src/runtime-error-store.mjs');
|
|
166
|
+
recordRuntimeErrorBestEffort(code);
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
145
170
|
|
|
146
171
|
async function showHelp() {
|
|
147
172
|
const { createRequire } = await import('node:module');
|
|
@@ -159,6 +184,12 @@ Usage:
|
|
|
159
184
|
Read only bounded completed user/assistant context
|
|
160
185
|
for an auditor; requires either --host plus --transcript,
|
|
161
186
|
or explicit pair identity/hashes; always requires --json
|
|
187
|
+
throughline factory-diagnostics --json
|
|
188
|
+
Read-only native factory readiness JSON. Never emits
|
|
189
|
+
session/prompt bodies, secrets, absolute paths, or raw state
|
|
190
|
+
throughline runtime-errors snapshot --json
|
|
191
|
+
Read bounded local runtime error aggregates. Also supports
|
|
192
|
+
diagnostics, ack <cursor>, resolve <fingerprint>, and compact
|
|
162
193
|
throughline codex-capture Capture active Codex rollout turns into DB
|
|
163
194
|
(requires --codex-thread-id or env thread id)
|
|
164
195
|
throughline codex-hook user-prompt-submit
|
package/docs/00_overview.md
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
| [09_rollback_context_trim_insight.md](09_rollback_context_trim_insight.md) | rollback を context delete primitive と見る設計メモ |
|
|
18
18
|
| [10_transcript_injection_plan.md](10_transcript_injection_plan.md) | transcript injection 検証計画と v0.5 実機結果 |
|
|
19
19
|
| [11_codex_monitor_implementation_plan.md](11_codex_monitor_implementation_plan.md) | Codex monitor 対応の実装記録 |
|
|
20
|
+
| [13_native_factory_diagnostics_plan.md](13_native_factory_diagnostics_plan.md) | native factory read-only readiness 診断の実装記録 |
|
|
21
|
+
| [BUGHUB_RUNTIME_ERROR_STORE_PLAN.md](BUGHUB_RUNTIME_ERROR_STORE_PLAN.md) | local runtime error aggregate store の契約と実装 TODO |
|
|
20
22
|
|
|
21
23
|
## Supporting Records
|
|
22
24
|
|
|
@@ -137,6 +137,8 @@ schema v4 で PostToolUse (`capture-tool`) は廃止、L2/L3 は Stop 内で一
|
|
|
137
137
|
| **npm 公開 (v0.4.0): /clear auto-handoff + memo / save-inflight / /tl-trim retire** | 2026-05-08 Claude Code 2.1.128 で `source='clear'` が reliable になったため、`/clear` で自動引継ぎがデフォルト ON になる auto path を追加。`THROUGHLINE_DISABLE_AUTO_HANDOFF=1` で OFF にできる。`/tl` slash command は明示意思マーカーへ簡素化 (memo 4 項目入力廃止、`save-inflight` CLI 削除、`/tl-trim` slash command 廃止、`updateBatonMemo` 関数削除、`handoff_batons.memo_text` を schema v8 で drop)。注入は L1 + L2 + L3 references のみに簡素化し、memo / 中断直前 thinking セクションを削除。Codex 側 trim path は維持。詳細は [CHANGELOG.md](../CHANGELOG.md) と [02_clear_auto_handoff_plan.md](02_clear_auto_handoff_plan.md) |
|
|
138
138
|
| **npm 公開 (v0.4.1): typed `/clear` も baton を書く + 2 経路の優先順位入れ替え** | 2026-05-09 `/clear` を UserPromptSubmit hook で検出した時点で当該セッションの `session_id` を `handoff_batons` に書き込み、次 SessionStart が確定的にそのセッションを引き継ぐ。これで multi-window で「最新更新セッション ≠ /clear したセッション」になるシナリオで `findLatestClaudePredecessor` heuristic が誤った前任を選ぶ問題を解消。2 経路の優先順位を **baton path = primary、auto path = fallback** に変更 (auto path は VSCode 拡張メニュー由来など UserPromptSubmit に届かない経路のフォールバック)。`THROUGHLINE_DISABLE_AUTO_HANDOFF=1` は fallback path のみに作用するようになった (typed `/clear` / `/tl` は env と無関係に発火する)。あわせて `.vscode/tasks.json` を git 追跡から外し (gitignore)、`ensureMonitorTaskFile` が hook 発火ごとに絶対パスを書き換える挙動による別環境での dirty diff を解消。`src/prompt-submit.test.mjs` を新設し、`isClearCommand` / `isBatonCommand` 判定 14 件と subprocess+DB 実体テスト 3 件を追加。詳細は [CHANGELOG.md](../CHANGELOG.md) |
|
|
139
139
|
| **npm 公開 (v0.4.7): Codex monitor direct discovery + 80% auto-refresh** | 2026-05-09 Codex Stop hook auto-refresh の verified usage threshold を 90% から 80% に変更し、Codex native auto-compact より先に Throughline DB memory refresh を試行する。estimate usage / estimated context window では mutation しない。`throughline monitor` は `~/.throughline/state` に加えて `~/.codex/sessions/**/rollout-*.jsonl` を直接 discovery し、Throughline state が未生成の現在 Codex thread も表示する。既存 state がある場合は state の usage snapshot を保持しつつ discovered rollout path / mtime を合流する。Codex 表示 ID は `codex:01` ではなく raw thread id 先頭 8 桁 (`019e085c`) にした。Codex open turn の transient `output_tokens` は token count に overlay するが、モデル欄の `live+<tokens>` marker は表示しない |
|
|
140
|
+
| **npm 公開 (v0.6.1): Spotter auditor context projection** | `throughline auditor-context`を追加。exact session/projectと最新完了L2 pairのorigin/turn/hash freshnessを検査し、fresh時だけbounded user/assistant本文をversioned JSONで返す。DBはread-onlyで開き、作成・migration・WAL書き込みをしない。Spotter側はproject opt-inでのみ利用する。580 tests、CI 6/6、pack scan、registry global install / doctorを通過し、npm / tag / GitHub Releaseを2026-07-13に公開した |
|
|
141
|
+
| **v0.6.2 release candidate(公開待ち): factory diagnostics + local runtime error store** | `throughline factory-diagnostics --json` と `throughline runtime-errors ... --json` を公開候補へ同期する。collection は canonical dotagents config の `collection.enabled: true` が明示された時だけ有効で既定OFF、store は network I/O を行わない。npm公開、tag、CI、registry由来installの確認はこの時点で未実施であり、公開済みとは扱わない |
|
|
140
142
|
| **未リリース: 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` も有効化する |
|
|
141
143
|
| **グローバル E2E 検証** | 2026-04-17 別ディレクトリから `throughline doctor` 全緑を確認 |
|
|
142
144
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Wave 4: Native Factory Diagnostics
|
|
2
|
+
|
|
3
|
+
## 目的
|
|
4
|
+
|
|
5
|
+
`throughline factory-diagnostics --json` を追加し、Throughline の native
|
|
6
|
+
factory 接続に必要な read-only readiness を、安定した機械可読 JSON として返す。
|
|
7
|
+
この診断は capture / restore / handoff を実行せず、DB・state・hook 設定・rollout・
|
|
8
|
+
session の本文、秘密、絶対パス、例外詳細を出力しない。
|
|
9
|
+
|
|
10
|
+
## 契約
|
|
11
|
+
|
|
12
|
+
- 診断 schema は versioned とし、CLI version、diagnostic schema、overall、database
|
|
13
|
+
schema/migration、hook、capture / restore / handoff の代表 read-only readiness を返す。
|
|
14
|
+
- 判定値は `ready` / `not_ready` / `not_applicable` / `unverified`。正常に未設定な
|
|
15
|
+
代表入力は `not_applicable`、観測不能・判定不能は `unverified` とし、pass に丸めない。
|
|
16
|
+
- `overall` は `ready` を、必須の applicable check がすべて `ready` の場合だけ返す。
|
|
17
|
+
`unverified` は成功に含めない。
|
|
18
|
+
- 既存の `doctor` / `status` の人間向け stdout 契約は変更しない。既存 helper の
|
|
19
|
+
read-only projection だけを利用し、runtime error store / telemetry は追加しない。
|
|
20
|
+
- CLIは`--json`をちょうど一つ必須とする。exit 0は有効snapshot、exit 1は固定文言の
|
|
21
|
+
internal diagnostic failure、exit 2は固定文言のusage errorを表す。
|
|
22
|
+
- DBは`PRAGMA user_version`だけでgreenにせず、正規table/column/index shapeをread-onlyで
|
|
23
|
+
検証する。handoff memoryは一致確認済みproject/threadだけを数える。
|
|
24
|
+
- restore capabilityとlive smoke evidenceを分離する。未実行smokeは`unverified`だが、
|
|
25
|
+
capability readinessを恒常的に失敗扱いにはしない。
|
|
26
|
+
- Codex hookはcanonical shapeを検査するが、trust実火は機械検証不能なので`unverified`に
|
|
27
|
+
留める。Claude connectorはこの単位では未検査として明示`unverified`にする。
|
|
28
|
+
|
|
29
|
+
## 実施 TODO
|
|
30
|
+
|
|
31
|
+
- [x] 既存の doctor / status、Codex capture、restore、handoff、state schema、hook
|
|
32
|
+
入口、および関連するテストを実読する。
|
|
33
|
+
- [x] 変更前のベースラインとして `npm test` を green で取得する。
|
|
34
|
+
- [x] privacy / schema / characterization fixture を先に追加し、本文・秘密・絶対パスを
|
|
35
|
+
含めない JSON 契約を固定する。
|
|
36
|
+
- [x] read-only factory diagnostics の projection と CLI entrypoint を実装する。
|
|
37
|
+
- [x] version、diagnostic schema、overall、state schema、hook、capture / restore /
|
|
38
|
+
handoff readiness を段階別に返す。
|
|
39
|
+
- [x] help / README の実装済み CLI 仕様を追記する。
|
|
40
|
+
- [x] 対象テスト、`npm test`、`git diff --check` を通し、差分を再読して完了確認する。
|
|
41
|
+
|
|
42
|
+
## 完了条件
|
|
43
|
+
|
|
44
|
+
- JSON は stdout に一つだけ出力され、read-only fixture で DB・state・設定を作成・更新しない。
|
|
45
|
+
- 代表 capture / restore / handoff の未設定と不明を区別し、unverified を ready にしない。
|
|
46
|
+
- 出力・エラーには session / prompt 本文、秘密、絶対パス、raw state / log が含まれない。
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# BugHub runtime error store plan
|
|
2
|
+
|
|
3
|
+
Status: implementation complete; v0.6.2 release pending
|
|
4
|
+
|
|
5
|
+
This plan is the implementation TODO for Throughline's product-owned, local
|
|
6
|
+
runtime error projection. It implements the cross-repository contract in
|
|
7
|
+
`dotagents/docs/plan_bughub-factory-integration.md` without changing the
|
|
8
|
+
existing transcript, handoff, or SQLite memory contracts.
|
|
9
|
+
|
|
10
|
+
## Contract
|
|
11
|
+
|
|
12
|
+
- Collection is disabled unless the canonical dotagents factory reporter
|
|
13
|
+
config exists and contains the JSON boolean `collection.enabled: true`.
|
|
14
|
+
- This module never performs network I/O and never reads
|
|
15
|
+
`reporting.enabled`, endpoints, or credentials.
|
|
16
|
+
- Persist only an allow-listed aggregate: product/version, component, stable
|
|
17
|
+
error code, fixed message template, severity, SHA-256 fingerprint, count,
|
|
18
|
+
first/last seen, state schema version, OS/arch, status, and sequence.
|
|
19
|
+
- Never accept or persist exception objects, stderr/stdout, stacks, prompts,
|
|
20
|
+
session/transcript bodies, absolute paths, file contents, tokens, cookies,
|
|
21
|
+
arbitrary context, or provider output.
|
|
22
|
+
- One failure has one owning observation layer. Existing stderr remains local
|
|
23
|
+
operator diagnostics but is not copied into this store.
|
|
24
|
+
- Store failure must not replace the product failure. Emit one fixed diagnostic
|
|
25
|
+
without reflecting the storage error. Observation runs in a bounded child
|
|
26
|
+
process so blocked config/state I/O cannot hold the original hook result.
|
|
27
|
+
- Use an owner-private directory/file, atomic replacement, monotonic cursor,
|
|
28
|
+
explicit acknowledgement, explicit resolution/reopen, and retention that
|
|
29
|
+
never removes an unacknowledged record.
|
|
30
|
+
- Serialize mutations with a private SQLite `BEGIN IMMEDIATE` mutex. The OS
|
|
31
|
+
releases the lock when a process crashes, so no PID/mtime stale-owner guess
|
|
32
|
+
or application-level reclaim can remove a newer writer's lock.
|
|
33
|
+
|
|
34
|
+
## TODO
|
|
35
|
+
|
|
36
|
+
- [x] Add characterization tests for disabled/missing/malformed config.
|
|
37
|
+
- [x] Add privacy negative fixtures and stable fingerprint aggregation tests.
|
|
38
|
+
- [x] Add acknowledgement, resolution/reopen, retention, mode, and atomic-write tests.
|
|
39
|
+
- [x] Implement the product-owned aggregate store and read-only snapshot API.
|
|
40
|
+
- [x] Add a machine-readable diagnostics projection without exposing state paths.
|
|
41
|
+
- [x] Connect only the top-level hook processing failure boundaries, with fixed
|
|
42
|
+
codes/templates and no duplicate lower-layer observation.
|
|
43
|
+
- [x] Run the complete test suite and update product documentation.
|
|
44
|
+
- [x] Commit and push this repository independently.
|
|
45
|
+
|
|
46
|
+
## Release wave
|
|
47
|
+
|
|
48
|
+
- [x] Bump the package and release-facing documentation to `0.6.2` without moving an existing tag.
|
|
49
|
+
- [x] Run the full test suite, pack inspection, secret/path scan, and temporary-prefix install smoke.
|
|
50
|
+
- [ ] Push the release commit and require the public CI gate to pass.
|
|
51
|
+
- [ ] After the owner H gate, publish `throughline@0.6.2`, create tag/release, and verify npm `latest` plus a registry-derived global install.
|
|
52
|
+
- [ ] Confirm `throughline factory-diagnostics --json` and the runtime-error snapshot/ack commands from the published package, then record the public SHA and results in the changelog and canonical docs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "throughline",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
|
|
6
6
|
"keywords": [
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"test": "node --import ./src/test-env.mjs --test src/*.test.mjs src/cli/*.test.mjs"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
|
-
"node": ">=22.
|
|
37
|
+
"node": ">=22.13"
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -283,7 +283,14 @@ function snapshotSqliteFiles(path) {
|
|
|
283
283
|
return [path, `${path}-wal`, `${path}-shm`].map((file) => {
|
|
284
284
|
if (!existsSync(file)) return { file, exists: false };
|
|
285
285
|
const stat = lstatSync(file);
|
|
286
|
-
|
|
286
|
+
try {
|
|
287
|
+
return { file, exists: true, size: stat.size, mtimeMs: stat.mtimeMs, bytes: readFileSync(file).toString('hex') };
|
|
288
|
+
} catch (error) {
|
|
289
|
+
// Windows denies byte reads for a live WAL handle. Metadata still
|
|
290
|
+
// proves the read-only auditor did not create or mutate a sidecar.
|
|
291
|
+
if (error?.code === 'EBUSY') return { file, exists: true, size: stat.size, mtimeMs: stat.mtimeMs, readError: 'EBUSY' };
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
287
294
|
});
|
|
288
295
|
}
|
|
289
296
|
|
package/src/cli/codex-hook.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { recordRuntimeErrorBestEffort } from '../runtime-error-store.mjs';
|
|
2
|
+
|
|
1
3
|
function parseArgs(argv) {
|
|
2
4
|
const out = {
|
|
3
5
|
event: null,
|
|
@@ -57,10 +59,9 @@ function parsePayload(raw) {
|
|
|
57
59
|
|
|
58
60
|
function codexHomeFromTranscriptPath(transcriptPath) {
|
|
59
61
|
if (typeof transcriptPath !== 'string') return null;
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return transcriptPath.slice(0, idx);
|
|
62
|
+
const match = /[\\/]sessions[\\/]/.exec(transcriptPath);
|
|
63
|
+
if (!match || match.index <= 0) return null;
|
|
64
|
+
return transcriptPath.slice(0, match.index);
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function suppressExperimentalWarnings() {
|
|
@@ -366,6 +367,7 @@ export async function run(argv = []) {
|
|
|
366
367
|
parsed = parseArgs(argv);
|
|
367
368
|
payload = parsePayload(await readStdin());
|
|
368
369
|
} catch (err) {
|
|
370
|
+
recordRuntimeErrorBestEffort('HOOK_CODEX_FAILED', { env: process.env });
|
|
369
371
|
const msg = err instanceof Error ? err.message : 'unknown';
|
|
370
372
|
process.stderr.write(`[codex-hook] ${msg}\n`);
|
|
371
373
|
process.exit(1);
|
|
@@ -399,6 +401,7 @@ export async function run(argv = []) {
|
|
|
399
401
|
}
|
|
400
402
|
process.exit(result.status === 'ok' || result.status === 'skipped' ? 0 : 1);
|
|
401
403
|
} catch (err) {
|
|
404
|
+
recordRuntimeErrorBestEffort('HOOK_CODEX_FAILED', { env: process.env });
|
|
402
405
|
const msg = err instanceof Error ? err.message : 'unknown';
|
|
403
406
|
if (parsed.json) {
|
|
404
407
|
process.stdout.write(
|
|
@@ -500,6 +500,10 @@ test('codex-hook stop skips cleanly when Codex thread id is unavailable', async
|
|
|
500
500
|
test('codexHomeFromTranscriptPath infers CODEX_HOME from rollout path', () => {
|
|
501
501
|
const path = '/tmp/codex-home/sessions/2026/05/06/rollout-2026-05-06T09-40-50-id.jsonl';
|
|
502
502
|
assert.equal(_internal.codexHomeFromTranscriptPath(path), '/tmp/codex-home');
|
|
503
|
+
assert.equal(
|
|
504
|
+
_internal.codexHomeFromTranscriptPath('C:\\codex-home\\sessions\\2026\\05\\06\\rollout-2026-05-06T09-40-50-id.jsonl'),
|
|
505
|
+
'C:\\codex-home',
|
|
506
|
+
);
|
|
503
507
|
});
|
|
504
508
|
|
|
505
509
|
test('parseArgs: accepts user-prompt-submit Codex hook event', () => {
|
|
@@ -326,7 +326,8 @@ export async function run(args) {
|
|
|
326
326
|
|
|
327
327
|
if (parsed.json) process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
328
328
|
else process.stdout.write(renderTextResult(payload) + '\n');
|
|
329
|
-
process.
|
|
329
|
+
process.exitCode =
|
|
330
|
+
payload.status === 'app-server-restart-stable' && !payload.restoreSafetyRiskInspected ? 0 : 1;
|
|
330
331
|
}
|
|
331
332
|
|
|
332
333
|
function buildRestoreTextNeedles(restoreSafety) {
|
|
@@ -296,7 +296,7 @@ export async function run(args) {
|
|
|
296
296
|
|
|
297
297
|
if (parsed.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
298
298
|
else process.stdout.write(renderTextResult(result) + '\n');
|
|
299
|
-
process.
|
|
299
|
+
process.exitCode = result.status === 'restore-source-audit-complete' ? 0 : 1;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
export const _internal = {
|
package/src/cli/doctor.mjs
CHANGED
|
@@ -493,6 +493,9 @@ function readCodexHookDiagnosis(codexHome) {
|
|
|
493
493
|
expectedPromptCommand,
|
|
494
494
|
expectedPostToolUseCommand,
|
|
495
495
|
hooksReadable: false,
|
|
496
|
+
hooksExists: existsSync(hooksPath),
|
|
497
|
+
configExists: existsSync(configPath),
|
|
498
|
+
configReadable: false,
|
|
496
499
|
featureEnabled: false,
|
|
497
500
|
codexHooksFeatureEnabled: false,
|
|
498
501
|
hooksFeatureEnabled: false,
|
|
@@ -513,6 +516,7 @@ function readCodexHookDiagnosis(codexHome) {
|
|
|
513
516
|
if (existsSync(configPath)) {
|
|
514
517
|
try {
|
|
515
518
|
const config = readFileSync(configPath, 'utf8');
|
|
519
|
+
out.configReadable = true;
|
|
516
520
|
out.codexHooksFeatureEnabled = /^\s*codex_hooks\s*=\s*true\s*$/m.test(config);
|
|
517
521
|
out.hooksFeatureEnabled = /^\s*hooks\s*=\s*true\s*$/m.test(config);
|
|
518
522
|
out.featureEnabled = out.codexHooksFeatureEnabled || out.hooksFeatureEnabled;
|
|
@@ -522,7 +526,7 @@ function readCodexHookDiagnosis(codexHome) {
|
|
|
522
526
|
}
|
|
523
527
|
}
|
|
524
528
|
|
|
525
|
-
if (!
|
|
529
|
+
if (!out.hooksExists) return out;
|
|
526
530
|
let parsed;
|
|
527
531
|
try {
|
|
528
532
|
parsed = JSON.parse(readFileSync(hooksPath, 'utf8'));
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
|
|
5
|
+
import { defaultAuditorContextDbPath } from '../auditor-context.mjs';
|
|
6
|
+
import { buildFactoryDiagnostics } from '../factory-diagnostics.mjs';
|
|
7
|
+
import { findCodexThreadCandidate, defaultCodexHome } from '../codex-thread-index.mjs';
|
|
8
|
+
import { resolveCodexThreadIdentity } from '../codex-thread-identity.mjs';
|
|
9
|
+
import { CURRENT_VERSION } from '../db.mjs';
|
|
10
|
+
import { _internal as doctorInternal } from './doctor.mjs';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const PACKAGE_VERSION = require('../../package.json').version;
|
|
14
|
+
const REQUIRED_DATABASE_COLUMNS = {
|
|
15
|
+
sessions: ['session_id', 'project_path', 'status', 'created_at', 'updated_at', 'merged_into'],
|
|
16
|
+
skeletons: ['id', 'session_id', 'turn_number', 'role', 'summary', 'created_at', 'origin_session_id'],
|
|
17
|
+
bodies: ['id', 'session_id', 'origin_session_id', 'turn_number', 'role', 'text', 'token_count', 'created_at'],
|
|
18
|
+
details: ['id', 'session_id', 'turn_number', 'tool_name', 'input_text', 'output_text', 'token_count', 'created_at', 'origin_session_id', 'kind', 'source_id'],
|
|
19
|
+
handoff_batons: ['project_path', 'session_id', 'created_at'],
|
|
20
|
+
};
|
|
21
|
+
const REQUIRED_DATABASE_INDEXES = ['uq_skeletons_turn_v3', 'uq_details_source'];
|
|
22
|
+
const REQUIRED_INDEX_SHAPES = {
|
|
23
|
+
uq_skeletons_turn_v3: {
|
|
24
|
+
table: 'skeletons',
|
|
25
|
+
columns: ['session_id', 'origin_session_id', 'turn_number', 'role'],
|
|
26
|
+
partial: false,
|
|
27
|
+
},
|
|
28
|
+
uq_details_source: {
|
|
29
|
+
table: 'details',
|
|
30
|
+
columns: ['session_id', 'origin_session_id', 'source_id'],
|
|
31
|
+
partial: true,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function parseArgs(argv = []) {
|
|
36
|
+
if (argv.length !== 1 || argv[0] !== '--json') throw new TypeError('usage error');
|
|
37
|
+
return { json: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function inspectFactoryDatabase({
|
|
41
|
+
dbPath = defaultAuditorContextDbPath(),
|
|
42
|
+
threadId = null,
|
|
43
|
+
projectPath = null,
|
|
44
|
+
} = {}) {
|
|
45
|
+
if (!existsSync(dbPath)) {
|
|
46
|
+
return databaseResult('not_applicable', null, false);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let db;
|
|
50
|
+
try {
|
|
51
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
52
|
+
const schemaVersion = Number(db.prepare('PRAGMA user_version').get()?.user_version ?? 0);
|
|
53
|
+
if (schemaVersion !== CURRENT_VERSION || !hasFactoryDatabaseShape(db)) {
|
|
54
|
+
return databaseResult('not_ready', schemaVersion, false);
|
|
55
|
+
}
|
|
56
|
+
if (!threadId || typeof projectPath !== 'string' || projectPath.length === 0) {
|
|
57
|
+
return databaseResult('ready', schemaVersion, false);
|
|
58
|
+
}
|
|
59
|
+
const sessionId = `codex:${threadId}`;
|
|
60
|
+
const counts = db.prepare(
|
|
61
|
+
`SELECT
|
|
62
|
+
(SELECT COUNT(*) FROM skeletons WHERE session_id = :sessionId) AS l1,
|
|
63
|
+
(SELECT COUNT(*) FROM bodies WHERE session_id = :sessionId) AS l2,
|
|
64
|
+
(SELECT COUNT(*) FROM details WHERE session_id = :sessionId) AS l3
|
|
65
|
+
WHERE EXISTS (
|
|
66
|
+
SELECT 1 FROM sessions
|
|
67
|
+
WHERE session_id = :sessionId AND lower(project_path) = lower(:projectPath)
|
|
68
|
+
)`,
|
|
69
|
+
).get({ sessionId, projectPath });
|
|
70
|
+
const handoffMemory = counts !== undefined &&
|
|
71
|
+
Number(counts.l1 ?? 0) + Number(counts.l2 ?? 0) + Number(counts.l3 ?? 0) > 0;
|
|
72
|
+
return databaseResult('ready', schemaVersion, handoffMemory);
|
|
73
|
+
} catch {
|
|
74
|
+
return databaseResult('unverified', null, false);
|
|
75
|
+
} finally {
|
|
76
|
+
db?.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function databaseResult(status, schemaVersion, handoffMemory) {
|
|
81
|
+
return {
|
|
82
|
+
status,
|
|
83
|
+
schemaVersion,
|
|
84
|
+
supportedSchemaVersion: CURRENT_VERSION,
|
|
85
|
+
handoffMemory,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hasFactoryDatabaseShape(db) {
|
|
90
|
+
const tableInfo = {};
|
|
91
|
+
for (const [table, requiredColumns] of Object.entries(REQUIRED_DATABASE_COLUMNS)) {
|
|
92
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
93
|
+
tableInfo[table] = new Map(rows.map((row) => [row.name, row]));
|
|
94
|
+
const actual = new Set(tableInfo[table].keys());
|
|
95
|
+
if (requiredColumns.some((column) => !actual.has(column))) return false;
|
|
96
|
+
}
|
|
97
|
+
if (tableInfo.sessions.get('session_id')?.pk !== 1 ||
|
|
98
|
+
tableInfo.handoff_batons.get('project_path')?.pk !== 1) return false;
|
|
99
|
+
for (const [table, columns] of Object.entries({
|
|
100
|
+
sessions: ['project_path', 'status', 'created_at', 'updated_at'],
|
|
101
|
+
handoff_batons: ['session_id', 'created_at'],
|
|
102
|
+
})) {
|
|
103
|
+
if (columns.some((column) => tableInfo[table].get(column)?.notnull !== 1)) return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const name of REQUIRED_DATABASE_INDEXES) {
|
|
107
|
+
const expected = REQUIRED_INDEX_SHAPES[name];
|
|
108
|
+
const index = db.prepare(`PRAGMA index_list(${expected.table})`).all()
|
|
109
|
+
.find((row) => row.name === name);
|
|
110
|
+
if (!index || index.unique !== 1 || Boolean(index.partial) !== expected.partial) return false;
|
|
111
|
+
const columns = db.prepare(`PRAGMA index_info(${name})`).all().map((row) => row.name);
|
|
112
|
+
if (columns.length !== expected.columns.length ||
|
|
113
|
+
columns.some((column, indexPosition) => column !== expected.columns[indexPosition])) return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const bodiesUnique = db.prepare('PRAGMA index_list(bodies)').all().some((index) => {
|
|
117
|
+
if (index.unique !== 1) return false;
|
|
118
|
+
const columns = db.prepare(`PRAGMA index_info(${index.name})`).all().map((row) => row.name);
|
|
119
|
+
return columns.join('\0') === ['session_id', 'origin_session_id', 'turn_number', 'role'].join('\0');
|
|
120
|
+
});
|
|
121
|
+
return bodiesUnique;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function eventStatus({ hooks, legacyHooks, featureEnabled, expectedCommand, timeoutSec }) {
|
|
125
|
+
if (hooks.length === 0) return featureEnabled ? 'not_ready' : 'not_applicable';
|
|
126
|
+
if (!featureEnabled || legacyHooks.length > 0 || hooks.length !== 1) return 'not_ready';
|
|
127
|
+
const hook = hooks[0];
|
|
128
|
+
return hook.type === 'command' && hook.command === expectedCommand && hook.timeoutSec === timeoutSec &&
|
|
129
|
+
hook.async === false ? 'ready' : 'not_ready';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function inspectFactoryHooks({ codexHome = defaultCodexHome(), readHooks = doctorInternal.readCodexHookDiagnosis } = {}) {
|
|
133
|
+
try {
|
|
134
|
+
const diagnosis = readHooks(codexHome);
|
|
135
|
+
if ((diagnosis.configExists && !diagnosis.configReadable) ||
|
|
136
|
+
(diagnosis.hooksExists && !diagnosis.hooksReadable)) {
|
|
137
|
+
return { status: 'unverified', reason: 'hook_configuration_unreadable', events: {} };
|
|
138
|
+
}
|
|
139
|
+
const events = {
|
|
140
|
+
userPromptSubmit: eventStatus({
|
|
141
|
+
hooks: diagnosis.managedPromptHooks,
|
|
142
|
+
legacyHooks: diagnosis.legacyManagedPromptHooks,
|
|
143
|
+
featureEnabled: diagnosis.featureEnabled,
|
|
144
|
+
expectedCommand: diagnosis.expectedPromptCommand,
|
|
145
|
+
timeoutSec: 30,
|
|
146
|
+
}),
|
|
147
|
+
postToolUse: eventStatus({
|
|
148
|
+
hooks: diagnosis.managedPostToolUseHooks,
|
|
149
|
+
legacyHooks: diagnosis.legacyManagedPostToolUseHooks,
|
|
150
|
+
featureEnabled: diagnosis.featureEnabled,
|
|
151
|
+
expectedCommand: diagnosis.expectedPostToolUseCommand,
|
|
152
|
+
timeoutSec: 30,
|
|
153
|
+
}),
|
|
154
|
+
stop: eventStatus({
|
|
155
|
+
hooks: diagnosis.managedStopHooks,
|
|
156
|
+
legacyHooks: diagnosis.legacyManagedStopHooks,
|
|
157
|
+
featureEnabled: diagnosis.featureEnabled,
|
|
158
|
+
expectedCommand: diagnosis.expectedStopCommand,
|
|
159
|
+
timeoutSec: 300,
|
|
160
|
+
}),
|
|
161
|
+
};
|
|
162
|
+
const values = Object.values(events);
|
|
163
|
+
return {
|
|
164
|
+
status: values.includes('not_ready')
|
|
165
|
+
? 'not_ready'
|
|
166
|
+
: values.includes('ready')
|
|
167
|
+
? 'unverified'
|
|
168
|
+
: 'not_applicable',
|
|
169
|
+
reason: 'hooks_inspected',
|
|
170
|
+
events,
|
|
171
|
+
};
|
|
172
|
+
} catch {
|
|
173
|
+
return { status: 'unverified', reason: 'hook_inspection_failed', events: {} };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function inspectFactoryThread({
|
|
178
|
+
env = process.env,
|
|
179
|
+
cwd = process.cwd(),
|
|
180
|
+
codexHome = env.CODEX_HOME || defaultCodexHome(),
|
|
181
|
+
resolveIdentity = resolveCodexThreadIdentity,
|
|
182
|
+
findCandidate = findCodexThreadCandidate,
|
|
183
|
+
} = {}) {
|
|
184
|
+
const identity = resolveIdentity({ codexThreadId: null }, env);
|
|
185
|
+
if (!identity.codexThreadId) {
|
|
186
|
+
return {
|
|
187
|
+
status: 'not_applicable',
|
|
188
|
+
reason: 'codex_thread_not_detected',
|
|
189
|
+
rolloutAvailable: false,
|
|
190
|
+
threadId: null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const candidate = findCandidate({
|
|
195
|
+
threadId: identity.codexThreadId,
|
|
196
|
+
codexHome,
|
|
197
|
+
projectPath: cwd,
|
|
198
|
+
requireProjectMatch: true,
|
|
199
|
+
});
|
|
200
|
+
return candidate
|
|
201
|
+
? { status: 'ready', reason: 'thread_and_rollout_detected', rolloutAvailable: true, threadId: identity.codexThreadId }
|
|
202
|
+
: { status: 'not_ready', reason: 'rollout_not_found_for_project', rolloutAvailable: false, threadId: null };
|
|
203
|
+
} catch {
|
|
204
|
+
return { status: 'unverified', reason: 'rollout_inspection_failed', rolloutAvailable: false, threadId: identity.codexThreadId };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function collectFactoryDiagnostics({
|
|
209
|
+
env = process.env,
|
|
210
|
+
cwd = process.cwd(),
|
|
211
|
+
version = PACKAGE_VERSION,
|
|
212
|
+
inspectThread = inspectFactoryThread,
|
|
213
|
+
inspectDatabase = inspectFactoryDatabase,
|
|
214
|
+
inspectHooks = inspectFactoryHooks,
|
|
215
|
+
} = {}) {
|
|
216
|
+
const thread = inspectThread({ env, cwd });
|
|
217
|
+
const database = inspectDatabase({ threadId: thread.threadId ?? null, projectPath: cwd });
|
|
218
|
+
const hooks = inspectHooks({ codexHome: env.CODEX_HOME || defaultCodexHome() });
|
|
219
|
+
return buildFactoryDiagnostics({ version, database, hooks, thread });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function run(argv = [], { stdout = process.stdout, ...dependencies } = {}) {
|
|
223
|
+
try {
|
|
224
|
+
parseArgs(argv);
|
|
225
|
+
} catch {
|
|
226
|
+
stdout.write(`${JSON.stringify({
|
|
227
|
+
schema: 'throughline.native_factory_diagnostics.v1',
|
|
228
|
+
version: PACKAGE_VERSION,
|
|
229
|
+
overall: { status: 'unverified' },
|
|
230
|
+
error: 'invalid_diagnostics_request',
|
|
231
|
+
})}\n`);
|
|
232
|
+
return 2;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
stdout.write(`${JSON.stringify(collectFactoryDiagnostics(dependencies))}\n`);
|
|
236
|
+
return 0;
|
|
237
|
+
} catch {
|
|
238
|
+
stdout.write(`${JSON.stringify({
|
|
239
|
+
schema: 'throughline.native_factory_diagnostics.v1',
|
|
240
|
+
version: PACKAGE_VERSION,
|
|
241
|
+
overall: { status: 'unverified' },
|
|
242
|
+
error: 'diagnostics_internal_error',
|
|
243
|
+
})}\n`);
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
}
|