codexteer 0.15.1 → 0.17.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.
- package/CHANGELOG.md +14 -0
- package/README.md +42 -10
- package/docs/distribution.md +28 -8
- package/docs/thread-create-native-investigation.md +136 -0
- package/docs/thread-create.md +115 -0
- package/package.json +4 -2
- package/src/cli.mjs +45 -4
- package/src/connection.mjs +11 -4
- package/src/creation-verification.mjs +16 -0
- package/src/distribution.mjs +4 -4
- package/src/help.mjs +39 -20
- package/src/monitor.mjs +36 -4
- package/src/native-command.mjs +92 -0
- package/src/native-rpc.mjs +103 -0
- package/src/native-worker.mjs +25 -0
- package/src/notify.mjs +157 -0
- package/src/projects.mjs +79 -0
- package/src/prompt.mjs +60 -29
- package/src/runtime.mjs +4 -2
- package/src/socket-endpoint.mjs +40 -0
- package/src/standalone-create.mjs +134 -0
- package/src/supervise.mjs +14 -9
- package/src/thread-create.mjs +127 -0
- package/src/wrapper.mjs +68 -5
package/src/cli.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { appServerDoctor, startDesktop } from "./launcher.mjs";
|
|
|
7
7
|
import { VERSION, helpData, renderHelp } from "./help.mjs";
|
|
8
8
|
import { observeThread, printObservation } from "./observe.mjs";
|
|
9
9
|
import { monitorCommand } from "./monitor.mjs";
|
|
10
|
+
import { notificationOptions } from "./notify.mjs";
|
|
10
11
|
import { createCheckpoint, listCheckpoints, getCheckpoint, checkpointSummary, verifyCheckpoint, runCheckpoint, attachArtifacts } from "./checkpoint.mjs";
|
|
11
12
|
import { acquireResource, resourceStatus, renewResource, releaseResource, listResources, runWithResource } from "./resource.mjs";
|
|
12
13
|
import { playSendSound } from "./sound.mjs";
|
|
@@ -14,6 +15,8 @@ import { prepareSupervisorPrompt, superviseAgent } from "./supervise.mjs";
|
|
|
14
15
|
import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
|
|
15
16
|
import { createFinding, findingStatus, listFindings, resolveFinding, dismissFinding, reopenFinding, evaluateFinding, findingStats } from "./findings.mjs";
|
|
16
17
|
import { connectionMode, observationConnection, desktopReadOnly } from "./connection.mjs";
|
|
18
|
+
import { listProjects } from "./projects.mjs";
|
|
19
|
+
import { createProjectThread } from "./thread-create.mjs";
|
|
17
20
|
|
|
18
21
|
const DEFAULT_SEND_BACKEND = "app-server";
|
|
19
22
|
|
|
@@ -31,6 +34,9 @@ function fail(error, json) {
|
|
|
31
34
|
message_id: error.message_id, client_message_id: error.client_message_id, journal_update_required: error.journal_update_required,
|
|
32
35
|
existing_message_id: error.existing_message_id,
|
|
33
36
|
supervision_update_required: error.supervision_update_required,
|
|
37
|
+
receipt_path: error.receipt_path,
|
|
38
|
+
creation_status: error.creation_status, attempt_id: error.attempt_id, receipt_update_required: error.receipt_update_required,
|
|
39
|
+
native_call_id: error.native_call_id,
|
|
34
40
|
} }));
|
|
35
41
|
else console.error(`codexteer: ${message}`);
|
|
36
42
|
process.exitCode = 1;
|
|
@@ -181,9 +187,10 @@ export async function main(argv) {
|
|
|
181
187
|
if (command === "supervise" && args[0] === "prompt") {
|
|
182
188
|
args.shift();
|
|
183
189
|
const agent = takeOption(args, "--agent", "claude");
|
|
190
|
+
const usePathCli = takeFlag(args, "--use-path-cli");
|
|
184
191
|
if (args.length < 1 || args.length > 2 || args.some(arg => arg.startsWith("--"))) throw new Error("Expected: supervise prompt <THREAD> [MESSAGE]. See codexteer help supervise prompt.");
|
|
185
192
|
if (supervisor) throw new Error("A supervisor cannot generate a replacement session.");
|
|
186
|
-
const data = await prepareSupervisorPrompt(args[0], args[1], { agent, connection });
|
|
193
|
+
const data = await prepareSupervisorPrompt(args[0], args[1], { agent, connection, usePathCli });
|
|
187
194
|
success("supervise.prompt", data, json);
|
|
188
195
|
if (!json) console.log(data.prompt);
|
|
189
196
|
return;
|
|
@@ -195,8 +202,9 @@ export async function main(argv) {
|
|
|
195
202
|
const ownArgs = separator < 0 ? args : args.slice(0, separator);
|
|
196
203
|
const agentArgs = separator < 0 ? [] : args.slice(separator + 1);
|
|
197
204
|
const agent = takeOption(ownArgs, "--agent", "claude");
|
|
198
|
-
|
|
199
|
-
|
|
205
|
+
const usePathCli = takeFlag(ownArgs, "--use-path-cli");
|
|
206
|
+
if (ownArgs.length < 1 || ownArgs.length > 2 || ownArgs.some(arg => arg.startsWith("--"))) throw new Error("Expected: supervise <THREAD> [MESSAGE] [--agent claude|codex] [--use-path-cli] [-- <AGENT-ARGS...>].");
|
|
207
|
+
process.exitCode = await superviseAgent(ownArgs[0], { agent, agentArgs, policy: ownArgs[1], connection, usePathCli });
|
|
200
208
|
return;
|
|
201
209
|
}
|
|
202
210
|
if (command === "resource") {
|
|
@@ -303,6 +311,10 @@ export async function main(argv) {
|
|
|
303
311
|
if (command === "watch") {
|
|
304
312
|
const until = takeOption(args, "--until", undefined), timeout = takeOption(args, "--timeout-ms", undefined);
|
|
305
313
|
if (stream && (until !== undefined || timeout !== undefined)) throw new Error("--stream runs until cancelled; do not combine it with --until or --timeout-ms. See codexteer help monitor.");
|
|
314
|
+
const notify = takeOption(args, "--notify", stream ? "digest" : "all"), settle = takeOption(args, "--settle-ms", undefined), maxHold = takeOption(args, "--max-hold-ms", undefined);
|
|
315
|
+
if (!stream && (notify === "digest" || settle !== undefined || maxHold !== undefined)) throw new Error("--notify digest, --settle-ms and --max-hold-ms require --stream.");
|
|
316
|
+
if (notify !== "digest" && (settle !== undefined || maxHold !== undefined)) throw new Error("--settle-ms and --max-hold-ms require --notify digest.");
|
|
317
|
+
Object.assign(options, notificationOptions({ notify, ...(settle === undefined ? {} : { settleMs: Number(settle) }), ...(maxHold === undefined ? {} : { maxHoldMs: Number(maxHold) }) }));
|
|
306
318
|
Object.assign(options, { watch: true, until: until ?? "change", timeoutMs: Number(timeout ?? "30000"), pollMs: Number(takeOption(args, "--poll-ms", "1000")) });
|
|
307
319
|
}
|
|
308
320
|
if (args.length !== 1) throw new Error(`Expected: ${command} <THREAD>. See codexteer help ${command}.`);
|
|
@@ -363,6 +375,15 @@ export async function main(argv) {
|
|
|
363
375
|
return;
|
|
364
376
|
}
|
|
365
377
|
|
|
378
|
+
if (command === "projects") {
|
|
379
|
+
if (connection === "desktop") throw desktopReadOnly();
|
|
380
|
+
if (args.shift() !== "list" || args.length) throw new Error("Expected: codexteer projects list");
|
|
381
|
+
const projects = await listProjects();
|
|
382
|
+
success("projects.list", { projects, count: projects.length }, json);
|
|
383
|
+
if (!json) for (const project of projects) console.log(`${project.project_id} ${project.name}\n${project.roots.map(root => ` ${root}`).join("\n")}`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
366
387
|
if (command === "threads") {
|
|
367
388
|
if (args.shift() !== "list") throw new Error("Expected: codexteer threads list");
|
|
368
389
|
const limit = Number.parseInt(takeOption(args, "--limit", "20"), 10);
|
|
@@ -378,7 +399,27 @@ export async function main(argv) {
|
|
|
378
399
|
}
|
|
379
400
|
|
|
380
401
|
if (command === "thread") {
|
|
381
|
-
|
|
402
|
+
const action = args.shift();
|
|
403
|
+
if (action === "create") {
|
|
404
|
+
if (connection === "desktop") throw desktopReadOnly();
|
|
405
|
+
if (supervisor) throw Object.assign(new Error("Managed supervision is bound to its existing task. Run thread create without --supervisor under the user's delegated creation scope."), { code: "SUPERVISOR_CONTROL_FORBIDDEN" });
|
|
406
|
+
const selector = takeOption(args, "--project", undefined);
|
|
407
|
+
const options = { title: takeOption(args, "--title", undefined), cwd: takeOption(args, "--cwd", undefined), model: takeOption(args, "--model", undefined),
|
|
408
|
+
permissions: takeOption(args, "--permissions", undefined), sandbox: takeOption(args, "--sandbox", undefined),
|
|
409
|
+
approvalPolicy: takeOption(args, "--approval-policy", undefined), approvalsReviewer: takeOption(args, "--approvals-reviewer", undefined), dryRun: takeFlag(args, "--dry-run"),
|
|
410
|
+
contextThread: takeOption(args, "--context-thread", undefined), thinking: takeOption(args, "--thinking", undefined), timeoutMs: takeOption(args, "--timeout-ms", undefined) };
|
|
411
|
+
if (options.timeoutMs !== undefined) {
|
|
412
|
+
options.timeoutMs = Number(options.timeoutMs);
|
|
413
|
+
}
|
|
414
|
+
const literal = args.indexOf("--");
|
|
415
|
+
if (args.slice(0, literal < 0 ? args.length : literal).some(arg => arg.startsWith("--"))) throw new Error("Unknown thread create option. Use -- before an instruction starting with --.");
|
|
416
|
+
const message = readMessage(args);
|
|
417
|
+
const data = await createProjectThread(selector, message, options);
|
|
418
|
+
success("thread.create", data, json);
|
|
419
|
+
if (!json) console.log(options.dryRun ? `Dry run: ${data.project_name} (${data.cwd}), ${data.message_characters} characters.` : `Created ${data.deep_link} in ${data.project_name}; initial instruction accepted${data.turn_id ? ` (turn ${data.turn_id})` : ""}.`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (action !== "resolve") throw new Error("Expected: codexteer thread create|resolve. See help thread.");
|
|
382
423
|
if (args.length !== 1) throw new Error("thread resolve requires exactly one thread ID or URL.");
|
|
383
424
|
const threadId = normalizeThreadId(args[0]);
|
|
384
425
|
const data = { thread_id: threadId, deep_link: threadDeepLink(threadId) };
|
package/src/connection.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { lstat, realpath } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { codexHome } from "./runtime.mjs";
|
|
4
4
|
import { RpcClient } from "./rpc.mjs";
|
|
5
|
+
import { inspectAppServerSocket } from "./socket-endpoint.mjs";
|
|
5
6
|
|
|
6
7
|
const failure = (message, code) => Object.assign(new Error(message), { code });
|
|
7
8
|
const reads = new Set(["thread/loaded/list", "thread/read", "thread/turns/list", "thread/items/list"]);
|
|
@@ -16,16 +17,22 @@ export function desktopReadOnly() {
|
|
|
16
17
|
async function identity(home) {
|
|
17
18
|
const directory = path.join(home, "app-server-control"), socket = path.join(directory, "app-server-control.sock");
|
|
18
19
|
const fingerprint = [];
|
|
19
|
-
for (const [file, type] of [[home, "home"], [directory, "directory"]
|
|
20
|
+
for (const [file, type] of [[home, "home"], [directory, "directory"]]) {
|
|
20
21
|
const info = await lstat(file);
|
|
21
22
|
if (info.uid !== process.getuid() || info.isSymbolicLink() || (info.mode & (type === "home" ? 0o022 : 0o077)) !== 0
|
|
22
|
-
||
|
|
23
|
+
|| !info.isDirectory()) {
|
|
23
24
|
throw failure("Desktop control endpoint has unsafe ownership, permissions, or file type.", "DESKTOP_CONNECTION_UNSAFE");
|
|
24
25
|
}
|
|
25
26
|
// Home directory timestamps change for unrelated profile activity.
|
|
26
|
-
fingerprint.push([info.dev, info.ino
|
|
27
|
+
fingerprint.push([info.dev, info.ino]);
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
let endpoint;
|
|
30
|
+
try { endpoint = await inspectAppServerSocket(socket); }
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code === "SOCKET_UNSAFE") throw failure(error.message, "DESKTOP_CONNECTION_UNSAFE");
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
return { home, directory, socket: endpoint.socket, fingerprint: JSON.stringify([...fingerprint, ...endpoint.fingerprint]) };
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export async function discoverDesktopRuntime(home = codexHome()) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function desktopMembership(home, threadId, projectId) {
|
|
5
|
+
const state = JSON.parse(await readFile(path.join(home, ".codex-global-state.json"), "utf8"));
|
|
6
|
+
const assignment = state["thread-project-assignments"]?.[threadId];
|
|
7
|
+
return assignment?.projectKind === "local" && assignment.projectId === projectId;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function initialInstructionMatches(history, prompt, sourceId) {
|
|
11
|
+
const item = history.turns?.[0]?.items?.find(item => item.type === "userMessage"
|
|
12
|
+
|| (item.type === "functionCallOutput" && item.namespace === "codex_app" && item.name === "create_thread"));
|
|
13
|
+
if (item?.type === "userMessage") return item.content?.filter(part => part.type === "text").map(part => part.text).join("\n") === prompt;
|
|
14
|
+
const expected = `<codex_delegation>\n <source_thread_id>${sourceId}</source_thread_id>\n <input>${prompt}</input>\n</codex_delegation>`;
|
|
15
|
+
return item?.output === expected;
|
|
16
|
+
}
|
package/src/distribution.mjs
CHANGED
|
@@ -38,7 +38,7 @@ async function checkAncestors(directory) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
async function
|
|
41
|
+
export async function prepareCodexHome(home) {
|
|
42
42
|
let existing = path.resolve(home), canonical;
|
|
43
43
|
const missing = [];
|
|
44
44
|
while (true) {
|
|
@@ -53,6 +53,8 @@ async function resolveHome(home) {
|
|
|
53
53
|
canonical = path.join(canonical, part);
|
|
54
54
|
await ownedDirectory(canonical, true);
|
|
55
55
|
}
|
|
56
|
+
const homeInfo = await lstat(canonical);
|
|
57
|
+
if (!homeInfo.isDirectory() || homeInfo.uid !== process.getuid() || (homeInfo.mode & 0o7022)) throw failure("CODEX_HOME must be an owner-controlled directory.");
|
|
56
58
|
return canonical;
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -150,9 +152,7 @@ export async function prepareDeployment(home, { sourceRoot = PACKAGE_ROOT } = {}
|
|
|
150
152
|
const distribution = await describeDistribution(sourceRoot);
|
|
151
153
|
// CODEX_HOME may itself be a user-configured symlink; only its canonical
|
|
152
154
|
// owner-writable directory is accepted. Never follow links below that root.
|
|
153
|
-
const canonicalHome = await
|
|
154
|
-
const homeInfo = await lstat(canonicalHome);
|
|
155
|
-
if (!homeInfo.isDirectory() || homeInfo.uid !== process.getuid() || (homeInfo.mode & 0o7022)) throw failure("CODEX_HOME must be an owner-controlled directory.");
|
|
155
|
+
const canonicalHome = await prepareCodexHome(home);
|
|
156
156
|
const stateRoot = path.join(canonicalHome, "codex-steer"), root = path.join(stateRoot, "runtimes");
|
|
157
157
|
await ownedDirectory(stateRoot, true); await ownedDirectory(root, true);
|
|
158
158
|
const target = path.join(root, distribution.id), lock = path.join(root, `.install-${distribution.id}`);
|
package/src/help.mjs
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
import { supervisionSteps } from "./prompt.mjs";
|
|
1
|
+
import { supervisionSteps, DIGEST_GUIDANCE } from "./prompt.mjs";
|
|
2
|
+
|
|
3
|
+
const notificationHelp = "watch --streamの既定はdigestです。--notify allを明示すると従来どおり変化ごとに通知します。単発watchは従来どおりで、digestの対象外です。digestはユーザー入力・ターン・質問・注意状態・読み取り専用以外の失敗は即時通知し、通常の進捗は最後の通常イベントから--settle-ms(既定20000、1000〜120000)後、読み取り専用コマンドなどは--max-hold-ms(既定600000、10000〜1800000)まで保留します。settle/max-holdはdigest専用です。連続する通常イベントもmax-holdで排出します。ポーリングとページ読み取りは継続するため、処理時間とポーリング間隔ぶんの遅れはあり得ます。接続状態・失敗は選別しません。";
|
|
2
4
|
|
|
3
5
|
export { VERSION } from "./version.mjs";
|
|
4
6
|
import { VERSION } from "./version.mjs";
|
|
5
7
|
|
|
6
8
|
const supervisionExecutionNotes = [
|
|
9
|
+
"--use-path-cliを付けると、本文はPATH上のcodexteerを案内します。CLIの保存・Nodeの固定・CODEX_HOMEの埋め込みを行わず、実行環境のPATHとCODEX_HOMEを使います。CLIの更新は次の実行から反映されます。--supervisor・--connectionと監督の制御は維持します。直接起動ではエージェント引数の区切り -- より前に指定してください。",
|
|
7
10
|
"各生成に新しい--supervisor IDと--connectionを埋め込みます。直接起動はagent起動前にregisterし、終了時にstopします。本文だけの生成は未登録で、貼り付け先が本文のregisterを実行して開始します。同じ本文の再登録は一時停止を解除せず、停止済みIDは復活しません。",
|
|
8
11
|
"--connectionの既定はshared(既存wrapper)。desktopは既存の共有制御ソケットを使う観測専用です。送信の実行例を含めず、方針に送信指示があっても送信を許可しません。詳細はhelp connectionを参照してください。",
|
|
9
12
|
"本文は必須の共通テンプレートと監督方針で構成します。MESSAGEを省略すると標準方針(目的・制約の範囲で必要最小限の介入と結果確認、簡潔な報告)を使い、指定すると標準方針全体をその文に置き換えます。対象ID、実行コマンド、help/read/watch、送信時の確認、停止手順はどちらにも含めます。",
|
|
10
13
|
"MESSAGEは引用して一つの引数として渡してください。空文字・空白だけはINVALID_SUPERVISION_POLICYで拒否します。改行等はそのまま本文に含め、プレースホルダー展開やシェル実行はしません。指定は今回の生成・起動だけに適用し、既定設定や保存したCLIは変更しません。",
|
|
11
|
-
"CLI本体と依存をCODEX_HOME/codex-steer/runtimes/<version>-<sha256>へ検証して保存し、そのコピーへの実行コマンドを本文に埋め込みます。supervise promptもこの保存処理を行います。同じ内容は検証して再利用し、既存のコピーは上書き・自動削除しません。保存・検証に失敗した場合は本文を出力せず、監督エージェントも起動しません。",
|
|
12
|
-
"
|
|
13
|
-
"Node本体はコピーせず、開始時の実体への絶対パスと--require-node-versionを各コマンドに埋め込みます。Nodeの版が変われば操作前にNODE_VERSION_MISMATCHで停止します。同じ版のNodeの改変や共有ライブラリまで固定するものではないため、元のNodeは保持してください。保存先やNodeが使えなければ介入を止めて報告し、意図した環境から生成し直すよう指示します。",
|
|
14
|
+
"既定ではCLI本体と依存をCODEX_HOME/codex-steer/runtimes/<version>-<sha256>へ検証して保存し、そのコピーへの実行コマンドを本文に埋め込みます。supervise promptもこの保存処理を行います。同じ内容は検証して再利用し、既存のコピーは上書き・自動削除しません。保存・検証に失敗した場合は本文を出力せず、監督エージェントも起動しません。",
|
|
15
|
+
"保存済みCLIを使う既定モードでは、同じPCで使い、監督中は保存したコピーを保持してください。全コマンドの先頭に生成時に検証したCODEX_HOMEの実体パスを指定するため、監督役のCODEX_HOMEが未設定・別設定でも生成元の接続先と履歴を使います。元のnpxキャッシュやチェックアウトを更新・削除しても、監督は保存した内容を使います。ヘルプ中のcodexteer表記も、先頭のCODEX_HOME指定・引用・オプションを含む指定の実行コマンドに置き換えてください。",
|
|
16
|
+
"既定ではNode本体はコピーせず、開始時の実体への絶対パスと--require-node-versionを各コマンドに埋め込みます。Nodeの版が変われば操作前にNODE_VERSION_MISMATCHで停止します。同じ版のNodeの改変や共有ライブラリまで固定するものではないため、元のNodeは保持してください。保存先やNodeが使えなければ介入を止めて報告し、意図した環境から生成し直すよう指示します。",
|
|
14
17
|
];
|
|
15
18
|
|
|
16
19
|
export const topics = {
|
|
17
20
|
supervise: {
|
|
18
21
|
title: "指定したCodexタスクの監督役としてClaudeまたはCodex CLIを起動する",
|
|
19
22
|
when: "対象タスクのIDを指定して、Claude CodeまたはCodex CLIを監督役の対話セッションとして起動するとき。",
|
|
20
|
-
usage: ["supervise <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [-- <AGENT-ARGS...>]", "supervise status <THREAD>", "supervise list", "supervise pause <THREAD>", "supervise resume <THREAD>", "supervise stop <THREAD>", "--supervisor <SESSION-ID> supervise register <THREAD> [--owner NAME]"],
|
|
23
|
+
usage: ["supervise <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [--use-path-cli] [-- <AGENT-ARGS...>]", "supervise status <THREAD>", "supervise list", "supervise pause <THREAD>", "supervise resume <THREAD>", "supervise stop <THREAD>", "--supervisor <SESSION-ID> supervise register <THREAD> [--owner NAME]"],
|
|
21
24
|
returns: "監督エージェントの標準入力・標準出力・標準エラーと終了コードを引き継ぎます。codexteerの出力は混ぜません。",
|
|
22
|
-
examples: ["codexteer supervise <THREAD>", 'codexteer supervise <THREAD> "セキュリティの問題だけを私へ報告し、Codexへは送信しないでください。"', 'codexteer supervise <THREAD> "テスト失敗を優先して監督してください。" --agent claude -- --model <MODEL> --effort <LEVEL>'],
|
|
25
|
+
examples: ["codexteer supervise <THREAD>", "codexteer supervise <THREAD> --use-path-cli", 'codexteer supervise <THREAD> "セキュリティの問題だけを私へ報告し、Codexへは送信しないでください。"', 'codexteer supervise <THREAD> "テスト失敗を優先して監督してください。" --agent claude -- --model <MODEL> --effort <LEVEL>'],
|
|
23
26
|
notes: [...supervisionExecutionNotes,
|
|
24
27
|
"status/listは登録状態active/paused/stopped、owner、launcherの生存確認、最後のCLI観測とその経過時間、接続状態、最後の配送を返します。statusは未解決の指摘も要約します。activeは登録状態であり、AIの読了や継続監視の証明ではありません。コピーした本文のlauncher_aliveはnullです。",
|
|
25
28
|
"pauseは登録済み監督の送信を拒否し、read/watchは続行できます。resumeで介入を再開、stopでその監督の以後の観測・送信を拒否します。Codexタスクやagentプロセス自体は終了しません。必要ならagentへ終了を指示してください。",
|
|
@@ -48,7 +51,7 @@ export const topics = {
|
|
|
48
51
|
connection: {
|
|
49
52
|
title: "共有wrapperまたは通常起動Desktopの観測接続を選ぶ",
|
|
50
53
|
when: "通常起動したDesktopを再起動せずに観測したいとき。",
|
|
51
|
-
usage: ["--connection shared|desktop doctor --thread <THREAD>", "--connection desktop read <THREAD>", "--connection desktop watch <THREAD> --stream [--since CURSOR]", "supervise <THREAD> [MESSAGE] --connection desktop [--agent claude|codex]", "supervise prompt <THREAD> [MESSAGE] --connection desktop [--agent claude|codex]"],
|
|
54
|
+
usage: ["--connection shared|desktop doctor --thread <THREAD>", "--connection desktop read <THREAD>", "--connection desktop watch <THREAD> --stream [--notify all|digest] [--settle-ms N] [--max-hold-ms N] [--since CURSOR]", "supervise <THREAD> [MESSAGE] --connection desktop [--agent claude|codex]", "supervise prompt <THREAD> [MESSAGE] --connection desktop [--agent claude|codex]"],
|
|
52
55
|
returns: "desktopでは既存ソケットを検証して読み取りだけを行います。doctorのconnection.observation_onlyはtrue、送信操作はunsupportedです。",
|
|
53
56
|
examples: ["codexteer --connection desktop doctor --thread <THREAD> --json", "codexteer --connection desktop read <THREAD> --include-output --json", "codexteer supervise <THREAD> --agent codex --connection desktop"],
|
|
54
57
|
notes: ["既定sharedは従来のdesktop startで用意するwrapperです。desktopはCODEX_HOME/app-server-control/app-server-control.sockを使います。Desktopが共有ローカルdaemonを使っている環境だけで利用でき、通常起動なら必ず存在するわけではありません。ソケットがなければDESKTOP_CONNECTION_UNAVAILABLEです。",
|
|
@@ -59,19 +62,19 @@ export const topics = {
|
|
|
59
62
|
"supervise prompt": {
|
|
60
63
|
title: "監督役(オーケストレーター)向けのプロンプトを出力する",
|
|
61
64
|
when: "Claude Codeなどの監督役へ渡す、対象タスク入りの初期プロンプト本文を取得・確認するとき。",
|
|
62
|
-
usage: ["supervise prompt <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop]"],
|
|
63
|
-
returns: "通常はプロンプト本文だけを標準出力へ返します。--jsonではdata.thread_id、data.prompt、元のホーム(codex_home)・保存先・版・ハッシュ・再利用の有無を示すdata.deployment、Nodeのパスと版を示すdata.node、セッションID・owner・connectionを示すdata.supervisor
|
|
64
|
-
examples: ["codexteer supervise prompt <THREAD>", 'codexteer supervise prompt <THREAD> "進捗を観測し、完了時だけ私へ報告してください。Codexへの送信は不要です。"', "codexteer supervise prompt codex://threads/<UUID> --json", "codexteer supervise <THREAD>"],
|
|
65
|
+
usage: ["supervise prompt <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [--use-path-cli]"],
|
|
66
|
+
returns: "通常はプロンプト本文だけを標準出力へ返します。--jsonではdata.thread_id、data.prompt、元のホーム(codex_home)・保存先・版・ハッシュ・再利用の有無を示すdata.deployment、Nodeのパスと版を示すdata.node、セッションID・owner・connectionを示すdata.supervisor、実行方式を示すdata.cli_mode(savedまたはpath)を返します。--use-path-cliではdeploymentとnodeはnullです。",
|
|
67
|
+
examples: ["codexteer supervise prompt <THREAD>", "codexteer supervise prompt <THREAD> --use-path-cli", 'codexteer supervise prompt <THREAD> "進捗を観測し、完了時だけ私へ報告してください。Codexへの送信は不要です。"', "codexteer supervise prompt codex://threads/<UUID> --json", "codexteer supervise <THREAD>"],
|
|
65
68
|
notes: [...supervisionExecutionNotes,
|
|
66
69
|
"出力は監督役(オーケストレーター)へ渡す初期指示です。THREADはUUIDまたはcodex://threads/...。IDの書式を確認して正規化し、本文へ埋め込みます。Desktopへの接続・履歴取得・送信・Claudeの起動・設定変更は行いません。タスクの存在と接続は監督開始時に確認します。", "Claudeを起動する場合はsupervise <THREAD> [MESSAGE]を使います。追加の起動引数は -- の後ろへ渡してください。", "共通の操作手順はhelp monitorと同じです。観測の観点・介入の判断基準・報告方法は選択した監督方針に従います。Monitorが使えない場合は短いwatchで観測します。", "生成した本文を既存のエージェントへ貼り付けても使えます。本文のregisterで開始し、監督停止は端末のsupervise stopかそのセッションへの指示を使います。プロンプトはCLIやOSの権限設定を変更しません。"],
|
|
67
70
|
},
|
|
68
71
|
monitor: {
|
|
69
72
|
title: "Codexタスクを観測・介入・結果確認の順で監督する",
|
|
70
73
|
when: "Claudeなどが指定された監督方針に従って対象タスクを観測し、送信が許されている場合は介入と結果確認を行う運用。",
|
|
71
|
-
usage: ["supervise prompt <THREAD> [MESSAGE]", "watch <THREAD> --stream [--since CURSOR] [--poll-ms N] [--limit N] [--max-chars N] [--include-output]", "read <THREAD> [--since CURSOR]", "help supervise", "help send", "help checkpoint", "help history"],
|
|
74
|
+
usage: ["supervise prompt <THREAD> [MESSAGE]", "watch <THREAD> --stream [--notify all|digest] [--settle-ms N] [--max-hold-ms N] [--since CURSOR] [--poll-ms N] [--limit N] [--max-chars N] [--include-output]", "read <THREAD> [--since CURSOR]", "help supervise", "help send", "help checkpoint", "help history"],
|
|
72
75
|
returns: "--streamは作業差分(data.type=observation)と接続状態(data.type=connection)をJSON Linesで返します。差分のevents、attention、cursorを読み、必要なときだけ次の操作を選びます。",
|
|
73
|
-
examples: ["codexteer watch <THREAD> --stream --since <CURSOR> --include-output --json", "codexteer read <THREAD> --since <LAST-CURSOR> --include-output --json", "codexteer send <THREAD> '失敗箇所を先に確認してください' --source claude-code --kind review --finding <FINDING-ID> --based-on <CURSOR> --json", "codexteer history check <THREAD> <MESSAGE-ID> --json"],
|
|
74
|
-
notes: [...supervisionSteps(), "--streamは常にJSON Linesです。初回にwatchingを通知し、平常時は差分だけを出力します。観測成功後の一時切断は1→2→4→8→最大10秒間隔で、接続・初期化・読み取りを含め60秒まで復帰を試みます。初回失敗はすぐ終了します。SIGINT/SIGTERMまたはMonitorのキャンセルで停止し、--until/--timeout-msとは併用しません。", "無効cursorはneeds_review、復帰上限・権限・プロトコル等の異常はfailedを含むok:falseのJSONを返し、終了コード1です。観測の再接続は送信の再試行を行いません。", "既存のログ監視を続け、その通知後にread --sinceを呼ぶ運用も可能です。複数AIの共有リソース利用調整はhelp resourceを参照してください。"],
|
|
76
|
+
examples: ["codexteer watch <THREAD> --stream --notify digest --since <CURSOR> --include-output --json", "codexteer read <THREAD> --since <LAST-CURSOR> --include-output --json", "codexteer send <THREAD> '失敗箇所を先に確認してください' --source claude-code --kind review --finding <FINDING-ID> --based-on <CURSOR> --json", "codexteer history check <THREAD> <MESSAGE-ID> --json"],
|
|
77
|
+
notes: [...supervisionSteps(), notificationHelp, "--streamは常にJSON Linesです。初回にwatchingを通知し、平常時は差分だけを出力します。観測成功後の一時切断は1→2→4→8→最大10秒間隔で、接続・初期化・読み取りを含め60秒まで復帰を試みます。初回失敗はすぐ終了します。SIGINT/SIGTERMまたはMonitorのキャンセルで停止し、--until/--timeout-msとは併用しません。", "無効cursorはneeds_review、復帰上限・権限・プロトコル等の異常はfailedを含むok:falseのJSONを返し、終了コード1です。観測の再接続は送信の再試行を行いません。", "既存のログ監視を続け、その通知後にread --sinceを呼ぶ運用も可能です。複数AIの共有リソース利用調整はhelp resourceを参照してください。"],
|
|
75
78
|
},
|
|
76
79
|
resource: {
|
|
77
80
|
title: "画面・Unity・Gitなどの利用予定をAI間で共有する",
|
|
@@ -106,15 +109,16 @@ export const topics = {
|
|
|
106
109
|
overview: {
|
|
107
110
|
title: "別のAIと、Codexタスクの進行を共有する",
|
|
108
111
|
when: "タスクを探す → 現状を読む → 指示を送る → 変化を確認する、という順で使います。",
|
|
109
|
-
usage: ["supervise <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [-- <AGENT-ARGS...>]", "supervise prompt <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop]", "supervise status|pause|resume|stop <THREAD>", "supervise list", "threads list [--desktop-only] [--limit N]", "status <THREAD>", "read <THREAD> [--since CURSOR] [--limit N]", "watch <THREAD> [--since CURSOR] [--until change|idle|attention]", "send <THREAD> <MESSAGE...> [--new-turn] [--dry-run] [--no-sound]", "doctor [--backend app-server|ui]", "desktop start [--dry-run]", "thread resolve <THREAD>", "open <THREAD>", "debug-ui <THREAD> [--wait-ms N]", "help <COMMAND>"],
|
|
112
|
+
usage: ["supervise <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [--use-path-cli] [-- <AGENT-ARGS...>]", "supervise prompt <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [--use-path-cli]", "supervise status|pause|resume|stop <THREAD>", "supervise list", "projects list", "thread create --project ID|NAME [--context-thread THREAD-ID] <MESSAGE>", "threads list [--desktop-only] [--limit N]", "status <THREAD>", "read <THREAD> [--since CURSOR] [--limit N]", "watch <THREAD> [--since CURSOR] [--until change|idle|attention]", "send <THREAD> <MESSAGE...> [--new-turn] [--dry-run] [--no-sound]", "doctor [--backend app-server|ui]", "desktop start [--dry-run]", "thread resolve <THREAD>", "open <THREAD>", "debug-ui <THREAD> [--wait-ms N]", "help <COMMAND>"],
|
|
110
113
|
returns: "対話起動のsupervise <THREAD> [MESSAGE]を除き、各コマンドに --json を付けると、AIやスクリプトが扱えるJSONを返します。supervise promptもJSONに対応します。",
|
|
111
|
-
examples: ["codexteer supervise <THREAD>", "codexteer threads list --desktop-only --json", "codexteer read <THREAD> --json", "codexteer help send"],
|
|
114
|
+
examples: ["codexteer supervise <THREAD>", "codexteer supervise <THREAD> --use-path-cli", "codexteer threads list --desktop-only --json", "codexteer read <THREAD> --json", "codexteer help send"],
|
|
112
115
|
use_cases: [
|
|
113
116
|
{ need: "ClaudeまたはCodex CLIを監督役にする", command: "help supervise / help monitor" },
|
|
114
117
|
{ need: "監督役(オーケストレーター)向けのプロンプトを取得", command: "help supervise prompt" },
|
|
115
118
|
{ need: "監督状態・一時停止・再開を確認", command: "help supervise" },
|
|
116
119
|
{ need: "指摘と解決条件・評価を追跡", command: "help findings" },
|
|
117
120
|
{ need: "通常起動Desktopの観測接続", command: "help connection" },
|
|
121
|
+
{ need: "プロジェクトへ初回指示付きチャットを作成", command: "help projects / help thread" },
|
|
118
122
|
{ need: "今の状態・前回からの変化を確認", command: "help read / help status" },
|
|
119
123
|
{ need: "Claude CodeのMonitorから使う", command: "help monitor" },
|
|
120
124
|
{ need: "根拠や仮説を付けて指示する", command: "help send" },
|
|
@@ -123,7 +127,7 @@ export const topics = {
|
|
|
123
127
|
{ need: "テストしたソースと成果物を照合", command: "help checkpoint" },
|
|
124
128
|
{ need: "画面・Unityなどの利用を調整", command: "help resource" },
|
|
125
129
|
],
|
|
126
|
-
notes: ["THREADにはUUIDまたはcodex://threads/...を指定します。", "監督役の対話起動: codexteer supervise <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [-- <AGENT-ARGS...>]。superviseの標準入出力と終了コードは監督エージェントのものです。", "コマンド別のhelpには、使い所・出力・例・制約を載せています。help自体も --json に対応します。"],
|
|
130
|
+
notes: ["THREADにはUUIDまたはcodex://threads/...を指定します。", "監督役の対話起動: codexteer supervise <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [--use-path-cli] [-- <AGENT-ARGS...>]。superviseの標準入出力と終了コードは監督エージェントのものです。", "コマンド別のhelpには、使い所・出力・例・制約を載せています。help自体も --json に対応します。"],
|
|
127
131
|
},
|
|
128
132
|
read: {
|
|
129
133
|
title: "直近の発言・コマンド・変更ファイルを読む",
|
|
@@ -152,10 +156,10 @@ export const topics = {
|
|
|
152
156
|
watch: {
|
|
153
157
|
title: "変化・完了・ユーザー対応待ちまで待つ",
|
|
154
158
|
when: "同じログを繰り返し読まず、次のレビューが必要なタイミングを待ちたいとき。",
|
|
155
|
-
usage: ["watch <THREAD> [--since CURSOR] [--until change|idle|attention] [--timeout-ms N] [--poll-ms N] [--limit N] [--max-chars N] [--include-output] [--full-history]", "watch <THREAD> --stream [--since CURSOR] [--poll-ms N] [--limit N] [--max-chars N] [--include-output] [--full-history]"],
|
|
159
|
+
usage: ["watch <THREAD> [--since CURSOR] [--until change|idle|attention] [--timeout-ms N] [--poll-ms N] [--limit N] [--max-chars N] [--include-output] [--full-history]", "watch <THREAD> --stream [--notify all|digest] [--settle-ms N] [--max-hold-ms N] [--since CURSOR] [--poll-ms N] [--limit N] [--max-chars N] [--include-output] [--full-history]"],
|
|
156
160
|
returns: "通常は条件成立または時間切れで1回返します(reason、timed_out、events、cursor)。--streamはdata.type=observationの差分とdata.type=connectionの接続状態をJSON Linesで返します。",
|
|
157
|
-
examples: ["codexteer watch <THREAD> --since <CURSOR> --json", "codexteer watch <THREAD> --until idle --timeout-ms 60000 --json"],
|
|
158
|
-
notes: ["既定はchange、30秒待ち、1秒間隔。timeoutは0〜60000ms、pollは250〜10000ms。", "readと同じ差分取得を使います。--full-history は毎回の全取得になるため、通常の監視は既定の方式を使ってください。旧cursorの移行と確認範囲は help read へ。", "--sinceなしのchangeは現在を基準に次の変化を待ちます。idle/attentionは既に成立していればすぐ返します。", "--streamの接続状態はwatching/reconnecting/recovered/needs_review/failedです。観測成功後の一時切断だけ、最大10秒間隔・合計60秒まで同じタスクへ再接続します。通常のwatchと初回接続失敗は再試行しません。", "接続行のresume_cursorはCLIの出力完了位置または初回の基準です。AIの読了確認ではありません。再起動時は監督側が読了済みcursorを指定してください。復帰後もhas_moreなら差分を読み切ります。", "--streamは--until/--timeout-msと併用不可。平常時の定期通知はなく、SIGINT/SIGTERMで接続中・復帰待ちでも停止します。詳細はhelp monitorへ。", "読み取り専用のポーリングです。自動送信・タスク再開・承認回答・常駐登録は行いません。"],
|
|
161
|
+
examples: ["codexteer watch <THREAD> --stream --notify digest --since <CURSOR> --include-output --json", "codexteer watch <THREAD> --since <CURSOR> --json", "codexteer watch <THREAD> --until idle --timeout-ms 60000 --json"],
|
|
162
|
+
notes: [notificationHelp, DIGEST_GUIDANCE, "既定はchange、30秒待ち、1秒間隔。timeoutは0〜60000ms、pollは250〜10000ms。", "readと同じ差分取得を使います。--full-history は毎回の全取得になるため、通常の監視は既定の方式を使ってください。旧cursorの移行と確認範囲は help read へ。", "--sinceなしのchangeは現在を基準に次の変化を待ちます。idle/attentionは既に成立していればすぐ返します。", "--streamの接続状態はwatching/reconnecting/recovered/needs_review/failedです。観測成功後の一時切断だけ、最大10秒間隔・合計60秒まで同じタスクへ再接続します。通常のwatchと初回接続失敗は再試行しません。", "接続行のresume_cursorはCLIの出力完了位置または初回の基準です。AIの読了確認ではありません。再起動時は監督側が読了済みcursorを指定してください。復帰後もhas_moreなら差分を読み切ります。", "--streamは--until/--timeout-msと併用不可。平常時の定期通知はなく、SIGINT/SIGTERMで接続中・復帰待ちでも停止します。詳細はhelp monitorへ。", "読み取り専用のポーリングです。自動送信・タスク再開・承認回答・常駐登録は行いません。"],
|
|
159
163
|
},
|
|
160
164
|
send: {
|
|
161
165
|
title: "実行中タスクに方針やレビューを伝える",
|
|
@@ -167,8 +171,23 @@ export const topics = {
|
|
|
167
171
|
},
|
|
168
172
|
doctor: { title: "接続・実行環境・観測APIを診断する", when: "接続できないとき、監督の開始時、またはDesktop更新後。", usage: ["doctor [--backend app-server|ui]", "doctor --thread <THREAD> [--backend app-server]"], returns: "ready、製品バージョン、connection、runtime_compatibility、desktop_subscription、compatibility.api_checks、failureと復旧案内。", examples: ["codexteer doctor --json", "codexteer doctor --thread <THREAD> --json"], notes: ["codex_steer_compatibilityは製品名・版の比較で、違いだけでは停止しません。runtime_compatibilityで通信仕様・機能・操作ごとのsupported/unsupported/unverifiedを返します。新規ターンだけ未対応でも観測と通常送信は継続できます。", "宣言なしの旧環境は読み取りで検証し、未知の送信仕様は試験送信せず停止します。CAPABILITY_UNSUPPORTED/UNVERIFIEDは該当機能、RUNTIME_PROTOCOL_UNSUPPORTED/UNVERIFIEDは共通仕様を確認してください。対象未指定では接続を診断し、観測の互換性はunverifiedです。--threadで指定した対象の読み取り経路を検証します。readyは接続条件と指定された検証の結果です。", "compatibility.statusはverified/unverified/unsupported/failed。verifiedは対象の初回観測に成功したことを表し、履歴全体・送信・画面表示・承認往復の保証ではありません。呼び出していないAPIもunverifiedです。", "--threadはapp-server専用。本文を診断出力に含めず、タスクの再開・送信・承認回答・Desktop再起動・設定変更は行いません。failureにはコードと失敗したメソッドを返します。"] },
|
|
169
173
|
desktop: { title: "共有接続を有効にしてDesktopを起動する", when: "現在の作業を終え、Desktopを終了した後の起動時。", usage: ["desktop start [--dry-run]"], returns: "起動結果とdeployment(保存先、内容ハッシュ、版、再利用の有無)。", examples: ["codexteer desktop start --dry-run --json", "codexteer desktop start"], notes: ["呼び出した配布物をCODEX_HOME/codex-steer/runtimes/<version>-<sha256>へ検証して配置し、そのwrapperで起動します。保存先の所有者・権限・リンク・内容を検証し、既存の配置は上書きしません。npxの取得元はcodexteerです。通常はバージョン指定なしで使えます。特定の版で開始したい場合は、任意でパッケージ名に@バージョンを付けてください。起動と操作の製品版が異なっても、必要な通信・機能仕様が対応していれば使用できます。", "dry-runは接続・配置・起動を行いません。配置ロックが残っている場合は自動で奪わずDEPLOYMENT_BUSYで停止します。", "起動中のDesktopは終了させません。通常起動への復旧は、終了後にDock等から開き直します。"] },
|
|
174
|
+
projects: { title: "Desktopのローカルプロジェクトを探す", when: "新規チャットの所属とソースフォルダを選ぶとき。", usage: ["projects list"], returns: "App Serverのproject_id、Desktopの旧ID、名前、全ソースフォルダ。", examples: ["codexteer projects list --json"], notes: ["既存shared接続を使います。ローカルプロジェクトのみ。Desktopの状態ファイルはID対応の読取りだけに使います。同名ならproject_idを指定してください。"] },
|
|
170
175
|
threads: { title: "送信先のタスクを探す", when: "宛先の正確なIDを確認するとき。", usage: ["threads list [--desktop-only] [--limit N]"], returns: "ローカルタスクのID、作業場所、更新日時。", examples: ["codexteer threads list --desktop-only --limit 20 --json"], notes: ["CODEX_HOMEに従います。本文は読みません。"] },
|
|
171
|
-
thread: {
|
|
176
|
+
thread: {
|
|
177
|
+
title: "プロジェクトへタスクを作成し、IDを解決する",
|
|
178
|
+
when: "新規チャットへ初回指示を渡すとき、URLをIDへ変換するとき。",
|
|
179
|
+
usage: ["thread create --project ID|NAME [--context-thread THREAD-ID] [--timeout-ms N] [--title TEXT] [--model MODEL] [--thinking EFFORT] [--dry-run] <MESSAGE...>|-", "thread resolve <THREAD>"],
|
|
180
|
+
returns: "createは作成ID・所属ID・全フォルダ・初回指示の照合状態・配送状態・receipt_path。",
|
|
181
|
+
examples: ["codexteer thread create --project '競馬' '関連リポジトリを読み、調査結果を報告してください' --json", "codexteer thread create --project '競馬' --dry-run '調査してください' --json", "codexteer projects list --json", "codexteer thread resolve codex://threads/<UUID> --json"],
|
|
182
|
+
notes: ["プロジェクト名と初回指示だけで作成できます。既存チャット・CODEX_THREAD_ID・中継AIは不要です。App Serverで全フォルダ付きチャットを作り、Desktop購読を確立して初回指示を送信後、その新規チャット自身の実在thread/turn IDでnative所属登録を行います。ローカルプロジェクトのみ対応します。",
|
|
183
|
+
"--context-threadは任意の明示的な関連付けです。指定時だけ保存済みターンのある実在チャットを呼出し元にnative create_threadを使います。そのチャットへの送信や追加AIターンはありません。省略時はstandalone作成となり、CODEX_THREAD_IDも参照しません。",
|
|
184
|
+
"モデル・権限・承認設定はプロジェクトのApp Server設定に従います。--model/--thinking/--titleは明示指定可能。cwd・権限・承認設定の上書きと旧--via-threadは非対応。--timeout-msは各RPCの待機期限で既定120000、1000〜600000。CLI終了後も承認はDesktopが担当し、自動回答しません。",
|
|
185
|
+
"standaloneで--titleを省略すると、初回指示の空白をまとめた先頭80文字を名前として保存します。タイトル用AIは起動しません。この名前はJSON/receiptにも含まれます。",
|
|
186
|
+
"作成後にDesktopの所属ストア、canonical projectId、全フォルダ、保存初回指示とタイトルを照合します。履歴の反映待ちは読取りだけで行います。詳細と実機検証はdocs/thread-create.md。",
|
|
187
|
+
"--dry-runは既存接続・プロジェクト・native schemaを読取り確認し、チャット・指示・保存記録は作りません。",
|
|
188
|
+
"作成・初回配送を自動再試行しません。creation_status、thread_id、receipt_path、native_call_idを確認してください。createdなら既存IDを使い、unknown/dispatchingは未作成の証拠ではありません。Desktop/DB/保存履歴を直接編集しません。--connection desktopと--supervisorでのcreateは拒否します。",
|
|
189
|
+
"resolveはID書式だけを検査し、タスク存在確認はstatusを使います。"],
|
|
190
|
+
},
|
|
172
191
|
open: { title: "タスクをDesktopで開く", when: "ユーザーが画面でタスクを見たいとき。", usage: ["open <THREAD>"], returns: "開いたタスクのURL。", examples: ["codexteer open <THREAD>"], notes: ["画面を操作する明示的なコマンドです。"] },
|
|
173
192
|
"debug-ui": { title: "UI送信のために画面構造を調べる", when: "明示的なUI方式を診断するとき。", usage: ["debug-ui <THREAD> [--wait-ms N]"], returns: "Accessibilityの診断情報。", examples: ["codexteer debug-ui <THREAD> --wait-ms 1500"], notes: ["画面を操作します。通常のバックグラウンド送信には不要です。"] },
|
|
174
193
|
};
|
package/src/monitor.mjs
CHANGED
|
@@ -2,9 +2,10 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
2
2
|
import { normalizeThreadId } from "./thread-id.mjs";
|
|
3
3
|
import { codexHome, discoverRuntime } from "./runtime.mjs";
|
|
4
4
|
import { RpcClient } from "./rpc.mjs";
|
|
5
|
-
import { decodeCursor, readOnClient } from "./observe.mjs";
|
|
5
|
+
import { decodeCursor, digest, readOnClient } from "./observe.mjs";
|
|
6
6
|
import { assertRuntimeOperation, compatibleClient } from "./compatibility.mjs";
|
|
7
7
|
import { assertSupervisor, recordSupervisorObservation, recordSupervisorFailure } from "./supervision.mjs";
|
|
8
|
+
import { NotificationBuffer, notificationOptions } from "./notify.mjs";
|
|
8
9
|
|
|
9
10
|
const retryable = new Set(["CONNECTION_FAILED", "TIMEOUT", "RUNTIME_UNAVAILABLE", "RUNTIME_NOT_READY"]);
|
|
10
11
|
const needsReview = new Set(["STALE_CURSOR", "CURSOR_UPGRADE_REQUIRED", "OBSERVATION_CHANGED"]);
|
|
@@ -15,11 +16,15 @@ export async function streamThread(threadInput, options, onChange, { discover =
|
|
|
15
16
|
const threadId = normalizeThreadId(threadInput), { signal, pollMs = 1000, reconnectTimeoutMs = 60000 } = options;
|
|
16
17
|
if (!Number.isInteger(pollMs) || pollMs < 250 || pollMs > 10000) throw new Error("--poll-ms must be between 250 and 10000.");
|
|
17
18
|
if (!Number.isInteger(reconnectTimeoutMs) || reconnectTimeoutMs < 1 || reconnectTimeoutMs > 60000) throw new Error("Invalid reconnect timeout.");
|
|
19
|
+
const notification = notificationOptions(options);
|
|
20
|
+
const buffer = notification.notify === "digest" ? new NotificationBuffer(notification) : null;
|
|
18
21
|
if (signal?.aborted) return;
|
|
19
22
|
let home = codexHome(), cursor = options.since, client, attempt;
|
|
23
|
+
let notifiedCursor = options.since;
|
|
24
|
+
let initialStateHash;
|
|
20
25
|
let established = false, lastObservedAt = null, outageStarted = null, attempts = 0, backoff = 1000, lastFailure;
|
|
21
26
|
const connection = (state, extra = {}) => ({
|
|
22
|
-
type: "connection", thread_id: threadId, state, resume_cursor: cursor ?? null,
|
|
27
|
+
type: "connection", thread_id: threadId, state, resume_cursor: (buffer ? notifiedCursor : cursor) ?? null,
|
|
23
28
|
last_observed_at: lastObservedAt, observed_at: new Date().toISOString(), reconnect_attempts: attempts, ...extra,
|
|
24
29
|
});
|
|
25
30
|
const expired = () => Object.assign(new Error("Watch could not restore observation within the reconnect deadline. Check doctor before restarting from a reviewed cursor."), {
|
|
@@ -28,6 +33,14 @@ export async function streamThread(threadInput, options, onChange, { discover =
|
|
|
28
33
|
const cancel = () => { attempt?.abort(new Error("Watch cancelled.")); client?.close(); };
|
|
29
34
|
signal?.addEventListener("abort", cancel, { once: true });
|
|
30
35
|
|
|
36
|
+
async function flush(reason) {
|
|
37
|
+
const data = buffer?.observation(reason, now());
|
|
38
|
+
if (!data) return;
|
|
39
|
+
await onChange(data);
|
|
40
|
+
notifiedCursor = data.cursor;
|
|
41
|
+
buffer.clear();
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
async function read() {
|
|
32
45
|
if (options.supervisor) await assertSupervisor(threadId, options.supervisor, { connection: options.connection });
|
|
33
46
|
const remaining = outageStarted === null ? null : reconnectTimeoutMs - (now() - outageStarted);
|
|
@@ -53,7 +66,7 @@ export async function streamThread(threadInput, options, onChange, { discover =
|
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
try {
|
|
56
|
-
if (cursor) decodeCursor(cursor, threadId);
|
|
69
|
+
if (cursor) initialStateHash = decodeCursor(cursor, threadId).state;
|
|
57
70
|
while (!signal?.aborted) {
|
|
58
71
|
let data;
|
|
59
72
|
try { data = await read(); }
|
|
@@ -65,6 +78,7 @@ export async function streamThread(threadInput, options, onChange, { discover =
|
|
|
65
78
|
lastFailure = error;
|
|
66
79
|
if (outageStarted === null) {
|
|
67
80
|
outageStarted = now(); attempts = 0; backoff = 1000;
|
|
81
|
+
await flush("reconnecting");
|
|
68
82
|
await onChange(connection("reconnecting", { cause_code: error.code, retry_timeout_ms: reconnectTimeoutMs }));
|
|
69
83
|
}
|
|
70
84
|
await sleep(Math.max(0, Math.min(backoff, reconnectTimeoutMs - (now() - outageStarted))), undefined, { signal });
|
|
@@ -76,7 +90,18 @@ export async function streamThread(threadInput, options, onChange, { discover =
|
|
|
76
90
|
lastObservedAt = data.observed_at;
|
|
77
91
|
if (options.supervisor) await recordSupervisorObservation(threadId, options.supervisor, data);
|
|
78
92
|
// Only network reads are retried. Consumer/output errors always stop here.
|
|
79
|
-
if (
|
|
93
|
+
if (buffer) {
|
|
94
|
+
if (!cursor) {
|
|
95
|
+
buffer.baseline(data);
|
|
96
|
+
notifiedCursor = data.cursor;
|
|
97
|
+
} else {
|
|
98
|
+
const { thread_id, status, active_turn_id, attention, cwd, title } = data;
|
|
99
|
+
const initialStateChanged = !buffer.previous && initialStateHash !== digest({ thread_id, status, active_turn_id, attention, cwd, title });
|
|
100
|
+
buffer.add(data, cursor, now(), initialStateChanged);
|
|
101
|
+
const reason = buffer.reason(now());
|
|
102
|
+
if (reason) await flush(reason);
|
|
103
|
+
}
|
|
104
|
+
} else if (cursor && data.changed) await onChange({ ...data, type: "observation", reason: "change" });
|
|
80
105
|
if (signal?.aborted) break;
|
|
81
106
|
cursor = data.cursor;
|
|
82
107
|
if (!established || outageStarted !== null) {
|
|
@@ -86,6 +111,9 @@ export async function streamThread(threadInput, options, onChange, { discover =
|
|
|
86
111
|
if (!data.has_more && !signal?.aborted) await sleep(pollMs, undefined, { signal });
|
|
87
112
|
}
|
|
88
113
|
} catch (error) {
|
|
114
|
+
// Flush the last successfully read page even when the next one failed.
|
|
115
|
+
// Keep has_more=true on a partial read; claiming completion would be false.
|
|
116
|
+
try { await flush(signal?.aborted ? "stopped" : "failed"); } catch { /* Preserve the original read/output error. */ }
|
|
89
117
|
if (!signal?.aborted) {
|
|
90
118
|
const failure = error instanceof Error ? error : new Error("Watch consumer failed.");
|
|
91
119
|
failure.code ??= "WATCH_FAILED";
|
|
@@ -97,6 +125,10 @@ export async function streamThread(threadInput, options, onChange, { discover =
|
|
|
97
125
|
} finally {
|
|
98
126
|
signal?.removeEventListener("abort", cancel);
|
|
99
127
|
client?.close();
|
|
128
|
+
// A cancelled read or sleep can exit the loop without entering catch.
|
|
129
|
+
if (signal?.aborted) {
|
|
130
|
+
try { await flush("stopped"); } catch { /* The consumer resumes from its own reviewed cursor. */ }
|
|
131
|
+
}
|
|
100
132
|
if (options.supervisor && signal?.aborted) {
|
|
101
133
|
try { await recordSupervisorObservation(threadId, options.supervisor, { type: "connection", state: "stopped", cause_code: "WATCH_CANCELLED" }); }
|
|
102
134
|
catch { /* A stopped/replaced supervisor must not be revived by cleanup. */ }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { BUNDLED_CLI, BUNDLED_NODE } from "./wrapper.mjs";
|
|
8
|
+
import { nativeRequest } from "./native-rpc.mjs";
|
|
9
|
+
|
|
10
|
+
const LIMIT = 8 * 1024 * 1024;
|
|
11
|
+
const failure = (message, code = "NATIVE_COMMAND_FAILED", uncertain = false) => Object.assign(new Error(message), { code, uncertain });
|
|
12
|
+
|
|
13
|
+
// An internal JSON-RPC error can arrive after a mutation has run. RpcFailure's
|
|
14
|
+
// default uncertain:false only describes the response, not rollback of effects.
|
|
15
|
+
// Only method/argument rejection proves that dispatch could not execute it.
|
|
16
|
+
export function mutationOutcomeUnknown(error) {
|
|
17
|
+
return Number.isSafeInteger(error.rpcCode) ? ![-32601, -32602].includes(error.rpcCode) : error.uncertain !== false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function validateNativeCommandSchema(params, response) {
|
|
21
|
+
for (const key of ["command", "cwd", "timeoutMs", "outputBytesCap", "sandboxPolicy"]) if (!params?.properties?.[key]) throw failure("Missing native helper command schema.", "NATIVE_COMMAND_UNVERIFIED");
|
|
22
|
+
const readOnly = params.definitions?.SandboxPolicy?.oneOf?.find(policy => policy.properties?.type?.enum?.includes("readOnly"));
|
|
23
|
+
if (readOnly?.properties?.networkAccess?.type !== "boolean" || !["exitCode", "stdout", "stderr"].every(key => response?.properties?.[key])) throw failure("Native helper sandbox/output schema is unverified.", "NATIVE_COMMAND_UNVERIFIED");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateStandaloneCreationSchemas(schemas) {
|
|
27
|
+
const required = { ThreadStartParams: ["cwd", "projectId", "runtimeWorkspaceRoots", "model", "config", "ephemeral"],
|
|
28
|
+
ThreadStartResponse: ["thread"], TurnStartParams: ["threadId", "input", "clientUserMessageId", "effort"],
|
|
29
|
+
TurnStartResponse: ["turn"], ThreadSetNameParams: ["threadId", "name"], ThreadMetadataUpdateParams: ["threadId", "projectId"] };
|
|
30
|
+
for (const [name, keys] of Object.entries(required)) {
|
|
31
|
+
if (!keys.every(key => schemas[name]?.properties?.[key])) throw failure(`Missing standalone creation schema: ${name}.`, "NATIVE_COMMAND_UNVERIFIED");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function inspectNativeCommandProtocol(state, { run = spawnSync, executable = BUNDLED_CLI, standalone = false } = {}) {
|
|
36
|
+
const version = run(executable, ["--version"], { encoding: "utf8", timeout: 5000 });
|
|
37
|
+
const installed = version.stdout?.match(/codex-cli ([\w.+-]+)/)?.[1];
|
|
38
|
+
if (version.status !== 0 || !installed || installed !== state.cli_version) throw failure("Native helper requires the installed bundled schema to match the running server.", "NATIVE_COMMAND_UNVERIFIED");
|
|
39
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "codexteer-native-schema-"));
|
|
40
|
+
try {
|
|
41
|
+
const generated = run(executable, ["app-server", "generate-json-schema", "--experimental", "--out", directory], { encoding: "utf8", timeout: 15000, maxBuffer: LIMIT });
|
|
42
|
+
if (generated.status !== 0) throw failure("Cannot inspect native helper command schema.", "NATIVE_COMMAND_UNVERIFIED");
|
|
43
|
+
const schemas = await Promise.all(["CommandExecParams", "CommandExecResponse"].map(async name => JSON.parse(await readFile(path.join(directory, "v2", `${name}.json`), "utf8"))));
|
|
44
|
+
validateNativeCommandSchema(...schemas);
|
|
45
|
+
if (standalone) {
|
|
46
|
+
const names = ["ThreadStartParams", "ThreadStartResponse", "TurnStartParams", "TurnStartResponse", "ThreadSetNameParams", "ThreadMetadataUpdateParams"];
|
|
47
|
+
const creation = Object.fromEntries(await Promise.all(names.map(async name => [name, JSON.parse(await readFile(path.join(directory, "v2", `${name}.json`), "utf8"))])));
|
|
48
|
+
validateStandaloneCreationSchemas(creation); schemas.push(creation);
|
|
49
|
+
}
|
|
50
|
+
return { status: "verified", source: "matching-bundled-command-schema", cli_version: installed, schema_sha256: createHash("sha256").update(JSON.stringify(schemas)).digest("hex"), ...(standalone ? { standalone_creation: "verified" } : {}) };
|
|
51
|
+
} catch (error) { if (error.code === "NATIVE_COMMAND_UNVERIFIED") throw error; throw failure("Cannot read native helper command schemas.", "NATIVE_COMMAND_UNVERIFIED"); }
|
|
52
|
+
finally { await rm(directory, { recursive: true, force: true }); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// command/exec is a deterministic App Server API, explicitly independent of
|
|
56
|
+
// threads/turns. It launches only our fixed signed-Node worker, never user code
|
|
57
|
+
// or a shell. The read-only sandbox allows the local native Unix socket; the
|
|
58
|
+
// worker has no HTTP/DNS behavior. No model is invoked.
|
|
59
|
+
export async function nativeOnAppServer(client, endpoint, action, args, context, { timeoutMs = 120000, callId } = {}) {
|
|
60
|
+
const mutation = ["create_thread", "move_thread_to_project"].includes(action);
|
|
61
|
+
const request = nativeRequest(action, args, context, callId);
|
|
62
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "codexteer-native-request-"));
|
|
63
|
+
let result, error, dispatched = false;
|
|
64
|
+
try {
|
|
65
|
+
const input = JSON.stringify({ endpoint: { socketPath: endpoint.socketPath, fingerprint: endpoint.fingerprint }, action, args, context, timeoutMs, callId: request.params?.callId });
|
|
66
|
+
if (Buffer.byteLength(input) > LIMIT) throw failure("Native helper request exceeds its limit.", "NATIVE_PROTOCOL_ERROR");
|
|
67
|
+
const file = path.join(directory, "request.json");
|
|
68
|
+
await writeFile(file, input, { mode: 0o600, flag: "wx" });
|
|
69
|
+
dispatched = true;
|
|
70
|
+
const output = await client.request("command/exec", {
|
|
71
|
+
command: [BUNDLED_NODE, fileURLToPath(new URL("./native-worker.mjs", import.meta.url)), file],
|
|
72
|
+
cwd: fileURLToPath(new URL("..", import.meta.url)), timeoutMs: timeoutMs + 1000, outputBytesCap: LIMIT * 2,
|
|
73
|
+
sandboxPolicy: { type: "readOnly", networkAccess: true },
|
|
74
|
+
}, { mutation });
|
|
75
|
+
let response;
|
|
76
|
+
try { response = JSON.parse(output.stdout); } catch { throw failure("Native helper returned no valid acknowledgement. Inspect the receipt before retrying.", "NATIVE_PROTOCOL_ERROR", mutation); }
|
|
77
|
+
if (response.ok === true && output.exitCode === 0 && Object.hasOwn(response, "result")) { result = response.result; return result; }
|
|
78
|
+
if (response.ok === false && response.error) {
|
|
79
|
+
const error = failure(response.error.message, response.error.code, !!response.error.uncertain);
|
|
80
|
+
if (Number.isSafeInteger(response.error.rpcCode)) error.rpcCode = response.error.rpcCode;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
throw failure("Native helper did not confirm success.", "NATIVE_PROTOCOL_ERROR", mutation);
|
|
84
|
+
} catch (caught) {
|
|
85
|
+
error = caught;
|
|
86
|
+
if (error.uncertain === undefined || Number.isSafeInteger(error.rpcCode)) error.uncertain = dispatched && mutation && mutationOutcomeUnknown(error);
|
|
87
|
+
throw error;
|
|
88
|
+
} finally {
|
|
89
|
+
try { await rm(directory, { recursive: true, force: true }); }
|
|
90
|
+
catch { if (result) result.native_request_cleanup_required = true; else if (error) error.native_request_cleanup_required = true; }
|
|
91
|
+
}
|
|
92
|
+
}
|