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
27 changes: 27 additions & 0 deletions .github/workflows/title-routing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Title routing

on:
pull_request:
paths:
- "skills/estack-better-title/**"
- "tests/estack-better-title/**"
- ".github/workflows/title-routing.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
- run: node --test tests/estack-better-title/test-routing.cjs
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Two things I care enough about to name explicitly: publishing and syncing. Publi
| `docs/manual-publish.md` | Publishing manually from the CLI when GitHub Actions are unavailable — prerequisite settings to re-enable, token bypass, cleanup checklist |
| `docs/changelog-maintenance.md` | Updating `CHANGELOG.md` — when and how to write unreleased entries, how to promote them on publish |
| `docs/research/codex-claude-cli-orchestration-findings.md` | Editing `estack-drive-cli-agent` — the full research record behind it (CLI flag semantics, sourced footguns, openai-codex plugin failure autopsy, prior art) |
| `docs/research/codex-session-titles-findings.md` | Editing `estack-better-title`, or making any skill work in both Claude Code and Codex — host detection, the `codex app-server` rename protocol, and what Codex does and doesn't support in SKILL.md |

- **Skills in the pack:** `estack-active-learning-tutor`, `estack-better-title`, `estack-book-extractor`, `estack-chris-voss`, `estack-claude-md-optimizer`, `estack-cold-message-writer`, `estack-customer-discovery`, `estack-doc-review-viewer`, `estack-drive-cli-agent`, `estack-email-writer`, `estack-flight-planner`, `estack-github-issue-tracker`, `estack-leadership-coach`, `estack-migrate-claude-session-history`, `estack-notify`, `estack-pdf-to-md`, `estack-pr-description`, `estack-productivity-prioritization-coach`, `estack-prompt-builder-coach`, `estack-purdue-stickers`, `estack-read-agent-history`, `estack-repo-search`, `estack-sponsorship-offer-builder`, `estack-vscode-file-recovery`
- **Hooks in the pack:** `estack-claude-startup.js`, `estack-codex-startup.js`, `estack-startup-update-core.js`, `estack-statusline.js`, `repo-search-nudge.js`
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- `estack-better-title` skill (2.0.2) — detects Claude Code or Codex at startup and routes title reads and renames through one entry point. Codex uses its app-server API; Claude retains append-only transcript updates. Hosts without a choice tool use numbered title options in chat.

### Fixed
- `estack-better-title` skill — requests host-tool approval for Codex startup and rename when `CODEX_HOME` is outside sandbox write roots, and explains permission failures without changing homes or retrying another transport.
- `estack-better-title` skill — recognizes Claude Agent SDK session ids and explains unknown hosts, nested hosts, missing or conflicting ids, and backend read failures. Host overrides retain the safeguard against renaming another session.
- `estack-better-title` skill — preserves Unicode and literal title whitespace, keeps startup errors visible to the agent, and configures explicit invocation for Codex.

---

