Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

### Added

- Restored progressive existing-task onboarding after installation: exact historical statuses receive plain icons, conservative first reads receive a quiet `✦`, and unknown tasks stay untouched. Classification is one ephemeral sequential pass, while every title write remains a serial mounted Codex reread/set with exact acknowledgement and no retries or persisted onboarding state.

### Changed

- Raised the minimum supported Codex Desktop version to 0.147.0 so onboarding can disable the independent local-image read capability before classifying untrusted task text.

## v3.0.2 - 2026-08-13

### Fixed
Expand Down
126 changes: 117 additions & 9 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Open with this orientation:
>
> ThreadBear adds one useful status icon while keeping the rest of each safe task title intact. Codex reads and applies the title itself.
>
> I'll check this Mac, show you exactly what will change, and ask before installing anything. Installation leaves existing task titles alone. Afterward, Codex needs one restart.
> I'll check this Mac, show you exactly what will change, and ask before installing anything. After installation, Codex asks once for permission to read bounded recent history; if allowed, existing tasks progressively receive best-guess icons when their history is clear enough. Codex needs one restart for future turns.

Codex collapses commentary after a turn finishes, so the final answer that asks for consent must repeat the orientation, readiness result, complete recommendation, and question. If a check fails, report it plainly and do not ask for install consent.

Expand Down Expand Up @@ -54,7 +54,7 @@ if [ -x "$HOME/.local/bin/threadbear" ]; then
fi
```

ThreadBear requires macOS 12 or newer, Apple silicon or Intel, Codex Desktop 0.146.0 or newer, and HTTPS access to the official guide and GitHub Releases. The check prints every fixed Codex Desktop command it finds; ThreadBear uses the first one that actually reports a compatible version. It needs no `sudo` or Full Disk Access. Ordinary title updates work with Codex's default workspace permissions. Uninstall cleanup asks once for permission to read the complete local task catalog. ThreadBear never opens Codex SQLite.
ThreadBear requires macOS 12 or newer, Apple silicon or Intel, Codex Desktop 0.147.0 or newer, and HTTPS access to the official guide and GitHub Releases. The check prints every fixed Codex Desktop command it finds; ThreadBear uses the first one that actually reports a compatible version. It needs no `sudo` or Full Disk Access. Ordinary title updates work with Codex's default workspace permissions. Uninstall cleanup asks once for permission to read the complete local task catalog. ThreadBear never opens Codex SQLite.

For an official release, run the verified bootstrap preview:

Expand All @@ -79,7 +79,8 @@ Only after the checks and dry run succeed, present this complete card in the sam
> ## Here's what will happen
>
> - ThreadBear adds one helpful status icon without rewriting your task's subject or emoji.
> - Existing task titles stay unchanged during installation.
> - After installation succeeds, ThreadBear reads bounded recent history and existing task icons appear progressively. A small sparkle marks a conservative first read; unclear tasks stay unchanged.
> - Codex asks once before that complete-catalog read. Declining leaves every existing title unchanged and does not affect the installed helper.
> - A small local helper, Codex instructions, and a ThreadBear skill are added.
> - Once a day, ThreadBear checks for and installs only verified official releases. Updates never read tasks or change titles.
> - Unclear or unsafe titles are left alone, and there is no persistent ThreadBear task.
Expand All @@ -94,7 +95,7 @@ A clear yes to the unchanged recommendation is consent. Ask again only if the ef

## 3. Install after consent

Say: “Thanks—I'll install ThreadBear now, then check that it is healthy. Existing task titles will not change in this step.”
Say: “Thanks—I'll install ThreadBear now, then check that it is healthy. Existing task titles will not change until those checks finish.”

Before a 2.2.1 reset, delete the exact fingerprinted `threadbear-maintenance` automation through supported native control and verify it is absent. Then unpin the preview's exact legacy main-task ID and verify the returned and reread task ID match with `pinned:false`. Do not rename that task. Any mismatch aborts before filesystem reset. The confirmed candidate command must include `--reset`.

Expand All @@ -121,23 +122,130 @@ Add `--reset` only after the exact legacy cleanup is verified. Then run:

Core `ready` is healthy when the installed binary, private lifecycle state, compatible Codex Desktop, managed guidance, and skill match the candidate. Report the daily updater separately; missing automatic updates do not make title handling globally unready. Core readiness does not depend on historical title counts.

No controller, worker, migration phase, persistent task, or hidden historical-title job should exist after installation. If installation fails after mutation starts, report `partial:true`, the failed stage, whether restart is required, and the one safe rerun action. `planned_changes` is a plan, not a claim that every item ran.
No controller, worker, migration phase, persistent task, durable onboarding state, queue, retry sweep, or additional automation should exist after installation. If installation fails after mutation starts, report `partial:true`, the failed stage, whether restart is required, and the one safe rerun action. `planned_changes` is a plan, not a claim that every item ran.

After all three checks pass, start this exact cell once. Its first output is the handoff boundary: when that handoff arrives, immediately send the friendly install recap below without waiting for the cell to finish. The recap must say that Codex will ask once for the complete-catalog read and that icons progress only if permission is granted. The same yielded cell then requests that permission, performs one ephemeral read-only classification pass, and applies each eligible title serially through mounted Codex tools. Do not start another cell, poll it from the conversation, or turn it into a task, automation, queue, or persisted job.

```js
// @exec: {"yield_time_ms": 30000, "max_output_tokens": 4000}
const handoff = {kind:"handoff",ready:true,activity:"existing-task-icons"};
if (Object.keys(handoff).sort().join(",") !== "activity,kind,ready" || handoff.ready !== true) exit();
text(JSON.stringify(handoff));
yield_control();

