codexteer 0.18.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +13 -5
  3. package/bin/codexteer.mjs +1 -1
  4. package/docs/distribution.md +23 -6
  5. package/docs/orchestration.md +4 -4
  6. package/package.json +1 -1
  7. package/src/app-server.mjs +4 -4
  8. package/src/checkpoint.mjs +10 -10
  9. package/src/cli.mjs +90 -79
  10. package/src/cloud-cli.mjs +29 -33
  11. package/src/connection-error.mjs +7 -0
  12. package/src/cursor.mjs +370 -0
  13. package/src/desktop-cloud-id.mjs +9 -1
  14. package/src/desktop-cloud-project.mjs +37 -4
  15. package/src/desktop-cloud.mjs +10 -2
  16. package/src/desktop.mjs +9 -9
  17. package/src/directive.mjs +7 -6
  18. package/src/distribution.mjs +58 -33
  19. package/src/evidence.mjs +3 -3
  20. package/src/findings.mjs +13 -8
  21. package/src/help-cloud-remote.mjs +399 -0
  22. package/src/help-errors.mjs +10 -0
  23. package/src/help-intervention.mjs +492 -0
  24. package/src/help-local.mjs +510 -0
  25. package/src/help-observation.mjs +200 -0
  26. package/src/help-supervision.mjs +257 -0
  27. package/src/help.mjs +126 -304
  28. package/src/journal.mjs +33 -12
  29. package/src/launcher.mjs +6 -6
  30. package/src/monitor.mjs +5 -4
  31. package/src/notify.mjs +3 -3
  32. package/src/observe.mjs +11 -40
  33. package/src/orchestrate-prompt.mjs +14 -12
  34. package/src/orchestrate.mjs +4 -7
  35. package/src/paged-read.mjs +10 -17
  36. package/src/prompt-command.mjs +45 -7
  37. package/src/prompt.mjs +15 -12
  38. package/src/resource.mjs +6 -6
  39. package/src/runner.mjs +5 -5
  40. package/src/store.mjs +2 -2
  41. package/src/supervise.mjs +5 -7
  42. package/src/task-id.mjs +29 -4
  43. package/src/thread-id.mjs +5 -5
  44. package/src/unknown-options.mjs +17 -0
  45. package/src/wrapper.mjs +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  公開日は日本時間です。
4
4
 