## [1.1.1] - 2026-10-19
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This installs skills to `~/.agents/skills/` and symlinks them into `~/.claude/sk
| Skill | Command | Description |
|-------|---------|-------------|
| **Active Learning Tutor** | `/estack-active-learning-tutor` | Tutors a student through exam preparation using active learning — questioning, gap diagnosis, and concept mastery tracking |
| **Better Title** | `/estack-better-title` | Renames Claude Code chat sessions — a short subject you can skim, plus project and keywords you can grep |
| **Better Title** | `/estack-better-title` | Renames Claude Code or Codex chat sessions — a short subject you can skim, plus project and keywords you can grep |
| **Book Extractor** | `/estack-book-extractor` | Turns a book or doc set (PDF, EPUB, DOCX, HTML, RTF, TXT/MD) into a standalone, on-demand Agent Skill — deterministic text extraction, then per-chapter reference files, glossary, cheatsheet, and a master index generated by the agent, installed outside the e-stack pack |
| **Chris Voss** | `/estack-chris-voss` | Applies negotiation principles from *Never Split the Difference* when preparing to negotiate with an outside counterpart; hard conversations with your own team go to Leadership Coach, and the message itself to Email Writer or Cold Message Writer |
| **CLAUDE.md Optimizer** | `/estack-claude-md-optimizer` | Creates, audits, and maintains concise CLAUDE.md / AGENTS.md instructions, preserving project conventions and the chosen canonical file while applying authorized edits |
Expand Down
200 changes: 200 additions & 0 deletions docs/research/codex-session-titles-findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# Codex session titles — how the rename actually works

The research record behind `estack-better-title` 2.0.2, which adds Codex support.
The original observations below came from codex-cli 0.153.4 on Windows
(2026-09-06). They are historical unless explicitly reverified below.

## Current audit — 2026-10-19

Verified with **codex-cli 0.162.1 on Windows**:

- Generated the installed app-server JSON schema. `thread/read` accepts
`threadId` and `includeTurns`; its thread response has a nullable `name`.
- Started an app-server with a disposable `CODEX_HOME`, created a throwaway
thread without a model turn, then round-tripped `thread/name/set` and
`thread/read`. The skill helper also read, renamed, and read that thread
through separate server processes. No existing user chat was renamed.
- Both `CODEX_THREAD_ID` and `CODEX_SESSION_ID` are present in this host.
- `node --test tests/estack-better-title/test-routing.cjs` verifies Claude
transcript writes in temporary homes, host detection, nested overrides,
stale-id rejection, and a simulated Codex protocol over real process pipes.

The audit supersedes two old assumptions: a host override must never disable
session-id validation, and finding frontmatter strings in a binary does not
prove that a host enforces those permissions. Permission behavior and UI repaint
latency were not runtime-tested. Claude was tested against disposable transcript
fixtures, not an active Claude UI. The proxy path was simulated; only the private
stdio transport was exercised against the real Codex binary.

The current implementation stops on backend read errors instead of treating a
missing thread or malformed response as an unnamed session. It preserves split
UTF-8 output and literal title whitespace. A nested Claude marker without an id
still counts as an ambiguous host, and conflicting id aliases are rejected.


Current official docs were fetched on 2026-10-19:

- [Claude Code skill context injection](https://code.claude.com/docs/en/skills#inject-dynamic-context)
documents both inline and fenced shell blocks. A nonzero injected command
aborts skill loading, so the entry block preserves the diagnostic as text and
prints `SESSION_CHECK_FAILED` while allowing the agent to explain it. Direct
calls to `session.sh` retain their nonzero exit codes.
- [Codex skill invocation policy](https://learn.chatgpt.com/docs/build-skills)
documents `agents/openai.yaml` with `policy.allow_implicit_invocation: false`.
This supplies the Codex equivalent of the Claude-only
`disable-model-invocation` setting.

## Sandbox approval boundary

The real-app-server round trip above was run through an approved escalated shell
with a disposable home. It did not establish that a private server can start
inside a workspace-only sandbox with the user's ordinary `CODEX_HOME`.
[Codex issue 52759](https://github.com/openai/codex/issues/52759), fetched during
the PR review on 2026-10-19, reports readonly startup database and operation-not-
permitted failures from a nested CLI. It corroborates this limitation rather
than a title-specific reproduction.

The skill now instructs the agent to request host-tool approval before startup
and rename when `CODEX_HOME` is outside writable roots. Permission-denied server
output produces `APPROVAL_REQUIRED`; it does not fall through to another
transport. The script cannot grant approval itself. A regression fixture
verifies this diagnostic and stop behavior; a denied real user home was not
modified to manufacture a reproduction.

## Host detection

Codex exports both of these into every shell it runs a command in, set to the
same value:

```
CODEX_SESSION_ID=01a0773b-bc21-7420-8afe-9945c2309c91
CODEX_THREAD_ID=01a0773b-bc21-7420-8afe-9945c2309c91
```

Claude Code exports `CLAUDE_CODE_SESSION_ID` and `CLAUDECODE=1`. It does **not**
always export `CLAUDE_SESSION_ID` — under the Claude Agent SDK (which is what
third-party harnesses embed) only `CLAUDE_CODE_SESSION_ID` is present. The old
skill read `${CLAUDE_SESSION_ID}` via SKILL.md interpolation, which is why the
id could come back blank there.

Host variables identify the session in ordinary tool shells. When one CLI drives the other (`codex exec` from Claude Code, or
`claude -p` from Codex), the inner agent inherits the outer agent's environment
and both sets are visible at once. There is no reliable way to tell which is the
inner one from the environment alone, so `lib/host.sh` treats that as an error
and asks rather than guessing.

## Historical storage observations (not reverified in the current audit)

`~/.codex/state_<N>.sqlite`, table `threads`, and there are two relevant columns:

| Column | What it is |
|---|---|
| `title` | Falls back to the first user message. Not the user-facing name. |
| `name` | The user-set thread name. `NULL` until something sets it. This is the one. |

The `<N>` in the filename is a schema generation that Codex bumps
(`state_5.sqlite` at the time of writing), and the table gained columns as
recently as this version — `name`, `is_pinned`, `project_id`, and
`thread_section_id` all sit after the original block. `~/.codex/session_index.jsonl`
(`{id, thread_name, updated_at}`) is a **legacy** index, last written 2026-08-29
on this machine; do not write it.

**Do not write the database.** A live Codex process owns it, the filename moves
with the schema generation, and the columns are not a stable interface. Use the
protocol below instead.

## The protocol

`codex app-server` speaks newline-delimited JSON-RPC. The relevant methods:

| Method | Params | Notes |
|---|---|---|
| `initialize` | `{clientInfo: {name, version}}` | Must be first. Reply carries `codexHome`, platform info. |
| `initialized` | — | A notification, not a request. Send it before anything else. |
| `thread/read` | `{threadId, includeTurns: false}` | Returns `{thread}` with `id` and `name`, straight from the state DB. Unknown id errors with `thread not loaded: <id>`. **This is the read to use.** |
| `thread/name/set` | `{threadId, name}` | Sets the name. Result is `{}`. Also emits a `thread/name/updated` notification. Unknown id errors with `no rollout found for thread id <id>`. |
| `thread/list` | `{limit, sortKey, sortDirection, sourceKinds, useStateDbOnly, cursor}` | Returns `{data, nextCursor, backwardsCursor}`; each thread carries `id` and `name`. Not needed for a by-id read, see below. |

Generate the full schema for any of this with:

```bash
codex app-server generate-json-schema --out <dir> # v1/ and v2/ subdirs
```

### Gotchas

1. **Use `thread/read`, not `thread/list`, for a by-id lookup.** The first
version of the script paged `thread/list` because the method table did not
list a direct read; the audit found `thread/read` in the generated schema.
If you ever do need `thread/list`, two traps: it has no by-id filter, and it
**hides `exec` and subagent threads by default**. The default `sourceKinds`
filter covers the interactive kinds only; pass every kind explicitly (`cli,
vscode, exec, appServer, subAgent, subAgentReview, subAgentCompact,
subAgentThreadSpawn, subAgentOther, unknown`) or a thread that is plainly in
the database comes back "not found".
2. **Every `codex` process start costs ~3.5 s on this machine**, before it does
anything. `codex --version` alone measured 3.6 s. Budget one spawn per
operation, not one per attempt.

### Two transports

- `codex app-server proxy` — forwards to the app-server daemon already serving
the live session, so a running UI repaints immediately. When no daemon is
listening it fails with `failed to connect to socket at
~/.codex/app-server-control/app-server-control.sock`, but only after the full
~3.5 s process start, and that is the common case for a plain CLI session
(the `app-server-control/` directory did not even exist here).
- `codex app-server` — starts a private stdio server. Writes the same state, but
a session that is already open won't repaint until the thread list reloads.

`scripts/codex-title.js` checks for the control socket on disk first and only
tries the proxy when it is there, so the no-daemon case is one spawn instead of
two. A JSON-RPC error from a method the server actually answered is surfaced
as-is rather than retried on the other transport; only a failure of the
connection itself (spawn error, exit before the handshake, timeout) falls
through.

## Historical skill-loader observations (permission claims superseded)

| | Claude Code | Codex |
|---|---|---|
| Reads skills from | `~/.claude/skills/` (symlink to `~/.agents/skills/`) | `~/.agents/skills/` |
| `allowed-tools` permissions | Host-specific; verify behavior | Not established by the old binary string-table probe; do not promise a permission grant |
| Interpolates `${CLAUDE_SESSION_ID}` / `${CLAUDE_SKILL_DIR}` in the body | yes | **no** |
| Runs ` ```! ` auto-run blocks on load | yes | **no** — the string does not appear in the binary |
| Choice tools | Use a choice tool when exposed | Tool availability varies by host and mode; fall back to numbered chat options |

