@akagilnc/pi-workflow-roles 0.1.3783 → 0.1.3795

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 (37) hide show
  1. package/README.md +3 -1
  2. package/README.zh-CN.md +3 -1
  3. package/dist/acp-host/production-host.js +681 -269
  4. package/dist/atomic-write.js +23 -0
  5. package/dist/collector-evidence.js +4 -1
  6. package/dist/collector-github.js +1 -0
  7. package/dist/collector-handbook.js +142 -0
  8. package/dist/collector-identity.js +11 -0
  9. package/dist/collector-ledger.js +109 -63
  10. package/dist/collector-role.js +182 -18
  11. package/dist/collector-tool-schemas.js +40 -3
  12. package/dist/headless-host/production-host.js +682 -270
  13. package/dist/pi/role-turn-host.js +1 -0
  14. package/dist/public-cli/invocation.js +15 -0
  15. package/dist/public-cli/main.js +45 -77
  16. package/dist/public-cli/option-definitions.js +16 -2
  17. package/dist/public-cli/run-lifecycle.js +8 -0
  18. package/dist/role-runtime.js +3 -0
  19. package/extensions/role-runtime.ts +2 -0
  20. package/package.json +1 -1
  21. package/resources/collector-bot-handbook.md +33 -0
  22. package/src/acp-host/production-host.ts +4 -0
  23. package/src/acp-host/role-envelope.ts +1 -0
  24. package/src/collector-evidence.ts +6 -1
  25. package/src/collector-github.ts +3 -0
  26. package/src/collector-handbook.ts +194 -0
  27. package/src/collector-identity.ts +14 -0
  28. package/src/collector-ledger.ts +132 -80
  29. package/src/collector-role.ts +198 -14
  30. package/src/collector-tool-schemas.ts +42 -3
  31. package/src/host-contracts.ts +2 -0
  32. package/src/pi/role-turn-host.ts +1 -0
  33. package/src/public-cli/collector-run.ts +2 -0
  34. package/src/public-cli/invocation.ts +21 -0
  35. package/src/public-cli/option-definitions.ts +16 -2
  36. package/src/public-cli/run-lifecycle.ts +10 -0
  37. package/src/role-runtime.ts +5 -0
@@ -17,6 +17,7 @@ import {
17
17
  type CollectorRepository,
18
18
  } from "./collector-config.ts";
19
19
  import {
20
+ COLLECTOR_DEFAULT_WAIT_WINDOW_MS,
20
21
  createSystemCollectorClock,
21
22
  type CollectorClock,
22
23
  } from "./collector-evidence.ts";
@@ -25,9 +26,16 @@ import {
25
26
  listPullRequestNumbersByTicket,
26
27
  type CollectorGitHubTransport,
27
28
  } from "./collector-github.ts";
29
+ import {
30
+ createCollectorHandbookStore,
31
+ resolveCollectorHandbookRoot,
32
+ type CollectorHandbookStore,
33
+ } from "./collector-handbook.ts";
28
34
  import {
29
35
  COLLECTOR_BIND_TARGET_TOOL,
36
+ COLLECTOR_HANDBOOK_WRITE_TOOL,
30
37
  COLLECTOR_OBSERVE_TOOL,
38
+ COLLECTOR_OPEN_WAIT_WINDOW_TOOL,
31
39
  COLLECTOR_OUTPUT_TOOL,
32
40
  COLLECTOR_READ_TOOL,
33
41
  COLLECTOR_REQUEST_TOOL,
@@ -42,7 +50,9 @@ import {
42
50
  } from "./collector-receipt.ts";
43
51
  import {
44
52
  collectorBindTargetArgsSchema,
53
+ collectorHandbookWriteArgsSchema,
45
54
  collectorObserveArgsSchema,
55
+ collectorOpenWaitWindowArgsSchema,
46
56
  collectorOutputArgsSchema,
47
57
  collectorReadArgsSchema,
48
58
  collectorRequestArgsSchema,
@@ -76,7 +86,9 @@ export class CollectorTargetBindError extends CorrectableSubmissionError {
76
86
 
77
87
  export {
78
88
  COLLECTOR_BIND_TARGET_TOOL,
89
+ COLLECTOR_HANDBOOK_WRITE_TOOL,
79
90
  COLLECTOR_OBSERVE_TOOL,
91
+ COLLECTOR_OPEN_WAIT_WINDOW_TOOL,
80
92
  COLLECTOR_OUTPUT_TOOL,
81
93
  COLLECTOR_READ_TOOL,
82
94
  COLLECTOR_REQUEST_TOOL,
@@ -88,7 +100,9 @@ export const COLLECTOR_REQUIRED_TOOLS = [
88
100
  COLLECTOR_OBSERVE_TOOL,
89
101
  COLLECTOR_READ_TOOL,
90
102
  COLLECTOR_REQUEST_TOOL,
103
+ COLLECTOR_OPEN_WAIT_WINDOW_TOOL,
91
104
  COLLECTOR_WAIT_TOOL,
105
+ COLLECTOR_HANDBOOK_WRITE_TOOL,
92
106
  COLLECTOR_OUTPUT_TOOL,
93
107
  ] as const;
94
108
 
@@ -118,26 +132,50 @@ export const COLLECTOR_TRANSPORT_FLAGS = Object.freeze([
118
132
  type: "string" as const,
119
133
  }),
120
134
  }),
135
+ Object.freeze({
136
+ name: "ak-collector-wait-ms",
137
+ definition: Object.freeze({
138
+ description:
139
+ `Collector wait-window duration in milliseconds (default ${COLLECTOR_DEFAULT_WAIT_WINDOW_MS}). Caller-configurable; opens at a work step, not session start (#678 D4).`,
140
+ type: "string" as const,
141
+ }),
142
+ }),
121
143
  ] as const);