5
+ ## 0.20.0 — 2026-11-02公開
6
+
7
+ - 介入も報告も不要な通知に「変化なし」とだけ応答する指示を、標準方針から共通手順へ移しました。`supervise prompt`のMESSAGEやオーケストレーションの担当方針で方針を置き換えても、方針に別の指定がなければ維持されます。
8
+ - 監督役が自分の判断・指示を`--kind decision`(ユーザー決定の伝達)で送っていたため、decisionはユーザー本人の決定を発言日時とともに引用する場合に限り、監督の指示は`review`で送るよう生成プロンプトで指示します。`supervise status`・`help send`・`help findings`の確認は最初の介入の前に一度にしました。
9
+ - 生成プロンプトは、タスクが待機中でも監督停止まで監視を続け、会話の要約・再起動の後は実行コマンド・読了cursorを確認し、手順を読み直してから再開するよう指示します。実行コマンドをシェル変数や関数に入れないこと(zshでは終了コード127になります)、調べれば分かる事実や決まっていることをユーザーに聞き直さないこと、別の論点には別のkeyを使うことも加えました。
10
+ - `orchestrate prompt`の本文は、本文と各担当のdata.promptをファイルに保存して要約・再起動の後に読み直すよう指示します。全体の完了時とユーザーの停止指示時には、各担当の監督を`supervise stop`で終了します。既存の成果物や以前のAIが選んだ仕組みを確定要件として扱わないことも明記しました。
11
+ - 子の監督本文だけで継続と監視の終了が決まるようにしました。共通手順は、監督方針で明示的に許可された`--new-turn`の再開と、方針の終了条件を認めます。オーケストレーションの担当方針は、idleの未完了作業の再開(paused/stopped・承認・質問待ちは迂回しない)と、ユーザーの停止指示か全体の完了を終了条件とすることを定めます。`orchestrate prompt`の本文から、子の規則を読み替える文を削除しました。標準方針は再開も終了条件も定めないため、単一スレッド監督の挙動は変わりません。
12
+ - 保存済みCLI方式の`supervise`・`supervise prompt`・`orchestrate`・`orchestrate prompt`は、300〜500文字の実行コマンド(`CODEX_HOME`指定・Node・保存したCLIのパス・`--require-node-version`・`--supervisor`・`--connection`・リモートの経路)を本文に20回ほど繰り返す代わりに、これらを固定した起動ファイル`CODEX_HOME/codex-steer/bin/codexteer-s-<hash>`(プロジェクト用は`codexteer-p-<hash>`)を生成し、そのパスを1語の実行コマンドとして示します。子の監督本文は約4割短くなります。起動ファイルは本人専用で内容のハッシュを名前にし、同じ名前で内容が違えば上書きせず`LAUNCHER_MODIFIED`、生成時に実行できなければ本文を出力せず`LAUNCHER_UNUSABLE`で止まります。`--json`は`data.command`も返します。
13
+ - `--supervisor`・`--connection`・`--require-node-version`・`--host`・`--remote-transport`・`--context-thread`を`--`より前に重ねて指定すると、全経路で`OPTIONS_INVALID`になります。生成した実行コマンドに付け直しても束縛は変わりません。以前の版が生成した長い実行コマンドはそのまま使えます。`--use-path-cli`の実行コマンドと動作は変わりません。
14
+ - `read`・`watch`・`status`が返すcursorを、タスクのターン数で長くならない`c1`で始まる形式にしました。全ターンの一覧の代わりにターン列のSHA-256を持ち、`--since`・`--based-on`では従来どおり現在のターン列・項目と照合するため、`STALE_CURSOR`・`STALE_OBSERVATION`の判定は変わりません。200ターンのタスクでは約18,600文字から約190文字になります。以前の版が出力したcursor(`eyJ`で始まる)も引き続き使え、続きは新しい形式で返します。保存済みの指摘・送信履歴の記録は変わりません。保存済みCLIなど以前の版に新しいcursorを渡すと`STALE_CURSOR`になるため、`--since`なしで読み直してください。
15
+ - 失敗した呼び出しは`error.help`に該当トピック、`error.action`にそのトピックに記載した対処を返し、引数エラー(`OPTIONS_INVALID`・`CLOUD_OPTIONS_INVALID`・`DESKTOP_CLOUD_OPTIONS_INVALID`)は`error.usage`に失敗したサブコマンドの使い方も返します。テキスト出力では標準エラーに`使い方:`・`対処:`の行を足します。代わりに`help <topic>`はテキストでも`--json`でもエラー一覧を載せず、`help <topic> --errors`を示す1行だけにしました(`--json`の`errors`配列は`errors_command`に置き換わります)。一覧は`help <topic> --errors`で取得できます。`help send`は約4,300から約2,300 o200kに減ります。エラーコード・メッセージ・終了コードは変わりません。
16
+ - `findings`の`create`・`reopen`・`show`・`list`・`resolve`・`dismiss`・`evaluate`(`revisions`を含む)と`history list`・`show`・`check`・`mark`、`instructions list`は、`based_on`のcursorをそのまま返す代わりに、cursorのSHA-256(16進64桁)を`based_on_sha256`として返します。保存済みの記録は全文のcursorを保持し、以前の記録もそのまま読めます。
17
+ - 生成プロンプトは、会話の要約・再起動の後に保存した本文を読み直し、保存していない場合だけ`help monitor`を読むよう指示します。単発の`watch --until change`の行の後に、完了まで待つ`--until idle`と本文を短くする`--max-chars N`(既定2000)も示します。
18
+ - `send`・`thread create`(ローカル・Cloud・Desktop Cloud)・`orchestrate`で`--`より前の未知のオプションを拒否するとき、エラー文と`error.unknown_options`がその語を示します(例: `Unknown send option: --kidn. Put instruction text that starts with -- after --.`)。示すのは`--`で始まる語を先頭から3件までで、本文を含まないよう各語を最初の空白の手前・40文字までに切り詰めます。拒否する入力・エラーコード・終了コードは変わりません。
19
+ - 生成済みの本文は変わりません。更新後のCLIで生成し直してください。
20
+
21
+ ## 0.19.0 — 2026-11-01公開
22
+
23
+ - タスクIDと接続指定の不一致エラーに、修正用の`--connection`とJSONの`expected_connection`・`connection`を追加しました。Desktop Cloudには`desktop-cloud`、Web Cloudには`cloud`、ローカルには既定の`shared`を示します。拒否・終了コード・既存エラーコードを維持し、自動切り替えは行いません。
24
+ - `help`の全30トピックを同じ構成に揃えました。各トピックは使い所・使い方・オプション(既定値)・確認できること・エラーと対処・手順・例・補足・関連を持ち、`--json`でも同じ項目名で取得できます。記述は実装と照合し、実装と食い違っていた説明(接続ソケットのリンク、Cloudの版確認の範囲、`status`の`unregistered`など)を直しました。`help desktop-cloud`と`help environments`の「使い所: undefined」表示と、`checkpoint run`・`resource run`の`--json`を`--`の後ろに示していた使い方表示も直しました。トピック名は変わりません。
25
+ - 文言や内部実装に固定されていたテストを整理し、ヘルプの記述とエラーコード・オプションの対応を検査するテストを追加しました。
26
+ - Desktop Cloudで受付不明の送信・プロジェクト割り当てが残ると、以後の送信・`thread assign`が永久に拒否される問題を修正しました。Desktopのプロフィール状態で割り当てを確認できた記録は自動で確定します。確認できない記録は、ユーザーが`history mark <TASK> <MESSAGE-ID> --resolve-unknown --note TEXT`または`thread receipt <ATTEMPT> --resolve-unknown --note TEXT`で理由を残して解除できます(`--supervisor`付きは`SUPERVISOR_CONTROL_FORBIDDEN`)。記録がないことを未配送の証拠にはしません。解除した送信は、結び付いた指摘でも受付不明として扱いません。拒否エラーには解除対象の`existing_message_id`・`existing_attempt_id`を含めます。
27
+ - ローカルの`send`が、`--`より前の未知のオプション(`--kidn`などの打ち間違い)を本文として送っていた問題を修正し、`OPTIONS_INVALID`で拒否するようにしました。`--`より後ろは従来どおり本文です。
28
+ - ローカルの`resource`で、`--supervisor`付きの`acquire`・`renew`・`release`・`status`・`list`が監督状態を確認していなかった問題を修正しました。`--supervisor`付きは`--thread`が必須になり、停止済み・別セッションは拒否、一時停止中は`acquire`・`renew`・`run`を`SUPERVISOR_PAUSED`で拒否します。
29
+ - ローカルタスクへの`diff`が送信の省略形として解釈されていた問題を修正し、接続前に`OPTIONS_INVALID`で拒否します。
30
+ - コードを持たなかった利用者向けの失敗に、エラーコードを付けました(`OPTIONS_INVALID`、`MESSAGE_NOT_FOUND`、`CHECKPOINT_NOT_FOUND`、`INSTRUCTION_NOT_REPLACEABLE`、`RESOURCE_NOT_OWNED`、`TASK_IDLE`、`TASK_BUSY`、`DESKTOP_START_UNSUPPORTED`、`DESKTOP_ALREADY_RUNNING`、`DESKTOP_START_FAILED`、`DESKTOP_UI_NOT_READY`、`DESKTOP_UI_FAILED`、`CURSOR_TOO_LARGE`、`STORE_UNSAFE`など)。実行中のターンがない送信と、idleでないタスクへの`--new-turn`は、`NOT_SENT`ではなく`TASK_IDLE`・`TASK_BUSY`になります。中断したローカルの`checkpoint run`・`resource run`は`error_code: CANCELLED`を返します。
31
+
5
32
  ## 0.18.2 — 2026-11-01公開
6
33
 
7
34
  - 停止済みの監督IDが、別の監督への交代後に古い本文から再登録できる問題を修正しました。停止履歴を監督記録に保持し、登録前のdoctor・再登録・観測・送信で`SUPERVISOR_STOPPED`を返します。旧形式の停止済み記録も、交代時に履歴へ引き継ぎます。保存済み旧CLIを使う監督には、更新後のCLIでプロンプトを生成し直してください。
package/README.md CHANGED
@@ -48,6 +48,8 @@ npx -y codexteer@0.18.0 supervise <thread-id>
48
48
 
49
49
  DesktopのCloud環境構成は、`codexteer --connection desktop-cloud environments list --json`で取得します。新規作成は`thread create --environment <ID_OR_EXACT_NAME>`、継続・監視・監督は`durable_`付きIDまたは`hostId=durable`付きリンクで共通コマンドを使います。所属情報と登録APIの可否は`doctor --thread ... --project ...`で区別します。公式の署名付き`thread assign`は対象がそのDesktopの実行環境を使う場合に限り利用でき、通常のCloud環境では`--project`付き作成を現在も事前拒否します。[操作・確認済み範囲と制限](docs/desktop-cloud.md)
50
50
 
51
+ タスクと明示した接続が合わない場合は拒否し、エラー文に修正用の`--connection`、JSONの`error.expected_connection`に修正候補、`error.connection`に今回の指定値を返します。`durable_`付きID・`hostId=durable`付きリンクには`--connection desktop-cloud`、Web Cloudの`task_`付きID・公式リンクには`--connection cloud`を使います。ローカルUUIDには既定の`--connection shared`を候補として示します。ローカルの観測には`--connection desktop`も利用できますが、送信には使えません。明示指定を自動で切り替えることはありません。
52
+
51
53
  ### Codex Cloudを使う
52
54
 