The practical consequence for any cross-host skill: put the work in a script,
have the script find its own directory from `$BASH_SOURCE`, and have it print
whatever the model needs — including the exact follow-up command with paths and
ids already filled in. Then the ` ```! ` block is a nice-to-have on Claude Code
and a plain "run this first" instruction everywhere else.

## Nested hosts

When one CLI drives the other (`codex exec` from Claude Code, or `claude -p`
from Codex) both variable sets are visible and `lib/host.sh` exits 2. The way
out is `ESTACK_BETTER_TITLE_HOST=claude|codex`, which is read *before* the
both-visible guard and skips it. The first version of the script checked in the
other order, so the escape hatch its own error message named could never be
reached; that is what the audit caught. Keep the override first. It selects only a host; missing, conflicting, invalid,
or stale session ids still fail before any write.

## Re-verifying

```bash
# Does Codex still export a thread id into command shells?
codex exec --skip-git-repo-check -s read-only \
"Run exactly this and paste raw stdout: env | sort" < /dev/null | grep CODEX_

# Is the state schema still shaped this way?
python -c "import sqlite3,os,glob;p=sorted(glob.glob(os.path.expanduser('~/.codex/state_*.sqlite')))[-1];\
print(sqlite3.connect('file:'+p.replace(os.sep,'/')+'?mode=ro',uri=True).execute(\
\"select sql from sqlite_master where name='threads'\").fetchone()[0])"