122
144
 
123
145
  const observeSchema = collectorObserveArgsSchema;
124
146
  const readSchema = collectorReadArgsSchema;
125
147
  const requestSchema = collectorRequestArgsSchema;
148
+ const openWaitWindowSchema = collectorOpenWaitWindowArgsSchema;
126
149
  const waitSchema = collectorWaitArgsSchema;
127
150
  const bindSchema = collectorBindTargetArgsSchema;
151
+ const handbookWriteSchema = collectorHandbookWriteArgsSchema;
128
152
  const outputSchema = collectorOutputArgsSchema;
129
153
 
130
154
  type RequestParams = Static<typeof requestSchema>;
131
155
  type ReadParams = Static<typeof readSchema>;
156
+ type OpenWaitWindowParams = Static<typeof openWaitWindowSchema>;
132
157
  type WaitParams = Static<typeof waitSchema>;
133
158
  type BindParams = Static<typeof bindSchema>;
159
+ type HandbookWriteParams = Static<typeof handbookWriteSchema>;
134
160
  type OutputParams = Static<typeof outputSchema>;
135
161
 
162
+ function parseWaitWindowMsFlag(raw: unknown): number {
163
+ if (raw === undefined || raw === null || raw === "") return COLLECTOR_DEFAULT_WAIT_WINDOW_MS;
164
+ if (typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 1) return raw;
165
+ if (typeof raw === "string" && /^[1-9]\d*$/.test(raw.trim())) {
166
+ const value = Number(raw.trim());
167
+ if (Number.isSafeInteger(value) && value >= 1) return value;
168
+ }
169
+ throw new Error("Collector --ak-collector-wait-ms must be a positive safe-integer millisecond string");
170
+ }
171
+
136
172
  export type CollectorRoleDependencies = {
137
173
  loadSoul(): Promise<string>;
138
174
  createTransport(): CollectorGitHubTransport;
139
175
  createClock?(): CollectorClock;
140
176
  createLedger(config: CollectorConfigState, clock: CollectorClock, ctx: HostContext): CollectorLedger;
177
+ /** Optional packaged seed for first-use general handbook (#677). */
178
+ loadHandbookSeed?(): Promise<string>;
141
179
  };
142
180
 