53
55
  CloudタスクのID・公式リンクで、同じ`supervise`・`read/watch`・`send`・`history/instructions/findings`を使えます。監督の`pause/resume/stop`や保存済みCLIも共通です。接続先をまだ選んでいない一覧と作成は`--connection cloud projects list`・`threads list`・`thread create --project <ENV_ID>`を使います。実行中のCloud送信は同じタスクの元ターンを中断して後続ターンを開始する方式です。[操作とサービス上の違い](docs/cloud.md)
@@ -157,11 +159,11 @@ codexteer supervise <thread-id> "セキュリティの問題だけを私へ報
157
159
 
158
160
  上書きは今回の生成・起動だけに適用し、次回の既定設定や保存したCLIには書き込みません。改行・空白・プレースホルダーに見える文字も指定文のまま本文に含めます。空文字・空白だけのメッセージはエラーです。呼び出し元のシェルで一つの引数として引用してください。`$`やバッククォートを文字として含めるときは、シェルが展開しない引用方法を使ってください。
159
161
 
160
- 既定では両方とも、起動したCLI本体と依存を`CODEX_HOME/codex-steer/runtimes/<version>-<sha256>`へ検証して保存し、そのコピーへの実行コマンドを監督プロンプトに埋め込みます。監督役に渡す切り替えオプションは不要です。npxのキャッシュが新版に更新された場合や、ソース導入先を`git pull`・削除した場合も、開始済みの監督は保存した内容を使い続けます。
162
+ 既定では両方とも、起動したCLI本体と依存を`CODEX_HOME/codex-steer/runtimes/<version>-<sha256>`へ検証して保存し、そのコピーを実行する起動ファイルを`CODEX_HOME/codex-steer/bin/codexteer-s-<hash>`に作って、そのパスを実行コマンドとして監督プロンプトに埋め込みます。監督役に渡す切り替えオプションは不要です。npxのキャッシュが新版に更新された場合や、ソース導入先を`git pull`・削除した場合も、開始済みの監督は保存した内容を使い続けます。
161
163
 
162
- 実行コマンドには、生成時に検証した`CODEX_HOME`の実体パス、監督ごとの`--supervisor` ID、選択した`--connection`も埋め込みます。先頭の指定とオプションを全て保持してください。貼り付け先の環境変数が未設定・別設定でも、生成元の接続先と履歴を使います。同じPCで使い、監督中は保存したコピーを保持してください。新しいCLIや別のプロファイルに切り替えるときは、その環境から監督を起動し直すか、プロンプトを生成し直します。同じ版・同じ内容のコピーは検証して再利用し、内容が変われば別の保存先を作ります。既存のコピーは自動削除・上書きしません。
164
+ 起動ファイルは、生成時に検証した`CODEX_HOME`の実体パス、Node、保存したCLI、監督ごとの`--supervisor` ID、選択した`--connection`(リモートでは経路も)を固定した本人専用のシェルスクリプトです。プロンプトにはそのパスが1語だけ現れ(空白等を含むホームでは引用します)、監督役は後ろにサブコマンドを付けて実行します。固定済みのオプションを付け直すと`OPTIONS_INVALID`で止まります。名前は内容のハッシュで、同じ内容なら検証して再利用し、異なる内容の既存ファイルは上書きせず`LAUNCHER_MODIFIED`で止まります。生成時に一度実行し、sandboxなどで実行できなければ本文を出力せず`LAUNCHER_UNUSABLE`で止まります。貼り付け先の環境変数が未設定・別設定でも、生成元の接続先と履歴を使います。同じPCで使い、監督中は保存したコピーを保持してください。新しいCLIや別のプロファイルに切り替えるときは、その環境から監督を起動し直すか、プロンプトを生成し直します。同じ版・同じ内容のコピーは検証して再利用し、内容が変われば別の保存先を作ります。既存のコピーは自動削除・上書きしません。
163
165
 
164
- Node本体はコピーせず、開始時の実体への絶対パスとNodeバージョンの検査を各コマンドに含めます。元のNodeは保持してください。バージョンが変われば`NODE_VERSION_MISMATCH`で操作を止めます。同じバージョンのNodeの改変や共有ライブラリまで固定するものではありません。保存先やNodeが使えなくなった場合は介入を止め、理由を報告するよう指示します。
166
+ Node本体はコピーせず、開始時の実体への絶対パスとNodeバージョンの検査を起動ファイルに含めます。元のNodeは保持してください。バージョンが変われば`NODE_VERSION_MISMATCH`で操作を止めます。同じバージョンのNodeの改変や共有ライブラリまで固定するものではありません。保存先やNodeが使えなくなった場合は介入を止め、理由を報告するよう指示します。
165
167
 