# Run deterministic fixture tests without accessing live session stores.
node --test tests/estack-better-title/test-routing.cjs

# For a live round trip, first start a throwaway thread in a disposable
# CODEX_HOME. Never substitute an existing user chat for a test thread.
```
4 changes: 3 additions & 1 deletion docs/skill-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,13 +271,15 @@ configuration details belong in the integration that uses them.

### Auto-run commands

Use `` ```! `` (triple backtick + `!`) code blocks in SKILL.md to run shell commands automatically when the skill is loaded. The output is presented to the model before it processes the rest of the skill.
Use `` ```! `` (triple backtick + `!`) code blocks in SKILL.md for Claude Code dynamic context injection. The output is presented to the model before it processes the rest of the skill. Other hosts need an explicit instruction to run the command themselves; this supersedes the earlier host-independent auto-run guidance. [Claude Code documents the syntax and failure behavior](https://code.claude.com/docs/en/skills#inject-dynamic-context).

Use this for setup tasks, environment checks, or gathering context that the skill needs upfront.

**Example:**
- `skills/estack-repo-search/SKILL.md` — clones and indexes a repo on load

For a diagnostic whose failure the agent must explain, preserve the error output and return success from the injected block (for example, `check-command || printf 'CHECK_FAILED: stop and explain the error above.\n'`). Otherwise a nonzero exit aborts skill loading before the agent receives its instructions. Keep the underlying script nonzero on failure for direct callers.

### Prefix transformations and stored state

The installer rewrites skill names and explicit `skills/<name>` paths when
Expand Down
11 changes: 7 additions & 4 deletions scripts/check-paths.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@ const ALLOWED_PREFIXES = [
'~/.claude/settings', // Claude's own config (read)
'~/.claude/hooks', // hook install location
'~/.claude-backups', // Claude's backups (read)
'~/.codex', // Codex's data, read-only for us — no skill writes here,
// so the whole subtree is allowed. ~/.claude is not:
// it gets named subpaths only, because state has been
// parked there before.
'~/.codex', // Codex's data, read-only for us — no skill writes to
// this path, so the whole subtree is allowed. ~/.claude
// is not: it gets named subpaths only, because state has
// been parked there before. estack-better-title does
// change a Codex thread's name, but through the
// `codex app-server` JSON-RPC API rather than by writing
// Codex's state database behind its back.
'~/.config/Code', // VS Code history (read)
'~/.config/Cursor', // Cursor history (read)
'~/Library/Application', // macOS app support (read)
Expand Down
Loading
Loading