143
181
  export type CollectorRoleHostActions = {
@@ -151,25 +189,52 @@ export type CollectorActivation = {
151
189
  ledger: CollectorLedger;
152
190
  transport: CollectorGitHubTransport;
153
191
  clock: CollectorClock;
192
+ handbook: CollectorHandbookStore;
193
+ /** Loaded at activate; refreshed after handbook write for same-session materials. */
194
+ handbookView: {
195
+ general: string;
196
+ repo: string;
197
+ generalSource: "book" | "seed" | "empty";
198
+ repoSource: "book" | "empty";
199
+ generalPath: string;
200
+ repoPath: string;
201
+ };
154
202
  };
155
203
 
156
204
  function buildMethodContext(activation: CollectorActivation): string {
157
205
  const pr = activation.ledger.config.prNumber;
158
- return [
206
+ const handbook = activation.handbookView;
207
+ const lines = [
159
208
  "<collector_method>",
160
209
  `host: github.com`,
161
210
  `repository: ${activation.repository.canonical}`,
162
- `prNumber: ${pr === undefined ? "unbound — call ak_collector_bind_target with the role-decided issue/PR before observe" : String(pr)}`,
211
+ `prNumber: ${pr === undefined ? "未绑定" : String(pr)}`,
212
+ `waitWindowMs: ${String(activation.ledger.config.waitWindowMs)}`,
163
213
  `requests: ${JSON.stringify(activation.manifest.requests.map((request) => ({ id: request.id })))}`,
214
+ `handbookGeneralSource: ${handbook.generalSource}`,
215
+ `handbookRepoSource: ${handbook.repoSource}`,
164
216
  "</collector_method>",
165
- ].join("\n");
217
+ ];
218
+ // Opaque working memory only — no directional instructions (ADR 0073).
219
+ // JSON + `<` → \u003c keeps bodies from forging the delivery close tag (#677).
220
+ if (handbook.general.length > 0 || handbook.repo.length > 0) {
221
+ const payload = JSON.stringify({
222
+ general: handbook.general,
223
+ repo: handbook.repo,
224
+ generalSource: handbook.generalSource,
225
+ repoSource: handbook.repoSource,
226
+ repository: activation.repository.canonical,
227
+ }).replaceAll("<", "\\u003c");
228
+ lines.push("", "<collector_handbook>", payload, "</collector_handbook>");
229
+ }
230
+ return lines.join("\n");
166
231
  }
167
232
 
168
233
  function parsePositiveTicket(raw: unknown, label: string): number | undefined {
169
234
  if (raw === undefined || raw === null) return undefined;
170
235
  if (typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 1) return raw;
171
236
  if (typeof raw === "string" && /^[1-9]\d*$/.test(raw.trim())) return Number(raw.trim());
172
- throw new CollectorTargetBindError(`ak_collector_bind_target ${label} must be a positive safe integer`);
237
+ throw new CollectorTargetBindError(`通进司绑定 ${label} 须为正安全整数`);
173
238
  }
174
239
 
175
240
  /**
@@ -205,6 +270,7 @@ export function createCollectorRoleRuntime(
205
270
  const repoFlag = pi.getFlag("ak-collector-repo");
206
271
  const prFlag = pi.getFlag("ak-collector-pr");
207
272
  const requestManifestFlag = pi.getFlag("ak-collector-request-manifest");
273
+ const waitMsFlag = pi.getFlag("ak-collector-wait-ms");
208
274
  if (typeof repoFlag !== "string" || repoFlag.trim().length === 0) {
209
275
  throw new Error("Collector requires --ak-collector-repo");
210
276
  }
@@ -219,15 +285,36 @@ export function createCollectorRoleRuntime(
219
285
  const manifest = typeof requestManifestFlag === "string" && requestManifestFlag.trim().length > 0
220
286
  ? await loadCollectorManifest(requestManifestFlag)
221
287
  : emptyCollectorManifest();
288
+ const waitWindowMs = parseWaitWindowMsFlag(waitMsFlag);
222
289
 
223
290
  const clock = dependencies.createClock?.() ?? createSystemCollectorClock();
224
291
  const transport = dependencies.createTransport();
225
292
  const ledger = dependencies.createLedger(
226
- { repository, prNumber, manifest },
293
+ { repository, prNumber, manifest, waitWindowMs },
227
294
  clock,
228
295
  ctx,
229
296
  );
230
297
 
298
+ // #677: handbook under admitted book topology (session path → books/<key>/collector-handbook).
299
+ const sessionPath = ctx.sessionManager?.getSessionFile?.()
300
+ ?? ctx.sessionManager?.getSessionDir?.();
301
+ if (typeof sessionPath !== "string" || sessionPath.length === 0) {
302
+ throw new Error("通进司手册要求 session 路径位于 books/<bookKey>/");
303
+ }
304
+ const placement = resolveCollectorHandbookRoot(sessionPath);
305
+ const seedGeneral = dependencies.loadHandbookSeed === undefined
306
+ ? undefined
307
+ : (await dependencies.loadHandbookSeed()).trim();
308
+ const handbook = createCollectorHandbookStore({
309
+ ledgerHome: placement.ledgerHome,
310
+ handbookRoot: placement.root,
311
+ repositoryCanonical: repository.canonical,
312
+ ...(seedGeneral === undefined || seedGeneral.length === 0
313
+ ? {}
314
+ : { seedGeneral }),
315
+ });
316
+ const handbookView = await handbook.read();
317
+
231
318
  return {
232
319
  soul,
233
320
  repository,
@@ -235,6 +322,8 @@ export function createCollectorRoleRuntime(
235
322
  ledger,
236
323
  transport,
237
324
  clock,
325
+ handbook,
326
+ handbookView,
238
327
  };
239
328
  },
240
329
 
@@ -297,7 +386,7 @@ export function createCollectorRoleRuntime(
297
386
  const issueNumber = parsePositiveTicket(params.issueNumber, "issueNumber");
298
387
  if (prNumber === undefined && issueNumber === undefined) {
299
388
  throw new CollectorTargetBindError(
300
- "ak_collector_bind_target requires role-decided prNumber and/or issueNumber",
389
+ "通进司绑定须由角色判定 prNumber 与/或 issueNumber",
301
390
  );
302
391
  }
303
392
 
@@ -310,18 +399,18 @@ export function createCollectorRoleRuntime(
310
399
  });
311
400
  if (associated.length === 0) {
312
401
  throw new CollectorTargetBindError(
313
- `no PR associated with issue #${issueNumber} in ${activation.repository.canonical}; pass an explicit --pr or a different issueNumber`,
402
+ `${activation.repository.canonical} 的 issue #${issueNumber} 无关联 PR;请改用明确 --pr 或其他 issueNumber`,
314
403
  );
315
404
  }
316
405
  if (associated.length > 1) {
317
406
  throw new CollectorTargetBindError(
318
- `multiple PRs associated with issue #${issueNumber}: ${associated.join(", ")}; pass an explicit prNumber or --pr`,
407
+ `issue #${issueNumber} 关联多个 PR:${associated.join("、")};请改用明确 prNumber 或 --pr`,
319
408
  );
320
409
  }
321
410
  const fromIssue = associated[0]!;
322
411
  if (prNumber !== undefined && prNumber !== fromIssue) {
323
412
  throw new CollectorTargetBindError(
324
- `prNumber ${prNumber} conflicts with issue #${issueNumber} association PR ${fromIssue}`,
413
+ `prNumber ${prNumber} 与 issue #${issueNumber} 关联 PR ${fromIssue} 冲突`,
325
414
  );
326
415
  }
327
416
  bound = fromIssue;
@@ -422,8 +511,8 @@ export function createCollectorRoleRuntime(
422
511
  pi.registerTool({
423
512
  name: COLLECTOR_REQUEST_TOOL,
424
513
  label: "通进司请求",
425
- description: "按配置请求体与关联标记,在所引最新快照 HEAD 发一次请求。",
426
- promptSnippet: "按配置发一次请求",
514
+ description: "在所引最新快照 HEAD 发一次请求。requestId 可取配置清单,或角色依手册/现场判定的稳定 id;后者须同时提供 body。",
515
+ promptSnippet: "发一次评审请求",
427
516
  parameters: requestSchema,
428
517
  async execute(toolCallId: string, params: RequestParams, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
429
518
  const activation = getActivation();
@@ -431,7 +520,11 @@ export function createCollectorRoleRuntime(
431
520
  try {
432
521
  activation.ledger.beginOperational(COLLECTOR_REQUEST_TOOL, toolCallId);
433
522
  const details = await activation.ledger.request(
434
- params,
523
+ {
524
+ requestId: params.requestId,
525
+ snapshotId: params.snapshotId,
526
+ ...(typeof params.body === "string" ? { body: params.body } : {}),
527
+ },
435
528
  activation.transport,
436
529
  activation.clock,
437
530
  signal,
@@ -451,11 +544,101 @@ export function createCollectorRoleRuntime(
451
544
  },
452
545
  });
453
546
 
547
+ pi.registerTool({
548
+ name: COLLECTOR_HANDBOOK_WRITE_TOOL,
549
+ label: "通进司手册写入",
550
+ description: "写入通用手册或当前仓库差异全文(整份替换)。",
551
+ promptSnippet: "更新 bot 手册",
552
+ parameters: handbookWriteSchema,
553
+ async execute(toolCallId: string, params: HandbookWriteParams, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
554
+ const activation = getActivation();
555
+ if (activation === undefined) throw new Error("通进司未激活");
556
+ try {
557
+ activation.ledger.beginOperational(COLLECTOR_HANDBOOK_WRITE_TOOL, toolCallId);
558
+ // scope/body shape authority = collectorHandbookWriteArgsSchema (host parameters).
559
+ const details = await activation.handbook.write(params.scope, params.body);
560
+ activation.handbookView = await activation.handbook.read();
561
+ activation.ledger.completeOperational(toolCallId);
562
+ return {
563
+ content: [{
564
+ type: "text" as const,
565
+ text: `手册已写入:${details.scope}`,
566
+ }],
567
+ details: {
568
+ scope: details.scope,
569
+ path: details.path,
570
+ byteLength: details.byteLength,
571
+ generalSource: activation.handbookView.generalSource,
572
+ repoSource: activation.handbookView.repoSource,
573
+ },
574
+ };
575
+ } catch (error) {
576
+ if (isCorrectableExecuteError(error)) throw error;
577
+ hostActions.failInfrastructure(error, ctx, toolCallId);
578
+ } finally {
579
+ try {
580
+ activation.ledger.completeOperational(toolCallId);
581
+ } catch {
582
+ // already completed or not begun
583
+ }
584
+ }
585
+ },
586
+ });
587
+
588
+ pi.registerTool({
589
+ name: COLLECTOR_OPEN_WAIT_WINDOW_TOOL,
590
+ label: "通进司开启等待窗",
591
+ description:
592
+ "在工作步骤上开启等待窗:新建并自动触发的 PR 传创建成功时刻;已有 PR 在触发阶段结束后省略 startedAt(=现在)。窗长由使用方配置,默认十分钟;开启后不因 PR 更新重置。",
593
+ promptSnippet: "按工作步骤开启等待窗",
594
+ parameters: openWaitWindowSchema,
595
+ async execute(toolCallId: string, params: OpenWaitWindowParams, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
596
+ const activation = getActivation();
597
+ if (activation === undefined) throw new Error("通进司未激活");
598
+ try {
599
+ activation.ledger.beginOperational(COLLECTOR_OPEN_WAIT_WINDOW_TOOL, toolCallId);
600
+ let startedAt: Date | undefined;
601
+ if (typeof params.startedAt === "string" && params.startedAt.trim().length > 0) {
602
+ const parsed = new Date(params.startedAt);
603
+ if (Number.isNaN(parsed.getTime())) {
604
+ throw new Error("通进司等待窗 startedAt 须为有效 ISO 时间");
605
+ }
606
+ startedAt = parsed;
607
+ }
608
+ activation.ledger.openWaitWindow(
609
+ activation.clock,
610
+ startedAt === undefined ? undefined : { startedAt },
611
+ );
612
+ activation.ledger.completeOperational(toolCallId);
613
+ return {
614
+ content: [{
615
+ type: "text" as const,
616
+ text: "等待窗已开启",
617
+ }],
618
+ details: {
619
+ activationTime: activation.ledger.activationTime?.toISOString(),
620
+ deadlineTime: activation.ledger.deadlineTime?.toISOString(),
621
+ waitWindowMs: activation.ledger.config.waitWindowMs,
622
+ },
623
+ };
624
+ } catch (error) {
625
+ if (isCorrectableExecuteError(error)) throw error;
626
+ hostActions.failInfrastructure(error, ctx, toolCallId);
627
+ } finally {
628
+ try {
629
+ activation.ledger.completeOperational(toolCallId);
630
+ } catch {
631
+ // already completed or not begun
632
+ }
633
+ }
634
+ },
635
+ });
636
+
454
637
  pi.registerTool({
455
638
  name: COLLECTOR_WAIT_TOOL,
456
639
  label: "通进司等待",
457
- description: "再观察前等待;单次上限五分钟且不超剩余资格。",
458
- promptSnippet: "资格截止前等待",
640
+ description: "再观察前等待;实际睡眠不超过剩余等待窗。",
641
+ promptSnippet: "等待窗内等待",
459
642
  parameters: waitSchema,
460
643
  async execute(toolCallId: string, params: WaitParams, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
461
644
  const activation = getActivation();
@@ -476,6 +659,7 @@ export function createCollectorRoleRuntime(
476
659
  details,
477
660
  };
478
661
  } catch (error) {
662
+ if (isCorrectableExecuteError(error)) throw error;
479
663
  hostActions.failInfrastructure(error, ctx, toolCallId);
480
664
  }
481
665
  },
@@ -1,18 +1,56 @@
1
1
  import { Type, type Static } from "typebox";
2
- import { COLLECTOR_ELIGIBILITY_MS } from "./collector-evidence.ts";
3
2
  import { openToolObject } from "./open-tool-schema.ts";
4
3
  import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
5
4
 
6
5
  export const collectorObserveArgsSchema = Type.Object({}, { additionalProperties: false });
6
+ /**
7
+ * Stable request identity: non-empty after trim, no leading/trailing whitespace.
8
+ * Host schema + ledger identity seam share this contract (#677 / D2 dedup).
9
+ */
10
+ export const COLLECTOR_REQUEST_ID_PATTERN = /^\S(?:.*\S)?$/;
7
11
  export const collectorRequestArgsSchema = Type.Object({
8
- requestId: Type.String({ minLength: 1, description: "配置请求身份" }),
12
+ requestId: Type.String({
13
+ minLength: 1,
14
+ pattern: COLLECTOR_REQUEST_ID_PATTERN.source,
15
+ description: "请求身份(配置 id 或角色判定的稳定 id;首尾无空白)",
16
+ }),
9
17
  snapshotId: Type.String({ minLength: 1, description: "最新留存观察快照" }),
18
+ body: Type.Optional(Type.String({
19
+ minLength: 1,
20
+ description: "角色判定的请求正文;request-manifest 未收录该 requestId 时必填",
21
+ })),
22
+ }, { additionalProperties: false });
23
+ /** UTF-8 byte ceiling per handbook body — keeps next activation materials inside context budget. */
24
+ export const COLLECTOR_HANDBOOK_MAX_BYTES = 64 * 1024;
25
+ export const collectorHandbookWriteArgsSchema = Type.Object({
26
+ scope: Type.Union([
27
+ Type.Literal("general"),
28
+ Type.Literal("repo"),
29
+ ], { description: "general=通用手册;repo=当前仓库差异" }),
30
+ body: Type.String({
31
+ description: `手册全文(opaque 工作记忆;整份替换;UTF-8 至多 ${COLLECTOR_HANDBOOK_MAX_BYTES} 字节)`,
32
+ }),
10
33
  }, { additionalProperties: false });
11
34
  export const collectorReadArgsSchema = Type.Object({
12
35
  evidenceId: Type.String({ minLength: 1, description: "observe 返回的材料证据 id(evidenceId)" }),
13
36
  }, { additionalProperties: false });
14
37
  export const collectorWaitArgsSchema = Type.Object({
15
- durationMs: Type.Integer({ minimum: 1, maximum: COLLECTOR_ELIGIBILITY_MS, description: "等待毫秒;单次上限五分钟且不超剩余资格" }),
38
+ durationMs: Type.Integer({
39
+ minimum: 1,
40
+ description: "等待毫秒;实际睡眠不超过剩余等待窗(#678;无包内单次任意上限)",
41
+ }),
42
+ }, { additionalProperties: false });
43
+
44
+ /**
45
+ * #678 D4: open the wait window at a work step.
46
+ * Omit startedAt for existing-PR trigger-phase end (= now).
47
+ * Pass PR creation success time for new-PR auto-trigger rounds.
48
+ */
49
+ export const collectorOpenWaitWindowArgsSchema = Type.Object({
50
+ startedAt: Type.Optional(Type.String({
51
+ minLength: 1,
52
+ description: "等待窗起点 ISO 时间;新建 PR 用创建成功时刻;省略=现在(触发阶段结束)",
53
+ })),
16
54
  }, { additionalProperties: false });
17
55
 
18
56
  /**
@@ -88,5 +126,6 @@ export type CollectorObserveArgs = Static<typeof collectorObserveArgsSchema>;
88
126
  export type CollectorRequestArgs = Static<typeof collectorRequestArgsSchema>;
89
127
  export type CollectorReadArgs = Static<typeof collectorReadArgsSchema>;
90
128
  export type CollectorWaitArgs = Static<typeof collectorWaitArgsSchema>;
129
+ export type CollectorOpenWaitWindowArgs = Static<typeof collectorOpenWaitWindowArgsSchema>;
91
130
  export type CollectorBindTargetArgs = Static<typeof collectorBindTargetArgsSchema>;
92
131
  export type CollectorOutputArgs = Static<typeof collectorOutputArgsSchema>;
@@ -106,6 +106,8 @@ export type RoleTurnActivation =
106
106
  /** Bound PR when known at admission; omit when role will bind from materials. */
107
107
  readonly pr?: string;
108
108
  readonly requestManifestPath?: string;
109
+ /** Wait-window ms as decimal string (#678 D4); omit → package default 10 minutes. */
110
+ readonly waitMs?: string;
109
111
  }
