diff --git a/CHANGELOG.md b/CHANGELOG.md index 83dc4e6..a7101fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +## v3.0.3 - 2026-08-21 + +### Fixed + +- Removed the multi-kilobyte model-generated title cell from every ordinary turn. ThreadBear now emits the same reviewed policy from its verified binary for a tiny in-app loader, while the mounted Codex app remains the sole title reader and writer. + ### 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. diff --git a/INSTALL.md b/INSTALL.md index db5457f..94e6394 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -256,7 +256,7 @@ After a successful install say: ~/.local/bin/threadbear update --json ``` -The managed guidance runs one injection-safe terminal JavaScript cell immediately before an ordinary final response. Replace only the status enum. The cell runs `title --status --json` exactly once; the stateless helper returns the calling task ID and fixed title policy without starting App Server or writing state. The mounted app then reads that exact task, derives one safe desired title, and—only when it differs—calls `tools.codex_app__set_thread_title({title:desired})` once with `threadId` omitted. Exact returned task ID/title is required. If the outer cell yields after 30 seconds, wait only for that same cell; the yield does not cancel a slow native call. Never retry, start another cell, poll the title, or reconcile. +The managed guidance runs one small terminal JavaScript loader immediately before an ordinary final response. Replace only the status enum. The loader runs `title-script --status ` exactly once. That stateless command binds the calling task ID and fixed policy into the reviewed JavaScript program embedded in the verified binary; Codex evaluates it inside the current in-app tool context instead of regenerating the program through the model. The program reads the exact current task through the mounted app and—only when the safe desired title differs—calls the mounted setter once with `threadId` omitted. Exact returned task ID/title is required. If the outer cell yields after 30 seconds, wait only for that same cell; the yield does not cancel a slow native call. Never cache source, retry, start another cell, poll the title, or reconcile. `update` verifies the official manifest, release URLs, architecture, checksum, embedded version, and candidate self-test before replacement. Network or verification failure leaves the old installation untouched. A later managed-surface write can truthfully leave a rerunnable partial; the binary is written last. Every successful update reports `restart_required`. The daily LaunchAgent runs only this command and never reads tasks or changes titles. @@ -317,7 +317,7 @@ Before release, run unit and integration tests, race tests, both Darwin builds, Release acceptance additionally requires one reviewed candidate live-tested end to end in Codex Desktop: -- the stateless terminal helper works under Codex's default workspace permissions and starts no App Server or title-state write; +- the stateless terminal loader works under Codex's default workspace permissions, remains under 250 source bytes, and starts no App Server or title-state write; - the mounted app-native reader supplies the exact current title, and the setter receives no explicit current-task ID and returns the exact task ID/title; - the rendered sidebar shows the expected title before and after a clean restart; - a full uninstall preview enumerates every unarchived task, confirmed preparation writes no title, and the consented serial app-native pass processes the initiating task last; diff --git a/README.md b/README.md index cd17c6e..eb5a8e6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ThreadBear -ThreadBear is a small local title decorator for Codex Desktop. Immediately before each ordinary final response, managed guidance runs one terminal cell: a stateless local helper returns the fixed icon policy, then the mounted Codex app reads and applies the title. ThreadBear changes only the leading status icon. +ThreadBear is a small local title decorator for Codex Desktop. Immediately before each ordinary final response, managed guidance runs one tiny terminal loader: the verified local binary emits its reviewed fixed icon program, Codex evaluates it in the current tool context, and the mounted app reads and applies the title. ThreadBear changes only the leading status icon. | Mark | Meaning | | --- | --- | @@ -32,7 +32,7 @@ threadbear version Every command accepts `--json`; the installed binary's `help` output is authoritative. -The terminal `title` command accepts exactly `complete`, `next_steps`, `needs_input`, `blocked`, or `automation`. It returns the calling task ID and fixed icon/safety policy without reading Codex or writing state. In the same cell, the mounted app reads the exact current title, safely derives the subject, and—only when needed—applies one title to the calling task. The exact returned task ID and title must match. A failure stays local and is never retried. +The public `title` command accepts exactly `complete`, `next_steps`, `needs_input`, `blocked`, or `automation` and returns the calling task ID plus fixed icon/safety policy for diagnostics. Ordinary managed guidance uses the internal `title-script` form instead: the verified binary binds that same typed plan into its embedded program, and the 219-byte loader evaluates the complete exit-zero output without caching it. The program reads the exact current title through the mounted app, safely derives the subject, and—only when needed—applies one title to the calling task with `threadId` omitted. The exact returned task ID and title must match. A failure stays local and is never retried. `uninstall --dry-run --json` asks once for permission to enumerate the complete unarchived App Server catalog and reports which exact ThreadBear prefixes will be removed before managed artifacts. After consent, `uninstall --prepare --noninteractive --confirm --json` takes one fresh complete snapshot. The mounted app rereads each prepared task and makes at most one exact prefix-removal write, with the initiating task last. Any drift or unconfirmed result stops before artifact removal; a fresh rerun makes a new plan. Successful cleanup is followed by `uninstall --commit --noninteractive --confirm --json`, with no final rescan or title call. A bare confirmed uninstall is refused. diff --git a/assets/AGENTS.threadbear.md b/assets/AGENTS.threadbear.md index d2db09d..99e56eb 100644 --- a/assets/AGENTS.threadbear.md +++ b/assets/AGENTS.threadbear.md @@ -7,79 +7,14 @@ For every ordinary interactive turn in a main Codex Desktop task: 3. Immediately before the final response, run this one terminal cell. Replace only `STATUS` with the exact enum: ```js -// @exec: {"yield_time_ms": 30000, "max_output_tokens": 1000} -const local = await tools.exec_command({ - cmd:"\"$HOME/.local/bin/threadbear\" title --status STATUS --json", - yield_time_ms:30000, - max_output_tokens:1000 +const source = await tools.exec_command({ + cmd:"\"$HOME/.local/bin/threadbear\" title-script --status STATUS" }); -if (local.exit_code !== 0) { text(local); exit(); } -let plan; -try { plan = JSON.parse(local.output); } catch { - text(JSON.stringify({ready:false, reason:"ThreadBear title helper returned malformed JSON"})); - exit(); -} -if (!plan || plan.ready !== true || typeof plan.task_id !== "string" || - typeof plan.icon !== "string" || !Array.isArray(plan.owned_prefixes) || - !Array.isArray(plan.blocked_prefixes) || !Array.isArray(plan.internal_markers) || - !Number.isInteger(plan.max_title_units)) { - text(JSON.stringify({ready:false, reason:"ThreadBear title helper returned an invalid policy"})); - exit(); -} -const decodeNative = value => { - if (typeof value !== "string") return value; - try { return JSON.parse(value); } catch { return null; } -}; -let current; -try { - current = decodeNative(await tools.codex_app__read_thread({threadId:plan.task_id, - includeOutputs:false,turnLimit:1,maxOutputCharsPerItem:1})); -} catch (error) { - text(JSON.stringify({ready:false, reason:"Codex title read failed", error:String(error)})); - exit(); -} -if (!current || current?.thread?.id !== plan.task_id || - typeof current.thread.title !== "string") { - text(JSON.stringify({ready:false, reason:"Codex title read was not confirmed exactly"})); - exit(); -} -const previous = current.thread.title; -if (plan.blocked_prefixes.some(prefix => previous.startsWith(prefix))) { - text(JSON.stringify({ready:false, reason:"The current title has an ambiguous old ThreadBear prefix"})); - exit(); -} -let subject = previous; -for (const prefix of plan.owned_prefixes) { - if (subject.startsWith(prefix)) { subject = subject.slice(prefix.length); break; } -} -const lower = subject.toLowerCase(); -if (subject.trim() === "" || /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(subject) || - plan.internal_markers.some(marker => lower.includes(marker)) || - (plan.icon + " " + subject).length > plan.max_title_units) { - text(JSON.stringify({ready:false, reason:"The current title is not safe to decorate"})); - exit(); -} -const desired = plan.icon + " " + subject; -if (desired === previous) { - text(JSON.stringify({ready:true, task_id:plan.task_id, title:previous, updated:false})); - exit(); -} -let renamed; -try { - renamed = decodeNative(await tools.codex_app__set_thread_title({title:desired})); -} catch (error) { - text(JSON.stringify({ready:false, reason:"Codex title write failed", error:String(error)})); - exit(); -} -if (!renamed || typeof renamed !== "object" || renamed.threadId !== plan.task_id || - renamed.title !== desired) { - text(JSON.stringify({ready:false, reason:"Codex title write was not confirmed exactly"})); - exit(); -} -text(JSON.stringify({ready:true, task_id:plan.task_id, title:renamed.title, updated:true})); +if (source.exit_code !== 0) { text(source); exit(); } +await (0,eval)(source.output)({tools,text,exit}); ``` -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 verified local binary emits its embedded title program with the calling task ID and fixed policy already bound. Codex evaluates that program inside the current in-app tool context; the mounted app reads the exact current title and is the sole writer. The writer receives no explicit task ID, 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. 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: diff --git a/assets/embed.go b/assets/embed.go index 8ab960d..dd912b6 100644 --- a/assets/embed.go +++ b/assets/embed.go @@ -10,3 +10,6 @@ var SkillManagedContent string //go:embed help.txt var HelpText string + +//go:embed ordinary-title.js +var OrdinaryTitleScript string diff --git a/assets/help.txt b/assets/help.txt index c37bd9a..bf8b75b 100644 --- a/assets/help.txt +++ b/assets/help.txt @@ -17,8 +17,8 @@ Every command accepts --json. Title: threadbear title --status --json -The managed guidance runs title exactly once immediately before a final response. -The helper is stateless. The mounted Codex app reads the exact current title and applies at most one icon change. A returned failure stays local and is never retried. +The managed guidance loads one embedded title program immediately before a final response. +The program is stateless. The mounted Codex app reads the exact current title and applies at most one icon change. A returned failure stays local and is never retried. Install flags: --dry-run Preview without changing the machine diff --git a/assets/ordinary-title.js b/assets/ordinary-title.js new file mode 100644 index 0000000..f9b9df9 --- /dev/null +++ b/assets/ordinary-title.js @@ -0,0 +1,58 @@ +(plan => async ({tools, text, exit}) => { + const decodeNative = value => { + if (typeof value !== "string") return value; + try { return JSON.parse(value); } catch { return null; } + }; + let current; + try { + current = decodeNative(await tools.codex_app__read_thread({ + threadId: plan.task_id, + includeOutputs: false, + turnLimit: 1, + maxOutputCharsPerItem: 1, + })); + } catch (error) { + text(JSON.stringify({ready: false, reason: "Codex title read failed", error: String(error)})); + exit(); + } + if (!current || current.thread?.id !== plan.task_id || typeof current.thread.title !== "string") { + text(JSON.stringify({ready: false, reason: "Codex title read was not confirmed exactly"})); + exit(); + } + const previous = current.thread.title; + if (plan.blocked_prefixes.some(prefix => previous.startsWith(prefix))) { + text(JSON.stringify({ready: false, reason: "The current title has an ambiguous old ThreadBear prefix"})); + exit(); + } + let subject = previous; + for (const prefix of plan.owned_prefixes) { + if (subject.startsWith(prefix)) { + subject = subject.slice(prefix.length); + break; + } + } + const lower = subject.toLowerCase(); + if (subject.trim() === "" || /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(subject) || + plan.internal_markers.some(marker => lower.includes(marker)) || + (plan.icon + " " + subject).length > plan.max_title_units) { + text(JSON.stringify({ready: false, reason: "The current title is not safe to decorate"})); + exit(); + } + const desired = plan.icon + " " + subject; + if (desired === previous) { + text(JSON.stringify({ready: true, task_id: plan.task_id, title: previous, updated: false})); + exit(); + } + let renamed; + try { + renamed = decodeNative(await tools.codex_app__set_thread_title({title: desired})); + } catch (error) { + text(JSON.stringify({ready: false, reason: "Codex title write failed", error: String(error)})); + exit(); + } + if (!renamed || typeof renamed !== "object" || renamed.threadId !== plan.task_id || renamed.title !== desired) { + text(JSON.stringify({ready: false, reason: "Codex title write was not confirmed exactly"})); + exit(); + } + text(JSON.stringify({ready: true, task_id: plan.task_id, title: renamed.title, updated: true})); +}) diff --git a/cmd/threadbear/install_test.go b/cmd/threadbear/install_test.go index 10f2ec1..90cd597 100644 --- a/cmd/threadbear/install_test.go +++ b/cmd/threadbear/install_test.go @@ -148,17 +148,19 @@ func TestLifecycleNeverTouchesCodexHooks(t *testing.T) { t.Fatalf("managed AGENTS content = %q", agents) } skill, _ := os.ReadFile(p.skill) - for label, text := range map[string]string{"AGENTS": string(agents), "skill": string(skill)} { - if strings.Count(text, "tools.codex_app__set_thread_title") != 1 { - t.Fatalf("%s must contain exactly one mounted app-native setter: %q", label, text) - } + if strings.Count(string(agents), "title-script --status STATUS") != 1 || + strings.Contains(string(agents), "tools.codex_app__set_thread_title") || + strings.Count(assets.OrdinaryTitleScript, "tools.codex_app__set_thread_title") != 1 { + t.Fatalf("installed AGENTS/program must define one loader and one mounted setter: AGENTS=%q program=%q", agents, assets.OrdinaryTitleScript) + } + for label, text := range map[string]string{"AGENTS": string(agents), "program": assets.OrdinaryTitleScript, "skill": string(skill)} { for _, obsolete := range []string{"plan.updated", "plan.unconfirmed", "thread/name/set"} { if strings.Contains(text, obsolete) { t.Fatalf("%s contains obsolete detached-writer contract %q", label, obsolete) } } } - if !strings.Contains(string(agents), "plan.owned_prefixes") || !strings.Contains(string(skill), `item.outcome === "prepared"`) { + if !strings.Contains(assets.OrdinaryTitleScript, "plan.owned_prefixes") || !strings.Contains(string(skill), `item.outcome === "prepared"`) { t.Fatalf("installed guidance lacks planner/prepared contract: AGENTS=%q skill=%q", agents, skill) } for _, required := range []string{ @@ -168,8 +170,8 @@ func TestLifecycleNeverTouchesCodexHooks(t *testing.T) { "decodeNative(await tools.codex_app__read_thread", "decodeNative(await tools.codex_app__set_thread_title", } { - if !strings.Contains(string(agents), required) { - t.Fatalf("installed AGENTS lacks JSON-string native result decoding %q: %q", required, agents) + if !strings.Contains(assets.OrdinaryTitleScript, required) { + t.Fatalf("embedded title program lacks JSON-string native result decoding %q: %q", required, assets.OrdinaryTitleScript) } } for _, required := range []string{ diff --git a/cmd/threadbear/main.go b/cmd/threadbear/main.go index 7ff40a6..282d302 100644 --- a/cmd/threadbear/main.go +++ b/cmd/threadbear/main.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "os" + "strings" "github.com/ericlitman/threadbear/assets" ) @@ -36,6 +37,28 @@ func run(ctx context.Context, args []string, _ io.Reader, stdout, stderr io.Writ } return 0 } + if args[0] == "title-script" { + flags := flag.NewFlagSet("title-script", flag.ContinueOnError) + flags.SetOutput(stderr) + selectedStatus := flags.String("status", "", "emit the mounted title program for this status") + if flags.Parse(args[1:]) != nil || flags.NArg() != 0 { + return 2 + } + plan, err := runCurrentTitle(ctx, os.Getenv("CODEX_THREAD_ID"), *selectedStatus) + if err != nil { + fmt.Fprintln(stderr, err) + return 1 + } + encoded, err := json.Marshal(plan) + if err != nil { + fmt.Fprintln(stderr, err) + return 1 + } + if _, err := fmt.Fprintf(stdout, "(%s)(%s)\n", strings.TrimSpace(assets.OrdinaryTitleScript), encoded); err != nil { + return 1 + } + return 0 + } command := args[0] flags := flag.NewFlagSet(command, flag.ContinueOnError) flags.SetOutput(stderr) diff --git a/cmd/threadbear/main_test.go b/cmd/threadbear/main_test.go index 540671e..999cc90 100644 --- a/cmd/threadbear/main_test.go +++ b/cmd/threadbear/main_test.go @@ -28,6 +28,50 @@ func TestRunRejectsInvalidTitleStatusBeforeMutation(t *testing.T) { } } +func TestRunTitleScriptEmitsOneEmbeddedMountedProgram(t *testing.T) { + t.Setenv("CODEX_THREAD_ID", testTaskID) + var stdout, stderr bytes.Buffer + code := run(t.Context(), []string{"title-script", "--status", "complete"}, strings.NewReader(""), &stdout, &stderr) + if code != 0 || stderr.Len() != 0 { + t.Fatalf("title-script = code %d, stdout %q, stderr %q", code, stdout.String(), stderr.String()) + } + value := stdout.String() + for _, required := range []string{testTaskID, `"status":"complete"`, `"icon":"✅"`, + "codex_app__read_thread", "codex_app__set_thread_title"} { + if !strings.Contains(value, required) { + t.Fatalf("title-script missing %q: %s", required, value) + } + } + for _, forbidden := range []string{"exec_command", "thread/name/set", "state_N.sqlite"} { + if strings.Contains(value, forbidden) { + t.Fatalf("title-script contains %q: %s", forbidden, value) + } + } + stdout.Reset() + stderr.Reset() + if code = run(t.Context(), []string{"title-script", "--status", "waiting"}, strings.NewReader(""), &stdout, &stderr); code != 1 || + !strings.Contains(stderr.String(), `unsupported ThreadBear status "waiting"`) || stdout.Len() != 0 { + t.Fatalf("invalid title-script = code %d, stdout %q, stderr %q", code, stdout.String(), stderr.String()) + } + for name, args := range map[string][]string{ + "missing status": {"title-script"}, + "extra argument": {"title-script", "--status", "complete", "extra"}, + } { + stdout.Reset() + stderr.Reset() + if code = run(t.Context(), args, strings.NewReader(""), &stdout, &stderr); code == 0 || stdout.Len() != 0 { + t.Fatalf("%s = code %d, stdout %q, stderr %q", name, code, stdout.String(), stderr.String()) + } + } + t.Setenv("CODEX_THREAD_ID", "") + stdout.Reset() + stderr.Reset() + if code = run(t.Context(), []string{"title-script", "--status", "complete"}, strings.NewReader(""), &stdout, &stderr); code != 1 || + !strings.Contains(stderr.String(), "CODEX_THREAD_ID is unavailable or invalid") || stdout.Len() != 0 { + t.Fatalf("missing task ID = code %d, stdout %q, stderr %q", code, stdout.String(), stderr.String()) + } +} + func TestRunHasNoOnboardCommand(t *testing.T) { var stdout, stderr bytes.Buffer code := run(context.Background(), []string{"onboard", "--json"}, strings.NewReader(""), &stdout, &stderr) diff --git a/cmd/threadbear/managed_javascript_test.go b/cmd/threadbear/managed_javascript_test.go index 628e0e8..9862d07 100644 --- a/cmd/threadbear/managed_javascript_test.go +++ b/cmd/threadbear/managed_javascript_test.go @@ -296,26 +296,42 @@ func TestEmbeddedOrdinaryJavaScriptAcceptsStringAndObjectNativeResults(t *testin if err != nil { t.Fatal(err) } + t.Setenv("CODEX_THREAD_ID", testTaskID) + var program, programErr strings.Builder + if code := run(t.Context(), []string{"title-script", "--status", "complete"}, strings.NewReader(""), &program, &programErr); code != 0 { + t.Fatalf("title-script code = %d, stdout %q, stderr %q", code, program.String(), programErr.String()) + } + if strings.Contains(program.String(), "exec_command") || + strings.Count(program.String(), "codex_app__read_thread") != 1 || + strings.Count(program.String(), "codex_app__set_thread_title") != 1 { + t.Fatalf("title program did not keep one mounted read/write boundary: %s", program.String()) + } + programJSON, err := json.Marshal(program.String()) + if err != nil { + t.Fatal(err) + } + taskIDJSON, err := json.Marshal(testTaskID) + if err != nil { + t.Fatal(err) + } harness := fmt.Sprintf(` const source = %s; -const policy = {ready:true,task_id:"current",status:"complete",icon:"✅", - owned_prefixes:["✅✦ ","➡️✦ ","🙋✦ ","🚨✦ ","🤖✦ ","✅ ","➡️ ","🙋 ","🚨 ","🤖 ","🐻 "], - blocked_prefixes:["➡ ","⏳ ","❔ ","🧵🐻"], - internal_markers:[""],max_title_units:60}; +const program = %s; +const taskID = %s; const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; -async function run(currentResult,setResult) { +async function run(currentResult,setResult,helperResult={exit_code:0,output:program}) { const trace = [], outputs = []; const tools = { exec_command: async args => { trace.push("exec"); - if (args.cmd !== "\"$HOME/.local/bin/threadbear\" title --status STATUS --json" || - args.yield_time_ms !== 30000 || args.max_output_tokens !== 1000) throw new Error("bad helper args"); - return {exit_code:0,output:JSON.stringify(policy)}; + if (Object.keys(args).join(",") !== "cmd" || + args.cmd !== "\"$HOME/.local/bin/threadbear\" title-script --status STATUS") throw new Error("bad helper args"); + return helperResult; }, codex_app__read_thread: async args => { trace.push("read"); - if (args.threadId !== policy.task_id || args.includeOutputs !== false || + if (args.threadId !== taskID || args.includeOutputs !== false || args.turnLimit !== 1 || args.maxOutputCharsPerItem !== 1) throw new Error("bad read args"); return currentResult; }, @@ -328,26 +344,29 @@ async function run(currentResult,setResult) { const text = value => outputs.push(typeof value === "string" ? value : JSON.stringify(value)); class Exit extends Error {} const exit = () => { throw new Exit(); }; + let error = ""; try { await new AsyncFunction("tools","text","exit",source)(tools,text,exit); } - catch (error) { if (!(error instanceof Exit)) throw error; } - return {trace,outputs}; + catch (caught) { if (!(caught instanceof Exit)) error = String(caught); } + return {trace,outputs,error}; } -const current = {thread:{id:"current",title:"🎉 exact subject"}}; -const expected = {threadId:"current",title:"✅ 🎉 exact subject"}; +const current = {thread:{id:taskID,title:"🎉 exact subject"}}; +const expected = {threadId:taskID,title:"✅ 🎉 exact subject"}; const stringRun = await run(JSON.stringify(current),JSON.stringify(expected)); const objectRun = await run(current,expected); -const sparkleRun = await run(JSON.stringify({thread:{id:"current",title:"✅✦ exact subject"}}), - JSON.stringify({threadId:"current",title:"✅ exact subject"})); +const sparkleRun = await run(JSON.stringify({thread:{id:taskID,title:"✅✦ exact subject"}}), + JSON.stringify({threadId:taskID,title:"✅ exact subject"})); const malformedRun = await run(JSON.stringify(current),"{malformed"); const wrongIDRun = await run(JSON.stringify(current),JSON.stringify({...expected,threadId:"wrong"})); const wrongTitleRun = await run(JSON.stringify(current),JSON.stringify({...expected,title:"wrong"})); -const noWriteRun = await run(JSON.stringify({thread:{id:"current",title:"✅ exact subject"}}),null); +const noWriteRun = await run(JSON.stringify({thread:{id:taskID,title:"✅ exact subject"}}),null); const badReadRun = await run("{malformed",null); const wrongReadIDRun = await run(JSON.stringify({thread:{id:"other",title:"exact subject"}}),null); -const blockedRun = await run(JSON.stringify({thread:{id:"current",title:"🧵🐻 needs input (you): approve"}}),null); +const blockedRun = await run(JSON.stringify({thread:{id:taskID,title:"🧵🐻 needs input (you): approve"}}),null); +const helperFailureRun = await run(null,null,{exit_code:1,output:"",error:"failed"}); +const malformedProgramRun = await run(null,null,{exit_code:0,output:"not valid JavaScript }"}); process.stdout.write(JSON.stringify({stringRun,objectRun,sparkleRun,malformedRun,wrongIDRun, - wrongTitleRun,noWriteRun,badReadRun,wrongReadIDRun,blockedRun})); -`, sourceJSON) + wrongTitleRun,noWriteRun,badReadRun,wrongReadIDRun,blockedRun,helperFailureRun,malformedProgramRun})); +`, sourceJSON, programJSON, taskIDJSON) output, err := exec.Command("node", "--input-type=module", "--eval", harness).CombinedOutput() if err != nil { @@ -355,16 +374,18 @@ process.stdout.write(JSON.stringify({stringRun,objectRun,sparkleRun,malformedRun } var got struct { - StringRun javascriptRun `json:"stringRun"` - ObjectRun javascriptRun `json:"objectRun"` - SparkleRun javascriptRun `json:"sparkleRun"` - MalformedRun javascriptRun `json:"malformedRun"` - WrongIDRun javascriptRun `json:"wrongIDRun"` - WrongTitleRun javascriptRun `json:"wrongTitleRun"` - NoWriteRun javascriptRun `json:"noWriteRun"` - BadReadRun javascriptRun `json:"badReadRun"` - WrongReadIDRun javascriptRun `json:"wrongReadIDRun"` - BlockedRun javascriptRun `json:"blockedRun"` + StringRun javascriptRun `json:"stringRun"` + ObjectRun javascriptRun `json:"objectRun"` + SparkleRun javascriptRun `json:"sparkleRun"` + MalformedRun javascriptRun `json:"malformedRun"` + WrongIDRun javascriptRun `json:"wrongIDRun"` + WrongTitleRun javascriptRun `json:"wrongTitleRun"` + NoWriteRun javascriptRun `json:"noWriteRun"` + BadReadRun javascriptRun `json:"badReadRun"` + WrongReadIDRun javascriptRun `json:"wrongReadIDRun"` + BlockedRun javascriptRun `json:"blockedRun"` + HelperFailureRun javascriptRun `json:"helperFailureRun"` + MalformedProgramRun javascriptRun `json:"malformedProgramRun"` } if err := json.Unmarshal(output, &got); err != nil { t.Fatalf("decode ordinary JavaScript harness output: %v\n%s", err, output) @@ -385,7 +406,7 @@ process.stdout.write(JSON.stringify({stringRun,objectRun,sparkleRun,malformedRun if err := json.Unmarshal([]byte(run.Outputs[0]), &receipt); err != nil { t.Fatalf("decode %s native result receipt: %v", name, err) } - if !receipt.Ready || receipt.TaskID != "current" || + if !receipt.Ready || receipt.TaskID != testTaskID || receipt.Title != "✅ 🎉 exact subject" || !receipt.Updated { t.Fatalf("unexpected %s native result receipt: %+v", name, receipt) } @@ -426,11 +447,20 @@ process.stdout.write(JSON.stringify({stringRun,objectRun,sparkleRun,malformedRun t.Fatalf("%s must stop before setter: %+v", name, run) } } + if !reflect.DeepEqual(got.HelperFailureRun.Trace, []string{"exec"}) || + len(got.HelperFailureRun.Outputs) != 1 || got.HelperFailureRun.Error != "" { + t.Fatalf("failed title-script command must stop in the loader: %+v", got.HelperFailureRun) + } + if !reflect.DeepEqual(got.MalformedProgramRun.Trace, []string{"exec"}) || + len(got.MalformedProgramRun.Outputs) != 0 || !strings.Contains(got.MalformedProgramRun.Error, "SyntaxError") { + t.Fatalf("malformed title program must fail before mounted reads or writes: %+v", got.MalformedProgramRun) + } } type javascriptRun struct { Trace []string `json:"trace"` Outputs []string `json:"outputs"` + Error string `json:"error"` } func extractJavaScriptCell(t *testing.T, markdown string) string { diff --git a/cmd/threadbear/site_contract_test.go b/cmd/threadbear/site_contract_test.go index 4c86872..431df2d 100644 --- a/cmd/threadbear/site_contract_test.go +++ b/cmd/threadbear/site_contract_test.go @@ -41,7 +41,9 @@ func TestPublishedInstallGuideMatchesCurrentProduct(t *testing.T) { t.Fatal("INSTALL.md and site/install must be byte-identical") } requireText(t, guide, - "the stateless helper returns the calling task ID and fixed title policy", + "The loader runs `title-script --status ` exactly once.", + "reviewed JavaScript program embedded in the verified binary", + "Codex evaluates it inside the current in-app tool context", "The check prints every fixed Codex Desktop command it finds", "For every lifecycle action, write the lasting summary after all tool calls.", "Nothing changes in this step.", @@ -76,8 +78,7 @@ func TestPublishedInstallGuideMatchesCurrentProduct(t *testing.T) { "with the initiating task last", "Any missing, drifted, malformed, wrong-target, wrong-title, or thrown result blocks teardown", "There is no final catalog scan, marker, queue, controller, or resume state.", - "tools.codex_app__set_thread_title({title:desired})", - "one injection-safe terminal JavaScript cell", + "one small terminal JavaScript loader", "wait only for that same cell", "yield does not cancel a slow native call", "Exact returned task ID/title is required.", @@ -130,24 +131,17 @@ func TestReleaseDocsKeepTheEstablishedImmediateRepaintGate(t *testing.T) { rejectText(t, checklist, "if Codex keeps it cached, reopen the project once") } -func TestInstalledGuidanceDefinesOneTerminalPlannerAndNativeWrite(t *testing.T) { +func TestInstalledGuidanceDefinesOneTerminalLoaderAndNativeProgram(t *testing.T) { guidance := readRepoFile(t, "assets", "AGENTS.threadbear.md") + program := readRepoFile(t, "assets", "ordinary-title.js") requireText(t, guidance, "Write the substantive response first.", - `\"$HOME/.local/bin/threadbear\" title --status STATUS --json`, - `// @exec: {"yield_time_ms": 30000, "max_output_tokens": 1000}`, + `\"$HOME/.local/bin/threadbear\" title-script --status STATUS`, "Replace only `STATUS` with the exact enum", - "if (local.exit_code !== 0) { text(local); exit(); }", - "plan = JSON.parse(local.output)", - `typeof plan.icon !== "string"`, - "tools.codex_app__read_thread({threadId:plan.task_id", - "tools.codex_app__set_thread_title({title:desired})", - "const decodeNative = value =>", - `typeof value !== "string"`, - "renamed = decodeNative(await tools.codex_app__set_thread_title", - "renamed.threadId !== plan.task_id", - "renamed.title !== desired", - "mounted Codex app reads the exact current title and is the sole writer", + "if (source.exit_code !== 0) { text(source); exit(); }", + "await (0,eval)(source.output)({tools,text,exit});", + "verified local binary emits its embedded title program", + "mounted app reads the exact current title and is the sole writer", "If the outer cell yields, wait only for that same cell", "yield does not cancel a slow native call", "Never start another cell, poll the title, retry, or reconcile.", @@ -155,16 +149,29 @@ func TestInstalledGuidanceDefinesOneTerminalPlannerAndNativeWrite(t *testing.T) "The status controls only the visible icon.", "It also recognizes the five `✦` first-read prefixes and the obsolete neutral bear prefix", ) + requireText(t, program, + "tools.codex_app__read_thread({", + "tools.codex_app__set_thread_title({title: desired})", + "const decodeNative = value =>", + "renamed.threadId !== plan.task_id", + "renamed.title !== desired", + ) if count := strings.Count(guidance, "```js"); count != 1 { t.Fatalf("managed guidance contains %d JavaScript cells; want one", count) } - if count := strings.Count(guidance, "title --status STATUS --json"); count != 1 { - t.Fatalf("managed guidance contains %d terminal planners; want one", count) + if count := strings.Count(guidance, "title-script --status STATUS"); count != 1 { + t.Fatalf("managed guidance contains %d terminal loaders; want one", count) + } + if len([]byte(extractJavaScriptCell(t, guidance))) > 250 { + t.Fatalf("managed title loader is too large: %d bytes", len([]byte(extractJavaScriptCell(t, guidance)))) } - if count := strings.Count(guidance, "tools.codex_app__set_thread_title("); count != 1 { - t.Fatalf("managed guidance contains %d native title calls; want one", count) + if count := strings.Count(program, "tools.codex_app__set_thread_title("); count != 1 { + t.Fatalf("embedded title program contains %d native title calls; want one", count) } rejectText(t, guidance, + "title --status STATUS --json", + "tools.codex_app__read_thread", + "tools.codex_app__set_thread_title", "thread/name/set", "Promise.race", "setTimeout", diff --git a/docs/architecture.md b/docs/architecture.md index 311add4..e7704f9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -5,11 +5,11 @@ ThreadBear is one Go executable, one managed instruction block, one installed sk ## Ordinary turn 1. The task writes its substantive response. Any owner or next action stays in that prose. -2. Immediately before the final response, managed guidance runs one terminal JavaScript cell containing `threadbear title --status --json`. -3. That stateless command validates the status and current task ID, then returns the fixed icon and safety policy. It starts no App Server and writes no state. -4. The mounted Codex app reads the exact calling task. The cell rejects a wrong ID, blank or unsafe title, raw internal text, an ambiguous old ThreadBear prefix, or a title that cannot fit intact. -5. The cell strips at most one of ThreadBear's five current status prefixes, five inferred `✦` prefixes, or the obsolete neutral bear prefix, preserves every other subject byte, and renders the selected plain status icon. If the title already matches, it stops. Otherwise it calls mounted `set_thread_title` once with no explicit task ID. -6. Success requires the exact returned task ID and title. A throw, malformed response, or mismatch stays local. The task never starts another cell, polls the title, retries, or reconciles. +2. Immediately before the final response, managed guidance runs one small terminal JavaScript loader containing `threadbear title-script --status `. +3. That stateless command validates the status and current task ID, binds the fixed icon and safety policy into the reviewed JavaScript program embedded in the verified binary, and prints only that program. It starts no App Server and writes no state. +4. Codex evaluates the emitted program inside the current in-app tool context. The mounted app reads the exact calling task; the program rejects a wrong ID, blank or unsafe title, raw internal text, an ambiguous old ThreadBear prefix, or a title that cannot fit intact. +5. The program strips at most one of ThreadBear's five current status prefixes, five inferred `✦` prefixes, or the obsolete neutral bear prefix, preserves every other subject byte, and renders the selected plain status icon. If the title already matches, it stops. Otherwise it calls mounted `set_thread_title` once with no explicit task ID. +6. Success requires the exact returned task ID and title. A command failure, throw, malformed response, or mismatch stays local. The task never starts another cell, caches program source, polls the title, retries, or reconciles. The enum controls only the icon and can emit exactly `✅ `, `➡️ `, `🙋 `, `🚨 `, or `🤖 `. ThreadBear also recognizes the five `✦` first-read prefixes and neutral `🐻 ` as removable decorations but never emits either during an ordinary turn. A title beginning with one of those exact prefixes is deliberately reserved. The obsolete `➡ `, `⏳ `, `❔ `, and `🧵🐻` forms are ambiguous after a clean v2 reset, so ThreadBear leaves the complete title unchanged rather than guessing whether its leading emoji is user-authored. Every other leading emoji remains user text. @@ -29,7 +29,7 @@ Mounted Codex tools are the only ordinary title reader and the sole title writer The official `codex app-server --stdio` process is used for the installation first read and complete-catalog uninstall cleanup. ThreadBear launches it from a fixed Codex Desktop path, never ambient repository `PATH`, initializes one bounded client, follows every unarchived `thread/list` page, tolerates notifications, deduplicates IDs, and closes it. Onboarding additionally reads only `thread/turns/list` with a one-turn descending limit. Native `name` is the user-facing title. Null or blank names stay raw; `preview` is never adopted. -Ordinary turns therefore work under Codex's default workspace permissions. Uninstall cleanup asks for one explicit command permission because App Server maintains Codex's own local state outside the workspace. ThreadBear never opens Codex SQLite, edits Desktop caches, runs an App Server daemon, keeps a shared client, retries a model call, or falls back to another title source. +Ordinary turns therefore work under Codex's default workspace permissions. The local Go process cannot access mounted app tools; emitting the embedded program keeps the actual title read and write inside Codex without making the model reproduce that program on every turn. Uninstall cleanup asks for one explicit command permission because App Server maintains Codex's own local state outside the workspace. ThreadBear never opens Codex SQLite, edits Desktop caches, runs an App Server daemon, keeps a shared client, caches executable policy, retries a model call, or falls back to another title source. ## Uninstall title cleanup diff --git a/docs/compatibility.md b/docs/compatibility.md index 46c4e68..752959a 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -4,7 +4,7 @@ ThreadBear supports macOS 12 or newer on Apple silicon and Intel, Codex Desktop ThreadBear resolves Codex only from fixed Desktop locations: the system or user Applications bundle and the Desktop-managed `~/.local/bin/codex`. It never executes `codex` from ambient repository `PATH`. -For an ordinary turn, the local `title` command returns only the validated task ID and fixed title policy. The mounted app reads the exact current task and, if needed, writes once with no explicit target ID. Raw JSON-text results are decoded once; already-decoded objects are also accepted. A wrong ID, unsafe title, throw, undecodable response, or non-exact setter result stays local with no alternate reader, writer, or retry. +For an ordinary turn, the local `title-script` command binds the validated task ID and fixed title policy into the reviewed program embedded in the verified binary. One 219-byte loader evaluates the complete exit-zero output uncached inside the current Codex tool context. The program uses the mounted app to read the exact current task and, if needed, write once with no explicit target ID. Raw JSON-text results are decoded once; already-decoded objects are also accepted. A command failure, malformed program, wrong ID, unsafe title, throw, undecodable response, or non-exact setter result stays local with no alternate reader, writer, or retry. The public `title` command remains a data-only diagnostic for the same typed plan. Ordinary title handling starts no App Server and writes no ThreadBear state, so it works under Codex's default workspace permissions. ThreadBear emits five exact status prefixes and recognizes the five inferred `✦` prefixes plus neutral `🐻 ` as removable decorations. Other safe leading emoji and subject bytes are preserved, while ambiguous old ThreadBear prefixes are deliberately left unchanged rather than guessed. Visible titles are at most 60 UTF-16 units and are never truncated. @@ -12,6 +12,6 @@ After a successful install, Codex requests complete-catalog read permission once Uninstall cleanup explicitly asks for command permission, then follows the complete unarchived App Server `thread/list` catalog, tolerates notifications, and deduplicates IDs. Null or blank `name` stays raw regardless of `preview`. A later-page failure returns no partial plan. Confirmed preparation stores no titles and writes no titles. The installed skill serially rereads each prepared target through the mounted app immediately before its one possible explicit-target setter call; drift, wrong IDs, or non-exact setter results block artifact teardown and receive no retry. -ThreadBear never opens Codex SQLite or edits Desktop storage. It runs no App Server daemon or proxy, keeps no task-title database or App Server cache, and has no controller, queue, reconciliation, or alternate title path. Model use is limited to ambiguous rows in the one ephemeral post-install first read; ordinary turns, updates, status, and uninstall use no model. +ThreadBear never opens Codex SQLite or edits Desktop storage. It runs no App Server daemon or proxy, keeps no task-title database, executable-source cache, or App Server cache, and has no controller, queue, reconciliation, or alternate title path. Model classification is limited to ambiguous rows in the one ephemeral post-install first read; the ordinary title policy is fixed binary-embedded code rather than model-generated policy. The supported public commands are `install`, `title`, `status`, `self-test`, `update`, `uninstall`, and `version`. The daily update-only LaunchAgent needs no `sudo`, Full Disk Access, model call, or persistent Codex task. Release binaries are checksum-verified but are not Developer ID signed or notarized. diff --git a/docs/experiments/registry.json b/docs/experiments/registry.json index 7950181..a136637 100644 --- a/docs/experiments/registry.json +++ b/docs/experiments/registry.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "updated_at": "2026-08-08", + "updated_at": "2026-08-21", "canonical_for": "ThreadBear title-path architecture and live experiments", "capabilities": [ { @@ -79,6 +79,17 @@ "decision": "TB-EXP-0116-003 established the mounted-writer path, TB-EXP-0116-004 repeated it against corrected candidate b5cc0cbe, and TB-EXP-0116-006 closes the final BEAR-117 artifact gate. Exact candidate a1ecec67 repainted the current and controlled historical rows immediately through the mounted writer, preserved both exact titles across a clean Desktop restart, and kept the friendly preview and committed-removal recap visible after reopen and restart. TB-EXP-0119-001 and TB-EXP-0119-002 validate the cleanup and corrected commit-admission seams only; neither adds rendered repaint or restart-persistence evidence.", "next_preflight": null }, + { + "id": "TB-CAP-BINARY-EMBEDDED-LOADER", + "premise": "A deterministic title program emitted by the verified ThreadBear binary can execute inside functions.exec, retain access to the mounted Codex title tools, fail closed before mutation on command or program failure, and visibly repaint Desktop without model regeneration of the policy.", + "status": "established", + "supported_by": [ + "TB-EXP-0107-003" + ], + "contradicted_by": [], + "decision": "TB-EXP-0107-003 closes the loader capability boundary for exact v3.0.3 candidate binary SHA-256 21710ced. The 219-byte uncached loader evaluated the complete exit-zero embedded program inside Codex, the program used the mounted reader and sole mounted writer, exact acknowledgement matched the calling task and title, and the active header and sidebar repainted immediately. Command and malformed-program fixtures stopped before mounted reads or writes. The issued live operation completed in 0.3 to 1.0 seconds; broader model-start timing remains model-scheduling evidence rather than issued-operation latency.", + "next_preflight": null + }, { "id": "TB-CAP-UPDATER-TEARDOWN-SERIALIZATION", "premise": "Confirmed uninstall waits for an in-flight verified updater before removing its lock or any managed surface, so that updater cannot later replace a fresh installation with its stale candidate.", @@ -1337,9 +1348,73 @@ "applicability": "Closes TB-PRE-0119-002 and validates the corrected BEAR-119 commit-admission seam for exact candidate 5f05b9f on Desktop 26.803.41515. It does not add rendered repaint or restart-persistence evidence to TB-CAP-MOUNTED-NATIVE-REPAINT.", "supersedes": [], "conflicts": [] + }, + { + "id": "TB-EXP-0107-003", + "date": "2026-08-21", + "issue": "BEAR-107", + "preflight_id": "TB-PRE-0107-003", + "question": "Can an exact v3.0.3 candidate replace the model-generated ordinary-turn policy with one binary-emitted program, evaluate it inside functions.exec, retain the mounted-only writer and visible repaint, and keep issued operations below five seconds?", + "invariant": "The mounted Codex reader and setter remain the only ordinary-turn title boundary; command or program failure stops before mutation; exact ID and title acknowledgement is mandatory; no source cache, alternate writer, retry, queue, controller, daemon, App Server, or SQLite path is allowed.", + "environment": { + "threadbear_version": "3.0.3 exact BEAR-107 local candidate; arm64 CGO=0 binary SHA-256 21710ced36b64b61ab20ca772fd1bcdcb8e21f22922f7216311d44ac2f69ef1f", + "git_sha": "32778b9dc2d0e6e710495816164459d3303d7df2", + "codex_version": "Desktop current on 2026-08-21; embedded CLI 0.149.0-alpha.4", + "codex_source": "vscode", + "host": "local macOS arm64", + "task_state": "one active mounted ThreadBear task with a controlled safe subject; exact status-prefix removal followed by one next_steps loader write and repeated no-write samples", + "restart_state": "clean Codex restart after exact candidate installation, visible canary, second exact install, and final clean restart", + "hook_fingerprint": "no ThreadBear title hook; ordinary title work used one functions.exec loader plus mounted app tools", + "guidance_fingerprint": "managed guidance SHA-256 336af25ac6405cb1aa1d4e70a2dd095f8e8573f7ccfc3e1f3841ddd66e6a2ff4; embedded program SHA-256 77c095dcbac18cf113d0671ae6306116b93828e303a2621099301e24317b4102" + }, + "invocation": { + "outer_tool": "one 219-byte functions.exec loader calling the exact installed ThreadBear binary, followed by indirect eval in the same Codex V8", + "code": "run title-script --status next_steps once; require exit zero; evaluate complete stdout with tools, text, and exit; embedded program performs one exact mounted read and at most one mounted setter with omitted threadId", + "native_tool_identity": "codex_app__read_thread and codex_app__set_thread_title inside the evaluated embedded program", + "target_identity_mode": "calling task ID bound by the verified binary; exact read uses that ID; setter omits threadId so it can target only the caller" + }, + "evidence": [ + "linear:BEAR-107#comment-9ec2542f-13c7-4a03-bbd7-5f0709b6cd59", + "github-pr:129", + "git:32778b9dc2d0e6e710495816164459d3303d7df2", + "codex-rollout:01a025c4-5da5-7ab1-b952-4ac77b0cfecb" + ], + "result": { + "status": "observed", + "summary": "The exact candidate passed isolated release smoke twice and installed byte-identically. After restart, a mounted preparation write visibly removed the status prefix, then the exact loader returned the exact calling task ID and next_steps title and both the active header and sidebar repainted. The final restarted no-write loader returned the exact unchanged title. Focused fixtures proved nonzero command and malformed emitted program failures stop before any mounted read or write.", + "timing_ms": "300 to 1000 for issued loader operations; final restarted no-write executor 400; mounted read samples 53 to 85; changed mounted setter 682; model-start samples were recorded separately and included pre-tool deliberation outliers", + "hook_participation": "none; no ThreadBear title hook, App Server, or detached writer participated", + "rendered_proof": "The active header and project sidebar both rendered the bare controlled subject after preparation and the exact next_steps title immediately after the loader. Privacy-safe retained capture SHA-256 3793f56a86ca2bb00d29bc96e68664f6fdb4297c60694cb8f7282300ab2fa965; the exact title remained visible after the final clean restart." + }, + "confidence": "high", + "applicability": "Establishes the binary-emitted uncached loader seam for exact candidate 21710ced on the recorded Desktop and CLI build. It does not authorize arbitrary binary stdout, cached executable source, detached writers, or a claim that general model deliberation is bounded by ThreadBear.", + "supersedes": [], + "conflicts": [] } ], "preflights": [ + { + "id": "TB-PRE-0107-003", + "issue": "BEAR-107", + "capability_id": "TB-CAP-BINARY-EMBEDDED-LOADER", + "status": "closed", + "consulted": [ + "TB-EXP-0107-001", + "TB-EXP-0107-002", + "TB-EXP-0116-003", + "TB-EXP-0116-004", + "TB-EXP-0116-006" + ], + "remaining_unknown": "Whether code evaluated inside the current functions.exec V8 can load a deterministic program from the verified local binary, retain the mounted Codex tool object, and preserve exact visible title behavior while removing model regeneration of the multi-kilobyte policy.", + "single_changed_variable": "Policy source delivery: replace the inline model-generated title policy with the same reviewed policy emitted from the exact verified binary and evaluated uncached inside the current functions.exec V8.", + "held_constant": "Mounted codex_app reader and setter identities; current-task setter target by omitted threadId; existing typed title plan, prefix ownership, safety checks, exact acknowledgement, 60-unit limit, status enum, controlled subject, local macOS host, and no retry or alternate writer.", + "predicted_outcomes": [ + "If the evaluated binary-emitted program retains the current mounted tool context, it will read the exact caller, return one exact mounted setter acknowledgement when the controlled prefix is absent, and the active header and sidebar will repaint immediately; command and malformed-program fixtures will stop before mutation.", + "If eval loses mounted access or the emitted boundary is incomplete, the program will fail before exact acknowledgement, call a different writer, or leave either rendered surface stale; the loader architecture must not ship." + ], + "stop_condition": "Stop the capability probe after one controlled exact loader write, immediate mounted header/sidebar inspection, and command/program failure fixtures. Use later no-write timing, restart, release-smoke, and full-suite checks only as seam and release evidence after recording the capability result; do not add a cache, retry, alternate writer, or second architectural variable.", + "result_experiment_id": "TB-EXP-0107-003" + }, { "id": "TB-PRE-0108-001", "issue": "BEAR-108", diff --git a/docs/live-eval.md b/docs/live-eval.md index 52aa82a..1249138 100644 --- a/docs/live-eval.md +++ b/docs/live-eval.md @@ -10,17 +10,17 @@ Exercise `complete`, `next_steps`, `needs_input`, `blocked`, and `automation`. I - there was no running title update; - one terminal JavaScript cell was the last tool action before the final response; -- that cell ran exactly one stateless local `threadbear title --status ENUM --json` helper and parsed its complete JSON only after exit zero; +- that cell ran exactly one stateless local `threadbear title-script --status ENUM` command and evaluated its complete embedded program only after exit zero; - the enum changed only the icon while the exact subject survived; - owners and actions remained in response prose; -- the helper started no App Server, opened no task store, and wrote no Codex title; +- the loader stayed under 250 source bytes; the command started no App Server, opened no task store, cached no program source, and wrote no Codex title; - the mounted app read the exact current task; when a change was needed, the cell made exactly one mounted setter call with `threadId` omitted and accepted only the exact returned task ID/title; -- if the outer cell yielded after 30 seconds, the agent waited only for that same running cell; it never started another cell, polled the title, retried, or reconciled; +- if the outer cell yielded after 30 seconds, the agent waited only for that same running cell; it never cached source, started another cell, polled the title, retried, or reconciled; - the active header and sidebar showed the exact expected title. Exercise a generated short title, continued task, user rename, non-reserved leading user emoji, user arrow, every reserved current ThreadBear prefix, every ambiguous old ThreadBear prefix, duplicate subject, maximum fitting subject, overlong subject, multiline or control text, and raw delegated envelope. Record both bounded prefix ambiguities; every other safe rename must survive byte-for-byte. Unsafe input must leave only that title unchanged without blocking the response. -Force missing or malformed current task ID, malformed helper JSON, mounted read failure, wrong read ID, blank title, ambiguous old prefix, internal envelope, and a read-to-write rename race. For the mounted writer, cover normal raw JSON-text success, already-decoded object compatibility, a throw, undecodable text, decoded non-object, wrong task ID, wrong title, and a slow call that outlasts the initial 30-second outer yield before returning. Require the yielded case to resume only the same running cell. Require zero ordinary App Server starts, zero binary title writes, at most one mounted setter call, and no blind retry, alternate source, repair command, pending proposal, or global failure. Run the exact cell under Codex's built-in `:workspace` profile and prove it succeeds without escalation or any write outside the workspace. +Force missing or malformed current task ID, title-script command failure, malformed emitted program, mounted read failure, wrong read ID, blank title, ambiguous old prefix, internal envelope, and a read-to-write rename race. For the mounted writer, cover normal raw JSON-text success, already-decoded object compatibility, a throw, undecodable text, decoded non-object, wrong task ID, wrong title, and a slow call that outlasts the initial 30-second outer yield before returning. Require the yielded case to resume only the same running cell. Require zero ordinary App Server starts, zero binary title writes, at most one mounted setter call, and no cache, blind retry, alternate source, repair command, pending proposal, or global failure. Run the exact loader under Codex's built-in `:workspace` profile and prove it succeeds without escalation or any write outside the workspace. Restart Codex after a successful write. Confirm the exact title remains in the sidebar and the next terminal turn still preserves the subject. diff --git a/docs/release-checklist.md b/docs/release-checklist.md index b0446c7..ad1cfd5 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -6,7 +6,7 @@ Before tagging a stable release: 2. Rename `Unreleased` to `vN.N.N - YYYY-MM-DD` and add a fresh `Unreleased` section. 3. Run `gofmt`, `go test ./...`, `go test -race ./...`, `go vet ./...`, both Darwin cross-builds, shell syntax checks, and installer/guide parity. Review the diff for unnecessary machinery; do not substitute a physical line-count gate for that judgment. 4. In isolated homes, prove fresh install, reinstall, dry-run collisions, status, update, uninstall, and the consented exact 2.2.1 reset while preserving unrelated AGENTS content, skills, settings, files, automations, and LaunchAgents. Confirm reset verifies automation deletion and exact-task unpin before filesystem mutation, imports no old state, and never renames the former persistent task. -5. Prove one bounded terminal JavaScript cell is the last tool action before the final response. It must run exactly one stateless local `threadbear title --status ENUM --json` helper, parse only complete exit-zero JSON, start no App Server, create no title state, and read the exact current task through the mounted app. It may make at most one mounted setter call with `threadId` omitted. Exercise raw JSON-text and object returns; reject malformed, wrong-ID, blank, unsafe, ambiguous old-prefix, and non-exact results. Cover every status, safe user renames, the five current and five inferred prefixes, the cleanup-only neutral bear prefix, non-reserved user emoji, internal envelopes, the read-to-write rename race, and a slow native call. Prove inferred input becomes one plain current prefix with exact subject preservation. Prove the exact cell succeeds under built-in `:workspace` without escalation or writes outside the workspace, and prove no SQLite, recurring model, daemon, proxy, cache, retry, fallback, queue, controller, or repair state. +5. Prove one bounded terminal JavaScript loader is the last tool action before the final response. It must stay under 250 source bytes, run exactly one stateless local `threadbear title-script --status ENUM` command, evaluate only its complete exit-zero output, start no App Server, create no title state or source cache, and read the exact current task through the mounted app. The embedded program may make at most one mounted setter call with `threadId` omitted. Exercise command failure, malformed program, raw JSON-text and object results; reject wrong-ID, blank, unsafe, ambiguous old-prefix, and non-exact results. Cover every status, safe user renames, the five current and five inferred prefixes, the cleanup-only neutral bear prefix, non-reserved user emoji, internal envelopes, the read-to-write rename race, and a slow native call. Prove inferred input becomes one plain current prefix with exact subject preservation. Prove the exact loader succeeds under built-in `:workspace` without escalation or writes outside the workspace, and prove no SQLite, recurring model, daemon, proxy, cache, retry, fallback, queue, controller, or repair state. Measure cold and warm end-to-end time from the loader model response start through its tool output: no sample may reach five seconds, with p95 at or below two seconds as the target. 6. Prove `uninstall --dry-run --json` requests one explicit command permission, launches App Server from a fixed supported Desktop path rather than ambient `PATH`, fully paginates, tolerates notifications, deduplicates IDs, treats null and blank names as raw, never adopts `preview`, and returns no partial plan after a later-page failure. After consent, prove exact `uninstall --prepare --noninteractive --confirm --json` takes one fresh complete snapshot, prepares every safe owned prefix for exact removal with the initiating task last, performs no per-target App Server read, and writes zero titles. Prove the embedded JavaScript resumes a yielded process, serially rereads each target, makes at most one setter call per exact match, blocks teardown on drift or any non-exact result, never retries, refuses a bare confirmed uninstall, and runs exact `uninstall --commit --noninteractive --confirm --json` only after every prepared write succeeds, with no final scan or post-commit title call. 7. Install the exact candidate locally and run the existing-task first-read corpus in `docs/live-eval.md`. Require the friendly handoff before onboarding finishes, truthful permission-conditional copy, one explicit catalog-read request, refusal with zero existing-title changes, progressive sidebar repaint after approval, exact versus `✦` provenance, untouched unknown/drifted rows, serial mounted reread/write acknowledgement, interruption safety, natural rerun behavior, several minutes of realistic activity, and next-turn sparkle removal. Prove the classifier has no callable hosted capability and rejects any tool-attempt event. Then restart Codex and verify titles persist. Require immediate mounted repaint for the active header and one controlled historical row after their sole cleanup writes, artifact removal afterward, unrelated-content preservation, then verify both titles after restart. Disable rewriting if it practically corrupts titles or blocks responses. Open SWE is review/merge only for Desktop behavior, not the implementation or live-proof environment. 8. Prove the daily LaunchAgent invokes only the verified updater. Network and verification failures preserve the old install; local write failures report `partial`, stage, restart implication, and one safe rerun with binary last; success reports `restart_required`. Prove updater health is separate from core `ready` and update/uninstall races are serialized. After uninstall commit, do not run the title command. diff --git a/docs/status-convention.md b/docs/status-convention.md index ebc4213..32453d6 100644 --- a/docs/status-convention.md +++ b/docs/status-convention.md @@ -1,13 +1,13 @@ # Status convention -Immediately before an ordinary final response, ThreadBear's managed guidance runs one terminal cell whose stateless local helper receives one of: +Immediately before an ordinary final response, ThreadBear's managed guidance runs one tiny terminal loader whose stateless local command receives one of: ```text -threadbear title --status complete --json -threadbear title --status next_steps --json -threadbear title --status needs_input --json -threadbear title --status blocked --json -threadbear title --status automation --json +threadbear title-script --status complete +threadbear title-script --status next_steps +threadbear title-script --status needs_input +threadbear title-script --status blocked +threadbear title-script --status automation ``` The status maps to one owned icon: @@ -20,7 +20,7 @@ The status maps to one owned icon: | `blocked` | `🚨 ` | | `automation` | `🤖 ` | -The enum controls only the icon. The helper returns the task ID and fixed policy without reading Codex or writing state. The mounted app reads the exact current title; when a change is needed, the same cell makes one native title call and accepts only the exact returned task ID/title. Any owner or next action stays in the substantive response. There is no current-turn footer, running icon, or neutral bear status. +The enum controls only the icon. The verified binary binds the task ID and fixed policy into its embedded reviewed program without reading Codex or writing state. The loader evaluates that complete program uncached inside the current Codex tool context. The mounted app reads the exact current title; when a change is needed, the program makes one native title call with `threadId` omitted and accepts only the exact returned task ID/title. The public `title --status ENUM --json` command remains a data-only diagnostic for the same plan. Any owner or next action stays in the substantive response. There is no current-turn footer, running icon, or neutral bear status. During the one post-install existing-task first read, conservative historical inference uses the corresponding `✅✦ `, `➡️✦ `, `🙋✦ `, `🚨✦ `, or `🤖✦ ` prefix. `✦` means first read, not warning; the next ordinary turn strips it and writes one plain current prefix. Unknown history gets no decoration. diff --git a/scripts/release-smoke.sh b/scripts/release-smoke.sh index 6b82642..03942fd 100755 --- a/scripts/release-smoke.sh +++ b/scripts/release-smoke.sh @@ -662,16 +662,24 @@ python3 - "$codex_home/AGENTS.md" <<'PY' import sys text = open(sys.argv[1], encoding="utf-8").read() -assert text.count("title --status STATUS --json") == 1, text +assert text.count("title-script --status STATUS") == 1, text +assert text.count("await (0,eval)(source.output)({tools,text,exit});") == 1, text +assert "tools.codex_app__set_thread_title" not in text, text +assert "tools.codex_app__read_thread" not in text, text +assert "thread/name/set" not in text, text +assert "PreToolUse" not in text and "PostToolUse" not in text, text +PY +run_threadbear title-script --status complete >"$root/title-complete-program.js" +python3 - "$root/title-complete-program.js" <<'PY' +import sys + +text = open(sys.argv[1], encoding="utf-8").read() assert text.count("tools.codex_app__set_thread_title") == 1, text assert text.count("tools.codex_app__read_thread") == 1, text -assert "const decodeNative = value =>" in text, text -assert "decodeNative(await tools.codex_app__read_thread" in text, text -assert "decodeNative(await tools.codex_app__set_thread_title" in text, text -assert "plan.owned_prefixes" in text and "plan.blocked_prefixes" in text, text +assert "tools.exec_command" not in text, text assert "thread/name/set" not in text, text -assert "PreToolUse" not in text and "PostToolUse" not in text, text PY +node --check "$root/title-complete-program.js" >/dev/null python3 - "$codex_home/skills/threadbear/SKILL.md" <<'PY' import sys diff --git a/site/install b/site/install index db5457f..94e6394 100644 --- a/site/install +++ b/site/install @@ -256,7 +256,7 @@ After a successful install say: ~/.local/bin/threadbear update --json ``` -The managed guidance runs one injection-safe terminal JavaScript cell immediately before an ordinary final response. Replace only the status enum. The cell runs `title --status --json` exactly once; the stateless helper returns the calling task ID and fixed title policy without starting App Server or writing state. The mounted app then reads that exact task, derives one safe desired title, and—only when it differs—calls `tools.codex_app__set_thread_title({title:desired})` once with `threadId` omitted. Exact returned task ID/title is required. If the outer cell yields after 30 seconds, wait only for that same cell; the yield does not cancel a slow native call. Never retry, start another cell, poll the title, or reconcile. +The managed guidance runs one small terminal JavaScript loader immediately before an ordinary final response. Replace only the status enum. The loader runs `title-script --status ` exactly once. That stateless command binds the calling task ID and fixed policy into the reviewed JavaScript program embedded in the verified binary; Codex evaluates it inside the current in-app tool context instead of regenerating the program through the model. The program reads the exact current task through the mounted app and—only when the safe desired title differs—calls the mounted setter once with `threadId` omitted. Exact returned task ID/title is required. If the outer cell yields after 30 seconds, wait only for that same cell; the yield does not cancel a slow native call. Never cache source, retry, start another cell, poll the title, or reconcile. `update` verifies the official manifest, release URLs, architecture, checksum, embedded version, and candidate self-test before replacement. Network or verification failure leaves the old installation untouched. A later managed-surface write can truthfully leave a rerunnable partial; the binary is written last. Every successful update reports `restart_required`. The daily LaunchAgent runs only this command and never reads tasks or changes titles. @@ -317,7 +317,7 @@ Before release, run unit and integration tests, race tests, both Darwin builds, Release acceptance additionally requires one reviewed candidate live-tested end to end in Codex Desktop: -- the stateless terminal helper works under Codex's default workspace permissions and starts no App Server or title-state write; +- the stateless terminal loader works under Codex's default workspace permissions, remains under 250 source bytes, and starts no App Server or title-state write; - the mounted app-native reader supplies the exact current title, and the setter receives no explicit current-task ID and returns the exact task ID/title; - the rendered sidebar shows the expected title before and after a clean restart; - a full uninstall preview enumerates every unarchived task, confirmed preparation writes no title, and the consented serial app-native pass processes the initiating task last;