codexteer 0.17.0 → 0.18.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 +28 -1
- package/README.md +127 -14
- package/assets/remote-device-key.swift +94 -0
- package/bin/codexteer-update.mjs +3 -0
- package/bin/codexteer.mjs +6 -2
- package/docs/cloud.md +113 -0
- package/docs/desktop-cloud.md +36 -0
- package/docs/distribution.md +11 -19
- package/docs/orchestration.md +120 -0
- package/docs/remote-cli-validation.json +139 -0
- package/docs/remote-control.md +65 -0
- package/docs/remote-device-enrollment.md +91 -0
- package/docs/remote-observation.md +57 -0
- package/docs/remote-operations.md +89 -0
- package/docs/thread-create.md +20 -45
- package/package.json +11 -2
- package/scripts/update-desktop.applescript +51 -0
- package/src/agent-launch.mjs +27 -0
- package/src/app-server.mjs +34 -10
- package/src/checkpoint.mjs +52 -16
- package/src/cli.mjs +201 -23
- package/src/cloud-api.mjs +118 -0
- package/src/cloud-cli.mjs +319 -0
- package/src/cloud-connection.mjs +64 -0
- package/src/cloud-id.mjs +16 -0
- package/src/cloud-observe.mjs +178 -0
- package/src/cloud-send.mjs +33 -0
- package/src/cloud.mjs +172 -0
- package/src/connection.mjs +12 -1
- package/src/creation-verification.mjs +30 -10
- package/src/desktop-cloud-api.mjs +119 -0
- package/src/desktop-cloud-id.mjs +18 -0
- package/src/desktop-cloud-project.mjs +148 -0
- package/src/desktop-cloud.mjs +98 -0
- package/src/desktop.mjs +3 -1
- package/src/directive.mjs +5 -2
- package/src/distribution.mjs +2 -2
- package/src/evidence.mjs +3 -0
- package/src/findings.mjs +7 -7
- package/src/help.mjs +140 -28
- package/src/journal.mjs +37 -9
- package/src/launcher.mjs +16 -9
- package/src/monitor.mjs +18 -6
- package/src/native-command.mjs +10 -2
- package/src/native-rpc.mjs +15 -2
- package/src/observe.mjs +21 -7
- package/src/orchestrate-prompt.mjs +96 -0
- package/src/orchestrate.mjs +29 -0
- package/src/paged-read.mjs +7 -1
- package/src/projects.mjs +27 -2
- package/src/prompt-command.mjs +25 -0
- package/src/prompt.mjs +25 -8
- package/src/remote-client-info.mjs +16 -0
- package/src/remote-control-auth.mjs +109 -0
- package/src/remote-control-socket.mjs +137 -0
- package/src/remote-control.mjs +134 -0
- package/src/remote-device-key.mjs +41 -0
- package/src/remote-device-login.mjs +76 -0
- package/src/remote-device-protocol.mjs +72 -0
- package/src/remote-device-store.mjs +71 -0
- package/src/remote-device-worker.mjs +48 -0
- package/src/remote-device.mjs +202 -0
- package/src/remote-io.mjs +163 -0
- package/src/remote-key-access.mjs +76 -0
- package/src/remote-key-worker.mjs +38 -0
- package/src/remote-keychain.mjs +72 -0
- package/src/remote-management.mjs +234 -0
- package/src/remote.mjs +132 -0
- package/src/resource.mjs +8 -6
- package/src/runner.mjs +6 -1
- package/src/standalone-create.mjs +4 -3
- package/src/store.mjs +6 -3
- package/src/supervise.mjs +25 -53
- package/src/supervision.mjs +41 -19
- package/src/target.mjs +25 -0
- package/src/task-id.mjs +23 -0
- package/src/thread-create.mjs +4 -3
- package/src/thread-id.mjs +21 -6
- package/src/thread-verify.mjs +108 -0
- package/src/turn-repair.mjs +228 -0
- package/src/update.mjs +158 -0
- package/docs/thread-create-native-investigation.md +0 -136
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { connectDesktopCloud, desktopCloudEnvironments } from "./desktop-cloud-api.mjs";
|
|
3
|
+
import { desktopCloudId, desktopCloudLink } from "./desktop-cloud-id.mjs";
|
|
4
|
+
import { fetchThread } from "./observe.mjs";
|
|
5
|
+
import { readRecord, writeRecord, storePath, withStoreLock, listRecords } from "./store.mjs";
|
|
6
|
+
const failure = (message, code = "DESKTOP_CLOUD_OPTIONS_INVALID", uncertain = false) => Object.assign(new Error(message), { code, uncertain });
|
|
7
|
+
const hash = value => createHash("sha256").update(value).digest("hex");
|
|
8
|
+
export async function desktopCloudReceipt(attempt, storage = {}) {
|
|
9
|
+
if (!/^[a-f0-9-]{36}$/.test(attempt ?? "")) throw failure("Invalid creation attempt ID.");
|
|
10
|
+
const receipt = await readRecord("desktop-cloud-creations", attempt, storage);
|
|
11
|
+
if (!receipt || receipt.attempt_id !== attempt) throw failure("Creation receipt not found.", "DESKTOP_CLOUD_CREATION_RECEIPT_NOT_FOUND");
|
|
12
|
+
return receipt;
|
|
13
|
+
}
|
|
14
|
+
export async function createDesktopCloudTask(selector, prompt, options = {}, { environments = desktopCloudEnvironments, connect = connectDesktopCloud, ...storage } = {}) {
|
|
15
|
+
if (options.project !== undefined) throw failure("Creation cannot currently register a Desktop project for a newly allocated Cloud environment. No chat was created. Use doctor --thread TARGET --project PROJECT to check an existing task's executor-bound assignment API.", "DESKTOP_CLOUD_PROJECT_UNAVAILABLE");
|
|
16
|
+
if (typeof selector !== "string" || !selector.trim() || typeof prompt !== "string" || !prompt.trim()) throw failure("An --environment ID or exact name and initial instruction are required.");
|
|
17
|
+
if (options.title !== undefined && (typeof options.title !== "string" || !options.title.trim() || options.title.length > 1000)) throw failure("Invalid --title.");
|
|
18
|
+
const matches = (await environments()).environments.filter(row => row.id === selector || row.name === selector);
|
|
19
|
+
if (matches.length !== 1) throw failure("Select exactly one accessible environment configuration.");
|
|
20
|
+
const selected = matches[0], attempt = randomUUID();
|
|
21
|
+
let client, receipt = { schema: 1, attempt_id: attempt, environment_config_id: selected.id, environment_name: selected.name, connection_mode: "desktop-cloud",
|
|
22
|
+
project_membership: "not_requested", creation_status: "not_created", delivery_status: "not_sent", sent: false, prompt_sha256: hash(prompt), dry_run: !!options.dryRun }, creating = false;
|
|
23
|
+
if (options.dryRun) return receipt;
|
|
24
|
+
receipt.receipt_path = storePath("desktop-cloud-creations", attempt, storage.home);
|
|
25
|
+
const save = () => writeRecord("desktop-cloud-creations", attempt, receipt, storage);
|
|
26
|
+
try {
|
|
27
|
+
client = await connect();
|
|
28
|
+
const models = await client.request("model/list", { limit: 100 });
|
|
29
|
+
const defaults = models?.data?.filter(row => row.isDefault === true);
|
|
30
|
+
if (defaults?.length !== 1 || typeof defaults[0].model !== "string") throw failure("Cloud did not identify one default model.");
|
|
31
|
+
receipt.creation_status = "dispatching"; await save(); creating = true;
|
|
32
|
+
const result = await client.request("thread/start", { model: defaults[0].model, serviceName: "codex_cloud", threadSource: "user", pluginsMcp: { productSku: "codex" },
|
|
33
|
+
config: { "features.mcp_app_ui": true }, environments: [{ environmentConfigId: selected.id }], deferredEnvironment: true, ephemeral: false }, { mutation: true });
|
|
34
|
+
receipt = { ...receipt, creation_status: "created", thread_id: desktopCloudId(result.thread.id), deep_link: desktopCloudLink(result.thread.id) }; creating = false; await save();
|
|
35
|
+
if (options.title) await client.request("thread/name/set", { threadId: receipt.thread_id, name: options.title }, { mutation: true });
|
|
36
|
+
const { sendTrackedMessage } = await import("./journal.mjs");
|
|
37
|
+
const sent = await sendTrackedMessage(receipt.thread_id, prompt, { newTurn: true, connection: "desktop-cloud" }, { ...storage,
|
|
38
|
+
send: (target, text, settings) => sendDesktopCloudInput(target, text, settings, { ...storage, connect: async () => ({ request: (...args) => client.request(...args), close() {} }) }) });
|
|
39
|
+
receipt = { ...receipt, ...sent }; await save(); return receipt;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (creating) receipt.creation_status = error.uncertain !== false ? "unknown" : "not_created";
|
|
42
|
+
if (error.delivery_status) Object.assign(receipt, { delivery_status: error.delivery_status, sent: error.sent });
|
|
43
|
+
if (error.message_id) Object.assign(receipt, { message_id: error.message_id, client_message_id: error.client_message_id });
|
|
44
|
+
receipt.error_code = error.code ?? "DESKTOP_CLOUD_FAILED";
|
|
45
|
+
try { await save(); } catch { error.receipt_update_required = true; }
|
|
46
|
+
Object.assign(error, { execution_environment: "cloud", creation_status: receipt.creation_status, thread_id: receipt.thread_id, attempt_id: attempt, receipt_path: receipt.receipt_path, sent: receipt.sent, delivery_status: receipt.delivery_status }); throw error;
|
|
47
|
+
} finally { try { client?.close(); } catch { receipt.runtime_cleanup_required = true; } }
|
|
48
|
+
}
|
|
49
|
+
export async function sendDesktopCloudInput(target, text, { dryRun = false, newTurn = false, clientMessageId = randomUUID(), beforeSend } = {}, { connect = connectDesktopCloud, ...storage } = {}) {
|
|
50
|
+
const id = desktopCloudId(target);
|
|
51
|
+
if (typeof text !== "string" || !text.trim()) throw failure("A non-empty message is required.");
|
|
52
|
+
const plan = { thread_id: id, deep_link: desktopCloudLink(id), client_message_id: clientMessageId, backend: "desktop-cloud", connection_mode: "desktop-cloud", delivery_action: newTurn ? "new-turn" : "steer", dry_run: dryRun, message_characters: [...text].length };
|
|
53
|
+
if (dryRun) return { ...plan, sent: false, delivery_status: "not_sent" };
|
|
54
|
+
return withStoreLock(`desktop-cloud-send-${id}`, async () => {
|
|
55
|
+
const pending = (await listRecords("desktop-cloud-sends", storage)).find(row => row.thread_id === id && row.delivery_status === "unknown");
|
|
56
|
+
if (pending) throw failure("An earlier Desktop Cloud delivery is unknown. Inspect history; it must not be replayed automatically.", "DESKTOP_CLOUD_DELIVERY_PENDING");
|
|
57
|
+
let client, dispatching = false, receipt = { ...plan, prompt_sha256: hash(text), sent: false, delivery_status: "not_sent" };
|
|
58
|
+
const save = () => writeRecord("desktop-cloud-sends", clientMessageId, receipt, storage);
|
|
59
|
+
try {
|
|
60
|
+
client = await connect();
|
|
61
|
+
const thread = await fetchThread(client, id), active = thread.turns.filter(turn => turn.status === "inProgress");
|
|
62
|
+
if (newTurn ? thread.status?.type !== "idle" || active.length !== 0 : active.length !== 1) throw failure("The task state does not match the requested send. Read it again; idle continuation requires --new-turn.", "STALE_OBSERVATION");
|
|
63
|
+
const environments = thread.environments;
|
|
64
|
+
if (!Array.isArray(environments) || environments.length !== 1 || typeof environments[0].environmentId !== "string" || !environments[0].environmentId.startsWith("ccarenv_") || typeof environments[0].environmentConfigId !== "string") throw failure("Cannot verify the task's Cloud environment.");
|
|
65
|
+
await beforeSend?.(thread, client);
|
|
66
|
+
receipt.delivery_status = "unknown"; await save(); dispatching = true;
|
|
67
|
+
const params = { threadId: id, input: [{ type: "text", text, text_elements: [] }], clientUserMessageId: clientMessageId };
|
|
68
|
+
const result = await client.request(newTurn ? "turn/start" : "turn/steer", newTurn ? { ...params, environments, deferredEnvironment: true } : { ...params, expectedTurnId: active[0].id }, { mutation: true });
|
|
69
|
+
const turnId = newTurn ? result?.turn?.id : result?.turnId;
|
|
70
|
+
if (typeof turnId !== "string" || (!newTurn && turnId !== active[0].id)) throw failure("Cloud did not confirm the expected turn. Inspect history before another send.", "DESKTOP_CLOUD_PROTOCOL_ERROR", true);
|
|
71
|
+
receipt = { ...receipt, turn_id: turnId, sent: true, delivery_status: "accepted" };
|
|
72
|
+
try { await save(); } catch { receipt.receipt_update_required = true; } return receipt;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const unknown = dispatching && error.uncertain !== false;
|
|
75
|
+
receipt = { ...receipt, sent: unknown ? null : false, delivery_status: unknown ? "unknown" : "not_sent", error_code: error.code ?? "DESKTOP_CLOUD_FAILED" };
|
|
76
|
+
try { await save(); } catch { error.receipt_update_required = true; }
|
|
77
|
+
Object.assign(error, { uncertain: unknown, sent: receipt.sent, delivery_status: receipt.delivery_status }); throw error;
|
|
78
|
+
} finally { try { client?.close(); } catch { receipt.runtime_cleanup_required = true; } }
|
|
79
|
+
}, storage);
|
|
80
|
+
}
|
|
81
|
+
export async function listDesktopCloudTasks(options = {}, { connect = connectDesktopCloud } = {}) {
|
|
82
|
+
if (!Number.isInteger(options.limit ?? 20) || options.limit < 1 || options.limit > 100) throw failure("--limit must be 1..100.");
|
|
83
|
+
const client = await connect();
|
|
84
|
+
try { const data = await client.request("thread/list", { limit: options.limit ?? 20, ...(options.cursor ? { cursor: options.cursor } : {}) });
|
|
85
|
+
if (!Array.isArray(data?.data)) throw failure("Invalid task inventory.");
|
|
86
|
+
return { threads: data.data.map(row => ({ thread_id: desktopCloudId(`durable_${row.id}`), deep_link: desktopCloudLink(`durable_${row.id}`), title: row.name ?? null, status: row.status })), next_cursor: data.nextCursor ?? null };
|
|
87
|
+
} finally { try { client.close(); } catch { /* Do not mask the read result. */ } }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function confirmDesktopCloudDelivery(entry, verification, storage = {}) {
|
|
91
|
+
if (verification.status !== "stored" || entry.delivery_status === "not_sent") return;
|
|
92
|
+
const id = desktopCloudId(entry.thread_id);
|
|
93
|
+
return withStoreLock(`desktop-cloud-send-${id}`, async () => {
|
|
94
|
+
const receipt = await readRecord("desktop-cloud-sends", entry.client_message_id, storage);
|
|
95
|
+
if (!receipt || receipt.thread_id !== id || receipt.prompt_sha256 !== hash(entry.wire_text) || receipt.delivery_status === "not_sent") return;
|
|
96
|
+
await writeRecord("desktop-cloud-sends", entry.client_message_id, { ...receipt, delivery_status: "accepted", sent: true, turn_id: verification.turn_id, delivery_verification: "stored" }, storage);
|
|
97
|
+
}, storage);
|
|
98
|
+
}
|
package/src/desktop.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { threadComposerDeepLink, threadDeepLink } from "./thread-id.mjs";
|
|
4
|
+
import { threadComposerDeepLink, threadDeepLink, parseThreadTarget } from "./thread-id.mjs";
|
|
5
5
|
import { readDesktopSettings } from "./settings.mjs";
|
|
6
|
+
import { targetHost, remoteReadOnly } from "./target.mjs";
|
|
6
7
|
|
|
7
8
|
const APP_PATH = "/Applications/ChatGPT.app";
|
|
8
9
|
const APP_BUNDLE_ID = "com.openai.codex";
|
|
@@ -179,6 +180,7 @@ export function sendDesktopMessage(
|
|
|
179
180
|
message,
|
|
180
181
|
{ dryRun = false, waitMs = 1500, newTurn = false, keepFocus = false } = {},
|
|
181
182
|
) {
|
|
183
|
+
if ((parseThreadTarget(threadId).hostId ?? targetHost()) !== "local") throw remoteReadOnly();
|
|
182
184
|
if (message.trim() === "") throw new Error("Message must not be empty.");
|
|
183
185
|
validateWaitMs(waitMs);
|
|
184
186
|
|
package/src/directive.mjs
CHANGED
|
@@ -21,7 +21,7 @@ export async function prepareDirective(threadId, body, options, id, storage = {}
|
|
|
21
21
|
...(concern ? { finding_id: concern.id, finding_revision: concern.revision } : {}), ...(supervisor ? { supervisor_id: supervisor } : {}) };
|
|
22
22
|
const lines = [`[codexteer ${id}]`, `送信者: ${metadata.source}`, `種類: ${kindLabels[metadata.kind]}`];
|
|
23
23
|
for (const ref of refs) lines.push(`根拠: ${JSON.stringify(ref.ref)}${ref.sha256 ? ` (sha256:${ref.sha256})` : " (参照URL)"}`);
|
|
24
|
-
if (observed) lines.push(`観測対象ターン: ${observed.active_turn_id ?? "
|
|
24
|
+
if (observed) lines.push(`観測対象ターン: ${observed.active_turn_id ?? "未確定"}`);
|
|
25
25
|
if (supersedes) lines.push(`置き換える指示: ${supersedes}。この指示を以後の方針として扱ってください。`);
|
|
26
26
|
if (retracts) lines.push(`撤回する指示: ${retracts}。以後の方針から除外してください。`);
|
|
27
27
|
if (expiresAt) lines.push(`有効期限: ${expiresAt}`);
|
|
@@ -34,7 +34,10 @@ export async function prepareDirective(threadId, body, options, id, storage = {}
|
|
|
34
34
|
else {
|
|
35
35
|
if (thread.turns?.some(t => t.itemsView && t.itemsView !== "full") || !thread.turns?.length) thread = await fetchThread(client, threadId);
|
|
36
36
|
const current = threadSnapshot(thread);
|
|
37
|
-
|
|
37
|
+
const latestChanged = observed.latest_turn_id !== undefined && observed.latest_turn_id !== (thread.turns?.at(-1)?.id ?? null);
|
|
38
|
+
// Older v1 cursors cannot distinguish two ambiguous active snapshots.
|
|
39
|
+
const unpinned = observed.latest_turn_id === undefined && thread.historyMode === "paginated" && thread.turns.filter(t => t.status === "inProgress").length > 1;
|
|
40
|
+
if (latestChanged || unpinned || current.user_revision !== observed.user_revision || current.state.active_turn_id !== observed.active_turn_id) throw Object.assign(new Error("New user input or a different turn was observed, or the old cursor cannot identify the latest turn. Read the task again before sending."), { code: "STALE_OBSERVATION" });
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
await verifyEvidence(refs);
|
package/src/distribution.mjs
CHANGED
|
@@ -88,8 +88,8 @@ export async function describeDistribution(root = PACKAGE_ROOT) {
|
|
|
88
88
|
root = await realpath(root);
|
|
89
89
|
await checkAncestors(root);
|
|
90
90
|
check(await lstat(root), true, false);
|
|
91
|
-
const names = ["package.json", "LICENSE", "bin/codexteer.mjs", "bin/codexteer-wrapper.mjs", "assets/send-pururu.wav",
|
|
92
|
-
"scripts/send.applescript", "scripts/inspect.applescript", "scripts/accessibility.applescript"];
|
|
91
|
+
const names = ["package.json", "LICENSE", "bin/codexteer.mjs", "bin/codexteer-wrapper.mjs", "bin/codexteer-update.mjs", "assets/send-pururu.wav", "assets/remote-device-key.swift",
|
|
92
|
+
"scripts/send.applescript", "scripts/inspect.applescript", "scripts/accessibility.applescript", "scripts/update-desktop.applescript"];
|
|
93
93
|
for (const directory of ["bin", "assets", "scripts", "src", "node_modules", "node_modules/ws", "node_modules/ws/lib"]) {
|
|
94
94
|
check(await lstat(path.join(root, directory)), true, false);
|
|
95
95
|
}
|
package/src/evidence.mjs
CHANGED
|
@@ -2,8 +2,11 @@ import { stat, realpath } from "node:fs/promises";
|
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { targetHost } from "./target.mjs";
|
|
6
|
+
import { remoteFingerprintFile } from "./remote-io.mjs";
|
|
5
7
|
|
|
6
8
|
export async function fingerprintFile(input) {
|
|
9
|
+
if (targetHost() !== "local") return remoteFingerprintFile(input);
|
|
7
10
|
const ref = path.resolve(input), canonical = await realpath(ref), before = await stat(canonical);
|
|
8
11
|
if (!before.isFile()) throw new Error("Evidence must be a file or an http(s)/codex URL.");
|
|
9
12
|
const hash = createHash("sha256");
|
package/src/findings.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeTaskId } from "./task-id.mjs";
|
|
3
3
|
import { readRecord, writeRecord, listRecords, withStoreLock } from "./store.mjs";
|
|
4
4
|
import { collectEvidence } from "./evidence.mjs";
|
|
5
5
|
import { decodeCursor, digest } from "./observe.mjs";
|
|
@@ -16,7 +16,7 @@ const text = (value, label, max = 8000) => {
|
|
|
16
16
|
const contextHash = record => digest({ condition: record.condition, evidence: record.evidence });
|
|
17
17
|
|
|
18
18
|
export async function getFinding(threadInput, id, storage = {}) {
|
|
19
|
-
const threadId =
|
|
19
|
+
const threadId = normalizeTaskId(threadInput), record = await readRecord("findings", id, storage);
|
|
20
20
|
if (!record || record.schema !== 1 || record.id !== id || record.thread_id !== threadId) throw failure("Finding not found for this task.", "FINDING_NOT_FOUND");
|
|
21
21
|
if (!Number.isSafeInteger(record.revision) || record.revision < 1 || !["open", "resolved", "dismissed"].includes(record.disposition)
|
|
22
22
|
|| !Array.isArray(record.evidence) || typeof record.condition !== "string" || !ratings.includes(record.evaluation?.rating)) throw failure("Invalid finding record.", "FINDING_INVALID");
|
|
@@ -30,7 +30,7 @@ export async function findingMessages(record, storage = {}) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export async function createFinding(threadInput, { key, title, condition, evidence = [], basedOn, ...storage }) {
|
|
33
|
-
const threadId =
|
|
33
|
+
const threadId = normalizeTaskId(threadInput);
|
|
34
34
|
if (typeof key !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,150}$/.test(key)) throw failure("--key requires a stable short identifier for this concern.", "FINDING_INVALID");
|
|
35
35
|
text(title, "--title", 240); text(condition, "--condition");
|
|
36
36
|
const refs = await collectEvidence(evidence), observed = basedOn ? decodeCursor(basedOn, threadId) : null;
|
|
@@ -77,7 +77,7 @@ export async function findingStatus(threadInput, id, storage = {}) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export async function listFindings(threadInput, { all = false, limit = 50, ...storage } = {}) {
|
|
80
|
-
const threadId =
|
|
80
|
+
const threadId = normalizeTaskId(threadInput);
|
|
81
81
|
if (!Number.isInteger(limit) || limit < 1 || limit > 1000) throw failure("--limit must be between 1 and 1000.", "FINDING_INVALID");
|
|
82
82
|
const records = (await listRecords("findings", storage)).filter(record => record?.thread_id === threadId).sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
83
83
|
// Recheck closed resolutions too: changed inputs make them unresolved again.
|
|
@@ -90,7 +90,7 @@ export async function listFindings(threadInput, { all = false, limit = 50, ...st
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
async function changeFinding(threadInput, id, update, storage) {
|
|
93
|
-
const threadId =
|
|
93
|
+
const threadId = normalizeTaskId(threadInput);
|
|
94
94
|
return withStoreLock(`message-${threadId}`, async () => {
|
|
95
95
|
const record = await getFinding(threadId, id, storage);
|
|
96
96
|
const updated = { ...await update(record), updated_at: new Date().toISOString() };
|
|
@@ -124,7 +124,7 @@ export async function dismissFinding(threadInput, id, { reason, ...storage }) {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
export async function reopenFinding(threadInput, id, { reason, evidence, basedOn, condition, ...storage }) {
|
|
127
|
-
const threadId =
|
|
127
|
+
const threadId = normalizeTaskId(threadInput);
|
|
128
128
|
text(reason, "--reason"); if (condition !== undefined) text(condition, "--condition");
|
|
129
129
|
const refs = evidence === undefined ? undefined : await collectEvidence(evidence);
|
|
130
130
|
const observed = basedOn === undefined ? undefined : decodeCursor(basedOn, threadId);
|
|
@@ -145,7 +145,7 @@ export async function evaluateFinding(threadInput, id, { rating, reason, ...stor
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
export async function findingStats(threadInput, storage = {}) {
|
|
148
|
-
const threadId =
|
|
148
|
+
const threadId = normalizeTaskId(threadInput), records = (await listRecords("findings", storage)).filter(record => record?.thread_id === threadId);
|
|
149
149
|
const counts = Object.fromEntries(ratings.map(rating => [rating, 0]));
|
|
150
150
|
let sent = 0;
|
|
151
151
|
for (const entry of records) {
|