110
112
  | { readonly role: "doctor"; readonly casePath: string }
111
113
  | {
@@ -116,6 +116,7 @@ function buildActivationFlagArgs(activation: RoleTurnActivation): string[] {
116
116
  ...(activation.requestManifestPath === undefined
117
117
  ? []
118
118
  : ["--ak-collector-request-manifest", activation.requestManifestPath]),
119
+ ...(activation.waitMs === undefined ? [] : ["--ak-collector-wait-ms", activation.waitMs]),
119
120
  ];
120
121
  case "doctor":
121
122
  return ["--ak-role", "doctor", "--ak-doctor-case", activation.casePath];
@@ -54,6 +54,7 @@ export function buildCollectorTurnRequest(
54
54
  // #676 A: omit pr when unbound — role binds via ak_collector_bind_target.
55
55
  ...(admitted.prNumber === undefined ? {} : { pr: String(admitted.prNumber) }),
56
56
  ...(admitted.requestManifestPath === undefined ? {} : { requestManifestPath: admitted.requestManifestPath }),
57
+ ...(admitted.waitWindowMs === undefined ? {} : { waitMs: String(admitted.waitWindowMs) }),
57
58
  },
58
59
  },
59
60
  options,
@@ -83,6 +84,7 @@ export async function runPublicCollector(
83
84
  ...(parsed.project === undefined ? {} : { project: parsed.project }),
84
85
  ...(parsed.repo === undefined ? {} : { repo: parsed.repo }),
85
86
  ...(parsed.requestManifestPath === undefined ? {} : { requestManifestPath: parsed.requestManifestPath }),
87
+ ...(parsed.waitWindowMs === undefined ? {} : { waitWindowMs: parsed.waitWindowMs }),
86
88
  ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
87
89
  ...(env.model === undefined ? {} : { model: env.model }),
88
90
  });
