pi-pier 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -5
- package/package.json +1 -1
- package/src/core/grid-shape.ts +10 -5
- package/src/core/subagent.ts +297 -96
- package/src/core/todo.ts +150 -69
- package/src/herdr-client.ts +6 -3
- package/src/history-store.ts +10 -0
- package/src/index.ts +72 -10
- package/src/pane-title.ts +11 -1
- package/src/pipe-channel.ts +3 -0
- package/src/settle-wake-core.ts +54 -0
- package/src/slim-frame.ts +219 -0
- package/src/stale-core.ts +76 -0
- package/src/subagent-core.ts +100 -0
- package/src/todo-core.ts +66 -15
- package/src/todo-read-hook.ts +122 -20
- package/src/todos-service.ts +13 -3
package/src/todo-read-hook.ts
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* D69:todo 阅读钩子规划(before_agent_start + display:false)。
|
|
3
|
+
* 反冻结(stale-core):全完成列表过期后不再复读——
|
|
4
|
+
* - stale(turns 维度)→ 警告 + 旧条目(供改写参照),每 N 轮一次、封顶;
|
|
5
|
+
* - archived(墙钟维度)→ 按不存在处理,归档通知与空列表共用守卫节奏。
|
|
3
6
|
*/
|
|
4
7
|
import { boundedView, currentActivity, type TodoItem } from './todo-core.ts';
|
|
5
8
|
import { countTodos } from './vocab.ts';
|
|
9
|
+
import { STALE_NOTICE_MAX, evaluateStaleness, formatAge } from './stale-core.ts';
|
|
6
10
|
|
|
11
|
+
/** 空守卫 / stale 警告 / 归档通知共用的注入节奏(每 N 轮)。 */
|
|
7
12
|
export const EMPTY_GUARD_EVERY_N = 4;
|
|
8
13
|
export const TODO_READ_CUSTOM_TYPE = 'pi-herdr.todo-read';
|
|
9
14
|
|
|
15
|
+
export type TodoReadEffect =
|
|
16
|
+
| 'recite' // 正常复读(列表新鲜)
|
|
17
|
+
| 'empty-guard' // 空列表守卫
|
|
18
|
+
| 'stale-notice' // A:turns 维度过期警告
|
|
19
|
+
| 'archive-notice' // B:墙钟维度归档通知
|
|
20
|
+
| 'none'; // 限频/封顶跳过
|
|
21
|
+
|
|
10
22
|
export interface TodoReadPlan {
|
|
11
23
|
inject: boolean;
|
|
24
|
+
effect: TodoReadEffect;
|
|
25
|
+
/** 当前归档态(与 inject 无关;调用方据此刷新 widget/title 投影)。 */
|
|
26
|
+
archived: boolean;
|
|
27
|
+
/** R1:归档通知本次注入即清空内存列表(调用方执行 todos.replace([]) + rm 全量
|
|
28
|
+
* 落 JSONL——死列表挡住空守卫的 before-stopping 驱动,01a03c0d 实证归档后
|
|
29
|
+
* 2h 多步工作零跟踪)。 */
|
|
30
|
+
clearArchived: boolean;
|
|
12
31
|
message: {
|
|
13
32
|
customType: string;
|
|
14
33
|
content: string;
|
|
@@ -16,39 +35,122 @@ export interface TodoReadPlan {
|
|
|
16
35
|
};
|
|
17
36
|
}
|
|
18
37
|
|
|
38
|
+
const MARKS: Record<TodoItem['status'], string> = {
|
|
39
|
+
pending: '○',
|
|
40
|
+
in_progress: '▶',
|
|
41
|
+
completed: '✓',
|
|
42
|
+
blocked: '■',
|
|
43
|
+
abandoned: '✗',
|
|
44
|
+
};
|
|
45
|
+
|
|
19
46
|
export function planTodoReadHook(opts: {
|
|
20
47
|
items: readonly TodoItem[];
|
|
21
48
|
turn: number;
|
|
22
49
|
lastEmptyGuardTurn: number | null;
|
|
50
|
+
/** stale-core 时钟锚点(TodosService.lastWriteAt)。 */
|
|
51
|
+
lastWriteAt: number | null;
|
|
52
|
+
/** 距上次 todo 写入的轮数(本进程内已知时)。 */
|
|
53
|
+
turnsSinceWrite: number | null;
|
|
54
|
+
now: number;
|
|
55
|
+
/** 本停滞期已注入的 stale 警告数。 */
|
|
56
|
+
staleNotices: number;
|
|
57
|
+
lastStaleGuardTurn: number | null;
|
|
23
58
|
}): TodoReadPlan {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
59
|
+
const msg = (content: string) => ({
|
|
60
|
+
customType: TODO_READ_CUSTOM_TYPE,
|
|
61
|
+
content,
|
|
62
|
+
display: false as const,
|
|
63
|
+
});
|
|
64
|
+
const guardDue = opts.lastEmptyGuardTurn == null
|
|
65
|
+
|| opts.turn - opts.lastEmptyGuardTurn >= EMPTY_GUARD_EVERY_N;
|
|
66
|
+
|
|
67
|
+
if (opts.items.length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
inject: guardDue,
|
|
70
|
+
effect: 'empty-guard',
|
|
71
|
+
clearArchived: false,
|
|
72
|
+
archived: false,
|
|
73
|
+
message: msg(
|
|
74
|
+
guardDue
|
|
75
|
+
? 'Your todo list is empty. If the current work needs tracking, call todo_write before stopping.'
|
|
76
|
+
: '',
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const st = evaluateStaleness({
|
|
82
|
+
items: opts.items,
|
|
83
|
+
lastWriteAt: opts.lastWriteAt,
|
|
84
|
+
turnsSinceWrite: opts.turnsSinceWrite,
|
|
85
|
+
now: opts.now,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (st.kind === 'archived') {
|
|
89
|
+
const c = countTodos(opts.items as TodoItem[]);
|
|
90
|
+
const age = st.ageMs == null ? '' : ` ${formatAge(st.ageMs)} ago`;
|
|
91
|
+
// R2:重写窗口——本停滞期还没给过任何 stale 警告时,先给一次带旧条目参照的
|
|
92
|
+
// 重写机会(idle 期不消耗 turn,墙钟先到 1h 而 turns 警告从未触发,
|
|
93
|
+
// 01a03c0d 实证:回来后直接吃终态通知,todo_write 5 次被无视)。
|
|
94
|
+
if (opts.staleNotices === 0 && guardDue) {
|
|
95
|
+
const lines = opts.items.map((it) => ` ${MARKS[it.status]} ${it.content}`);
|
|
96
|
+
const head = `todos ✓${c.completed} (all completed, last updated${age}) — about to be archived`;
|
|
97
|
+
const warn = 'One rewrite window before archiving: if the work you are doing now is multi-step, rewrite the full list with todo_write to track it (old entries below as reference — reuse what still applies). Single-step Q&A may skip tracking. If the list stays untouched, it will be archived and cleared on the next reminder.';
|
|
98
|
+
return {
|
|
99
|
+
inject: true,
|
|
100
|
+
effect: 'stale-notice',
|
|
101
|
+
archived: true,
|
|
102
|
+
clearArchived: false,
|
|
103
|
+
message: msg([head, ...lines, warn].join('\n')),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// B + R1/R3:终态通知——明细不再注入,本次注入即清空;去掉 [] 豁免出口,
|
|
107
|
+
// 多步工作必须建清单(单步问答明确放行)。
|
|
108
|
+
return {
|
|
109
|
+
inject: guardDue,
|
|
110
|
+
effect: 'archive-notice',
|
|
111
|
+
archived: true,
|
|
112
|
+
clearArchived: guardDue,
|
|
113
|
+
message: msg(
|
|
114
|
+
guardDue
|
|
115
|
+
? `Your previous todo list (${c.completed} completed entries, last updated${age}) has been archived and cleared from tracking; the list is now empty. If the current work is multi-step, call todo_write with a fresh list now — multi-step work must be tracked. Single-step answers may proceed without a list.`
|
|
116
|
+
: '',
|
|
117
|
+
),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (st.kind === 'stale') {
|
|
122
|
+
// A:复读改警告;保留旧条目行供模型改写参照。封顶 STALE_NOTICE_MAX。
|
|
123
|
+
const staleDue = opts.lastStaleGuardTurn == null
|
|
124
|
+
|| opts.turn - opts.lastStaleGuardTurn >= EMPTY_GUARD_EVERY_N;
|
|
125
|
+
if (!staleDue || opts.staleNotices >= STALE_NOTICE_MAX) {
|
|
126
|
+
return { inject: false, effect: 'none', archived: false, clearArchived: false, message: msg('') };
|
|
127
|
+
}
|
|
128
|
+
const c = countTodos(opts.items as TodoItem[]);
|
|
129
|
+
const head = `todos ▶${c.inProgress} ○${c.pending} ■${c.blocked} ✓${c.completed}`
|
|
130
|
+
+ ` · unchanged for ${opts.turnsSinceWrite} turns, nothing open`;
|
|
131
|
+
const lines = opts.items.map((it) => ` ${MARKS[it.status]} ${it.content}`);
|
|
132
|
+
const warn = 'This list no longer reflects the work you are doing. Rewrite the full list with todo_write to match current work, or send [] to clear it.';
|
|
28
133
|
return {
|
|
29
|
-
inject:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
134
|
+
inject: true,
|
|
135
|
+
effect: 'stale-notice',
|
|
136
|
+
clearArchived: false,
|
|
137
|
+
archived: false,
|
|
138
|
+
message: msg([head, ...lines, warn].join('\n')),
|
|
35
139
|
};
|
|
36
140
|
}
|
|
141
|
+
|
|
142
|
+
// fresh:原有每轮复读。
|
|
37
143
|
const c = countTodos(opts.items);
|
|
38
144
|
const view = boundedView(opts.items, 6);
|
|
39
|
-
const lines = view.visible.map((it) => {
|
|
40
|
-
|
|
41
|
-
return `${mark} ${it.content}`;
|
|
42
|
-
});
|
|
43
|
-
const activity = currentActivity(opts.items as TodoItem[]);
|
|
145
|
+
const lines = view.visible.map((it) => `${MARKS[it.status]} ${it.content}`);
|
|
146
|
+
const activity = currentActivity(opts.items);
|
|
44
147
|
const head = `todos ▶${c.inProgress} ○${c.pending} ■${c.blocked} ✓${c.completed}`
|
|
45
148
|
+ (activity ? ` · ${activity}` : '');
|
|
46
149
|
return {
|
|
47
150
|
inject: true,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
},
|
|
151
|
+
effect: 'recite',
|
|
152
|
+
clearArchived: false,
|
|
153
|
+
archived: false,
|
|
154
|
+
message: msg([head, ...lines].join('\n')),
|
|
53
155
|
};
|
|
54
156
|
}
|
package/src/todos-service.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { EventEmitter } from 'node:events';
|
|
7
7
|
import {
|
|
8
8
|
applyTodoEdits,
|
|
9
|
-
|
|
9
|
+
foldLatestTodosMeta,
|
|
10
10
|
type TodoEdit,
|
|
11
11
|
type TodoItem,
|
|
12
12
|
} from './todo-core.ts';
|
|
@@ -20,6 +20,11 @@ export interface TodosConfig {
|
|
|
20
20
|
|
|
21
21
|
export class TodosService extends EventEmitter {
|
|
22
22
|
items: TodoItem[] = [];
|
|
23
|
+
/**
|
|
24
|
+
* 列表最后一次真实变更时间(epoch ms);分支重建取自条目时间戳,无则 null。
|
|
25
|
+
* 陈旧度判定(stale-core)的时钟锚点;null → 保守永不判 stale。
|
|
26
|
+
*/
|
|
27
|
+
lastWriteAt: number | null = null;
|
|
23
28
|
readonly config: TodosConfig;
|
|
24
29
|
|
|
25
30
|
constructor(config: TodosConfig) {
|
|
@@ -38,6 +43,7 @@ export class TodosService extends EventEmitter {
|
|
|
38
43
|
replace(items: readonly TodoItem[]): void {
|
|
39
44
|
const before = this.items;
|
|
40
45
|
this.items = items.map((it) => ({ ...it }));
|
|
46
|
+
this.lastWriteAt = Date.now();
|
|
41
47
|
this.emitCompletedTransitions(before, this.items);
|
|
42
48
|
this.emit('todo.updated', { items: this.items });
|
|
43
49
|
}
|
|
@@ -45,6 +51,7 @@ export class TodosService extends EventEmitter {
|
|
|
45
51
|
applyEdits(edits: readonly TodoEdit[]): void {
|
|
46
52
|
const before = this.items;
|
|
47
53
|
this.items = applyTodoEdits(this.items, edits);
|
|
54
|
+
this.lastWriteAt = Date.now();
|
|
48
55
|
this.emitCompletedTransitions(before, this.items);
|
|
49
56
|
this.emit('todo.edited', { edits, items: this.items });
|
|
50
57
|
this.emit('todo.updated', { items: this.items });
|
|
@@ -59,8 +66,11 @@ export class TodosService extends EventEmitter {
|
|
|
59
66
|
rebuild(entries: readonly unknown[]): void {
|
|
60
67
|
// 与 v1.x rebuildFromBranch 同语义:分支上折叠不到快照时保留现值
|
|
61
68
|
//(重建失败不影响主流程;下次 todo_write 会重新锚定)。
|
|
62
|
-
const folded =
|
|
63
|
-
if (folded)
|
|
69
|
+
const folded = foldLatestTodosMeta(entries as Parameters<typeof foldLatestTodosMeta>[0]);
|
|
70
|
+
if (folded) {
|
|
71
|
+
this.items = folded.items;
|
|
72
|
+
this.lastWriteAt = folded.writtenAt;
|
|
73
|
+
}
|
|
64
74
|
this.emit('todo.updated', { items: this.items, reason: 'rebuild' });
|
|
65
75
|
}
|
|
66
76
|
}
|