pi-firecode 0.8.0 → 1.0.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.
Files changed (79) hide show
  1. package/README.md +35 -15
  2. package/activity.ts +92 -0
  3. package/busy.ts +188 -0
  4. package/config.example.jsonc +38 -47
  5. package/config.ts +198 -172
  6. package/deliver.ts +100 -13
  7. package/flame.ts +149 -0
  8. package/format.ts +84 -31
  9. package/header.ts +208 -82
  10. package/index.ts +17 -11
  11. package/master/actions.ts +293 -0
  12. package/master/activity-list.ts +281 -0
  13. package/master/event-card.ts +18 -37
  14. package/master/event-format.ts +71 -40
  15. package/master/guard.ts +46 -0
  16. package/master/index.ts +88 -1036
  17. package/master/list-view.ts +129 -0
  18. package/master/outbox.ts +188 -0
  19. package/master/prompts/master.zh.md +18 -13
  20. package/master/prompts/worker.zh.md +1 -1
  21. package/master/role.ts +6 -1
  22. package/master/run.ts +315 -0
  23. package/master/runtime.ts +259 -0
  24. package/master/spawn.ts +79 -38
  25. package/master/state.ts +30 -6
  26. package/master/worker-view.ts +517 -0
  27. package/package.json +1 -1
  28. package/provider/claude-sub.ts +35 -13
  29. package/provider/openai-native/src/compact-client.ts +2 -14
  30. package/provider/openai-native/src/options.ts +4 -2
  31. package/review/advisor.ts +0 -2
  32. package/review/card.ts +36 -23
  33. package/review/checkpoint.ts +2 -8
  34. package/review/evidence.ts +42 -44
  35. package/review/index.ts +271 -525
  36. package/review/occupancy.ts +25 -0
  37. package/review/outcome.ts +31 -7
  38. package/review/prompt.ts +7 -8
  39. package/review/prompts/review.en.md +7 -6
  40. package/review/prompts/review.zh.md +7 -6
  41. package/review/reviewer.ts +2 -5
  42. package/review/session.ts +11 -30
  43. package/review/state.ts +32 -60
  44. package/review/ui.ts +30 -469
  45. package/round-recorder.ts +18 -0
  46. package/session/herdr-display.ts +1 -1
  47. package/session/presets.ts +130 -87
  48. package/session/quota.ts +123 -0
  49. package/session/stats.ts +4 -1
  50. package/statusbar/index.ts +242 -104
  51. package/statusbar/render.ts +95 -163
  52. package/theme.ts +2 -41
  53. package/tools/actions.ts +53 -0
  54. package/tools/assistant-view.ts +64 -0
  55. package/tools/click-anchor.ts +56 -0
  56. package/tools/group-view.ts +420 -0
  57. package/tools/grouping.ts +146 -221
  58. package/tools/host.ts +369 -0
  59. package/tools/index.ts +108 -105
  60. package/tools/line.ts +95 -58
  61. package/tools/machine.ts +78 -0
  62. package/tools/parts.ts +2 -2
  63. package/tools/round.ts +95 -0
  64. package/tools/turn-clock.ts +41 -0
  65. package/tools/turn-summary.ts +109 -0
  66. package/truncated-write.ts +18 -0
  67. package/watcher/card.ts +28 -22
  68. package/watcher/index.ts +16 -44
  69. package/watcher/observer.ts +3 -3
  70. package/watcher/prompts/watch.zh.md +1 -1
  71. package/watcher/transcript.ts +4 -12
  72. package/flame-frames.ts +0 -460
  73. package/review/progress.ts +0 -324
  74. package/session/bark.ts +0 -157
  75. package/session/working-flame.ts +0 -116
  76. package/statusbar/quota-cache.ts +0 -60
  77. package/statusbar/quota-parse.ts +0 -85
  78. package/statusbar/quota.ts +0 -203
  79. package/statusbar/tps.ts +0 -87
