pi-pier 0.1.4 → 0.2.0
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/package.json +1 -1
- package/schemas/role-manifest.schema.json +1 -2
- package/src/bootstrap.ts +18 -22
- package/src/core/grid-shape.ts +13 -14
- package/src/core/subagent.ts +325 -857
- package/src/core/terminal.ts +25 -28
- package/src/core/todo.ts +47 -53
- package/src/gc-core.ts +15 -15
- package/src/git-adapter.ts +104 -0
- package/src/herdr-client.ts +76 -68
- package/src/history-store.ts +70 -36
- package/src/index-locks.ts +105 -0
- package/src/index-master.ts +113 -0
- package/src/index-notices.ts +43 -0
- package/src/index-pipe.ts +73 -0
- package/src/index-runtime.ts +23 -0
- package/src/index-worker.ts +39 -0
- package/src/index.ts +134 -386
- package/src/ledger.ts +15 -17
- package/src/lock-core.ts +15 -29
- package/src/manifest-compose.ts +9 -23
- package/src/notice-buffer.ts +9 -14
- package/src/pane-title.ts +24 -24
- package/src/pi-surface.ts +22 -29
- package/src/pipe-channel.ts +67 -34
- package/src/platform-paths.ts +72 -0
- package/src/progress-core.ts +26 -24
- package/src/reconcile-core.ts +25 -24
- package/src/role-loader.ts +32 -35
- package/src/role-manifest.ts +24 -28
- package/src/roles/master.json +2 -2
- package/src/roles/worker-default.json +3 -3
- package/src/runtime-policy.ts +66 -0
- package/src/session-tail.ts +46 -45
- package/src/settle-wake-core.ts +19 -18
- package/src/slim-frame.ts +45 -44
- package/src/stale-core.ts +23 -21
- package/src/storage-layout.ts +80 -0
- package/src/subagent-core.ts +151 -94
- package/src/subagent-gc.ts +237 -0
- package/src/subagent-git-io.ts +80 -0
- package/src/subagent-launch.ts +110 -0
- package/src/subagent-poll-loop.ts +285 -0
- package/src/subagent-poller.ts +90 -0
- package/src/subagent-port.ts +27 -0
- package/src/subagent-scope.ts +4 -4
- package/src/subagent-session-io.ts +143 -0
- package/src/subagent-spawn.ts +156 -0
- package/src/terminal-core.ts +56 -54
- package/src/todo-core.ts +27 -64
- package/src/todo-read-hook.ts +28 -24
- package/src/todo-reminder-core.ts +89 -0
- package/src/todos-service.ts +36 -25
- package/src/tool-gate.ts +7 -28
- package/src/vocab.ts +18 -19
package/src/todo-read-hook.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* D69
|
|
3
|
-
*
|
|
4
|
-
* - stale
|
|
5
|
-
* - archived
|
|
2
|
+
* D69: Plan the todo read hook (before_agent_start + display:false).
|
|
3
|
+
* Stale-core prevents a fully completed list from being recited forever:
|
|
4
|
+
* - stale (turn-based) → warn with old entries for rewriting, rate-limited and capped;
|
|
5
|
+
* - archived (wall-clock based) → treat it as absent, sharing the guard cadence with empty lists.
|
|
6
6
|
*/
|
|
7
7
|
import { boundedView, currentActivity, type TodoItem } from './todo-core.ts';
|
|
8
8
|
import { countTodos } from './vocab.ts';
|
|
9
9
|
import { STALE_NOTICE_MAX, evaluateStaleness, formatAge } from './stale-core.ts';
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/** Share one injection cadence across empty-list guards, stale warnings, and archive notices. */
|
|
12
12
|
export const EMPTY_GUARD_EVERY_N = 4;
|
|
13
13
|
export const TODO_READ_CUSTOM_TYPE = 'pi-herdr.todo-read';
|
|
14
14
|
|
|
15
15
|
export type TodoReadEffect =
|
|
16
|
-
| 'recite' //
|
|
17
|
-
| 'empty-guard' //
|
|
18
|
-
| 'stale-notice' // A
|
|
19
|
-
| 'archive-notice' // B
|
|
20
|
-
| 'none'; //
|
|
16
|
+
| 'recite' // Recite normally while the list is fresh.
|
|
17
|
+
| 'empty-guard' // Guard against an empty list.
|
|
18
|
+
| 'stale-notice' // A: warn when the list is stale by turns.
|
|
19
|
+
| 'archive-notice' // B: notify when the list is archived by wall-clock age.
|
|
20
|
+
| 'none'; // Skip because of rate limits or caps.
|
|
21
21
|
|
|
22
22
|
export interface TodoReadPlan {
|
|
23
23
|
inject: boolean;
|
|
24
24
|
effect: TodoReadEffect;
|
|
25
|
-
/**
|
|
25
|
+
/** Current archive state, independent of injection, so callers can refresh widget/title projections. */
|
|
26
26
|
archived: boolean;
|
|
27
|
-
/** R1
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
/** R1: Clear the in-memory list when injecting the archive notice; callers persist the empty JSONL
|
|
28
|
+
* so a dead list cannot block the before-stopping empty guard (01a03c0d observed 2h of untracked
|
|
29
|
+
* multi-step work after archiving). */
|
|
30
30
|
clearArchived: boolean;
|
|
31
31
|
message: {
|
|
32
32
|
customType: string;
|
|
@@ -47,12 +47,12 @@ export function planTodoReadHook(opts: {
|
|
|
47
47
|
items: readonly TodoItem[];
|
|
48
48
|
turn: number;
|
|
49
49
|
lastEmptyGuardTurn: number | null;
|
|
50
|
-
/** stale-core
|
|
50
|
+
/** stale-core clock anchor (TodosService.lastWriteAt). */
|
|
51
51
|
lastWriteAt: number | null;
|
|
52
|
-
/**
|
|
52
|
+
/** Turns since the last todo write, when known in this process. */
|
|
53
53
|
turnsSinceWrite: number | null;
|
|
54
54
|
now: number;
|
|
55
|
-
/**
|
|
55
|
+
/** Number of stale warnings injected during the current idle period. */
|
|
56
56
|
staleNotices: number;
|
|
57
57
|
lastStaleGuardTurn: number | null;
|
|
58
58
|
}): TodoReadPlan {
|
|
@@ -88,9 +88,12 @@ export function planTodoReadHook(opts: {
|
|
|
88
88
|
if (st.kind === 'archived') {
|
|
89
89
|
const c = countTodos(opts.items as TodoItem[]);
|
|
90
90
|
const age = st.ageMs == null ? '' : ` ${formatAge(st.ageMs)} ago`;
|
|
91
|
-
// R2
|
|
92
|
-
//
|
|
93
|
-
// 01a03c0d
|
|
91
|
+
// R2: Preserve one rewrite window with old entries as a reference before archiving.
|
|
92
|
+
// Idle periods do not consume turns, so the wall clock can reach one hour before a turns-based
|
|
93
|
+
// warning fires; 01a03c0d showed that the final notice then caused five todo_write calls to be ignored.
|
|
94
|
+
// B + R1/R3: The terminal notice no longer injects details and clears the list in the same
|
|
95
|
+
// injection; removing the [] escape hatch ensures multi-step work gets tracked while single-step
|
|
96
|
+
// Q&A remains explicitly allowed.
|
|
94
97
|
if (opts.staleNotices === 0 && guardDue) {
|
|
95
98
|
const lines = opts.items.map((it) => ` ${MARKS[it.status]} ${it.content}`);
|
|
96
99
|
const head = `todos ✓${c.completed} (all completed, last updated${age}) — about to be archived`;
|
|
@@ -103,8 +106,9 @@ export function planTodoReadHook(opts: {
|
|
|
103
106
|
message: msg([head, ...lines, warn].join('\n')),
|
|
104
107
|
};
|
|
105
108
|
}
|
|
106
|
-
// B + R1/R3
|
|
107
|
-
//
|
|
109
|
+
// B + R1/R3: The terminal notice no longer injects details and clears the list in the same
|
|
110
|
+
// injection; removing the [] escape hatch ensures multi-step work gets tracked while single-step
|
|
111
|
+
// Q&A remains explicitly allowed.
|
|
108
112
|
return {
|
|
109
113
|
inject: guardDue,
|
|
110
114
|
effect: 'archive-notice',
|
|
@@ -119,7 +123,7 @@ export function planTodoReadHook(opts: {
|
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
if (st.kind === 'stale') {
|
|
122
|
-
// A
|
|
126
|
+
// A: Turn recitation into a warning while retaining old entries as rewrite references; cap at STALE_NOTICE_MAX.
|
|
123
127
|
const staleDue = opts.lastStaleGuardTurn == null
|
|
124
128
|
|| opts.turn - opts.lastStaleGuardTurn >= EMPTY_GUARD_EVERY_N;
|
|
125
129
|
if (!staleDue || opts.staleNotices >= STALE_NOTICE_MAX) {
|
|
@@ -139,7 +143,7 @@ export function planTodoReadHook(opts: {
|
|
|
139
143
|
};
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
//
|
|
146
|
+
// Fresh lists are recited every turn to keep current work visible.
|
|
143
147
|
const c = countTodos(opts.items);
|
|
144
148
|
const view = boundedView(opts.items, 6);
|
|
145
149
|
const lines = view.visible.map((it) => `${MARKS[it.status]} ${it.content}`);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision core for reminders about unfinished todos when stopping (D41 revision 2;
|
|
3
|
+
* evidenced by session 01a040cc).
|
|
4
|
+
*
|
|
5
|
+
* Evidence: the model's closing prose said that pushing should be left to the user,
|
|
6
|
+
* yet the todo list still contained `[pending] push to repository`. The old D41 injected
|
|
7
|
+
* “Continue working on them before stopping” as a user-role message 116 ms after agent_settled;
|
|
8
|
+
* that channel overrode the model's judgment from 20 seconds earlier, and git push ran within
|
|
9
|
+
* 12 seconds without authorization.
|
|
10
|
+
*
|
|
11
|
+
* Revision 3 keeps the decision and wording here while the timing grace period is wired in
|
|
12
|
+
* core/subagent:
|
|
13
|
+
* - use sendMessage(custom), not the user role, so the reminder source is identifiable rather than impersonating the user;
|
|
14
|
+
* - replace “keep working” with a reconciliation request, making human-blocked items a first-class
|
|
15
|
+
* outcome alongside continuing authorized work and deleting obsolete entries;
|
|
16
|
+
* - preserve the guard matrix: suppress on abort, cap reminders, and skip while a subagent is
|
|
17
|
+
* running, a human gate is blocked, or no open items remain.
|
|
18
|
+
*/
|
|
19
|
+
import { ABORT_STOP_REASON } from './settle-wake-core.ts';
|
|
20
|
+
|
|
21
|
+
/** Cap reminders for the lifetime of the process, matching the previous behavior. */
|
|
22
|
+
export const TODO_REMINDERS_MAX = 3;
|
|
23
|
+
|
|
24
|
+
/** Custom message type for reminder injection (registerMessageRenderer can customize its TUI appearance). */
|
|
25
|
+
export const TODO_REMINDER_CUSTOM_TYPE = 'pi-herdr.todo-reminder';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* B3 grace window (milliseconds): delay from settled to injection, giving the user a chance to
|
|
29
|
+
* read the closing answer and intervene; starting any agent during the window cancels it. This
|
|
30
|
+
* follows the MACHINE_INJECT_GRACE_MS pattern; PI_HERDR_TODO_GRACE_MS is read per call so tests
|
|
31
|
+
* can use a small value rather than freezing configuration at module load.
|
|
32
|
+
*/
|
|
33
|
+
export function todoReminderGraceMs(): number {
|
|
34
|
+
return Number(process.env.PI_HERDR_TODO_GRACE_MS ?? 30_000) || 30_000;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TodoReminderInput {
|
|
38
|
+
/** Stop reason of the last assistant turn before this settlement (null means treat it as natural completion). */
|
|
39
|
+
lastStopReason: string | null;
|
|
40
|
+
/** Number of reminders already injected. */
|
|
41
|
+
reminders: number;
|
|
42
|
+
/** Number of running background subagents; a positive value means the master is already waiting. */
|
|
43
|
+
runningSubs: number;
|
|
44
|
+
/** ask_user_question wait depth; a positive value means the master is waiting for a human response. */
|
|
45
|
+
blockedDepth: number;
|
|
46
|
+
/** Current todo list. */
|
|
47
|
+
items: ReadonlyArray<{ content: string; status: string }>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TodoReminderPlan {
|
|
51
|
+
/** Whether a reminder should be injected. */
|
|
52
|
+
due: boolean;
|
|
53
|
+
/** Complete injected content, or null when due is false. */
|
|
54
|
+
content: string | null;
|
|
55
|
+
/** New count after successful injection, or the original count when due is false. */
|
|
56
|
+
nextReminders: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function noInject(reminders: number): TodoReminderPlan {
|
|
60
|
+
return { due: false, content: null, nextReminders: reminders };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A stop reminder is due only when every guard passes and at least one open (pending/in_progress)
|
|
65
|
+
* item exists. blocked, abandoned, and completed items are not unfinished: once human-waiting work
|
|
66
|
+
* is modeled correctly, reminding again would only pressure the model to act without authorization
|
|
67
|
+
* (the missing guard in the 01a040cc incident).
|
|
68
|
+
*/
|
|
69
|
+
export function planStopTodoReminder(input: TodoReminderInput): TodoReminderPlan {
|
|
70
|
+
if (input.lastStopReason === ABORT_STOP_REASON) return noInject(input.reminders);
|
|
71
|
+
if (input.reminders >= TODO_REMINDERS_MAX) return noInject(input.reminders);
|
|
72
|
+
if (input.runningSubs > 0) return noInject(input.reminders);
|
|
73
|
+
if (input.blockedDepth > 0) return noInject(input.reminders);
|
|
74
|
+
const open = input.items.filter((it) => it.status === 'pending' || it.status === 'in_progress');
|
|
75
|
+
if (open.length === 0) return noInject(input.reminders);
|
|
76
|
+
const list = open
|
|
77
|
+
.map((it) => (it.status === 'in_progress' ? `▶ ${it.content}` : `· ${it.content}`))
|
|
78
|
+
.join('\n');
|
|
79
|
+
const content = [
|
|
80
|
+
`<system-reminder>You stopped with unfinished todos (Reminder ${input.reminders + 1}/${TODO_REMINDERS_MAX}):`,
|
|
81
|
+
list,
|
|
82
|
+
'Reconcile the list instead of blindly continuing:',
|
|
83
|
+
'- work that is yours AND already authorized by the user → continue it;',
|
|
84
|
+
'- work waiting on a human (decision / approval / ops action) → mark it blocked with a blocker note, and ask the user via ask_user_question when you need their input — never execute it yourself just to clear the list;',
|
|
85
|
+
'- entries that no longer apply → remove them.',
|
|
86
|
+
'Update the list with todo_write in any case.</system-reminder>',
|
|
87
|
+
].join('\n');
|
|
88
|
+
return { due: true, content, nextReminders: input.reminders + 1 };
|
|
89
|
+
}
|
package/src/todos-service.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 档 0:todos 升为进程内 Service(D65 / D75 阶段 2)。
|
|
3
|
-
* 持久化仍是 pi 会话分支重放;D75 阶段 3:config 从 role manifest services 注入
|
|
4
|
-
* (无 manifest 的 master / 纯标签 worker 走默认——worker serial、master parallel)。
|
|
5
|
-
*/
|
|
1
|
+
/** Why: Preserve the established compatibility and safety behavior (D65, D75). */
|
|
6
2
|
import { EventEmitter } from 'node:events';
|
|
7
3
|
import {
|
|
8
4
|
applyTodoEdits,
|
|
9
5
|
foldLatestTodosMeta,
|
|
6
|
+
listsEqual,
|
|
10
7
|
type TodoEdit,
|
|
11
8
|
type TodoItem,
|
|
12
9
|
} from './todo-core.ts';
|
|
@@ -19,11 +16,14 @@ export interface TodosConfig {
|
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
export class TodosService extends EventEmitter {
|
|
22
|
-
|
|
19
|
+
private _items: TodoItem[] = [];
|
|
23
20
|
/**
|
|
24
|
-
*
|
|
25
|
-
* 陈旧度判定(stale-core)的时钟锚点;null → 保守永不判 stale。
|
|
21
|
+
* Live list (read-only). Mutations must go through replace/applyEdits/rebuild.
|
|
26
22
|
*/
|
|
23
|
+
get items(): readonly TodoItem[] {
|
|
24
|
+
return this._items;
|
|
25
|
+
}
|
|
26
|
+
/** Why: Preserve the established compatibility and safety behavior. */
|
|
27
27
|
lastWriteAt: number | null = null;
|
|
28
28
|
readonly config: TodosConfig;
|
|
29
29
|
|
|
@@ -32,7 +32,7 @@ export class TodosService extends EventEmitter {
|
|
|
32
32
|
this.config = config;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
35
|
+
/** Why: Preserve the established compatibility and safety behavior (D75). */
|
|
36
36
|
static configFromRuntime(manifest: RuntimeRoleManifest | null, isSubagent: boolean): TodosConfig {
|
|
37
37
|
const strict = isSubagent;
|
|
38
38
|
const mode = manifest?.services?.todos?.mode;
|
|
@@ -40,37 +40,48 @@ export class TodosService extends EventEmitter {
|
|
|
40
40
|
return { strict, allowParallelInProgress };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.
|
|
43
|
+
/** Defensive copy for callers that must not mutate service state. */
|
|
44
|
+
getSnapshot(): TodoItem[] {
|
|
45
|
+
return this._items.map((it) => ({ ...it }));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
replace(items: readonly TodoItem[]): { changed: boolean } {
|
|
49
|
+
const next = items.map((it) => ({ ...it }));
|
|
50
|
+
if (listsEqual(this._items, next)) return { changed: false };
|
|
51
|
+
const before = this._items;
|
|
52
|
+
this._items = next;
|
|
46
53
|
this.lastWriteAt = Date.now();
|
|
47
|
-
this.emitCompletedTransitions(before, this.
|
|
48
|
-
this.emit('todo.updated', { items: this.
|
|
54
|
+
this.emitCompletedTransitions(before, this._items);
|
|
55
|
+
this.emit('todo.updated', { items: this._items });
|
|
56
|
+
return { changed: true };
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
applyEdits(edits: readonly TodoEdit[]):
|
|
52
|
-
const before = this.
|
|
53
|
-
|
|
59
|
+
applyEdits(edits: readonly TodoEdit[]): { changed: boolean } {
|
|
60
|
+
const before = this._items;
|
|
61
|
+
const next = applyTodoEdits(this._items, edits);
|
|
62
|
+
if (listsEqual(before, next)) return { changed: false };
|
|
63
|
+
this._items = next;
|
|
54
64
|
this.lastWriteAt = Date.now();
|
|
55
|
-
this.emitCompletedTransitions(before, this.
|
|
56
|
-
this.emit('todo.edited', { edits, items: this.
|
|
57
|
-
this.emit('todo.updated', { items: this.
|
|
65
|
+
this.emitCompletedTransitions(before, this._items);
|
|
66
|
+
this.emit('todo.edited', { edits, items: this._items });
|
|
67
|
+
this.emit('todo.updated', { items: this._items });
|
|
68
|
+
return { changed: true };
|
|
58
69
|
}
|
|
59
70
|
|
|
60
|
-
/**
|
|
71
|
+
/** Why: Preserve the established compatibility and safety behavior (M16). */
|
|
61
72
|
private emitCompletedTransitions(before: readonly TodoItem[], after: readonly TodoItem[]): void {
|
|
62
73
|
const count = countCompletedTransitions(before, after);
|
|
63
74
|
if (count > 0) this.emit('todo.completed', { count, at: Date.now() });
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
rebuild(entries: readonly unknown[]): void {
|
|
67
|
-
//
|
|
68
|
-
|
|
78
|
+
// Why: Preserve the established compatibility and safety behavior.
|
|
79
|
+
// Why: Preserve the established compatibility and safety behavior.
|
|
69
80
|
const folded = foldLatestTodosMeta(entries as Parameters<typeof foldLatestTodosMeta>[0]);
|
|
70
81
|
if (folded) {
|
|
71
|
-
this.
|
|
82
|
+
this._items = folded.items;
|
|
72
83
|
this.lastWriteAt = folded.writtenAt;
|
|
73
84
|
}
|
|
74
|
-
this.emit('todo.updated', { items: this.
|
|
85
|
+
this.emit('todo.updated', { items: this._items, reason: 'rebuild' });
|
|
75
86
|
}
|
|
76
87
|
}
|
package/src/tool-gate.ts
CHANGED
|
@@ -1,40 +1,28 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 档2 Week4-5:worker 执行期强制(2026-08-18;C7 v2 / D76)。
|
|
3
|
-
*
|
|
4
|
-
* master 合成的 manifest 经 env 下发后,这里是 worker 侧的最后一道闸:
|
|
5
|
-
* - `planToolGate`:工具不在 manifest / 权限 deny → deny(安全护栏);
|
|
6
|
-
* ask → 标记(v1 退化:放行 + APPROVAL_NEEDED 日志,硬审批留 v2 拍板);
|
|
7
|
-
* - `planActiveTools`(D77 可见层):manifest 外的工具从模型视野移除——
|
|
8
|
-
* 不进 system prompt、不诱导尝试(上下文卫生,不止权限)。
|
|
9
|
-
* 限速机制已整体移除(WS-D6):权限边界是我们的职责,资源配额是
|
|
10
|
-
* 插件引入者/provider 的职责(D79 声明式引入,用户自负责)。
|
|
11
|
-
* 纯逻辑零依赖;接缝在 index.ts 的 tool_call 钩子与 session_start。
|
|
12
|
-
*/
|
|
1
|
+
/** Why: Preserve the established compatibility and safety behavior (C7, D76, D77, D79, WS-D6). */
|
|
13
2
|
import type { PermissionAction, UnknownToolStance } from './role-manifest.ts';
|
|
14
3
|
|
|
15
|
-
/**
|
|
4
|
+
/** Why: Preserve the established compatibility and safety behavior. */
|
|
16
5
|
export interface RuntimeRoleManifest {
|
|
17
6
|
role: string;
|
|
18
7
|
version?: string;
|
|
19
8
|
tools: string[];
|
|
20
9
|
permissions: Record<string, PermissionAction>;
|
|
21
|
-
/**
|
|
10
|
+
/** Why: Preserve the established compatibility and safety behavior (D82). */
|
|
22
11
|
unknownTools?: UnknownToolStance;
|
|
23
12
|
services?: {
|
|
24
13
|
todos?: {
|
|
25
14
|
mode?: 'serial' | 'parallel';
|
|
26
|
-
reminderLimit?: number;
|
|
27
15
|
};
|
|
28
16
|
};
|
|
29
17
|
}
|
|
30
18
|
|
|
31
|
-
/**
|
|
19
|
+
/** Why: Preserve the established compatibility and safety behavior. */
|
|
32
20
|
export function parseRuntimeManifest(envValue: string | undefined): RuntimeRoleManifest | null {
|
|
33
21
|
if (!envValue) return null;
|
|
34
22
|
try {
|
|
35
23
|
const m = JSON.parse(envValue) as RuntimeRoleManifest;
|
|
36
24
|
if (!m || typeof m.role !== 'string' || !Array.isArray(m.tools)) return null;
|
|
37
|
-
//
|
|
25
|
+
// Why: Preserve the established compatibility and safety behavior (D82).
|
|
38
26
|
if (m.unknownTools !== undefined && m.unknownTools !== 'allow' && m.unknownTools !== 'deny') {
|
|
39
27
|
m.unknownTools = 'deny';
|
|
40
28
|
}
|
|
@@ -55,7 +43,7 @@ export function planToolGate(toolName: string, manifest: RuntimeRoleManifest | n
|
|
|
55
43
|
const perm = manifest.permissions[toolName] ?? manifest.permissions['*'] ?? 'allow';
|
|
56
44
|
const known = manifest.tools.includes(toolName);
|
|
57
45
|
const stance = manifest.unknownTools ?? 'deny';
|
|
58
|
-
//
|
|
46
|
+
// Why: Preserve the established compatibility and safety behavior (D82).
|
|
59
47
|
if (perm === 'deny' || (!known && stance === 'deny')) {
|
|
60
48
|
return {
|
|
61
49
|
kind: 'deny',
|
|
@@ -71,16 +59,7 @@ export function planToolGate(toolName: string, manifest: RuntimeRoleManifest | n
|
|
|
71
59
|
return { kind: 'allow' };
|
|
72
60
|
}
|
|
73
61
|
|
|
74
|
-
/**
|
|
75
|
-
* D77 可见层:计算 setActiveTools 的下一状态。
|
|
76
|
-
* 交集语义(而非替换)——空交集返回 null(fail-open 保留现状),
|
|
77
|
-
* 防"manifest 列了但插件没装"把工具集清空(如 web_search 装之前的 websearch 档案)。
|
|
78
|
-
* 保持 currentActive 原顺序;与现状相同 → changed:false(调用方可跳过 set)。
|
|
79
|
-
*
|
|
80
|
-
* D82 姿态分支:unknownTools=allow → 不按 manifest 交集裁剪,只隐藏
|
|
81
|
-
* permissions 中显式 deny 的工具(worker 排除族在继承制下仍不可见);
|
|
82
|
-
* 未知工具(用户装的扩展)保持可见。deny(缺省)→ 原交集语义。
|
|
83
|
-
*/
|
|
62
|
+
/** Why: Preserve the established compatibility and safety behavior (D77, D82). */
|
|
84
63
|
export function planActiveTools(
|
|
85
64
|
manifestTools: string[],
|
|
86
65
|
currentActive: string[],
|
package/src/vocab.ts
CHANGED
|
@@ -1,55 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi-herdr
|
|
2
|
+
* pi-herdr bridge vocabulary (M22: title is the board).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 显示投影 = pane.report_metadata 的 title / state_labels(见 pane-title.ts)。
|
|
4
|
+
* DESIGN.md §4.2: no new transport. Wire = herdr NDJSON; todo authority = pi
|
|
5
|
+
* session JSONL; display = pane.report_metadata title / state_labels.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
/**
|
|
8
|
+
/** Todo five-state (D34: three-state + blocked/abandoned, aligned with OMP). */
|
|
10
9
|
export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'blocked' | 'abandoned';
|
|
11
10
|
|
|
12
11
|
export const TODO_STATUSES: readonly TodoStatus[] = ['pending', 'in_progress', 'completed', 'blocked', 'abandoned'];
|
|
13
12
|
|
|
14
|
-
/**
|
|
13
|
+
/** Todo item: content + status; optional blocker (blocked only) and phase (D43). No stable id (last-write-wins). */
|
|
15
14
|
export interface TodoItem {
|
|
16
15
|
content: string;
|
|
17
16
|
status: TodoStatus;
|
|
18
|
-
/**
|
|
17
|
+
/** Non-empty string only when status==='blocked' (D34). */
|
|
19
18
|
blocker?: string;
|
|
20
|
-
/**
|
|
19
|
+
/** Optional group name (D43); non-empty, ≤30 chars. */
|
|
21
20
|
phase?: string;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
/**
|
|
23
|
+
/** Full todo snapshot (each todo_write submits this; last-wins). */
|
|
25
24
|
export interface TodoSnapshot {
|
|
26
|
-
/**
|
|
25
|
+
/** Shape version; consumers drop stale snapshots. */
|
|
27
26
|
version: 1;
|
|
28
27
|
items: TodoItem[];
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
/**
|
|
30
|
+
/** Snapshot counts (confirmation copy and title projection). */
|
|
32
31
|
export interface TodoCounts {
|
|
33
32
|
pending: number;
|
|
34
33
|
inProgress: number;
|
|
35
34
|
completed: number;
|
|
36
|
-
/** D91
|
|
35
|
+
/** D91: blocked as its own column (title quartet ▶○■✓). */
|
|
37
36
|
blocked: number;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
/**
|
|
39
|
+
/** Source name for pane.report-agent. */
|
|
41
40
|
export const REPORT_AGENT_SOURCE = 'pi-herdr';
|
|
42
41
|
|
|
43
|
-
/**
|
|
42
|
+
/** First-upgrade key used to clear the old 16-key chunked tokens (M13b leftover). */
|
|
44
43
|
export const PI_HERDR_META_KEY = 'pi-herdr';
|
|
45
44
|
|
|
46
|
-
/** todo
|
|
45
|
+
/** Model-facing todo tool name (DSH-compatible). */
|
|
47
46
|
export const TODO_TOOL_NAME = 'todo_write';
|
|
48
47
|
|
|
49
|
-
/**
|
|
48
|
+
/** Snapshot key in tool-result details (persisted in session JSONL). */
|
|
50
49
|
export const TODO_DETAILS_KEY = 'pi-herdr.todo';
|
|
51
50
|
|
|
52
|
-
/**
|
|
51
|
+
/** Confirmation copy (byte-aligned with DSH so model vocab transfers). */
|
|
53
52
|
export function formatTodoConfirmation(items: TodoItem[]): string {
|
|
54
53
|
const c = countTodos(items);
|
|
55
54
|
return `Updated todo list: ${c.pending} pending, ${c.inProgress} in progress, ${c.completed} completed.`;
|
|
@@ -65,12 +64,12 @@ export function countTodos(items: TodoItem[]): TodoCounts {
|
|
|
65
64
|
else if (it.status === 'in_progress') inProgress++;
|
|
66
65
|
else if (it.status === 'completed') completed++;
|
|
67
66
|
else if (it.status === 'blocked') blocked++;
|
|
68
|
-
// abandoned
|
|
67
|
+
// abandoned is omitted (D34); blocked is its own column (D91)
|
|
69
68
|
}
|
|
70
69
|
return { pending, inProgress, completed, blocked };
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
/**
|
|
72
|
+
/** Subagent settlement notice (aligned with DSH). */
|
|
74
73
|
export function formatSettlementNotice(
|
|
75
74
|
agentId: string,
|
|
76
75
|
closingMessage: string | null,
|