@@ -178,6 +178,8 @@ export type AdmittedCollectorInvocation = AdmittedRoleInvocationBase & {
178
178
  readonly repository: CollectorRepository;
179
179
  readonly requestManifestPath?: string;
180
180
  readonly manifestDigest: string;
181
+ /** Wait-window ms (#678 D4); default applied at role activate when omitted. */
182
+ readonly waitWindowMs?: number;
181
183
  };
182
184
 
183
185
  export type AdmittedDoctorInvocation = AdmittedRoleInvocationBase & {
@@ -727,6 +729,8 @@ export type ParseCollectorArgvResult = {
727
729
  project?: string;
728
730
  repo?: string;
729
731
  requestManifestPath?: string;
732
+ /** Optional wait-window ms (#678 D4). */
733
+ waitWindowMs?: number;
730
734
  };
731
735
 
732
736
  export type ParseDoctorArgvResult = {
@@ -1722,6 +1726,7 @@ export function parseCollectorArgv(args: readonly string[]): ParseCollectorArgvR
1722
1726
  let repo: string | undefined;
1723
1727
  let prNumber: number | undefined;
1724
1728
  let requestManifestPath: string | undefined;
1729
+ let waitWindowMs: number | undefined;
1725
1730
  const positional: string[] = [];
1726
1731
  const tokens = [...args];
1727
1732
  const definitions = roleOptions("collector");
@@ -1754,6 +1759,17 @@ export function parseCollectorArgv(args: readonly string[]): ParseCollectorArgvR
1754
1759
  requestManifestPath = requireOptionPath(taken.def.canonical, taken.value);
1755
1760
  continue;
1756
1761
  }
1762
+ if (taken.def.id === "wait-ms") {
1763
+ if (taken.value === undefined || !/^[1-9]\d*$/.test(taken.value.trim())) {
1764
+ throw new CliUsageError("--wait-ms requires a positive safe-integer millisecond value");
1765
+ }
1766
+ const parsed = Number(taken.value.trim());
1767
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
1768
+ throw new CliUsageError("--wait-ms requires a positive safe-integer millisecond value");
1769
+ }
1770
+ waitWindowMs = parsed;
1771
+ continue;
1772
+ }
1757
1773
  throw new CliUsageError(`unknown collector option: ${taken.def.canonical}`);