const parseNative = value => {
if (typeof value !== "string") return value;
try { return JSON.parse(value); } catch { return null; }
};
const exactKeys = (value, keys) => value && typeof value === "object" && !Array.isArray(value) &&
Object.keys(value).sort().join(",") === [...keys].sort().join(",");
const statuses = new Set(["complete","next_steps","needs_input","blocked","automation"]);
const icons = {complete:"✅",next_steps:"➡️",needs_input:"🙋",blocked:"🚨",automation:"🤖"};
const seen = new Set();
let previousID = "", terminal = false, invalid = false, carry = "";
let received = 0, updated = 0, unknown = 0, drifted = 0, unconfirmed = 0;

const acceptLine = async line => {
let record;
try { record = JSON.parse(line); } catch { invalid = true; return; }
if (terminal || !record || typeof record.kind !== "string") { invalid = true; return; }
if (record.kind === "summary") {
if (!exactKeys(record,["kind","total","eligible","exact","inferred","unknown","skipped"]) ||
![record.total,record.eligible,record.exact,record.inferred,record.unknown,record.skipped]
.every(Number.isInteger) || record.total !== record.eligible + record.skipped ||
record.eligible !== record.exact + record.inferred + record.unknown ||
record.eligible !== received) { invalid = true; return; }
terminal = true;
return;
}
if (record.kind !== "candidate" ||
!exactKeys(record,["kind","task_id","snapshot_title","status","provenance"]) ||
typeof record.task_id !== "string" || typeof record.snapshot_title !== "string" ||
typeof record.status !== "string" || typeof record.provenance !== "string" ||
seen.has(record.task_id) || (previousID && record.task_id <= previousID)) {
invalid = true; return;
}
const semantic = statuses.has(record.status);
if ((!semantic && (record.status !== "unknown" || record.provenance !== "unknown")) ||
(semantic && record.provenance !== "exact" && record.provenance !== "inferred")) {
invalid = true; return;
}
seen.add(record.task_id); previousID = record.task_id; received++;
let current;
try {
current = parseNative(await tools.codex_app__read_thread({threadId:record.task_id,
includeOutputs:false,turnLimit:1,maxOutputCharsPerItem:1}));
} catch { current = null; }
if (current?.thread?.id !== record.task_id || current.thread.title !== record.snapshot_title) {
drifted++;
} else if (!semantic) {
unknown++;
} else {
const mark = record.provenance === "inferred" ? "✦" : "";
const desired = icons[record.status] + mark + " " + record.snapshot_title;
const lower = record.snapshot_title.toLowerCase();
if (record.snapshot_title.trim() === "" ||
/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(record.snapshot_title) ||
["<codex_delegation>","<codex_internal_context","<environment_context>","<app-context",
"<collaboration_mode","<multi_agent_mode","<permissions_instructions","<permissions instructions",
"<apps_instructions","<plugins_instructions","<skills_instructions","<recommended_plugins"]
.some(marker => lower.includes(marker)) || desired.length > 60) {
drifted++;
} else {
let renamed;
try {
renamed = parseNative(await tools.codex_app__set_thread_title({threadId:record.task_id,title:desired}));
} catch { renamed = null; }
if (renamed?.threadId === record.task_id && renamed.title === desired) updated++;
else unconfirmed++;
}
}
if (received % 25 === 0) notify(`ThreadBear: first read ${received} tasks`);
};

