Skip to content
Closed
4 changes: 4 additions & 0 deletions .obsidian/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"useMarkdownLinks": true,
"newLinkFormat": "shortest"
}
4 changes: 4 additions & 0 deletions .obsidian/appearance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"baseFontSize": 14,
"theme": "obsidian"
}
16 changes: 16 additions & 0 deletions docs/knowledge-base/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# openpi-dev 知识库

> openpi 本地开发库(clone from github.com/tt-a1i/openpi)。
> 目标:commit→tasks 自动同步 hook 尝试 + openpi 架构理解。

## 结构
- `architecture/` — openpi 整体架构(extension 系统 / pi.on 事件 / 工具注册)
- `extensions/` — 各 extension 分析(tasks / post-edit / plan-mode / sessions 等)
- `hook-design/` — commit→tasks hook 设计文档
- `tasks-mechanism/` — tasks 残留根因分析 + 机制改进
- `decisions/` — 开发决策记录

## 起因
批次二完成后 tasks 残留(4 次:T3/T1/T4/T6 状态没同步 commit 完成事实)。
根因:openpi tasks 无自动同步 hook(纯手动 tasks_update)→ 遗忘必然。
改进:commit→tasks hook(PostToolUse bash commit → tasks 残留提示)。
21 changes: 21 additions & 0 deletions docs/knowledge-base/hook-design/commit-task-sync-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# commit→tasks hook 设计

## 范本
post-edit extension(`extensions/post-edit/index.ts`):
- 监听 `agent_settled`(turn 结束 debounce)
- `MUTATING_TOOLS = Set(["write","edit"])` 检测工具成功
- tool_result 翻标志 → agent_settled 时 pi.exec(fire-and-forget)

## 设计
新 extension `commit-task-sync`:
1. 监听 `message_end`:检查 bash tool_result 含 `git commit` + exit 0 → 标志
2. 下轮 context injection:committedThisTurn + in_progress tasks → 提示
3. 最小侵入(提示 agent,不自动改 tasks)

## pi.on 可用事件
session_start/shutdown, model_select, agent_start/settled, message_start/update/end, turn_end, session_compact, session_tree

## 约束
- 不阻塞 tool pipeline(fire-and-forget,仿 post-edit)
- 不自动改 tasks(提示 agent 决策——agent 知道哪个 task 对应哪个 commit)
- 最小信任面(检测 commit 字符串,不执行任意命令)
77 changes: 77 additions & 0 deletions docs/knowledge-base/hook-design/commit-task-sync-implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# commit-task-sync extension 实现 + 验证