1758
1774
  }
1759
1775
  const token = tokens.shift()!;
@@ -1772,6 +1788,7 @@ export function parseCollectorArgv(args: readonly string[]): ParseCollectorArgvR
1772
1788
  ...(project === undefined ? {} : { project }),
1773
1789
  ...(repo === undefined ? {} : { repo }),
1774
1790
  ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
1791
+ ...(waitWindowMs === undefined ? {} : { waitWindowMs }),
1775
1792
  };
1776
1793
  }
1777
1794
 
@@ -1845,6 +1862,8 @@ export type AdmitCollectorInvocationOptions = {
1845
1862
  repo?: string;
1846
1863
  /** Optional public request configuration; copied into the admitted run. */
1847
1864
  requestManifestPath?: string;
1865
+ /** Optional wait-window ms (#678 D4). */
1866
+ waitWindowMs?: number;
1848
1867
  createRunId?: () => string;
1849
1868
  /** Effective model for this invocation — written onto invocation.json. */
1850
1869
  model?: InvocationEffectiveModel;
@@ -1947,6 +1966,7 @@ export async function admitCollectorInvocation(
1947
1966
  repository: repository.canonical,
1948
1967
  repositoryDisplay: repository.display,
1949
1968
  ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
1969
+ ...(options.waitWindowMs === undefined ? {} : { waitWindowMs: options.waitWindowMs }),
1950
1970
  manifestDigest,
1951
1971
  attachments: attachments.map((a) => ({
1952
1972
  provenancePath: a.provenancePath,
@@ -1977,6 +1997,7 @@ export async function admitCollectorInvocation(
1977
1997
  ...(prNumber === undefined ? {} : { prNumber }),
1978
1998
  repository,
1979
1999
  ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
2000
+ ...(options.waitWindowMs === undefined ? {} : { waitWindowMs: options.waitWindowMs }),
1980
2001
  manifestDigest,
1981
2002
  };
1982
2003
  }
@@ -598,8 +598,22 @@ const COLLECTOR_OPTIONS = [
598
598
  repeatable: false,
599
599
  form: "option",
600
600
  description: {
601
- en: "Optional request manifest JSON path ({requests:[{id,body}]}).",
602
- zh: "可选 request manifest JSON 路径({requests:[{id,body}]})。",
601
+ en: "Optional request manifest JSON path ({requests:[{id,body}]}). Role may also decide request bodies from handbook/field activity without this file.",
602
+ zh: "可选 request manifest JSON 路径({requests:[{id,body}]})。角色亦可依手册/现场自行判定请求正文,不必依赖本文件。",
603
+ },
604
+ },
605
+ {
606
+ id: "wait-ms",
607
+ owner: "collector",
608
+ canonical: "--wait-ms",
609
+ aliases: [],
610
+ valueMetavar: "ms",
611
+ required: false,
612
+ repeatable: false,
613
+ form: "option",
614
+ description: {
615
+ en: "Wait-window duration in milliseconds after the work step opens (default 600000 = 10 minutes). Does not require a code change to adjust.",
616
+ zh: "工作步骤开启后的等待窗时长(毫秒;默认 600000=十分钟)。改配置即可,无需改代码。",
603
617
  },
604
618
  },
605
619
  ] as const satisfies readonly PublicOptionDefinition[];
@@ -1149,6 +1149,8 @@ type LoadedAdmittedRequestFields = {
1149
1149
  readonly repositoryDisplay?: string;
1150
1150
  readonly requestManifestPath?: string;
1151
1151
  readonly manifestDigest?: string;
1152
+ /** Collector wait-window ms restored on resume (#678). */
1153
+ readonly waitWindowMs?: number;
1152
1154
  /** Doctor — admitted single-case identity restored on resume (#633). */
1153
1155
  readonly issueNumber?: number;
1154
1156
  readonly caseRunsPath?: string;
@@ -1244,6 +1246,7 @@ async function loadResumableRunRecord(
1244
1246
  let repositoryDisplay: string | undefined;
1245
1247
  let requestManifestPath: string | undefined;
1246
1248
  let manifestDigest: string | undefined;
1249
+ let waitWindowMs: number | undefined;
1247
1250
  let issueNumber: number | undefined;
1248
1251
  let caseRunsPath: string | undefined;
1249
1252
  let caseIdentity: DoctorCaseIdentity | undefined;
@@ -1304,6 +1307,9 @@ async function loadResumableRunRecord(
1304
1307
  if (typeof record.manifestDigest === "string" && record.manifestDigest.trim() !== "") {
1305
1308
  manifestDigest = record.manifestDigest;
1306
1309
  }
1310
+ if (typeof record.waitWindowMs === "number" && Number.isSafeInteger(record.waitWindowMs) && record.waitWindowMs >= 1) {
1311
+ waitWindowMs = record.waitWindowMs;
1312
+ }
1307
1313
  // Doctor — admitted single-case identity (#633 resume).
1308
1314
  if (typeof record.issueNumber === "number" && Number.isSafeInteger(record.issueNumber) && record.issueNumber >= 1) {
1309
1315
  issueNumber = record.issueNumber;
@@ -1471,6 +1477,7 @@ async function loadResumableRunRecord(
1471
1477
  ...(repositoryDisplay === undefined ? {} : { repositoryDisplay }),
1472
1478
  ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
1473
1479
  ...(manifestDigest === undefined ? {} : { manifestDigest }),
1480
+ ...(waitWindowMs === undefined ? {} : { waitWindowMs }),
1474
1481
  ...(issueNumber === undefined ? {} : { issueNumber }),
1475
1482
  ...(caseRunsPath === undefined ? {} : { caseRunsPath }),
1476
1483
  ...(caseIdentity === undefined ? {} : { caseIdentity }),
@@ -1913,6 +1920,9 @@ export async function loadResumableCollectorRun(
1913
1920
  ...(loaded.admittedFields.requestManifestPath === undefined
1914
1921
  ? {}
1915
1922
  : { requestManifestPath: loaded.admittedFields.requestManifestPath }),
1923
+ ...(loaded.admittedFields.waitWindowMs === undefined
1924
+ ? {}
1925
+ : { waitWindowMs: loaded.admittedFields.waitWindowMs }),
1916
1926
  manifestDigest,
1917
1927
  };
1918
1928
  return seatLoadedResult(loaded, admitted);
@@ -611,6 +611,8 @@ export type RoleRuntimeDependencies = {
611
611
  /** Shared-seam issue-fetch capability for Reviewer Spec self-fetch (#343). */
612
612
  createReviewerIssueFetcher?(): ReviewerIssueFetcher;
613
613
  loadCollectorSoul?(): Promise<string>;
614
+ /** #677: optional packaged seed for first-use general bot handbook. */
615
+ loadCollectorHandbookSeed?(): Promise<string>;
614
616
  createCollectorTransport?(): CollectorGitHubTransport;
615
617
  loadDoctorSoul?(): Promise<string>;
616
618
  loadNotarySoul?(): Promise<string>;
@@ -1717,6 +1719,9 @@ export function createRoleRuntimeExtension(
1717
1719
  ...(dependencies.createCollectorClock === undefined
1718
1720
  ? {}
1719
1721
  : { createClock: dependencies.createCollectorClock }),
1722
+ ...(dependencies.loadCollectorHandbookSeed === undefined
1723
+ ? {}
1724
+ : { loadHandbookSeed: dependencies.loadCollectorHandbookSeed }),
1720
1725
  },
1721
1726
  hostActions,
1722
1727
  );