const consume = async chunk => {
carry += chunk || "";
for (;;) {
const newline = carry.indexOf("\n");
if (newline < 0) return;
const line = carry.slice(0,newline); carry = carry.slice(newline + 1);
if (line.trim() !== "" && !invalid) await acceptLine(line);
}
};

let call = await tools.exec_command({
cmd:"\"$HOME/.local/bin/threadbear\" onboard-stream",
yield_time_ms:1000,
max_output_tokens:200000,
sandbox_permissions:"require_escalated",
justification:"Allow ThreadBear to read bounded recent history from the complete Codex task list for the first-read icons you approved?"
});
Comment thread
ericlitman marked this conversation as resolved.
await consume(call.output);
while (call.session_id !== undefined) {
call = await tools.write_stdin({session_id:call.session_id,yield_time_ms:30000,max_output_tokens:200000});
await consume(call.output);
}
if (carry.trim() !== "" || call.exit_code !== 0 || !terminal) invalid = true;
text(JSON.stringify({ready:!invalid,finished:true,received,updated,unknown,drifted,unconfirmed}));
```

The helper emits strict ordered JSON Lines and never writes a title. Exact historical ThreadBear footers produce plain icons. Ambiguous completed turns are classified in fixed sequential batches by `gpt-5.6-luna` at medium reasoning; malformed or failed batches become unknown. Unknown tasks are reread but not renamed. Every semantic candidate gets one immediate mounted reread and at most one explicit-target mounted setter call, with exact acknowledgement required. Drift and unconfirmed writes affect only that row and are never retried. The pass ends without durable state; restarting early leaves unfinished tasks untouched, and a later confirmed reinstall naturally skips already decorated titles.

After the checks finish, end the final response with this plain-language receipt, filled with the real result:

> ## ThreadBear recap 🐻
>
> - ThreadBear is installed and automatic updates are [ready / need attention].
> - Existing task titles and unrelated Codex settings stayed untouched.
> - Next: restart Codex so open tasks load the new instructions.
> - Codex will ask once for permission to read the complete task list. If you allow it, existing tasks with enough evidence will gain best-guess status icons over the next several minutes. A small sparkle marks a first read and disappears after that task's next turn; declining or unclear evidence leaves a task untouched.
> - You can keep working while this finishes. Restart Codex when ready so open tasks load the new instructions; restarting early simply leaves unfinished tasks untouched.

## 4. Restart

Say: “Installation is finished. One restart loads the new instructions. Existing task titles were not changed.”
Say: “Installation is finished. One restart loads the new instructions. The background first read may still be adding icons to existing tasks.”

After a successful install say:

> ThreadBear is installed. Restart Codex so open tasks load the new managed guidance.
> ThreadBear is installed. Codex will ask once for permission to read the complete task list. If you allow it, existing tasks with clear enough history will gain best-guess status icons over the next several minutes. A small sparkle marks a first read and disappears after that task's next turn; declining or unclear evidence leaves a task untouched. You can keep working while it runs. Restart Codex when ready so open tasks load the new managed guidance; restarting early leaves unfinished tasks untouched.

## Commands and updater

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ ThreadBear is a small local title decorator for Codex Desktop. Immediately befor
| ➡️ | next steps |
| ✅ | complete |

The visible shape is `<mark> <exact subject>`. ThreadBear writes only those five status prefixes. It also recognizes the obsolete neutral `🐻 ` prefix so it can remove that decoration without ever emitting it. Every other safe leading emoji and subject byte stays exact, except an ambiguous old ThreadBear prefix is deliberately left unchanged rather than guessed. Owners and actions stay in response prose. A title it cannot handle safely stays unchanged rather than truncated.
The ordinary visible shape is `<mark> <exact subject>`. ThreadBear writes those five status prefixes. During the first read after installation, a conservative historical inference uses `<mark>✦ <exact subject>`; the sparkle disappears when that task next takes a real turn. ThreadBear also recognizes the obsolete neutral `🐻 ` prefix so it can remove that decoration without ever emitting it. Every other safe leading emoji and subject byte stays exact, except an ambiguous old ThreadBear prefix is deliberately left unchanged rather than guessed. Owners and actions stay in response prose. A title it cannot handle safely stays unchanged rather than truncated.

## Install

Open [INSTALL.md](INSTALL.md) in a Codex task and follow the guided preview and consent flow. There is no persistent ThreadBear task or controller. Installation leaves historical task titles unchanged; restart Codex so open tasks load the new managed guidance.
Open [INSTALL.md](INSTALL.md) in a Codex task and follow the guided preview and consent flow. There is no persistent ThreadBear task or controller. After installation reports success, Codex asks once for complete-catalog read permission. If allowed, one yielded in-app cell progressively reads bounded latest-turn history and applies exact or conservative `✦` status icons through mounted Codex title tools; declining leaves existing titles unchanged. Unknown tasks stay unchanged, interruption loses only unfinished work, and no onboarding state is persisted. Restart Codex so open tasks load the new managed guidance.

ThreadBear installs one Go binary, one managed instruction block, one skill, and one daily update-only LaunchAgent. It keeps no per-task title database. A consented reset from 2.2.1 deletes the exact old automation, unpins the exact former persistent task without renaming it, replaces managed artifacts, imports no old state, and does not guess at legacy title cleanup.

Expand All @@ -38,7 +38,7 @@ The terminal `title` command accepts exactly `complete`, `next_steps`, `needs_in