166
168
  起動時にバージョンを指定するかどうかに関係なく、監督用CLIの保存処理は同じです。キャッシュの動作と保存処理の詳細は[配布ドキュメント](docs/distribution.md#監督役が使うcli)を参照してください。
167
169
 
@@ -172,7 +174,7 @@ codexteer supervise <thread-id> --use-path-cli
172
174
  codexteer supervise prompt <thread-id> --use-path-cli
173
175
  ```
174
176
 
175
- この場合の実行例は`codexteer --supervisor '<SESSION-ID>' --connection shared read <thread-id> --json`の形式です。CLIのコピー・Nodeの固定・`CODEX_HOME`の埋め込みを行わず、実行環境のPATHと`CODEX_HOME`を使います。CLIの更新は次の実行から反映されます。監督ID・接続方式・一時停止等の制御は維持するため、対象Desktopと同じプロファイルで実行してください。直接起動時も同じプロンプトを渡し、登録と終了時の停止を行います。`--agent`・監督方針・`--connection desktop`と併用でき、エージェント用の`--`区切りより前に指定します。`supervise prompt --json`の`data.cli_mode`は`path`、`data.deployment`と`data.node`は`null`です。指定しなければ`cli_mode`は`saved`で、従来の保存済みCLIを使います。
177
+ この場合の実行例は`codexteer --supervisor '<SESSION-ID>' --connection shared read <thread-id> --json`の形式です。CLIのコピー・起動ファイルの生成・Nodeの固定・`CODEX_HOME`の埋め込みを行わず、実行環境のPATHと`CODEX_HOME`を使います。CLIの更新は次の実行から反映されます。監督ID・接続方式・一時停止等の制御は維持するため、対象Desktopと同じプロファイルで実行してください。直接起動時も同じプロンプトを渡し、登録と終了時の停止を行います。`--agent`・監督方針・`--connection desktop`と併用でき、エージェント用の`--`区切りより前に指定します。`supervise prompt --json`の`data.cli_mode`は`path`、`data.deployment`と`data.node`は`null`です。指定しなければ`cli_mode`は`saved`で、従来の保存済みCLIを使います。
176
178
 
177
179
  モデルなどClaude側の起動引数は、`--`の後ろへ渡します。
178
180
 
@@ -396,6 +398,9 @@ codexteer help send
396
398
  codexteer send --help
397
399
  codexteer help send --json
398
400
 
401
+ # そのコマンドのエラーと対処の一覧(通常のヘルプには載せず、失敗時はerror.actionに返る)
402
+ codexteer help send --errors
403
+
399
404
  # Claude CodeのMonitorとの連携
400
405
  codexteer help monitor
401
406
 
@@ -405,6 +410,8 @@ codexteer --version
405
410
 
406
411
  引数なしの`codexteer`と`codexteer --help`でも全体のヘルプを表示します。
407
412
 
413
+ 失敗した呼び出しは`--json`の`error.help`に該当トピックを、`error.action`にそのトピックに記載した対処を返します。引数エラー(`OPTIONS_INVALID`など)は`error.usage`に失敗したサブコマンドの使い方も返します。テキスト出力では標準エラーの`使い方:`・`対処:`の行に出ます。
414
+
408
415
  #### 監督役の起動
409
416
 
410
417
  ```bash
@@ -449,7 +456,7 @@ codexteer help supervise prompt
449
456
 
450
457
  `supervise prompt`は、ClaudeやCodexなどの監督役(オーケストレーター)へ渡す初期プロンプトを生成します。対象IDと引数を検証し、既定ではCLI一式を内容ハッシュ別の保存先へ配置・検証してから、正規化したIDと保存先を本文へ埋め込みます。保存・検証に失敗した場合は本文を出力せず、直接起動の場合も監督エージェントを起動しません。Desktopの起動・接続、履歴取得、送信、監督エージェントの起動は行いません。タスクの存在と接続は監督開始時に確認します。本文には観測・根拠付き介入・結果確認・停止までの手順を含みます。
451
458
 
452
- 既定の各コマンドは開始時のNodeと保存したCLIの絶対パスを使うため、本文を貼り付ける先のPATHに`codexteer`がなくても実行できます。先頭の`CODEX_HOME='…'`で生成元のプロファイルを指定します。相対パスやシンボリックリンクで指定していたホームも、検証した実体への絶対パスを使います。空白や引用符を含むパスはシェル引数として引用します。本文に埋め込むパスに改行等の制御文字があれば、本文を出力する前に`SUPERVISION_PATH_UNSAFE`で拒否します。`CODEX_HOME`指定・引用・`--require-node-version`を含め、実行コマンドをそのまま使ってください。別のPCや別のプロファイルを監督する場合は、そこで本文を生成し直してください。
459
+ 既定の実行コマンドは起動ファイルの絶対パスで、起動ファイルが開始時のNodeと保存したCLIの絶対パスを使うため、本文を貼り付ける先のPATHに`codexteer`がなくても実行できます。起動ファイルは`CODEX_HOME`に生成元のプロファイルを設定します。相対パスやシンボリックリンクで指定していたホームも、検証した実体への絶対パスを使います。空白や引用符を含むパスはシェル引数として引用します。本文に埋め込むパスに改行等の制御文字があれば、本文を出力する前に`SUPERVISION_PATH_UNSAFE`で拒否します。実行コマンドはシェル変数に入れず、毎回そのまま書いてください。以前の版が生成した`CODEX_HOME='…'`から始まる実行コマンドも、そのまま使えます。別のPCや別のプロファイルを監督する場合は、そこで本文を生成し直してください。
453
460
 
454
461
  `--use-path-cli`を指定すると、本文はPATH上の`codexteer`を使い、CLIの保存や絶対パスの埋め込みを省略します。接続先と履歴は実行環境の`CODEX_HOME`に従います。
455
462
 
@@ -462,6 +469,7 @@ codexteer help supervise prompt
462
469
  "data": {
463
470
  "thread_id": "aaaaaaaa-aaaa-4aaa-8aaa-000000000001",
464
471
  "prompt": "対象IDと保存したCLIの実行コマンドを含む監督プロンプト本文…",
472
+ "command": "/Users/me/.codex/codex-steer/bin/codexteer-s-<hash>",
465
473
  "cli_mode": "saved",
466
474
  "deployment": {
467
475
  "codex_home": "/Users/me/.codex",
package/bin/codexteer.mjs CHANGED
@@ -5,5 +5,5 @@ import { cloudInvocation, mainCloud } from "../src/cloud-cli.mjs";
5
5
 
6
6
  const args = process.argv.slice(2);
7
7
  if (cloudInvocation(args)) {
8
- try { await mainCloud(args); } catch (failure) { fail(failure, args.includes("--json")); }
8
+ try { await mainCloud(args); } catch (failure) { fail(failure, args.includes("--json"), args); }
9
9
  } else await main(args);
@@ -7,6 +7,10 @@
7
7
 
8
8
  `0.18.2`は、監督が交代した後も停止済みIDを履歴に保持し、古い本文からの再登録・観測・送信を`SUPERVISOR_STOPPED`で拒否します。登録前の`doctor`にも同じ判定を適用します。同じIDの別タスクへの登録を拒否し、旧版で重複したIDを持つ記録も`supervise list --json`で全件と衝突先を返します。該当タスクはそれぞれ`supervise stop`で停止できます。更新前の停止済み記録は交代時に引き継ぎますが、すでに上書きされて失われた停止履歴は復元できません。保存済み旧CLIには修正が反映されないため、更新後のCLIで監督を起動し直すか、プロンプトを生成し直してください。runtime protocolとcapability契約はv1を維持し、Desktopの再起動は必要ありません。
9
9
 
10
+ `0.19.0`は、`help`の全トピックを同じ構成に揃えて実装と照合し、Desktop Cloudで受付不明の記録が残ると以後の送信・割り当てが止まる問題、ローカル`send`の未知オプションの送信、`--supervisor`付きローカル`resource`の監督状態の未確認を修正します。利用者向けの失敗にはエラーコードを付けました。実行中のターンがない送信の`NOT_SENT`は`TASK_IDLE`・`TASK_BUSY`に変わるため、コードで分岐している呼び出し側は更新してください。runtime protocolとcapability契約はv1を維持し、Desktopの再起動は必要ありません。保存済み旧CLIで動く監督には反映されないため、更新後のCLIで監督を起動し直すか、プロンプトを生成し直してください。
11
+
12
+ `0.20.0`は、実際の監督・オーケストレーションの記録に合わせて生成プロンプトを直し、監督役が読む量と打ち込む量を減らします。保存済みCLI方式の実行コマンドは、`CODEX_HOME/codex-steer/bin/`に置く所有者専用の起動ファイル1語になります。起動ファイルを実行できない環境では生成時に`LAUNCHER_UNUSABLE`で止まるので、`--use-path-cli`を使ってください。`read`・`watch`・`status`のcursorは`c1`で始まる短い形式になり、旧形式の`eyJ`も引き続き読めます。0.19.0以前のCLIに`c1`のcursorを渡すと`STALE_CURSOR`になるため、`--since`なしで読み直してください。失敗した呼び出しは`error.help`・`error.action`・`error.usage`で対処を返し、`help <topic>`の既定の出力はエラー一覧を含みません(`help <topic> --errors`で表示し、`--json`は`errors`の代わりに`errors_command`を返します)。`findings`・`history`・`instructions list`の出力は`based_on`の代わりに`based_on_sha256`を返し、保存する記録は変わりません。これらのJSONの項目で分岐している呼び出し側は更新してください。生成済みのプロンプトは変わらないので、監督を続ける場合は更新後のCLIで生成し直してください。runtime protocolとcapability契約はv1を維持し、Desktopの再起動は不要です。
13
+
10
14
  `package.json` は `private` を持たず、`publishConfig` でnpm公式レジストリへのpublic公開を指定しています。公開可能な設定であることと、実際に公開済みであることは別です。
11
15
 
12
16
  公開後の基本的な実行形式は次のとおりです。通常はバージョン指定なしで使えます。監督開始後は保存したCLIを使い続けるため、監督中の一貫性を保つために版を明示する必要はありません。起動側と操作側の製品バージョンも、必要な通信仕様が対応していれば異なっていても使えます。
@@ -30,25 +34,25 @@ CLIはDesktopを自動終了しません。
30
34
 
31
35
  ## 監督役が使うCLI
32
36
 
33
- 既定は以下の保存済みCLI方式です。`supervise <THREAD> --use-path-cli`または`supervise prompt <THREAD> --use-path-cli`を指定すると、本文はPATH上の`codexteer`を使います。このモードではCLIを保存せず、本文は現在実行中のCLIから生成します。Nodeのパス・版・CODEX_HOMEを埋め込まず、各コマンドは実行環境のPATHとCODEX_HOMEに従います。更新されたCLIは次の実行から使われます。対象Desktopと同じプロファイルで実行してください。--supervisor・--connectionと監督の登録・停止・送信制御は維持します。本文だけの生成はファイルを書き込まず、直接起動では現在のプロファイルを検証・準備して監督を登録します。JSONの`cli_mode`は`path`、`deployment`と`node`は`null`です。既定方式では`cli_mode`は`saved`です。
37
+ 既定は以下の保存済みCLI方式です。`supervise <THREAD> --use-path-cli`または`supervise prompt <THREAD> --use-path-cli`を指定すると、本文はPATH上の`codexteer`を使います。このモードではCLIを保存せず起動ファイルも作らず、本文は現在実行中のCLIから生成します。Nodeのパス・版・CODEX_HOMEを埋め込まず、各コマンドは実行環境のPATHとCODEX_HOMEに従います。更新されたCLIは次の実行から使われます。対象Desktopと同じプロファイルで実行してください。--supervisor・--connectionと監督の登録・停止・送信制御は維持します。本文だけの生成はファイルを書き込まず、直接起動では現在のプロファイルを検証・準備して監督を登録します。JSONの`cli_mode`は`path`、`deployment`と`node`は`null`です。既定方式では`cli_mode`は`saved`です。
34
38
 
35
39
  以下は`--use-path-cli`を指定しない場合の動作です。
36
40
 
37
- `0.14.1`以降の`supervise`と`supervise prompt`は、開始時のCLI本体と依存を内容ハッシュ別の永続配置へコピーし、その保存先を使う実行コマンドを本文に埋め込みます。npxで取得したパッケージもソースのチェックアウトも、同じ保存・検証・生成処理を使います。起動経路を推測する環境変数や切り替えオプションは不要です。本文の生成処理自体も保存したCLI一式から読み込みます。
41
+ `0.14.1`以降の`supervise`と`supervise prompt`は、開始時のCLI本体と依存を内容ハッシュ別の永続配置へコピーし、その保存先を使う実行コマンドを本文に埋め込みます。この版からは、実行コマンドを固定した起動ファイルを生成し、本文にはそのパスを1語で埋め込みます(後述の[起動ファイル](#起動ファイル))。npxで取得したパッケージもソースのチェックアウトも、同じ保存・検証・生成処理を使います。起動経路を推測する環境変数や切り替えオプションは不要です。本文の生成処理自体も保存したCLI一式から読み込みます。
38
42
 
39
43
  両形式で対象IDの後ろに任意の`MESSAGE`を一つの引数として渡せます。省略時は保存したCLIの標準方針を使い、指定時は標準方針全体をその文に置き換えます。必須の共通テンプレートは保存したCLIから常に生成し、対象・実行コマンド・操作時の確認を保ちます。指定文は生成・起動ごとの入力で、永続配置のファイルやマニフェストへ書き込まず、内容ハッシュにも含めません。同じCLIなら異なる方針でも保存先を検証して再利用します。空文字・空白だけの文は保存処理前に`INVALID_SUPERVISION_POLICY`で拒否します。本文の文字列はテンプレートやシェルとして評価しません。
40
44
 
41
45
  直接起動の`supervise`はClaudeを既定にし、`--agent claude`の明示と`--agent codex`にも対応します。監督方針は最初の`--`より前、エージェント側の起動引数はその後ろへ置いてください。
42
46
 
43
- 監督役の`doctor`・`read`・`watch`・`send`等はこのコピーを使います。パスはシェル引数として引用し、空白・引用符・コマンド置換に見える文字も文字列として扱います。本文に埋め込むパスに改行や制御文字があれば、`SUPERVISION_PATH_UNSAFE`で拒否します。ヘルプの`codexteer`表記も、監督役には指定された実行コマンドに置き換えるよう指示します。
47
+ 監督役の`doctor`・`read`・`watch`・`send`等は起動ファイル経由でこのコピーを使います。起動ファイル内のパスはシェル引数として引用し、空白・引用符・コマンド置換に見える文字も文字列として扱います。本文に埋め込むパスに改行や制御文字があれば、`SUPERVISION_PATH_UNSAFE`で拒否します。ヘルプの`codexteer`表記も、監督役には指定された実行コマンドに置き換えるよう指示します。
44
48
 
45
- `supervise prompt`も読み取り専用ではなく、IDと引数の検証後にCLI一式の保存・再利用の検証を行ってから本文を出力します。保存・検証に失敗した場合は部分的な本文を出力せず、直接起動の場合もエージェントを起動しません。Desktopへの接続・設定変更・履歴取得・送信は行いません。`--json`には対象IDと本文に加えて`data.deployment`(元のホームを示す`codex_home`・保存先・版・ハッシュ・再利用の有無)、`data.node`(Nodeの実体パス・版)、`data.supervisor`(session_id・owner・connection)を返します。ヘルプの表示は保存処理を行いません。
49
+ `supervise prompt`も読み取り専用ではなく、IDと引数の検証後にCLI一式の保存・再利用の検証を行ってから本文を出力します。保存・検証に失敗した場合は部分的な本文を出力せず、直接起動の場合もエージェントを起動しません。Desktopへの接続・設定変更・履歴取得・送信は行いません。`--json`には対象IDと本文、実行コマンドの`data.command`に加えて`data.deployment`(元のホームを示す`codex_home`・保存先・版・ハッシュ・再利用の有無)、`data.node`(Nodeの実体パス・版)、`data.supervisor`(session_id・owner・connection)を返します。ヘルプの表示は保存処理を行いません。
46
50
 
47
- 本文は同じPC向けです。全コマンドの先頭に`CODEX_HOME='…'`を埋め込み、配置時に検証したホームの実体パスへ固定します。生成時に`CODEX_HOME`が未設定なら、既定の`~/.codex`の実体パスを使います。相対パスやシンボリックリンクも絶対パスに解決するため、貼り付け先の作業ディレクトリや`CODEX_HOME`が異なっても、生成元のruntimeと履歴を使います。別のプロファイルを監督する場合は、そこで本文を生成し直してください。
51
+ 本文は同じPC向けです。起動ファイルが`CODEX_HOME`を設定し、配置時に検証したホームの実体パスへ固定します。生成時に`CODEX_HOME`が未設定なら、既定の`~/.codex`の実体パスを使います。相対パスやシンボリックリンクも絶対パスに解決するため、貼り付け先の作業ディレクトリや`CODEX_HOME`が異なっても、生成元のruntimeと履歴を使います。別のプロファイルを監督する場合は、そこで本文を生成し直してください。
48
52
 
49
53
  監督中は保存したコピーを保持してください。元のチェックアウトやnpxキャッシュを更新・移動・削除しても、そのコピーは変わりません。同じ製品バージョンでも内容が変われば別の保存先を作り、既存のコピーは検証して再利用します。参照先が使えない場合は介入を止めて理由を報告するよう指示し、別の版の自動取得やPATHへのフォールバックは行いません。
50
54
 
51
- Node本体はコピーせず、開始時のNodeの実体への絶対パスと`--require-node-version <version>`を各コマンドに埋め込みます。このオプションは全コマンドで使え、実行中のNodeバージョンが異なる場合は、操作や保存処理を始める前に`NODE_VERSION_MISMATCH`で拒否します。Nodeを削除した場合も実行できません。同じバージョンのNodeの改変や共有ライブラリまで固定・検証するものではないため、元のNodeは保持し、更新時は新しい環境から監督を開始し直してください。通常のCLI操作にNodeバージョンの一致は要求せず、生成した監督用コマンドでのみ自動指定します。
55
+ Node本体はコピーせず、開始時のNodeの実体への絶対パスと`--require-node-version <version>`を起動ファイルに埋め込みます。このオプションは全コマンドで使え、実行中のNodeバージョンが異なる場合は、操作や保存処理を始める前に`NODE_VERSION_MISMATCH`で拒否します。Nodeを削除した場合も実行できません。同じバージョンのNodeの改変や共有ライブラリまで固定・検証するものではないため、元のNodeは保持し、更新時は新しい環境から監督を開始し直してください。通常のCLI操作にNodeバージョンの一致は要求せず、生成した監督用コマンドでのみ自動指定します。
52
56
 
53
57
  元のパスだけを固定しない理由は、npmキャッシュやチェックアウトの内容が更新されるためです。npm 11.6.2は要求したパッケージ指定からキャッシュ先を計算し、同じ指定の再実行で取得対象が変われば、その保存先でインストールを更新します。新版の公開だけで自動更新されるわけではありません。保存したCLIはこの更新の影響を受けません。[npm 11.6.2のキャッシュ解決・更新実装](https://github.com/npm/cli/blob/v11.6.2/workspaces/libnpmexec/lib/index.js)
54
58
 
@@ -99,6 +103,19 @@ Desktop起動時の`CODEX_CLI_PATH`はこの保存先のwrapperを指し、wrapp
99
103
  これは、信頼する配布物を安全に配置・再利用するための検証です。各操作の実行時に全ファイルを再検証する仕組みや、配布元の真正性を独立した署名で証明する機能ではありません。
100
104
  同じOSユーザー権限で任意のコードを実行できる相手や、root権限からの改変を隔離する仕組みでもありません。
101
105
 
106
+ ### 起動ファイル
107
+
108
+ 保存済みCLI方式の`supervise`・`supervise prompt`・`orchestrate`・`orchestrate prompt`は、次の起動ファイルを作り、そのパスを本文の実行コマンドにします。
109
+
110
+ ```text
111
+ <canonical CODEX_HOME>/codex-steer/bin/codexteer-s-<hash> 監督ごと(--supervisorを含む)
112
+ <canonical CODEX_HOME>/codex-steer/bin/codexteer-p-<hash> オーケストレーションのプロジェクト用
113
+ ```
114
+
115
+ 起動ファイルは`/bin/sh`のスクリプトで、`CODEX_HOME`を設定し、開始時のNodeで保存したCLIを`--require-node-version`・`--supervisor`・`--connection`(リモートでは`--host`・`--remote-transport`・`--context-thread`も)付きで`exec`し、利用者の引数を後ろに渡します。以前の版の本文の実行コマンドと同じ引数でCLIを起動するため、旧形式のコマンドもそのまま使えます。固定済みのオプションを重ねて指定すると`OPTIONS_INVALID`で拒否します。
116
+
117
+ `<hash>`は内容のSHA-256の先頭12桁です。`bin`は本人所有の0700ディレクトリで、シンボリックリンクを辿りません。起動ファイルは一時ファイルから0700の通常ファイルとして名前を確定し、同じ名前があれば内容と権限を照合して再利用します。一致しなければ上書きせず`LAUNCHER_MODIFIED`で止まります。配置は`bin/.install-<名前>`のロックで排他し、中断したロックは自動で奪いません。生成時に一度`--version`で実行し、noexecのホームやsandboxの拒否で実行できなければ本文を出力せず`LAUNCHER_UNUSABLE`で止まります。その場合は実行できる環境で生成し直すか、`--use-path-cli`を使ってください。監督ごとの起動ファイルは自動削除しません。
118
+
102
119
  ## 互換性契約
103
120
 
104
121
  runtimeには製品名・版、共通の通信仕様 `codex_steer_protocol`、機能ごとの対応版 `codex_steer_capabilities`、配置を識別する `distribution_sha256` を記録します。
@@ -14,7 +14,7 @@ codexteer orchestrate --project 'Example Project' '達成したい目標' --agen
14
14
 
15
15
  目標は一つの引用した引数か`-`で渡します。`--`で始まる目標はstdinの`-`を使ってください。stdinの目標は起動前に読み取るため、その入力は対話CLIへ再送しません。通常の引数で目標を渡す場合はstdinもそのまま対話CLIへ引き継ぎます。
16
16
 
17
- `--use-path-cli`は本文内のcodexteerコマンドをPATH方式にし、保存コピーを作りません。省略時は保存済みCLI・Node版・canonical `CODEX_HOME`を本文へ固定します。起動するエージェント自体は、どちらの方式でもPATHから選びます。PATH方式の直接起動でも`CODEX_HOME`の所有者と権限を検証し、必要ならディレクトリを作成します。入力不正や保存コピーの改変、unsafe profileではエージェントを起動しません。
17
+ `--use-path-cli`は本文内のcodexteerコマンドをPATH方式にし、保存コピーを作りません。省略時は保存済みCLI・Node版・canonical `CODEX_HOME`・接続指定を固定した起動ファイル(`CODEX_HOME/codex-steer/bin/codexteer-p-<hash>`)のパスを、本文の実行コマンドにします。起動するエージェント自体は、どちらの方式でもPATHから選びます。PATH方式の直接起動でも`CODEX_HOME`の所有者と権限を検証し、必要ならディレクトリを作成します。入力不正や保存コピーの改変、unsafe profileではエージェントを起動しません。
18
18
 
19
19
  起動側はDesktopへ接続せず、本文を受け取ったオーケストレーターがプロジェクト照合・作成・監視を開始します。プロジェクト用の単一スレッド監督IDは作りません。各子の監督登録と制御は本文の手順に従います。
20
20
 
@@ -86,13 +86,13 @@ Felix Riesebergの[How to be more ambitious](https://felixrieseberg.com/how-to-b
86
86
  5. 個別の監督ID、読了cursor、findings、配送照合を使って介入する。必要な継続は同じ委任の未完了作業に限る。
87
87
  6. 最終差分と検証・成果物を検収し、統合と統合後の確認を担当へ依頼して全体の目的達成まで追う。
88
88
 
89
- 作成用コマンドは特定スレッドの `--supervisor` を持ちません。子の観測・送信は、生成した個別の監督コマンドを使います。既定の保存済みCLI方式とPATH方式を子の生成にも引き継ぎます。既存監督IDからの `orchestrate prompt` と、観測専用の `--connection desktop` は拒否します。
89
+ 作成用コマンドは特定スレッドの `--supervisor` を持ちません。子の観測・送信は、子ごとに生成した監督用の起動ファイル(`supervise prompt --json`の`data.command`)を使います。既定の保存済みCLI方式とPATH方式を子の生成にも引き継ぎます。既存監督IDからの `orchestrate prompt` と、観測専用の `--connection desktop` は拒否します。
90
90
 
91
- 担当ごとの生成方針には、目的の継続、Codexの独立した判断、三つの観点を含めます。元からある単一スレッド監督の標準方針や操作手順は変更しません。
91
+ 担当ごとの生成方針には、目的の継続(idleの担当への`--new-turn`による再開の許可)、監視の終了条件(ユーザーの停止指示か全体の完了)、Codexの独立した判断、三つの観点を含めます。共通手順は方針による再開の許可と終了条件を受け付けますが、単一スレッド監督の標準方針はどちらも定めないため、その挙動は変わりません。
92
92
 
93
93
  ## 制御と確認範囲
94
94
 
95
- `supervise pause/stop`は個別スレッドの監督制御です。プロジェクト全体の一括pause/stopや常駐スケジューラーは追加していません。オーケストレーターへの監督停止指示は、新規作成・追加送信・自分のMonitor/watchを止める指示として扱い、実行中のCodex担当の作業自体を止める指示とは区別します。
95
+ `supervise pause/stop`は個別スレッドの監督制御です。プロジェクト全体の一括pause/stopや常駐スケジューラーは追加していません。オーケストレーターへの監督停止指示は、新規作成・追加送信・自分のMonitor/watchを止め、各担当の監督を`supervise stop`で終了する指示として扱い、実行中のCodex担当の作業自体を止める指示とは区別します。全体の完了時も各担当の監督を終了します。
96
96
 
97
97
  `resource`は参加者間の協調用リースで、OSの強制ロックや公平な待ち行列ではありません。利用順はオーケストレーターが調整します。監督登録がactiveでも、継続して監視できていることを証明しません。
98
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexteer",
3
- "version": "0.18.2",
3
+ "version": "0.20.0",
4
4
  "description": "Send steering messages to running Codex Desktop threads from a terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,7 +74,7 @@ export async function sendOnClient(client, threadId, message, { newTurn = false,
74
74
  const input = [{ type: "text", text: message }];
75
75
  if (!newTurn) {
76
76
  const turn = activeTurn(thread);
77
- if (!turn) throw new Error("The task has no eligible active turn. Read it again before sending; use --new-turn only for an idle task.");
77
+ if (!turn) throw Object.assign(new Error("The task has no eligible active turn. Read it again before sending; use --new-turn only for an idle task."), { code: "TASK_IDLE" });
78
78
  await beforeSend?.(thread, client);
79
79
  // Desktop uses clientId on the resulting userMessage to render external
80
80
  // steering as a user bubble instead of just an anonymous "steered" marker.
@@ -84,7 +84,7 @@ export async function sendOnClient(client, threadId, message, { newTurn = false,
84
84
  if (accepted?.turnId !== turn.id) throw new RpcFailure("App Server returned an unexpected turn ID. Do not retry automatically.", { code: "PROTOCOL_ERROR", uncertain: true });
85
85
  return { turn_id: accepted.turnId, client_message_id: clientUserMessageId };
86
86
  }
87
- if (!["idle", "notLoaded"].includes(thread.status?.type) || thread.turns.some(turn => turn.status === "inProgress")) throw new Error("--new-turn requires an idle task. Nothing was sent.");
87
+ if (!["idle", "notLoaded"].includes(thread.status?.type) || thread.turns.some(turn => turn.status === "inProgress")) throw Object.assign(new Error("--new-turn requires an idle task. Nothing was sent."), { code: "TASK_BUSY" });
88
88
  // Resume through Desktop's long-lived connection so approval ownership
89
89
  // survives the short-lived sender. Never resume only on the sender connection.
90
90
  if (!subscribe) throw new Error("Desktop subscription support is required for --new-turn.");
@@ -93,7 +93,7 @@ export async function sendOnClient(client, threadId, message, { newTurn = false,
93
93
  await beforeSend?.(thread, client);
94
94
  await subscribe(threadId);
95
95
  const resumed = await readSendState(client, threadId);
96
- if (resumed.status?.type !== "idle" || resumed.turns.some(turn => turn.status === "inProgress")) throw new Error("Task became active while resuming. Nothing was sent.");
96
+ if (resumed.status?.type !== "idle" || resumed.turns.some(turn => turn.status === "inProgress")) throw Object.assign(new Error("Task became active while resuming. Nothing was sent."), { code: "TASK_BUSY" });
97
97
  await beforeSend?.(resumed, client);
98
98
  const clientUserMessageId = clientMessageId;
99
99
  const accepted = await client.request("turn/start", { threadId, input, clientUserMessageId }, { mutation: true });
@@ -110,7 +110,7 @@ export async function sendAppServerMessage(threadInput, message, { dryRun = fals
110
110
  const route = remote ? remoteControlConnection() : {};
111
111
  connect ??= route.connect ?? RpcClient.connect; discover ??= route.discover ?? discoverRuntime;
112
112
  const threadId = normalizeThreadId(threadInput);
113
- if (typeof message !== "string" || !message.trim()) throw new Error("Message must not be empty.");
113
+ if (typeof message !== "string" || !message.trim()) throw Object.assign(new Error("Message must not be empty."), { code: "OPTIONS_INVALID" });
114
114
  const plan = { thread_id: threadId, ...hostFields(), backend: "app-server", delivery_action: newTurn ? "new-turn" : "steer", message_characters: [...message].length, dry_run: dryRun };
115
115
  if (dryRun) return { ...plan, sent: false, delivery_status: "not_sent" };
116
116
  let client;
@@ -22,13 +22,13 @@ export async function captureInputs(roots, excludes = [], { allowMissing = false
22
22
  try { info = await lstat(file); } catch (e) { if (e.code === "ENOENT" && allowMissing) return; throw e; }
23
23
  if (info.isSymbolicLink()) {
24
24
  const target = await realpath(file), targetInfo = await lstat(target);
25
- if (targetInfo.isDirectory()) throw new Error("Directory symlinks are not snapshot inputs. Specify the real directory explicitly.");
25
+ if (targetInfo.isDirectory()) throw Object.assign(new Error("Directory symlinks are not snapshot inputs. Specify the real directory explicitly."), { code: "OPTIONS_INVALID" });
26
26
  records.set(file, { ...await fingerprintFile(file), link: await readlink(file), mtime_ms: targetInfo.mtimeMs, ctime_ms: targetInfo.ctimeMs });
27
27
  } else if (info.isDirectory()) {
28
28
  records.set(file, { type: "directory", ref: file, mode: info.mode & 0o777 });
29
29
  for (const name of (await readdir(file)).sort()) await visit(path.join(file, name));
30
30
  } else if (info.isFile()) records.set(file, { ...await fingerprintFile(file), mtime_ms: info.mtimeMs, ctime_ms: info.ctimeMs });
31
- else throw new Error("Snapshot inputs must be files or directories.");
31
+ else throw Object.assign(new Error("Snapshot inputs must be files or directories."), { code: "OPTIONS_INVALID" });
32
32
  }
33
33
  for (const root of roots) await visit(root);
34
34
  const files = [...records.values()].sort((a, b) => a.ref.localeCompare(b.ref));
@@ -42,18 +42,18 @@ function differences(before, after) {
42
42
 
43
43
  export async function getCheckpoint(threadInput, id, storage = {}) {
44
44
  const threadId = normalizeTaskId(threadInput), entry = await readRecord("checkpoints", id, storage);
45
- if (!entry || entry.thread_id !== threadId || entry.id !== id) throw new Error("Checkpoint not found for this task.");
45
+ if (!entry || entry.thread_id !== threadId || entry.id !== id) throw Object.assign(new Error("Checkpoint not found for this task."), { code: "CHECKPOINT_NOT_FOUND" });
46
46
  return entry;
47
47
  }
48
48
 
49
49
  export async function createCheckpoint(threadInput, name, { paths, excludes = [], cwd = process.cwd(), ...storage }) {
50
50
  const threadId = normalizeTaskId(threadInput);
51
- if (!name?.trim() || !paths?.length) throw new Error("capture requires a name and at least one --path.");
51
+ if (!name?.trim() || !paths?.length) throw Object.assign(new Error("capture requires a name and at least one --path."), { code: "OPTIONS_INVALID" });
52
52
  const scope = targetHost() === "local" ? { cwd: path.resolve(cwd), roots: [...new Set(paths.map(p => path.resolve(cwd, p)))], excludes: excludes.map(p => path.resolve(cwd, p)) }
53
53
  : await remoteCheckpointScope(threadId, paths, excludes);
54
54
  const { roots, excludes: omit } = scope;
55
55
  const snapshot = await captureInputs(roots, omit);
56
- if (!snapshot.files.some(f => f.type === "file")) throw new Error("No input files were selected.");
56
+ if (!snapshot.files.some(f => f.type === "file")) throw Object.assign(new Error("No input files were selected."), { code: "OPTIONS_INVALID" });
57
57
  const entry = { schema: 1, id: randomUUID(), thread_id: threadId, name, cwd: scope.cwd, created_at: new Date().toISOString(), snapshot, runs: [], artifacts: [] };
58
58
  await writeRecord("checkpoints", entry.id, entry, storage);
59
59
  return checkpointSummary(entry);
@@ -115,13 +115,13 @@ function watchInputs(snapshot) {
115
115
  }
116
116
 
117
117
  export async function runCheckpoint(threadInput, id, argv, { timeoutMs = 0, execBackend, remoteShell, supervisor, connection, ...storage } = {}) {
118
- if (!argv.length || !argv[0] || !Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 86400000) throw new Error("A command and timeout-ms between 0 and 86400000 are required.");
118
+ if (!argv.length || !argv[0] || !Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 86400000) throw Object.assign(new Error("A command and timeout-ms between 0 and 86400000 are required."), { code: "OPTIONS_INVALID" });
119
119
  const threadId = normalizeTaskId(threadInput);
120
120
  return withStoreLock(`checkpoint-${id}`, async () => {
121
121
  const entry = await getCheckpoint(threadId, id, storage), watcher = targetHost() === "local" ? watchInputs(entry.snapshot) : await remoteWatchInputs(entry.snapshot);
122
122
  try {
123
123
  const before = await captureInputs(entry.snapshot.roots, entry.snapshot.excludes, { allowMissing: true });
124
- if (before.digest !== entry.snapshot.digest) throw new Error("Inputs changed since capture. Capture a new checkpoint before running.");
124
+ if (before.digest !== entry.snapshot.digest) throw Object.assign(new Error("Inputs changed since capture. Capture a new checkpoint before running."), { code: "STALE_CHECKPOINT" });
125
125
  const run = { id: randomUUID(), argv, status: "running", started_at: new Date().toISOString() };
126
126
  entry.runs.push(run); await writeRecord("checkpoints", id, entry, storage);
127
127
  let result;
@@ -145,12 +145,12 @@ export async function runCheckpoint(threadInput, id, argv, { timeoutMs = 0, exec
145
145
  }
146
146
 
147
147
  export async function attachArtifacts(threadInput, id, refs, storage = {}) {
148
- if (!refs.length) throw new Error("At least one --artifact file is required.");
148
+ if (!refs.length) throw Object.assign(new Error("At least one --artifact file is required."), { code: "OPTIONS_INVALID" });
149
149
  return withStoreLock(`checkpoint-${id}`, async () => {
150
150
  const verified = await verifyCheckpoint(threadInput, id, storage);
151
- if (!verified.valid) throw new Error("A successful run against unchanged inputs is required before attaching artifacts.");
151
+ if (!verified.valid) throw Object.assign(new Error("A successful run against unchanged inputs is required before attaching artifacts."), { code: "STALE_CHECKPOINT" });
152
152
  const artifacts = await collectEvidence(refs);
153
- if (artifacts.some(a => a.type !== "file")) throw new Error("Artifacts must be local files.");
153
+ if (artifacts.some(a => a.type !== "file")) throw Object.assign(new Error("Artifacts must be local files."), { code: "OPTIONS_INVALID" });
154
154
  const entry = await getCheckpoint(threadInput, id, storage);
155
155
  entry.artifacts.push(...artifacts.map(a => ({ ...a, run_id: entry.runs.at(-1).id, attached_at: new Date().toISOString() })));
156
156
  await writeRecord("checkpoints", id, entry, storage);