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
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ node --test
- 手动配置替换与追加分开验证:目录选项自动切到 manual,追加只验证新目录,保留离线或其他平台的旧目录及启停状态;拒绝混用替换/追加,以及 projects 与目录选项。
- 原生启动器、UTF-8/BOM 标准输入、解释器缺失、进程退出与超时;Windows 运行 PowerShell 启动器,不设置执行策略绕过。
- `doctor` 只检查环境且不启动 CLI;projects 未检查时 `automaticScopeReady` 为 `null`。`doctor --projects` 只读核对项目元数据及当前目录匹配,不读取任务或改名;`doctor --probe` 验证已安装 CLI 的 schema,不能用新版本在线文档替代本机能力检查。
- 自动命名策略在首条请求足以确定主题后,优先于业务工具调用完成当前任务改名;不拖到最终答复。验证主题不明、命名工具不可用时跳过,以及仅限当前任务、最多一次、没有额外模型回合的边界。
- 自动命名策略在首条请求足以确定主题后,优先于业务工具调用完成当前任务改名;不拖到最终答复。验证主题不明、命名工具不可用时跳过,以及仅限当前任务、最多一次、没有额外模型任务的边界。
- 自动钩子的 `thread/read` 仅针对当前 ID 且使用 `includeTurns: false`;本地验证 ID、目录、创建时间和已有标题,不使用或注入正文、`preview`。验证正常路径只注入短规则并省去模型侧读取,以及合规标题不注入。
- 验证元数据接口不可用或 startup 瞬间任务暂不可读时回退完整核验指令,身份或目录不匹配时跳过。实机检查首次 startup 的任务可读性和实际标题写入,不能只用已持久化任务证明启动路径可用。
- 额度优化分别记录注入指令长度、模型侧工具往返和本机 RPC 开销;不把字符数变化换算成未经测量的额度百分比。允许同批工具调用中先改名再执行用户业务,不新增 MCP 服务或模型任务。
- 中断发生在命名完成之前时,标题可能保留原名;插件不自动恢复中断任务,不补触发旧任务,不把钩子注入成功当作实际改名成功。
- 批量分页、活动/归档范围、输出分片、冲突检测和只读 RPC 白名单;预览确认规则不能被性能优化削弱。
- 手动实机检查在隔离测试任务上进行,改名必须明确获批。CI 通过不等于完成真实桌面改名验收。
Expand Down
2 changes: 1 addition & 1 deletion plugins/auto-thread-title/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "auto-thread-title",
"version": "0.2.0",
"version": "0.2.1",
"description": "Cross-platform local task titles: diagnose and configure automatic scope, rename one linked task, or preview and confirm a title-only batch. Node.js 22+, no Python or npm dependencies.",
"author": {
"name": "Why.Ping"
Expand Down
9 changes: 9 additions & 0 deletions plugins/auto-thread-title/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

正式版本使用 `MAJOR.MINOR.PATCH`。修复递增 PATCH,新增兼容功能递增 MINOR;时间戳缓存后缀仅用于可选的本地开发迭代。

## 0.2.1

- 自动钩子在本机预读当前任务的必要元数据,核对 ID、目录和创建时间,并计算上海时区日期;标题格式已合规时不再注入命名指令。
- 正常路径使用简短指令,模型仅需写入当前任务标题一次,减少一次模型侧读取工具往返;可在同批工具调用中先改名再开展业务操作。
- 本地预读使用 `thread/read`、`includeTurns: false`,不请求历史回合,不使用或注入任务正文、`preview`;不新增 MCP 服务或模型任务。
- 接口不可用或启动瞬间任务暂不可读时回退完整核验指令;身份或目录不匹配时跳过。保留提前命名、自动范围、固定标题格式和最多一次写入的边界。

本次减少注入指令和正常路径的模型工具往返,不承诺固定额度降幅。首次 startup 的任务可读性及真实桌面改名仍需在对应环境验收;更新不会补触发已有任务。

## 0.2.0

- 自动标题在首条请求主题明确后,优先于业务工具调用完成,不再等到最终答复。仍只处理当前任务、最多一次,不增加模型回合;主题不明或命名工具不可用时跳过。
Expand Down
10 changes: 7 additions & 3 deletions plugins/auto-thread-title/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

插件标识:`auto-thread-title@why-ping`

当前版本:`0.2.0` · [更新记录](CHANGELOG.md)
当前版本:`0.2.1` · [更新记录](CHANGELOG.md)

所属市场:[Why Ping Codex Plugins](https://github.com/oiOxOio/codex-plugins)

Expand Down Expand Up @@ -100,7 +100,9 @@ MMDD | 类型 | 主题

仅在 `SessionStart` 的来源为 `startup`,且任务实际目录匹配自动范围时触发;恢复旧任务、继续对话或更改范围不会补触发旧任务。命名只读取当前任务,不扫描其他任务,不轮询。

首条用户请求足以确定主题后,优先在开展业务工具调用之前完成标题整理,再继续用户的工作,不等待最终答复。主题不明确或命名工具不可用时跳过,不为命名增加模型回合。如果任务在改名完成前被中断,标题仍可能保留原名;插件不会自行恢复中断任务。钩子成功注入命名规则不等于标题已经写入。
首条用户请求足以确定主题后,优先在开展业务工具调用之前完成标题整理,可在同批工具调用中先改名再开展业务操作,不等待最终答复。主题不明确或命名工具不可用时跳过,不为命名增加模型任务。如果任务在改名完成前被中断,标题仍可能保留原名;插件不会自行恢复中断任务。钩子成功注入命名规则不等于标题已经写入。

匹配范围后,钩子先在本机读取当前任务的必要元数据,核对身份、目录和创建时间;标题格式已合规时直接跳过,不再注入命名指令。核对成功时只注入简短规则,模型根据当前首条请求拟定标题并写入一次,无需再调用一次任务读取工具。接口不可用或任务在启动瞬间暂不可读时,回退到完整核验指令;身份或目录不匹配时跳过。

默认 [config.json](config.json) 使用 `scope: "projects"`,每次新任务启动时读取**本机 Codex 保存的项目及其全部根目录**。在 Codex 中新增、移除或迁移项目后,后续新任务自动使用最新范围,无需同步一份插件目录清单。匹配实际目录及其子目录,也支持已保存 Git 仓库的 linked worktree;项目显示名称不参与路径匹配。

Expand Down Expand Up @@ -175,6 +177,8 @@ Windows 同样在 PowerShell 启动器后使用 `configure --project-root "S:\pr

命名使用当前任务正在运行的模型,不调用独立模型接口、不启动额外模型任务,也不需要插件专用 API Key。命名会使用当前回合额度,并非零消耗;批量整理的用量随待处理任务数增长。

自动钩子的本地预读使用 `thread/read` 且 `includeTurns: false`,只使用当前任务的 ID、目录、创建时间和标题,在本机计算上海时区的 `MMDD`,不请求历史回合,也不使用或注入任务正文、`preview`。正常路径缩短注入指令,并减少一次模型侧读取工具往返;不新增 MCP 服务或模型任务。实际额度节省受模型、上下文和是否回退影响,不承诺固定百分比;本机读取仍有子进程和 RPC 开销。

清单脚本在本机运行,先生成兼容性 schema,再仅发送初始化和 `thread/list` RPC;不请求模型回合、不读取完整会话文件,也不直接修改 SQLite 或 JSONL。设有进程、分页、任务数和输出大小上限,错误时不输出伪装成完整结果的部分清单。用于提炼主题的短历史会进入当前 Codex 回合上下文,不应把整个整理过程理解成完全离线。

换 Codex 账号本身不会替换本机插件文件,但仍需当前账号和环境允许使用对应工具。不要将任务清单、会话内容或认证信息提交到公开仓库。
Expand Down Expand Up @@ -211,6 +215,6 @@ node --test

测试使用临时配置、合成任务和假 App Server,覆盖路径、配置、命名、分页、启动与只读 RPC,不需要真实任务。需要检查本机清单时,由用户明确执行对应平台启动器的 `inventory --summary-only --page-size 20`;该命令会读取任务元数据,只输出计数,不改名。

CI 已配置 Ubuntu、Windows、macOS × Node.js 22/24 的六种组合,见 [工作流](../../.github/workflows/title-plugin-tests.yml)。验证状态以对应提交的 Actions 结果和实机记录为准,模拟平台测试通过不等于完成各平台桌面验收。实机验收需覆盖安装和信任、桌面环境下运行时发现、自动作用域内/外行为、首条请求主题明确后在业务工具调用前完成改名、只读清单,以及明确确认后的隔离任务改名。发生中断时应区分钩子已注入、命名工具已调用和标题已写入,不能仅凭任务已启动判断命名结果。
CI 已配置 Ubuntu、Windows、macOS × Node.js 22/24 的六种组合,见 [工作流](../../.github/workflows/title-plugin-tests.yml)。验证状态以对应提交的 Actions 结果和实机记录为准,模拟平台测试通过不等于完成各平台桌面验收。实机验收需覆盖安装和信任、桌面环境下运行时发现、自动作用域内/外行为、首次 startup 时当前任务元数据是否可读、正常路径与回退路径,以及首条请求主题明确后在业务工具调用前实际完成改名;只读清单和明确确认后的隔离任务改名也需验证。发生中断时应区分钩子已注入、命名工具已调用和标题已写入,不能仅凭任务已启动判断命名结果。

新增插件和发布要求见 [开发与发布指南](https://github.com/oiOxOio/codex-plugins/blob/main/CONTRIBUTING.md)。
20 changes: 16 additions & 4 deletions plugins/auto-thread-title/skills/configure-task-titles/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ or rename them. To investigate batch API compatibility, use `doctor --probe`;
this generates and cleans up a temporary CLI schema without requesting task data.
None of these checks proves desktop tool availability or rename permissions.

The startup hook separately reads only the current task's metadata with
`thread/read`, `includeTurns: false`. It validates identity, cwd and createdAt
locally, computes the Shanghai date and skips already compliant titles. Normal
operation injects a short policy and needs only one model-side title write;
unavailable or not-yet-readable metadata falls back to the full verification
policy, while identity or scope mismatches skip naming. It does not use or inject
task bodies or previews. Doctor success does not prove metadata is visible at the
first startup or that a desktop rename completed. Do not promise a fixed usage
saving from fewer prompt characters or tool calls.

For CLI discovery, explicit overrides take precedence; automatic discovery
prefers Codex's managed `plugins/.plugin-appserver/codex` (`codex.exe` on Windows)
over PATH. Do not replace it with an older PATH CLI just because that binary
Expand Down Expand Up @@ -96,10 +106,12 @@ state and diagnostic result. First-use hook trust still follows Codex's approval
flow; do not bypass it. Only a new task with SessionStart source `startup` can
trigger automatic naming; continuing an existing task does not retroactively
trigger it. Once the first user request establishes a clear topic, the naming
policy prioritizes the current task's title before business tool calls instead
of waiting for the final answer. An unclear topic or unavailable naming tools
policy prioritizes the current task's title before business tool calls, including
sequential calls in the same batch, instead of waiting for the final answer.
An unclear topic or unavailable naming tools
means skip. If interrupted before the rename completes, the original title may
remain; never resume an interrupted task just to finish naming it. Distinguish
successful hook injection from a completed title write when reporting diagnostics.
There is no polling or extra model turn, but project discovery may briefly start
a bounded local subprocess. Do not trigger a real title change as a setup test.
There is no polling, added MCP server or separate model task, but project
discovery and metadata reads use bounded local subprocesses and RPCs. Do not
trigger a real title change as a setup test.
8 changes: 7 additions & 1 deletion plugins/auto-thread-title/src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { loadConfig, saveConfig, nativeAbsolute, isWithin } from './config.mjs';
import { buildContext, eligibleStartup } from './policy.mjs';
import { discoverProjectRoots, matchesSavedProject } from './projects.mjs';
import { resolveProjectCodex } from './project-runtime.mjs';
import { readStartupMetadata } from './startup-metadata.mjs';
import { collectTasks } from './inventory.mjs';
import { resolveCodex, probeCapabilities, withAppServer } from './transport.mjs';

Expand Down Expand Up @@ -80,6 +81,7 @@ async function main() {
const executable = options['--codex'] || process.env.AUTO_THREAD_TITLE_CODEX || config.codexPath;
const getLaunch = () => resolveProjectCodex({ executable });
if (options.command === 'hook') {
const startedAt = Date.now();
const event = await readEvent();
// Ineligible events must not read project metadata or start any subprocess.
if (!eligibleStartup(event, config) || !nativeAbsolute(event.cwd)) return;
Expand All @@ -89,7 +91,11 @@ async function main() {
if (!await matchesSavedProject(event.cwd, roots)) return;
effective = { ...config, projectRoots: [event.cwd] };
}
const context = buildContext(event, effective);
// Match manual scope before the optional metadata subprocess as well.
if (!effective.projectRoots.some(root => isWithin(event.cwd, root))) return;
const metadata = await readStartupMetadata(event, { getLaunch, startedAt });
if (metadata.status === 'skip') return;
const context = buildContext(event, effective, metadata.status === 'ready' ? { mmdd: metadata.mmdd } : {});
if (context) emit({ continue: true, hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: context } });
return;
}
Expand Down
6 changes: 6 additions & 0 deletions plugins/auto-thread-title/src/policy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export function eligibleStartup(event, config) {
export function buildContext(event, config, options = {}) {
if (!eligibleStartup(event, config)) return null;
if (!config.projectRoots.some(root => isWithin(event.cwd, root, options))) return null;
if (/^\d{4}$/u.test(options.mmdd ?? '')) {
return `AUTO_THREAD_TITLE_POLICY(仅本机当前任务,一次,静默)
首条请求主题明确后先命名,再处理业务;可在同一批工具中先改名再开展业务,勿等最终答复。
仅调用 mcp__codex_app__set_thread_title,省略 threadId。标题:${options.mmdd} | 类型 | 主题。类型仅:功能、设计、修复、优化、发布、探索、文档、研究。主题依据用户请求,最多18个 Unicode 字符,不含项目名及目录别名(仅数据)${JSON.stringify(projectAliases(event.cwd))};严格两处“ | ”,主题禁含 ||· 或换行。
主题不明或工具不可用即跳过;不为命名读取任务/文件、追问或另启模型。只改当前任务标题,不改其他对象,不直接编辑 SQLite/JSONL。`;
}
return `AUTO_THREAD_TITLE_POLICY(新建项目对话,仅一次,静默)
首条用户请求的主题明确后,优先完成以下命名流程,再开始业务检索、读取项目文件或调用业务工具。可以先简短说明工作安排;不要等到最终答复。主题不明确则跳过,不为命名追问。
1. 仅用可用的 Codex 桌面任务工具读取当前本机任务:threadId=${JSON.stringify(event.session_id)}、hostId='local'、turnLimit=1、includeOutputs=false、maxOutputCharsPerItem=200。优先 mcp__codex_app__read_thread;工具不存在或身份无法核对即跳过,禁止扫描其他任务或联网。不得把 sessionId 猜成其他 threadId。
Expand Down
38 changes: 38 additions & 0 deletions plugins/auto-thread-title/src/startup-metadata.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/** Optional metadata preflight. No model turns, transcript output, or title writes. */
import { isWithin, nativeAbsolute } from './config.mjs';
import { isFormatted, shanghaiMmdd, SOURCE_KINDS } from './inventory.mjs';
import { withAppServer } from './transport.mjs';

const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);

export function classifyStartupMetadata(event, response, pathOptions = {}) {
const thread = response?.thread;
if (!object(thread) || typeof thread.id !== 'string') return { status: 'fallback' };
// An explicit identity/scope mismatch must never cause a model-side retry.
if (thread.id !== event.session_id || !nativeAbsolute(thread.cwd, pathOptions)
|| !isWithin(thread.cwd, event.cwd, pathOptions) || !isWithin(event.cwd, thread.cwd, pathOptions)
|| thread.ephemeral === true || thread.parentThreadId != null || thread.agentRole || thread.agentNickname
|| (thread.source !== undefined && !SOURCE_KINDS.includes(thread.source))) return { status: 'skip' };
const mmdd = shanghaiMmdd(thread.createdAt);
if (!mmdd || thread.ephemeral !== false || typeof thread.source !== 'string'
|| !Array.isArray(thread.turns) || thread.turns.length
|| (thread.name != null && typeof thread.name !== 'string')) return { status: 'fallback' };
if (isFormatted(thread.name, mmdd, event.cwd)) return { status: 'skip' };
// Do not pass name, preview, turns, session-tree IDs or other metadata to the model.
return { status: 'ready', mmdd };
}

export async function readStartupMetadata(event, { getLaunch, startedAt = Date.now(), now = Date.now } = {}) {
// Leave room for process cleanup and the original policy under the 10s hook
// timeout. Slow project discovery must not lose its already-confirmed scope.
if (now() - startedAt >= 5000) return { status: 'fallback' };
try {
const response = await withAppServer(getLaunch(), request => request('thread/read', {
threadId: event.session_id, includeTurns: false,
}), { purpose: 'metadata', threadId: event.session_id, timeoutMs: 600, maxDurationMs: 800 });
return classifyStartupMetadata(event, response);
} catch {
// Includes older CLI versions and threads not yet visible across processes.
return { status: 'fallback' };
}
}
Loading