## Boundaries

Ordinary turns use only mounted Codex reads and writes, so they work under Codex's default workspace permissions. The short-lived official App Server is used only for explicitly approved uninstall cleanup and is launched from a fixed Desktop executable path, never repository `PATH`. ThreadBear does not open Codex SQLite, edit Desktop caches or task prose, archive tasks, retry title writes, or maintain a database, queue, controller, repair pass, or persistent management task.
Ordinary turns use only mounted Codex reads and writes, so they work under Codex's default workspace permissions. Installation's explicitly permissioned ephemeral first read and explicitly approved uninstall cleanup use short-lived official Codex processes launched from a fixed Desktop executable path, never repository `PATH`. The first read uses deterministic footer parsing before fixed sequential Luna-medium batches; every hosted classifier capability is disabled and its event trace rejects tool activity. Its local helper remains read-only and mounted Codex tools perform every title write. ThreadBear does not open Codex SQLite, edit Desktop caches or task prose, archive tasks, retry title writes, or maintain a database, queue, controller, repair pass, or persistent management task.

The daily LaunchAgent does one job: check for a verified official update. Network and candidate-verification failures leave the old install untouched. A later managed-surface write can produce a truthful rerunnable partial, with the binary written last. Successful updates report whether Codex must restart. Updater health is separate from title-core `ready`; it never reads tasks or changes titles.

Expand Down
2 changes: 1 addition & 1 deletion assets/AGENTS.threadbear.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ text(JSON.stringify({ready:true, task_id:plan.task_id, title:renamed.title, upda

The local command only returns the calling task ID and fixed title policy. The mounted Codex app reads the exact current title and is the sole writer. It receives no explicit task ID when writing, so it can target only the calling task. Make at most one native write attempt. Never run the cell as a progress update. If the outer cell yields, wait only for that same cell; the yield does not cancel a slow native call. Never start another cell, poll the title, retry, or reconcile. A returned failure is local to this turn.

The status controls only the visible icon. ThreadBear emits five exact status prefixes and recognizes the obsolete neutral bear prefix only so it can remove it. It preserves every other safe subject and user-authored emoji, and leaves an ambiguous old ThreadBear prefix unchanged rather than guessing. It never puts an owner or action in the title. Use:
The status controls only the visible icon. ThreadBear emits five exact status prefixes. It also recognizes the five `✦` first-read prefixes and the obsolete neutral bear prefix only so the next ordinary turn can replace them with one exact current status. It preserves every other safe subject and user-authored emoji, and leaves an ambiguous old ThreadBear prefix unchanged rather than guessing. It never puts an owner or action in the title. Use:

- `complete` when the work is finished with no warranted follow-up.
- `next_steps` when the response establishes one concrete next action for the user, agent, or an external party.
Expand Down
Loading
Loading