@@ -0,0 +1,129 @@
1
+ /** subagents / subagents_list 的工具行与 /fire-master status 文本:纯投影,不改状态。 */
2
+ import { statSync } from "node:fs";
3
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { formatDuration } from "../format.js";
5
+ import { ToolLine, makeResultRenderer } from "../tools/line.js";
6
+ import type { Part } from "../tools/parts.js";
7
+ import type { WorkerLive } from "./runtime.js";
8
+ import type { WorkerRef, WorkerStatus } from "./state.js";
9
+
10
+ export const renderSubagentsResult = makeResultRenderer(false);
11
+ const STATUS_WORD = { working: "工作", idle: "空闲", reviewing: "审查" } satisfies Record<WorkerStatus, string>;
12
+ const ACTION_VERB: Record<string, string> = { start: "启动", list: "查看", kill: "移除", send: "发送", interrupt: "中断", review: "审查", tail: "近况", ack: "待命" };
13
+
14
+ export function subagentsCallParts(args: Record<string, unknown>): Part[] {
15
+ const action = typeof args.action === "string" ? args.action : "?";
16
+ const parts: Part[] = [{ text: ACTION_VERB[action] ?? action, bold: true }];
17
+ const target = optionalText(args.worker);
18
+ if (target) parts.push({ text: ` ${target}`, color: "accent" });
19
+ const role = optionalText(args.role);
20
+ if ((action === "start" || action === "send") && role) parts.push({ text: ` · ${role}`, color: "muted" });
21
+ const prompt = optionalText(args.prompt)?.split("\n", 1)[0];
22
+ if (prompt && action === "start") parts.push({ text: ` — ${prompt}`, color: "muted" });
23
+ return parts;
24
+ }
25
+
26
+ export type CompactWorker = ReturnType<typeof compactWorker>;
27
+
28
+ /** 给模型的池快照:只有档案事实。 */
29
+ export function compactWorker(worker: WorkerRef) {
30
+ return {
31
+ name: worker.name,
32
+ role: worker.role,
33
+ status: worker.status,
34
+ model: worker.model,
35
+ thinking: worker.thinking,
36
+ session: worker.sessionPath,
37
+ ...(worker.interruptedAt ? { interruptedAt: worker.interruptedAt } : {}),
38
+ ...(worker.reviewNeeded ? { reviewNeeded: true } : {}),
39
+ ...(worker.disposition ? { disposition: worker.disposition } : {}),
40
+ };
41
+ }
42
+
43
+ /** 展开行用的当前动作:审查进度、落定时刻(reload 后退到会话文件 mtime)或最近的工具。 */
44
+ export function currentWorkerAction(worker: CompactWorker, live: WorkerLive | undefined) {
45
+ if (worker.status === "reviewing") return live?.reviewProgress && { kind: "review" as const, ...live.reviewProgress };
46
+ if (worker.status === "idle") {
47
+ let since = live?.idleAt;
48
+ try {
49
+ since ??= statSync(worker.session).mtimeMs;
50
+ } catch {
51
+ // 缺失档案仍可 list;真正恢复时由 send 明确报错。
52
+ }
53
+ return { kind: "idle" as const, ...(since ? { since } : {}) };
54
+ }
55
+ const current = [...(live?.currentTools.values() ?? [])].at(-1);
56
+ return current ? { kind: "tool" as const, ...current } : undefined;
57
+ }
58
+
59
+ export function expandedWorkerList(
60
+ workers: unknown[],
61
+ theme: ExtensionContext["ui"]["theme"],
62
+ context: Parameters<typeof renderSubagentsResult>[3],
63
+ ) {
64
+ return {
65
+ invalidate() {},
66
+ render(width: number): string[] {
67
+ return ["", ...workers.flatMap((value) => {
68
+ const worker = value as Record<string, unknown>;
69
+ const action = worker.currentAction as {
70
+ kind?: string;
71
+ tool?: string;
72
+ startedAt?: number;
73
+ since?: number;
74
+ round?: number;
75
+ settled?: number;
76
+ total?: number;
77
+ } | undefined;
78
+ const actionParts: Part[] = action?.kind === "tool" && action.tool && action.startedAt
79
+ ? [{
80
+ text: ` · ${action.tool} · 已 ${formatDuration(Math.max(0, Date.now() - action.startedAt))}`,
81
+ color: "accent",
82
+ }]
83
+ : action?.kind === "idle"
84
+ ? [{
85
+ text: action.since ? ` · 落定 ${formatDuration(Date.now() - action.since)}前` : " · 已落定",
86
+ color: "muted",
87
+ }]
88
+ : action?.kind === "review"
89
+ ? [{ text: ` · 第 ${action.round} 轮 · 审查者 ${action.settled}/${action.total}`, color: "accent" }]
90
+ : [];
91
+ return new ToolLine({
92
+ label: String(worker.name),
93
+ value: [
94
+ { text: roleStatusText(worker), color: "accent" },
95
+ ...actionParts,
96
+ { text: ` · ${String(worker.model).split("/").pop()}/${String(worker.thinking)}`, color: "muted" },
97
+ ],
98
+ clip: "end",
99
+ theme,
100
+ ctx: { ...context, state: {}, expanded: false },
101
+ }).render(width);
102
+ })];
103
+ },
104
+ };
105
+ }
106
+
107
+ export function listMeta(workers: unknown[]): Part[] {
108
+ if (!workers.length) return [{ text: " — 池 0", color: "muted" }];
109
+ return [{ text: ` — 池 ${workers.length}:${workers.map((value) => {
110
+ const worker = value as Record<string, unknown>;
111
+ return `${String(worker.name)} ${roleStatusText(worker)}`;
112
+ }).join(" · ")}`, color: "muted" }];
113
+ }
114
+
115
+ export function statusText(workers: WorkerRef[]): string {
116
+ return workers.length
117
+ ? workers.map((worker) => `${worker.name} ${roleStatusText(worker)} ${worker.model.split("/").pop()}`).join("\n")
118
+ : "没有子代理";
119
+ }
120
+
121
+ /** 角色为主的状态投影:「工程师·工作」;档案缺角色时退到纯状态词。 */
122
+ function roleStatusText(worker: { role?: unknown; status?: unknown }): string {
123
+ const status = STATUS_WORD[worker.status as WorkerStatus] ?? String(worker.status);
124
+ return worker.role ? `${String(worker.role)}·${status}` : status;
125
+ }
126
+
127
+ function optionalText(value: unknown): string | undefined {
128
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
129
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * 事件发件箱:落定事件先以 pending entry 写进主会话,再经 deliver.ts 投递,成功后写 ack;reload 重投差集。
3
+ * 同一节拍内的落定合并成一条消息。在飞子代理数也在这里算:指挥官在等的 working/reviewing 加上它们的事件还在队列
4
+ * 或投递中的子代理,所以归零只发生在事件交给指挥官之后。视图起的运行(见 RunOrigin)不算在飞,它的事件只告知不唤醒。
5
+ */
6
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import { deliver, inform, wrapEnvelope } from "../deliver.js";
8
+ import { roundFromEntry } from "../tools/round.js";
9
+ import { MASTER_EVENT_TYPE, withElapsed, type MasterEvent } from "./event-format.js";
10
+ import type { MasterRuntime } from "./runtime.js";
11
+
12
+ const PENDING_EVENT_TYPE = "firecode-master-pending-event";
13
+ const EVENT_ACK_TYPE = "firecode-master-event-ack";
14
+ const EVENT_RETRY_MS = 5_000;
15
+ /** 指挥官空闲时,从第一条结果入队起最多等这么久就唤醒,即使结果还在陆续到达。 */
16
+ const WAKE_MAX_MS = 6_000;
17
+
18
+ export interface PendingMasterEvent {
19
+ id: string;
20
+ content: string;
21
+ worker?: string;
22
+ /** 指挥官没在等这个结果(视图起的运行):只告知不唤醒,也不计入在飞。 */
23
+ inform?: true;
24
+ }
25
+
26
+ export class Outbox {
27
+ private readonly queued: PendingMasterEvent[] = [];
28
+ /** 已交给 deliver、尚未确认送达的事件:投递完成前对应子代理仍算在飞。 */
29
+ private readonly delivering = new Set<PendingMasterEvent>();
30
+ private flushTimer?: NodeJS.Timeout;
31
+ private retrying = false;
32
+ /** 空闲合并窗口里第一条事件入队的时刻。 */
33
+ private firstQueuedAt?: number;
34
+ private inFlightScheduled = false;
35
+
36
+ constructor(private readonly active: MasterRuntime) {}
37
+
38
+ /** 新产出的事件:追加耗时、持久化为 pending、排队投递。 */
39
+ enqueue(produced: MasterEvent, worker?: string): void {
40
+ if (this.active.closed) return;
41
+ const informOnly = worker !== undefined && this.active.live.get(worker)?.origin === "view";
42
+ const event: PendingMasterEvent = {
43
+ id: crypto.randomUUID(),
44
+ content: this.withElapsed(produced, worker),
45
+ ...(worker ? { worker } : {}),
46
+ ...(informOnly ? { inform: true as const } : {}),
47
+ };
48
+ try {
49
+ this.active.setup.pi.appendEntry(PENDING_EVENT_TYPE, event);
50
+ } catch (error) {
51
+ this.active.ctx.ui.notify(`子代理结果持久化失败,crash 时可能丢失:${String(error)}`, "warning");
52
+ }
53
+ this.push(event);
54
+ }
55
+
56
+ /** reload 重投 pending 与 ack 的差集:正文已带落定当时的耗时,原样再投。 */
57
+ replayUnacked(ctx: ExtensionContext): void {
58
+ for (const event of unackedEvents(ctx)) this.push(event);
59
+ }
60
+
61
+ /**
62
+ * 在飞 = working/reviewing + 已落定但结果事件还在队列或投递中(投递失败重试期间也算)。
63
+ * 落定先改 store、随后才入队事件:同一同步段内合并成一次计算,避免中间闪出一次归零。
64
+ */
65
+ scheduleInFlight(): void {
66
+ if (this.inFlightScheduled) return;
67
+ this.inFlightScheduled = true;
68
+ queueMicrotask(() => {
69
+ this.inFlightScheduled = false;
70
+ if (this.active.closed) return;
71
+ const names = new Set<string>();
72
+ for (const worker of this.active.store.state.workers)
73
+ if ((worker.status === "working" || worker.status === "reviewing") && this.active.live.get(worker.name)?.origin !== "view")
74
+ names.add(worker.name);
75
+ for (const event of [...this.queued, ...this.delivering]) if (event.worker && !event.inform) names.add(event.worker);
76
+ this.active.setup.publishInFlight(names.size);
77
+ });
78
+ }
79
+
80
+ close(): void {
81
+ clearTimeout(this.flushTimer);
82
+ }
83
+
84
+ /**
85
+ * 指挥官在跑:立即投(句缝送达,不打断)。指挥官空闲:每次唤醒都是一个完整回合,陆续到达的一批结果
86
+ * 等一个安静窗口合并成一次唤醒——最后一条入队后 wakeQuietMs 内没有新结果才唤醒,从第一条起最多等 WAKE_MAX_MS。
87
+ * 只告知的事件不开窗口、立即追加;窗口已开就随那一批唤醒送达。
88
+ */
89
+ private push(event: PendingMasterEvent): void {
90
+ this.queued.push(event);
91
+ this.scheduleInFlight();
92
+ if (this.retrying) return;
93
+ const quiet = this.active.setup.wakeQuietMs;
94
+ if (!this.active.ctx.isIdle() || quiet <= 0) {
95
+ if (!this.flushTimer || this.firstQueuedAt !== undefined) this.schedule(0);
96
+ return;
97
+ }
98
+ if (event.inform) {
99
+ if (this.firstQueuedAt === undefined) this.schedule(0);
100
+ return;
101
+ }
102
+ this.firstQueuedAt ??= Date.now();
103
+ this.schedule(Math.min(quiet, Math.max(0, this.firstQueuedAt + WAKE_MAX_MS - Date.now())));
104
+ }
105
+
106
+ private schedule(delay: number): void {
107
+ clearTimeout(this.flushTimer);
108
+ this.flushTimer = setTimeout(() => this.flush(), delay);
109
+ this.flushTimer.unref?.();
110
+ }
111
+
112
+ private flush(): void {
113
+ const { active } = this;
114
+ if (active.closed) return;
115
+ this.flushTimer = undefined;
116
+ this.retrying = false;
117
+ this.firstQueuedAt = undefined;
118
+ if (!this.queued.length) return;
119
+ const batch = this.queued.splice(0);
120
+ for (const event of batch) this.delivering.add(event);
121
+ const send = batch.every((event) => event.inform) ? inform : deliver;
122
+ send(active.setup.pi, active.ctx, {
123
+ customType: MASTER_EVENT_TYPE,
124
+ content: batch.map((event) => wrapEnvelope("firecode_master_event", event.content)).join("\n\n"),
125
+ }).then(() => {
126
+ if (active.closed) return;
127
+ for (const event of batch) this.delivering.delete(event);
128
+ try {
129
+ active.setup.pi.appendEntry(EVENT_ACK_TYPE, { ids: batch.map((event) => event.id) });
130
+ } catch (error) {
131
+ active.ctx.ui.notify(`子代理结果确认写入失败,reload 后可能重复投递:${String(error)}`, "warning");
132
+ }
133
+ for (const event of batch) {
134
+ if (!event.worker) continue;
135
+ const worker = active.store.state.workers.find((candidate) => candidate.name === event.worker);
136
+ if (worker?.status === "idle" && worker.disposition !== "reminded")
137
+ active.store.dispatch({ type: "UPSERT_WORKER", worker: { ...worker, disposition: "pending" } });
138
+ }
139
+ this.scheduleInFlight();
140
+ }, (error) => {
141
+ if (active.closed) return;
142
+ for (const event of batch) this.delivering.delete(event);
143
+ this.queued.unshift(...batch);
144
+ active.ctx.ui.notify(`子代理结果投递失败,将自动重试:${String(error)}`, "warning");
145
+ this.retrying = true;
146
+ this.schedule(EVENT_RETRY_MS);
147
+ });
148
+ }
149
+
150
+ /**
151
+ * 本次运行耗时 = 这次运行(start/send/review 起点之后)在子代理会话里写下的轮记录之和:fallback 续跑或审查里的
152
+ * 修复回合各自成段,合起来才是这次运行。子代理排除了 FireCode(没有轮记录器)或 reload 后起点丢失时省略。
153
+ * 读得到:宿主在 prompt 结束前发 agent_settled,审查则先写终态 checkpoint 再释放占用,记录都早于这里的读取。
154
+ */
155
+ private withElapsed(produced: MasterEvent, worker?: string): string {
156
+ const live = worker === undefined ? undefined : this.active.live.get(worker);
157
+ const since = live?.runStartedAt;
158
+ const branch = live?.observed?.session.sessionManager.getBranch() ?? [];
159
+ const rounds = since === undefined ? [] : branch.flatMap((entry) => {
160
+ const round = roundFromEntry(entry);
161
+ return round && round.at >= since ? [round] : [];
162
+ });
163
+ const sessionSince = this.active.setup.sessionSince();
164
+ return withElapsed(produced, {
165
+ ...(rounds.length ? { run: rounds.reduce((total, round) => total + round.elapsed, 0) } : {}),
166
+ ...(sessionSince === undefined ? {} : { task: Date.now() - sessionSince }),
167
+ });
168
+ }
169
+ }
170
+
171
+ function unackedEvents(ctx: ExtensionContext): PendingMasterEvent[] {
172
+ const pending = new Map<string, PendingMasterEvent>();
173
+ const acked = new Set<string>();
174
+ for (const entry of ctx.sessionManager.getEntries()) {
175
+ if (entry.type !== "custom" || !entry.data || typeof entry.data !== "object") continue;
176
+ const data = entry.data as Record<string, unknown>;
177
+ if (entry.customType === PENDING_EVENT_TYPE && typeof data.id === "string" && typeof data.content === "string")
178
+ pending.set(data.id, {
179
+ id: data.id,
180
+ content: data.content,
181
+ ...(typeof data.worker === "string" ? { worker: data.worker } : {}),
182
+ ...(data.inform === true ? { inform: true as const } : {}),
183
+ });
184
+ if (entry.customType === EVENT_ACK_TYPE && Array.isArray(data.ids))
185
+ for (const id of data.ids) if (typeof id === "string") acked.add(id);
186
+ }
187
+ return [...pending.values()].filter((event) => !acked.has(event.id));
188
+ }
@@ -1,17 +1,22 @@
1
- subagents 激活时,你是唯一的指挥官(Master),负责委派与最终验收,把任务推进到交付完成。
1
+ subagents 激活时,你是唯一的指挥官(Master):亲手把任务推进到交付完成,委派是工具不是职责。终审 diff 与交付裁决始终亲手。
2
2
 
3
- - 派发:role 只使用调研员、工程师、全栈、架构师、设计师、哨兵六个固定词;start 按已配置角色表显式传 role,send 需要换职能时也传 role;thinking 仅在用户明确要求时覆盖角色原子档。供应商额度或计费耗尽、模型不可用、宿主重试用尽的持续 5xx 会自动按该角色 fallback 降级并回报;架构师费用高,启用前必须先征求用户确认。以等待为主的盯守(CI、部署、长测试)使用哨兵。
4
- - 动手边界:短小明确的活亲手做——单文件小改、数条命令内可完成并验证的任务,委派开销大于任务本身就不委派。委派用于长时间运行、可并行、产生大量中间输出、需要干净上下文或独立验证的任务。终审 diff 与交付裁决始终亲手。
5
- - 并行:并行实现前先冻结共享边界——接口、共享类型、注册表、schema 由你亲手或单一 Worker 先行落地,其余 Worker 按路径划界同时开跑,每个共享文件只有一个写者,工作说明写明各自路径;集成以测试门裁决。
3
+ - 动手边界:委派换来的是几块大工作同时推进,代价是每个 Worker 几十秒的冷启动、整体被最慢的一块拖住、最后还要汇总;你的 codemode 本身就能并行调工具、过滤输出。能拆成几块、每块单独都要做很久(十几分钟以上)的工作,并行委派;几分钟内能在主会话用并行脚本做完的,即使是调研或审计,也亲手做。按互不重叠的范围(文件、模块、对象)拆,各块工作量相近;不让几个 Worker 按关注点读同一份代码,也不把同一件工作按写代码、写测试、审查拆给不同 Worker。派出去的部分不再亲手重做。事件末尾的已用时间用来判断还值不值得并行。
4
+ - 派发:按角色表的适用场景选 role;start 必须显式传 role,send 需要换职能时也传 role;thinking 仅在用户明确要求时覆盖角色原子档。工作说明只给指针——检出路径、工单号与相关提交、本票特有约束——不复述工单正文,Worker 自己读工单。Worker 续派到另一个检出时 send 带 cwd。
5
+ - 并行:相互无阻塞边的工作默认并行派发;并行实现前先冻结共享边界——接口、共享类型、注册表、schema 由你亲手或单一 Worker 先行落地,其余 Worker 按路径划界同时开跑,每个共享文件只有一个写者,工作说明写明各自路径;集成以测试门裁决。
6
6
  - 工单:项目存在工单库时,每次派发前按约定认领工单、对齐他人工单状态,并基于最新代码开工;新问题由你决策开新工单或交给上下文合适的在场 Worker 顺手解决,Worker 不开单。无工单库则以用户验收为交付边界。
7
- - 实现标准:实现类工作说明要求 Worker 先读 implement 技能(~/.agents/skills/matt/implement)再动手;你终审时按同一标准裁决。
8
- - 收割:调研与盯守 Worker 收割要点后立即 kill;实现 Worker 保留到收口。计划产物存在时,其维护责任随指挥权归你。
7
+ - 实现标准:Worker 按 implement 技能实现;你终审时按同一标准裁决,并核对三件事:验收测试验的是需求而非实现、红的原因是尚未实现、实现提交未触碰验收测试;缺一项退回。
8
+ - 收割:调研与盯守 Worker 收割要点后立即 kill。计划产物存在时,其维护责任随指挥权归你。
9
9
  - 调研衔接:执行档模型的聚焦探索(读代码定位、方案设计)直接 send 续派原 Worker 执行,现场就是工作上下文。廉价档或高噪声调研(大量搜索、日志、死路)只收割结构化线索(事实、路径、候选、证据来源、未决问题),核实后另派执行 Worker,不复用 transcript。
10
- - 投递:Worker 结果、中断与审查终态会自动送达;tail 仅用于按需读取执行细节。
11
- - 何时审查:复杂且影响大的实现,以及无法靠窄测可靠验收的任务,需要对抗性审查,在 start 时传 review:true;其余任务省略。
12
- - 如何启动:review:true 只是在任务开始时标记“这个任务完成后需要审查”,不会自动启动审查。这个标记不会因 send、reload、中断或失败而丢失,审查结束前不能 ack。Worker 返回结果并完成验证后,指挥官主动执行 review,才会开始对抗性审查。任务开始时没有标记 review:true,也可以在 Worker 空闲后主动执行 review;如果整个任务已经放弃,直接 kill。
13
- - 审查过程:review 会在原 Worker 会话中启动。独立模型读取 Worker 的工作记录,核对相关文件和验证结果。发现问题时,审查意见会交回同一个 Worker 核实和修复,然后再次审查;审查通过、顾问决定停止或达到最大轮数后结束。审查通过时仍可能附带不阻塞交付的建议,由指挥官判断是否需要继续处理。
14
- - 审查收尾:审查通过但附带的非阻塞建议确有必要解决时,在原 Worker 会话收口干净再 ack。
15
- - 交付:终审 diff 后,重要改动亲手 dogfood 试用;完全符合原则才 PR 与合并,看到发布成功后关闭工单并同步状态。合并冲突按 resolving-merge-conflicts 技能处理。
10
+ - 投递:Worker 结果、中断与审查终态会自动送达(陆续返回的一批会合并成一次唤醒);派发后手头没有亲手可做的活就结束回合等送达,不用 sleep、tail 或 subagents_list 轮询等结果。tail 仅用于按需读取执行细节。
11
+ - 等待:你直接与用户对话,单次工具调用累计阻塞不超过 60 秒的 sleep、wait 或 watch(含串联多个 sleep、gh run watch),否则这段时间用户无法和你沟通;更长的等待派哨兵执行或放后台,回来再查结果。
12
+ - 收到结果:界面已用一行把每个结果给用户看了。同批还有子代理在跑、这条结果不需要你此刻动作时,本回合不输出任何文字(不写“X 已完成”“继续等”之类的进度话),直接结束回合;等这一批全部返回再给用户一次汇总。需要你动作(失败、要续派、要拍板)就直接动作,只说动作和理由,不复述结果原文。
13
+ - 标题带“(你在子代理视图里直接派的)”的结果是用户在子代理视图里直接跟它说的(正文“你说:”是原话):你没在等它,所以它不唤醒你,随下一回合到达;用户已亲眼看过,不复述。
14
+ - `<firecode_review>` 信封来自你自己这个会话的 fire-review(审查的是你的改动),与子代理无关;子代理的审查终态只经 `<firecode_master_event>` 送达。
15
+ - 对用户说人话:不转述池快照里的内部字段与状态词(disposition、待发落、处置状态、working/idle 之类),只说结果、风险和下一步。
16
+ - 对抗性审查是什么:review 动作起的是内建的 fire-review 状态机,不是另派 Worker 看代码,也不要手工模拟。它在原 Worker 会话里跑:多个独立模型并行读该 Worker 的完整工作记录、核对文件、跑只读验证,各自出 PASS 或带证据的 FAIL;连续失败到阈值召顾问模型仲裁继续还是叫停。FAIL 的发现自动回流给同一个 Worker 修复,修完进下一轮,直到通过、顾问叫停或轮数用尽,终态自动送达。通过时仍可能附带不阻塞交付的建议。
17
+ - 何时与怎么审:复杂且影响大的实现,以及无法靠窄测可靠验收的任务,start 时传 review:true;其余省略。review:true 只登记审查义务,不自动开审——它不因 send、reload、中断或失败丢失,未履行就不能 ack。Worker 返回结果并完成验证后由你发起 review;没登记义务的票也可以在 Worker 空闲时主动审;整票放弃直接 kill。
18
+ - 交付:顺序是审查通过 → 非阻塞建议确有必要就在原 Worker 会话收口 → ack → 终审 diff,重要改动亲手或派子代理 dogfood 试用 → 推送、开 PR 与合并(本地工单库则合回主干)。ack 之前禁止推送与开 PR,draft 也不行——提前开 PR 只换来几分钟 CI 并行,代价是打回重修、CI 重跑和 PR 噪音。CI 与发布等待派哨兵执行项目的上线等待入口(项目 AGENTS.md 指明),一次阻塞到终态后转述。看到发布成功后关闭工单并同步状态。
19
+ - 收口:清理调试产物、检出与本地栈(用项目的拆检出入口)、因此失效的文档和测试,并行指挥官的除外;实现 Worker 保留到用户验收通过,验出问题就地续派。
20
+ - 汇报:给用户的收尾汇报第一句说交付状态(已上线、待合并或待你验收)和用户此刻要做的事,并列出还没收口的:在跑或保留的子代理、未合并的分支、未删的检出;验证只说没覆盖到的和失败的,跑过的命令与退出码不复述。
16
21
 
17
- 调用样板:start {"worker":"fix-auth","role":"工程师","prompt":"自包含工作说明"};send {"worker":"fix-auth","role":"架构师","prompt":"自包含工作说明"}。
22
+ 调用样板:start {"worker":"fix-auth","role":"工程师","prompt":"检出 <路径>,实现工单 #<n>;本票约束:…"};send {"worker":"fix-auth","prompt":"后续说明"};续派到新检出:send {"worker":"fix-auth","prompt":"…","cwd":"<新检出路径>"}。
@@ -1 +1 @@
1
- 你是指挥官委派的 Worker,只在当前 checkout 内完成工作说明。验证改动并报告结果、证据与遗留风险;无法完成或验证时如实报告阻塞原因和现场,不得假成功。Git 操作限于本地且仅覆盖自己修改的路径。
1
+ 你是指挥官委派的 Worker,只在当前 checkout 内完成工作说明。工作说明关联工单或要求实现时,动手前先读 implement 技能并按其步骤交付证据。验证改动并报告结果、证据与遗留风险;无法完成或验证时如实报告阻塞原因和现场,不得假成功。Git 操作限于本地且仅覆盖自己修改的路径。你是委派出去的子代理,不直接与用户对话:任务需要的长命令(sleep、长测试、构建、盯守)照常前台执行;等待后台测试、构建或服务时,用一次带超时的阻塞等待直到它结束,不分段轮询;指挥官需要时会中断你。回合只在交付物完成或真正被阻塞时结束:不以宣布下一步、询问要不要继续、列出并不阻塞后续工作的待决事项或阶段性汇报结束回合;盯守类任务等到工作说明定义的终态才回复。
package/master/role.ts CHANGED
@@ -2,7 +2,12 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
 
3
3
  export type SubsessionRole = "worker" | "observer" | "reviewer" | "advisor";
4
4
 
5
- const ROLE = new AsyncLocalStorage<SubsessionRole>();
5
+ // pi 的扩展加载器(jiti,moduleCache: false)在子会话 cwd 变化或宿主 reload 后会重新
6
+ // 求值整个模块图:模块级变量在两份拷贝间互不相通,角色标记必须挂在进程唯一的 globalThis
7
+ // 上,否则新拷贝读不到 spawn 侧设置的角色,watcher/master 会级联注册进子会话。
8
+ const ROLE_KEY = Symbol.for("firecode.subsession-role");
9
+ const shared = globalThis as Record<symbol, AsyncLocalStorage<SubsessionRole> | undefined>;
10
+ const ROLE = (shared[ROLE_KEY] ??= new AsyncLocalStorage<SubsessionRole>());
6
11
 
7
12
  export function currentSubsessionRole(): SubsessionRole | undefined {
8
13
  return ROLE.getStore();