> openpi tasks 残留根治尝试。2026-08-11/12。
> **状态:PR 已提交**(tt-a1i/openpi#6),双通道验证通过。

## 问题
openpi tasks(session 级跟踪)无自动同步——commit/reviewer/授权后 tasks_update 靠 agent 手动记忆 → 遗忘必然(4 次复发:批次一 T3 + 批次二 T1/T4/T6)。

## 演进:v1 → v2(dual-channel)

### v1:ui.notify(TUI 提示,建议性)
- `pi.on("tool_result")`:检测 bash `git commit` 成功 → 翻标志
- `pi.on("agent_settled")`:`ctx.ui.notify("⚠️ git commit 检测到")` TUI 警告
- **局限**:ui.notify 是建议性(agent 可能在对话中忽略 TUI 提示)

### v2:context injection(对话注入,强制)——增强
- 加 `pi.on("context")`:commit 检测后下轮注入 `<commit-task-sync>` 块到 messages
- **仿 `injectTaskProjection`**(tasks/index.ts:483 `pi.on("context") return { messages }`)
- **agent 不能忽略**(在对话上下文,不是 TUI 建议性)
- 注入后 reset(仅提醒一次/commit)

## 双通道设计(v2 最终形态)

| 通道 | 事件 | 效果 | 强度 |
|---|---|---|---|
| 1 ui.notify | `agent_settled` | TUI 警告(user 看到) | 建议性 |
| 2 context injection | `context` | `<commit-task-sync>` 块注入下轮 messages(agent 看到) | **强制** |

**为什么双通道**:ui.notify alone agent 可能忽略;context injection alone user 看不到。双通道 = user + agent 都不可忽略,无盲区。

## Pattern(遵循 openpi 既有 extension 规范)
- `pi.on("tool_result")`:hot path,boolean flag only(no await/exec)—— same as post-edit's MUTATING_TOOLS
- `pi.on("agent_settled")`:debounce commit burst → single notify —— same as post-edit
- `pi.on("context")`:inject reminder → reset flag(remind once per commit)—— same as injectTaskProjection
- Trust surface: detect `git commit` regex + `!event.isError`;不执行命令,不自动改 tasks(agent decides)
- TUI only for notify(headless RPC skipped,like post-edit)

## 验证记录(2026-08-12)

### tsc
- v1: `npx tsc -p tsconfig.json --noEmit` → exit 0(类型通过)
- v2: 初版 TS2769(pi.on("context") overload 不匹配显式 event 类型)→ 修复(去掉显式 event 类型,仿 tasks:483 推断)→ exit 0

### v1 ui.notify 验证 ✅
- pi 重启加载 commit-task-sync(settings.json `"../../work/openpi-dev"` extension path)
- 执行 `echo "git commit test"`(bash 含 git commit + exit 0)
- **user 确认 TUI 看到**:「⚠️ git commit 检测到 — 请检查 tasks 状态同步」

### v2 context injection 验证 ✅
- pi 重启加载 v2
- 执行 `echo "git commit context injection test"`(触发 tool_result → commitDetected=true)
- **agent 确认下轮上下文看到 `<commit-task-sync>` 块**(在 messages 里,agent 不能忽略)
- tasks_list 响应提醒 → 无 items(session 重启后空,无残留——正确)

## openpi hook 机制发现(本次调查关键)

| 发现 | 意义 |
|---|---|
| openpi **有 `pi.on("tool_result")`** | PostToolUse 等价(之前 rg `PostToolUse` 没命中因事件名不同)|
| openpi **有 `pi.on("context")`** | context injection 入口(`return { messages }` 替换上下文)|
| **post-edit extension** 是最佳 hook 范本 | tool_result 翻标志 + agent_settled 执行 + fire-and-forget |
| **injectTaskProjection**(tasks:483)是 context injection 范本 | `pi.on("context") return { messages }` |

## pi.on 可用事件清单(本次整理)
`tool_result` / `agent_settled` / `context` / `session_start/shutdown` / `model_select` / `agent_start` / `message_start/update/end` / `turn_end` / `session_compact` / `session_tree`

## PR
- **tt-a1i/openpi#6**:https://github.com/tt-a1i/openpi/pull/6
- fork: agnitum2009/openpi:feat/commit-task-sync → tt-a1i/openpi:main
- 等上游 review/merge

## 文件
- `extensions/commit-task-sync/index.ts`(5557B,dual-channel)
- `docs/knowledge-base/`(obsidian vault: 本文件 + residual-root-cause + design + README)

## 结论
tasks 残留从「靠 agent 记忆力(4 次复发)」变「**机制强制提醒**(commit → 下轮 context 注入 `<commit-task-sync>` 块,agent 不能忽略)」。机制补强(PostToolUse hook),不靠 agent 完美记忆。
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# multi-signal-sync extension 实现 + 验证(MECE 完整方案)

> openpi tasks 残留根治,MECE 五类完成信号。2026-08-12。
> **状态:PR #6(tt-a1i/openpi),A/B/C 信号验证通过。**

## 第一性原理
tasks 状态(in_progress/blocked → done)应由**真实完成信号**驱动,非 agent 记忆。

## MECE 五类完成信号

| 信号 | 完成事件 | 信号来源 | 触发机制 | 验证 |
|---|---|---|---|---|
| A commit | git commit exit 0 | tool_result | COMMIT_PATTERNS 检测 | ✅ |
| B verify | tsc/test/verify PASS | tool_result | VERIFY_PATTERNS 检测 | ✅ |
| C authorization | 业主授权/裁定 | context user message | AUTHORIZATION_PATTERNS 检测 | ✅ |
| D 无变更完成 | 分析/设计结论 | 无信号 | 收口审计纪律 | ✅ 设计 |
| E 取消/吸收 | dropped/superseded | 无信号 | 收口审计纪律(agent 决策)| ✅ 设计 |

## 演进(commit-task-sync → multi-signal-sync)

- **v1 commit-task-sync**:A commit 单信号,ui.notify(瞬时)
- **v2 commit-task-sync**:+ context injection(`<commit-task-sync>` 块,agent 不可忽略)
- **v3 multi-signal-sync**:MECE A/B/C 三信号(tool_result + context),dual-channel
- **v4 修复**:分离 signals(context 注入)vs pendingNotify(agent_settled notify)——context 注入 reset signals 导致 notify 不触发的 bug
- **v5 驻留**:notify → footer setStatus(Persistent Status Indicator,跨 render 驻留显示)

## 验证记录(2026-08-12)

### A commit(v2 commit-task-sync 验证)
- 用户确认 TUI notify「⚠️ git commit 检测到」
- agent 确认 `<commit-task-sync>` 块注入(context injection)

### B verify + C authorization(v4 multi-signal-sync 验证)
- 用户发「授权」→ context 检测 → authorization 信号
- **用户确认 notify「检测到完成信号(commit + 验证通过 + 业主授权)」—— A/B/C 三信号同时触发**

### v5 驻留(footer setStatus)
- 瞬时 notify → footer setStatus(`ctx.ui.setStatus("multi-signal-sync", ...)`)
- 跨 render 驻留显示(tui.md Pattern 4: Persistent Status Indicator)
- 下轮无新信号清除(`setStatus(undefined)`)
- **待重启加载验证 footer 驻留**

## 关键机制发现(openpi)

| 发现 | 意义 |
|---|---|
| `pi.on("tool_result")` | PostToolUse 等价(检测工具成功)|
| `pi.on("context")` return { messages } | context injection(注入 agent 上下文)|
| `ctx.ui.setStatus(ext, text)` | footer 驻留状态(跨 render,非瞬时 notify)|
| `emitter.on(channel, safeHandler)` | EventEmitter 多 handler(多 extension 不冲突)|
| `ctx.ui.setWidget(key, ...)` | 驻留面板(tasks 的 session-tasks-panel)|

## PR
- **tt-a1i/openpi#6**(feat/commit-task-sync,含 multi-signal-sync v5)
- fork: agnitum2009/openpi → tt-a1i/openpi

## 结论
tasks 残留从「靠 agent 记忆力(4 次复发)」变「**机制强制提醒(MECE 三信号:commit/验证/授权)+ 驻留显示(footer setStatus)+ context 注入(agent 不可忽略)**」。
21 changes: 21 additions & 0 deletions docs/knowledge-base/tasks-mechanism/residual-root-cause.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# tasks 残留根因分析

## 现象
- 批次一 T3(刀6 申诉):commit 264af92 + sa-1 复核,但漏标 done
- 批次二 T1(刀1):note 更新但 status pending(半同步)
- 批次二 T4(刀4):blocked→业主授权落码 2c933d9,但 T4 仍 blocked
- 批次二 T6(刀6):blocked→ADR-0009 落地 b9a9341,但 T6 仍 blocked

## 根因(双重)
1. **遗忘(直接)**:commit/reviewer/授权后没 tasks_update
2. **机制不全(根本)**:openpi tasks 无自动同步 hook → 遗忘必然 + 无兜底

## 机制缺口(5 点)
1. 无 commit→tasks 自动同步 hook
2. 无 tasks 使用规范文档
3. session 级不持久(compaction 重置)
4. batch 关闭清空不可追溯
5. 无完成证据约束(status 转换无 commit SHA 强制)

## 改进方向
commit→tasks hook(仿 post-edit extension,agent_settled 检测 git commit → 残留提示)
142 changes: 142 additions & 0 deletions extensions/multi-signal-sync/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import assert from "node:assert/strict";
import test from "node:test";
import type {
ExtensionAPI,
ExtensionContext,
} from "@earendil-works/pi-coding-agent";
import multiSignalSync from "./index.ts";

type Handler = (event: any, ctx: ExtensionContext) => unknown;
type Message = {
role: string;
content: string | Array<{ type: string; text: string }>;
};

function harness() {
const handlers = new Map<string, Handler[]>();
const statuses: Array<string | undefined> = [];
const ctx = {
mode: "tui",
hasUI: true,
ui: {
setStatus(_key: string, value?: string) {
statuses.push(value);
},
},
} as unknown as ExtensionContext;
const pi = {
on(event: string, handler: Handler) {
handlers.set(event, [...(handlers.get(event) ?? []), handler]);
},
} as unknown as ExtensionAPI;
multiSignalSync(pi);

const emit = async (event: string, value: unknown = {}) => {
let current = value;
for (const handler of handlers.get(event) ?? []) {
const result = await handler(current, ctx);
if (
event === "context" &&
result &&
typeof result === "object" &&
"messages" in result
) {
current = {
...(current as object),
messages: (result as { messages: unknown }).messages,
};
}
}
return current as { messages?: Message[] };
};
return { emit, statuses };
}

function injectedText(messages: Message[] | undefined): string {
const content = messages?.at(-1)?.content;
return Array.isArray(content)
? content.map((block) => block.text).join("\n")
: String(content ?? "");
}

test("authorization is consumed once for one user message", async () => {
const h = harness();
const messages: Message[] = [{ role: "user", content: "我同意这个变更" }];
const first = await h.emit("context", { messages });
assert.match(injectedText(first.messages), /业主授权/);

const second = await h.emit("context", { messages });
assert.equal(second.messages, messages);
});

test("negated authorization does not trigger", async () => {
for (const content of [
"我不同意这个变更",
"我未同意这个变更",
"这个操作未经授权",
"not approved",
"文档示例是“我同意这个变更”",
]) {
const h = harness();
const messages: Message[] = [{ role: "user", content }];
const result = await h.emit("context", { messages });
assert.equal(result.messages, messages);
}
});

test("a new authorization message can trigger after the previous one", async () => {
const h = harness();
const first: Message[] = [{ role: "user", content: "我同意方案 A" }];
await h.emit("context", { messages: first });

const second: Message[] = [
...first,
{ role: "user", content: "我批准方案 B" },
];
const result = await h.emit("context", { messages: second });
assert.match(injectedText(result.messages), /业主授权/);
});

test("command detection ignores quoted examples and dry runs", async () => {
for (const command of [
'echo "git commit -m test"',
'echo "example; git commit -m test"',
"git commit --dry-run",
'echo "tsc --noEmit"',
'printf -- "--test"',
]) {
const h = harness();
await h.emit("tool_result", {
toolName: "bash",
isError: false,
input: { command },
});
await h.emit("agent_settled");
assert.equal(h.statuses.length, 0, command);
}
});

test("failed verification does not trigger", async () => {
const h = harness();
await h.emit("tool_result", {
toolName: "bash",
isError: true,
input: { command: "bun run test" },
});
await h.emit("agent_settled");
assert.equal(h.statuses.length, 0);
});

test("session shutdown clears a persistent footer status", async () => {
const h = harness();
await h.emit("tool_result", {
toolName: "bash",
isError: false,
input: { command: "git commit -m test" },
});
await h.emit("agent_settled");
assert.match(h.statuses.at(-1) ?? "", /commit/);

await h.emit("session_shutdown");
assert.equal(h.statuses.at(-1), undefined);
});
Loading