@akagilnc/pi-workflow-roles 0.1.3789 → 0.1.3797

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.
@@ -16645,7 +16645,8 @@ function buildActivationFlagArgs(activation) {
16645
16645
  "--ak-collector-repo",
16646
16646
  activation.repo,
16647
16647
  ...activation.pr === void 0 ? [] : ["--ak-collector-pr", activation.pr],
16648
- ...activation.requestManifestPath === void 0 ? [] : ["--ak-collector-request-manifest", activation.requestManifestPath]
16648
+ ...activation.requestManifestPath === void 0 ? [] : ["--ak-collector-request-manifest", activation.requestManifestPath],
16649
+ ...activation.waitMs === void 0 ? [] : ["--ak-collector-wait-ms", activation.waitMs]
16649
16650
  ];
16650
16651
  case "doctor":
16651
16652
  return ["--ak-role", "doctor", "--ak-doctor-case", activation.casePath];
@@ -18491,6 +18492,7 @@ async function loadResumableRunRecord(home, runId, authority) {
18491
18492
  let repositoryDisplay;
18492
18493
  let requestManifestPath;
18493
18494
  let manifestDigest;
18495
+ let waitWindowMs;
18494
18496
  let issueNumber;
18495
18497
  let caseRunsPath;
18496
18498
  let caseIdentity2;
@@ -18544,6 +18546,9 @@ async function loadResumableRunRecord(home, runId, authority) {
18544
18546
  if (typeof record4.manifestDigest === "string" && record4.manifestDigest.trim() !== "") {
18545
18547
  manifestDigest = record4.manifestDigest;
18546
18548
  }
18549
+ if (typeof record4.waitWindowMs === "number" && Number.isSafeInteger(record4.waitWindowMs) && record4.waitWindowMs >= 1) {
18550
+ waitWindowMs = record4.waitWindowMs;
18551
+ }
18547
18552
  if (typeof record4.issueNumber === "number" && Number.isSafeInteger(record4.issueNumber) && record4.issueNumber >= 1) {
18548
18553
  issueNumber = record4.issueNumber;
18549
18554
  }
@@ -18659,6 +18664,7 @@ async function loadResumableRunRecord(home, runId, authority) {
18659
18664
  ...repositoryDisplay === void 0 ? {} : { repositoryDisplay },
18660
18665
  ...requestManifestPath === void 0 ? {} : { requestManifestPath },
18661
18666
  ...manifestDigest === void 0 ? {} : { manifestDigest },
18667
+ ...waitWindowMs === void 0 ? {} : { waitWindowMs },
18662
18668
  ...issueNumber === void 0 ? {} : { issueNumber },
18663
18669
  ...caseRunsPath === void 0 ? {} : { caseRunsPath },
18664
18670
  ...caseIdentity2 === void 0 ? {} : { caseIdentity: caseIdentity2 },
@@ -18916,6 +18922,7 @@ async function loadResumableCollectorRun(home, runId, authority) {
18916
18922
  ...prNumber === void 0 ? {} : { prNumber },
18917
18923
  repository: parsedRepository,
18918
18924
  ...loaded.admittedFields.requestManifestPath === void 0 ? {} : { requestManifestPath: loaded.admittedFields.requestManifestPath },
18925
+ ...loaded.admittedFields.waitWindowMs === void 0 ? {} : { waitWindowMs: loaded.admittedFields.waitWindowMs },
18919
18926
  manifestDigest
18920
18927
  };
18921
18928
  return seatLoadedResult(loaded, admitted);
@@ -19744,6 +19751,20 @@ var init_option_definitions = __esm({
19744
19751
  en: "Optional request manifest JSON path ({requests:[{id,body}]}). Role may also decide request bodies from handbook/field activity without this file.",
19745
19752
  zh: "\u53EF\u9009 request manifest JSON \u8DEF\u5F84\uFF08{requests:[{id,body}]}\uFF09\u3002\u89D2\u8272\u4EA6\u53EF\u4F9D\u624B\u518C/\u73B0\u573A\u81EA\u884C\u5224\u5B9A\u8BF7\u6C42\u6B63\u6587\uFF0C\u4E0D\u5FC5\u4F9D\u8D56\u672C\u6587\u4EF6\u3002"
19746
19753
  }
19754
+ },
19755
+ {
19756
+ id: "wait-ms",
19757
+ owner: "collector",
19758
+ canonical: "--wait-ms",
19759
+ aliases: [],
19760
+ valueMetavar: "ms",
19761
+ required: false,
19762
+ repeatable: false,
19763
+ form: "option",
19764
+ description: {
19765
+ en: "Wait-window duration in milliseconds after the work step opens (default 600000 = 10 minutes). Does not require a code change to adjust.",
19766
+ zh: "\u5DE5\u4F5C\u6B65\u9AA4\u5F00\u542F\u540E\u7684\u7B49\u5F85\u7A97\u65F6\u957F\uFF08\u6BEB\u79D2\uFF1B\u9ED8\u8BA4 600000\uFF1D\u5341\u5206\u949F\uFF09\u3002\u6539\u914D\u7F6E\u5373\u53EF\uFF0C\u65E0\u9700\u6539\u4EE3\u7801\u3002"
19767
+ }
19747
19768
  }
19748
19769
  ];
19749
19770
  DOCTOR_OPTIONS = [
@@ -21024,6 +21045,7 @@ function parseCollectorArgv(args) {
21024
21045
  let repo;
21025
21046
  let prNumber;
21026
21047
  let requestManifestPath;
21048
+ let waitWindowMs;
21027
21049
  const positional = [];
21028
21050
  const tokens = [...args];
21029
21051
  const definitions = roleOptions("collector");
@@ -21056,6 +21078,17 @@ function parseCollectorArgv(args) {
21056
21078
  requestManifestPath = requireOptionPath(taken.def.canonical, taken.value);
21057
21079
  continue;
21058
21080
  }
21081
+ if (taken.def.id === "wait-ms") {
21082
+ if (taken.value === void 0 || !/^[1-9]\d*$/.test(taken.value.trim())) {
21083
+ throw new CliUsageError("--wait-ms requires a positive safe-integer millisecond value");
21084
+ }
21085
+ const parsed = Number(taken.value.trim());
21086
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
21087
+ throw new CliUsageError("--wait-ms requires a positive safe-integer millisecond value");
21088
+ }
21089
+ waitWindowMs = parsed;
21090
+ continue;
21091
+ }
21059
21092
  throw new CliUsageError(`unknown collector option: ${taken.def.canonical}`);
21060
21093
  }
21061
21094
  const token = tokens.shift();
@@ -21071,7 +21104,8 @@ function parseCollectorArgv(args) {
21071
21104
  attachmentPaths,
21072
21105
  ...project === void 0 ? {} : { project },
21073
21106
  ...repo === void 0 ? {} : { repo },
21074
- ...requestManifestPath === void 0 ? {} : { requestManifestPath }
21107
+ ...requestManifestPath === void 0 ? {} : { requestManifestPath },
21108
+ ...waitWindowMs === void 0 ? {} : { waitWindowMs }
21075
21109
  };
21076
21110
  }
21077
21111
  function resolveGitHubRemoteRepository(projectRoot) {
@@ -21196,6 +21230,7 @@ async function admitCollectorInvocation(options) {
21196
21230
  repository: repository.canonical,
21197
21231
  repositoryDisplay: repository.display,
21198
21232
  ...requestManifestPath === void 0 ? {} : { requestManifestPath },
21233
+ ...options.waitWindowMs === void 0 ? {} : { waitWindowMs: options.waitWindowMs },
21199
21234
  manifestDigest,
21200
21235
  attachments: attachments.map((a) => ({
21201
21236
  provenancePath: a.provenancePath,
@@ -21225,6 +21260,7 @@ async function admitCollectorInvocation(options) {
21225
21260
  ...prNumber === void 0 ? {} : { prNumber },
21226
21261
  repository,
21227
21262
  ...requestManifestPath === void 0 ? {} : { requestManifestPath },
21263
+ ...options.waitWindowMs === void 0 ? {} : { waitWindowMs: options.waitWindowMs },
21228
21264
  manifestDigest
21229
21265
  };
21230
21266
  }
@@ -23597,11 +23633,11 @@ var init_reviewer_dispatch_rejection = __esm({
23597
23633
  });
23598
23634
 
23599
23635
  // src/collector-evidence.ts
23600
- var COLLECTOR_ELIGIBILITY_MS;
23636
+ var COLLECTOR_DEFAULT_WAIT_WINDOW_MS;
23601
23637
  var init_collector_evidence = __esm({
23602
23638
  "src/collector-evidence.ts"() {
23603
23639
  "use strict";
23604
- COLLECTOR_ELIGIBILITY_MS = 15 * 60 * 1e3;
23640
+ COLLECTOR_DEFAULT_WAIT_WINDOW_MS = 10 * 60 * 1e3;
23605
23641
  }
23606
23642
  });
23607
23643
 
@@ -27949,7 +27985,8 @@ function buildCollectorTurnRequest(admitted, options) {
27949
27985
  repo: admitted.repository.display,
27950
27986
  // #676 A: omit pr when unbound — role binds via ak_collector_bind_target.
27951
27987
  ...admitted.prNumber === void 0 ? {} : { pr: String(admitted.prNumber) },
27952
- ...admitted.requestManifestPath === void 0 ? {} : { requestManifestPath: admitted.requestManifestPath }
27988
+ ...admitted.requestManifestPath === void 0 ? {} : { requestManifestPath: admitted.requestManifestPath },
27989
+ ...admitted.waitWindowMs === void 0 ? {} : { waitMs: String(admitted.waitWindowMs) }
27953
27990
  }
27954
27991
  },
27955
27992
  options
@@ -27969,6 +28006,7 @@ async function runPublicCollector(argv, env, io, parseCollectorArgv2) {
27969
28006
  ...parsed.project === void 0 ? {} : { project: parsed.project },
27970
28007
  ...parsed.repo === void 0 ? {} : { repo: parsed.repo },
27971
28008
  ...parsed.requestManifestPath === void 0 ? {} : { requestManifestPath: parsed.requestManifestPath },
28009
+ ...parsed.waitWindowMs === void 0 ? {} : { waitWindowMs: parsed.waitWindowMs },
27972
28010
  ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId },
27973
28011
  ...env.model === void 0 ? {} : { model: env.model }
27974
28012
  });
@@ -487,6 +487,20 @@ const COLLECTOR_OPTIONS = [
487
487
  zh: "可选 request manifest JSON 路径({requests:[{id,body}]})。角色亦可依手册/现场自行判定请求正文,不必依赖本文件。",
488
488
  },
489
489
  },
490
+ {
491
+ id: "wait-ms",
492
+ owner: "collector",
493
+ canonical: "--wait-ms",
494
+ aliases: [],
495
+ valueMetavar: "ms",
496
+ required: false,
497
+ repeatable: false,
498
+ form: "option",
499
+ description: {
500
+ en: "Wait-window duration in milliseconds after the work step opens (default 600000 = 10 minutes). Does not require a code change to adjust.",
501
+ zh: "工作步骤开启后的等待窗时长(毫秒;默认 600000=十分钟)。改配置即可,无需改代码。",
502
+ },
503
+ },
490
504
  ];
491
505
  const DOCTOR_OPTIONS = [
492
506
  bindOwner("doctor", SHARED_PROJECT_SEMANTICS),
@@ -880,6 +880,7 @@ async function loadResumableRunRecord(home, runId, authority) {
880
880
  let repositoryDisplay;
881
881
  let requestManifestPath;
882
882
  let manifestDigest;
883
+ let waitWindowMs;
883
884
  let issueNumber;
884
885
  let caseRunsPath;
885
886
  let caseIdentity;
@@ -934,6 +935,9 @@ async function loadResumableRunRecord(home, runId, authority) {
934
935
  if (typeof record.manifestDigest === "string" && record.manifestDigest.trim() !== "") {
935
936
  manifestDigest = record.manifestDigest;
936
937
  }
938
+ if (typeof record.waitWindowMs === "number" && Number.isSafeInteger(record.waitWindowMs) && record.waitWindowMs >= 1) {
939
+ waitWindowMs = record.waitWindowMs;
940
+ }
937
941
  // Doctor — admitted single-case identity (#633 resume).
938
942
  if (typeof record.issueNumber === "number" && Number.isSafeInteger(record.issueNumber) && record.issueNumber >= 1) {
939
943
  issueNumber = record.issueNumber;
@@ -1074,6 +1078,7 @@ async function loadResumableRunRecord(home, runId, authority) {
1074
1078
  ...(repositoryDisplay === undefined ? {} : { repositoryDisplay }),
1075
1079
  ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
1076
1080
  ...(manifestDigest === undefined ? {} : { manifestDigest }),
1081
+ ...(waitWindowMs === undefined ? {} : { waitWindowMs }),
1077
1082
  ...(issueNumber === undefined ? {} : { issueNumber }),
1078
1083
  ...(caseRunsPath === undefined ? {} : { caseRunsPath }),
1079
1084
  ...(caseIdentity === undefined ? {} : { caseIdentity }),
@@ -1338,6 +1343,9 @@ export async function loadResumableCollectorRun(home, runId, authority) {
1338
1343
  ...(loaded.admittedFields.requestManifestPath === undefined
1339
1344
  ? {}
1340
1345
  : { requestManifestPath: loaded.admittedFields.requestManifestPath }),
1346
+ ...(loaded.admittedFields.waitWindowMs === undefined
1347
+ ? {}
1348
+ : { waitWindowMs: loaded.admittedFields.waitWindowMs }),
1341
1349
  manifestDigest,
1342
1350
  };
1343
1351
  return seatLoadedResult(loaded, admitted);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3789",
3
+ "version": "0.1.3797",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -8,37 +8,39 @@
8
8
  **一、立法。** 先为本票立「适用法」;后面每一步对着它问,未立法不得往下走。适用法三部分:
9
9
  1 第一法源立法(最高法):该票起居录,全局宪法,仓内治理文件;
10
10
 
11
- 2 第二发源立法:
11
+ 2 第二法源立法:
12
12
  适用 ADR——由你自行检索,按票面触及的席位、接缝、术语在 docs/adr 与 CONTEXT.md 查全,票面引没引都算——并逐键列出其决策键;质量法、审刑院法典;
13
13
 
14
- 3 第二法源合法性检查:判断第二法源 以及 输入的 prompt等 对比第一法源是否有超出或不足,曲解。如有,除非有陛下原话批准,否则直接封驳 要求改正或陛下批准。
14
+ 3 第二法源合法性检查:判断第二法源 以及 输入的 prompt等 对比第一法源是否有超出,不足,曲解。如有,除非有陛下原话批准,否则直接封驳 要求改正或陛下批准。
15
15
 
16
- **二、相抵。** 票面,相关票与在飞分支与法源有无抵触。
16
+ **二、相抵** 票面,相关票与在飞分支与法源有无抵触。
17
17
 
18
- **三、缺失。** 法源要求的,票面有没有漏:适用 ADR 未被引用即缺失,引用须写 `docs/adr/...md` 路径;ADR 每个决策键须有落点或明写「由 #x 承接」;验收面须覆盖票面每条主张;前置产物、决定、答复须在;承重主张的源头不存在、不支持、已过时,同为缺失。
18
+ **三、缺失** 法源要求的,票面有没有漏:适用 ADR 未被引用即缺失,引用须写 `docs/adr/...md` 路径;ADR 每个决策键须有落点或明写「由 #x 承接」;验收面须覆盖票面每条主张;前置产物、决定、答复须在;承重主张的源头不存在、不支持、已过时,同为缺失。缺失列表在能力范围内一次扫完,尽量避免因缺失导致多轮往返。
19
19
 
20
- **四、擅加。** 票面有没有超出法源的机制、约束、验收、术语。
20
+ **四、擅加** 票面有没有超出法源的机制、约束、验收、术语。
21
+
22
+ **五、符合陛下意图** 先根据起居录对话,判断陛下意图。再证实票面是否是陛下想要的,对于修复票,判断 根因是什么、证据在哪 。对于新功能设计票,判断设计是不是合理,是不是陛下想要的方向。以及 因果链本身站不站得住
21
23
 
22
24
  各步取证按《审法·取证边界》:读码、开卷、传召皆可;探针只落 worktree 与临时目录,不得删改其外任何路径。交付时候清理回原样
23
25
 
24
26
  ## 审读五问
25
27
 
26
- 四步是审读,五问是裁决口径,对应关系固定,不重做:
28
+ 五步是审读,五问是裁决口径,对应关系固定,不重做:
27
29
  1. 票面是否符合既定制度——由「相抵」答;
28
30
  2. 授权是否真实——由「立法②」答:不认转述与口头;
29
- 3. 文书是否与原意一致——由「缺失」「擅加」答:无过度解释、无夹带、无缺口;
31
+ 3. 文书是否与原意一致——由「缺失」「擅加」「符合陛下意图」答:无过度解释、无夹带、无缺口、符合原意;
30
32
  4. 是否存在必须退回重议的问题;
31
33
  5. 是否已具备正式发布与执行资格。
32
34
 
33
- **每次开庭都是全票审读,复审亦然——四步五问对全票重新过一遍;上轮封驳条目只是必须核销的部分,不划定审读范围。永远全审,没有局部一说。**
35
+ **每次开庭都是全票审读,复审亦然——五步五问对全票重新过一遍;上轮封驳条目只是必须核销的部分,不划定审读范围。永远全审,没有局部一说。**
34
36
 
35
37
  ## 判词
36
38
 
37
39
  事实与处方分开对宪:裁决落法度与事实;施工设计与实现细节过早堆上票面即为失职。
38
40
 
39
- - 署(converged)——四步无碍、五问皆有据可答,具备开工资格;
41
+ - 署(converged)——五步无碍、五问皆有据可答,具备开工资格;
40
42
  - 封驳(continue)——存在必须退回重议的问题;写明退给谁、缺什么、属哪一步;
41
- - 上呈(escalate)——无法正常暑/封驳时。上呈求助
43
+ - 上呈(escalate)——无法正常署/封驳时。上呈求助
42
44
 
43
45
 
44
46
  主张必须附证据或法的具体条目。finding 提类别,同类一次说全(审刑院法典·审官纪律)。
@@ -98,6 +98,7 @@ export function projectAcpActivationFlags(request: RoleTurnRequest): Map<string,
98
98
  // #676 D1: pr optional at admission; omit flag when role binds from materials.
99
99
  if (activation.pr !== undefined) flags.set("ak-collector-pr", activation.pr);
100
100
  if (activation.requestManifestPath !== undefined) flags.set("ak-collector-request-manifest", activation.requestManifestPath);
101
+ if (activation.waitMs !== undefined) flags.set("ak-collector-wait-ms", activation.waitMs);
101
102
  }
102
103
  return flags;
103
104
  }
@@ -11,7 +11,8 @@ import type {
11
11
  GitHubUser,
12
12
  } from "./collector-github.ts";
13
13
 
14
- export const COLLECTOR_ELIGIBILITY_MS = 15 * 60 * 1000;
14
+ /** Default collector wait window (D4): 10 minutes from the work-step open, not from session activation. */
15
+ export const COLLECTOR_DEFAULT_WAIT_WINDOW_MS = 10 * 60 * 1000;
15
16
 
16
17
  export type WindowRelation = "before" | "within" | "after" | "uncertain";
17
18
  export type HeadRelation = "current" | "prior";
@@ -73,6 +74,8 @@ export type CollectorSnapshot = {
73
74
  prNumber: number;
74
75
  prState: string;
75
76
  headOid: string;
77
+ /** PR create success time when GitHub supplies it (#678 new-PR wait start). */
78
+ prCreatedAt?: string;
76
79
  complete: boolean;
77
80
  evidenceIds: string[];
78
81
  pageDiagnostics: GitHubPageDiagnostics[];
@@ -177,6 +180,7 @@ export function normalizePullRequestEvidence(
177
180
  number: pr.number,
178
181
  state: pr.state,
179
182
  headOid: pr.headOid,
183
+ createdAt: pr.createdAt ?? null,
180
184
  updatedAt: pr.updatedAt ?? null,
181
185
  htmlUrl: pr.url,
182
186
  });
@@ -190,6 +194,7 @@ export function normalizePullRequestEvidence(
190
194
  state: pr.state,
191
195
  commitOid: pr.headOid,
192
196
  htmlUrl: pr.url,
197
+ // Version clock stays updatedAt. Create success for the wait window is snapshot.prCreatedAt only (#678).
193
198
  authoritativeTime: pr.updatedAt ?? null,
194
199
  firstObservedAt: observedAt,
195
200
  raw: pr.raw,
@@ -7,6 +7,8 @@ export type GitHubPullRequest = {
7
7
  number: number;
8
8
  state: string;
9
9
  headOid: string;
10
+ /** GitHub PR created_at — work-step start for new-PR auto-trigger rounds (#678 D4). */
11
+ createdAt?: string;
10
12
  updatedAt?: string;
11
13
  url: string;
12
14
  raw: unknown;
@@ -491,6 +493,7 @@ export function normalizePullRequest(raw: unknown): GitHubPullRequest {
491
493
  number,
492
494
  state,
493
495
  headOid: head["sha"],
496
+ ...(typeof raw["created_at"] === "string" ? { createdAt: raw["created_at"] } : {}),
494
497
  ...(typeof raw["updated_at"] === "string" ? { updatedAt: raw["updated_at"] } : {}),
495
498
  url: htmlUrl,
496
499
  raw,
@@ -151,6 +151,20 @@ export class CollectorNonOpenRequestError extends CorrectableSubmissionError {
151
151
  }
152
152
  }
153
153
 
154
+ /** #678: wait window not open or already ended — bounce without latching fatal so materials still seal. */
155
+ export class CollectorWaitWindowClosedError extends CorrectableSubmissionError {
156
+ constructor(action: "request" | "wait" | "wait-before-open") {
157
+ super(
158
+ action === "request"
159
+ ? "通进司请求不在资格截止前"
160
+ : action === "wait-before-open"
161
+ ? "通进司等待需要先在工作步骤开启等待窗(新建 PR 用创建成功时刻;已有 PR 在触发阶段结束后开启)"
162
+ : "通进司等待不在资格截止前",
163
+ );
164
+ this.name = "CollectorWaitWindowClosedError";
165
+ }
166
+ }
167
+
154
168
  function candidateRecord(candidate: unknown): Record<string, unknown> | undefined {
155
169
  if (candidate === undefined || candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
156
170
  return undefined;
@@ -2,7 +2,7 @@ import type { CollectorManifest, CollectorRepository } from "./collector-config.
2
2
  import {
3
3
  applyEvidenceVersionHistory,
4
4
  assignWindowRelations,
5
- COLLECTOR_ELIGIBILITY_MS,
5
+ COLLECTOR_DEFAULT_WAIT_WINDOW_MS,
6
6
  measureNormalizedBytes,
7
7
  normalizeAuthenticatedUserEvidence,
8
8
  normalizeIssueCommentEvidence,
@@ -23,13 +23,18 @@ import {
23
23
  type GitHubPageDiagnostics,
24
24
  type GitHubPullRequest,
25
25
  } from "./collector-github.ts";
26
- import { CollectorNonOpenRequestError } from "./collector-identity.ts";
26
+ import {
27
+ CollectorNonOpenRequestError,
28
+ CollectorWaitWindowClosedError,
29
+ } from "./collector-identity.ts";
27
30
  import { COLLECTOR_OUTPUT_TOOL } from "./package-contracts/collector-output.ts";
28
31
 
29
32
  export const COLLECTOR_OBSERVE_TOOL = "ak_collector_observe";
30
33
  export const COLLECTOR_READ_TOOL = "ak_collector_read";
31
34
  export const COLLECTOR_REQUEST_TOOL = "ak_collector_request";
32
35
  export const COLLECTOR_WAIT_TOOL = "ak_collector_wait";
36
+ /** #678 D4: open wait window at a work step — business tool, ledger-booked. */
37
+ export const COLLECTOR_OPEN_WAIT_WINDOW_TOOL = "ak_collector_open_wait_window";
33
38
  /** #676 A: role-decided target bind — business tool, ledger-booked. */
34
39
  export const COLLECTOR_BIND_TARGET_TOOL = "ak_collector_bind_target";
35
40
  /** #677: opaque handbook write — business tool, ledger-booked. */
@@ -41,6 +46,7 @@ export const COLLECTOR_OPERATIONAL_TOOLS = [
41
46
  COLLECTOR_OBSERVE_TOOL,
42
47
  COLLECTOR_READ_TOOL,
43
48
  COLLECTOR_REQUEST_TOOL,
49
+ COLLECTOR_OPEN_WAIT_WINDOW_TOOL,
44
50
  COLLECTOR_WAIT_TOOL,
45
51
  COLLECTOR_HANDBOOK_WRITE_TOOL,
46
52
  ] as const;
@@ -83,6 +89,8 @@ export type ObserveModelView = {
83
89
  completedAt: string;
84
90
  prState: string;
85
91
  headOid: string;
92
+ /** PR create success time when known — new-PR wait-window startedAt (#678 D4). */
93
+ prCreatedAt?: string;
86
94
  complete: boolean;
87
95
  evidence: ObserveEvidenceEntry[];
88
96
  requestAttempts: Array<{
@@ -158,6 +166,15 @@ export type CollectorConfigState = {
158
166
  /** Bound PR target; undefined until explicit flag, admission bind, or role bind-target tool. */
159
167
  prNumber: number | undefined;
160
168
  manifest: CollectorManifest;
169
+ /**
170
+ * Wait window duration (#678 D4). Default 10 minutes; caller-configurable without code change.
171
+ * Window opens at a work step (PR create success / trigger-phase end), not session activation.
172
+ */
173
+ waitWindowMs: number;
174
+ };
175
+
176
+ export type CollectorLedgerConfigInput = Omit<CollectorConfigState, "waitWindowMs"> & {
177
+ waitWindowMs?: number;
161
178
  };
162
179
 
163
180
  /** Durable session journal sink owned by the shared execution seam. */
@@ -177,8 +194,11 @@ export type CollectorLedger = {
177
194
  readonly fatal: boolean;
178
195
  readonly fatalReason: string | undefined;
179
196
  readonly outputCandidate: boolean;
197
+ /** Session ready (before_agent_start); not the wait window. */
180
198
  readonly activationRecorded: boolean;
199
+ /** Wait-window start (receipt activationTime); undefined until openWaitWindow. */
181
200
  readonly activationTime: Date | undefined;
201
+ /** Wait-window end; undefined until openWaitWindow. */
182
202
  readonly deadlineTime: Date | undefined;
183
203
  readonly activationMono: number | undefined;
184
204
  readonly deadlineMono: number | undefined;
@@ -192,7 +212,13 @@ export type CollectorLedger = {
192
212
 
193
213
  latchFatal(reason: string, cause?: unknown): Error;
194
214
  assertNotFatal(): void;
215
+ /** Mark session ready for prep observe/request; does not open the wait window (#678). */
195
216
  recordActivation(clock: CollectorClock): void;
217
+ /**
218
+ * Open the wait window once at a work step. Optional startedAt covers new-PR create success;
219
+ * omit for existing-PR trigger-phase end (= now). Second call is a no-op (D6: updates do not reset).
220
+ */
221
+ openWaitWindow(clock: CollectorClock, options?: { startedAt?: Date }): void;
196
222
  recordOutputCandidate(): void;
197
223
  /** #676 A: role-decided unique PR bind (business fact on the ledger). */
198
224
  bindTarget(prNumber: number): void;
@@ -237,16 +263,31 @@ function isOperationalTool(name: string): name is CollectorOperationalTool {
237
263
  return (COLLECTOR_OPERATIONAL_TOOLS as readonly string[]).includes(name);
238
264
  }
239
265
 
266
+ function resolveWaitWindowMs(raw: number | undefined): number {
267
+ if (raw === undefined) return COLLECTOR_DEFAULT_WAIT_WINDOW_MS;
268
+ if (!Number.isSafeInteger(raw) || raw < 1) {
269
+ throw new Error("通进司 waitWindowMs 须为正安全整数毫秒");
270
+ }
271
+ return raw;
272
+ }
273
+
240
274
  export function createCollectorLedger(
241
- config: CollectorConfigState,
275
+ input: CollectorLedgerConfigInput,
242
276
  options?: CollectorLedgerOptions,
243
277
  ): CollectorLedger {
278
+ const config: CollectorConfigState = {
279
+ repository: input.repository,
280
+ prNumber: input.prNumber,
281
+ manifest: input.manifest,
282
+ waitWindowMs: resolveWaitWindowMs(input.waitWindowMs),
283
+ };
244
284
  const clock = options?.clock;
245
285
  const journal = options?.journal;
246
286
  let fatal = false;
247
287
  let fatalReason: string | undefined;
248
288
  let outputCandidate = false;
249
289
  let pendingOutputCallId: string | undefined;
290
+ let sessionReady = false;
250
291
  let activationTime: Date | undefined;
251
292
  let deadlineTime: Date | undefined;
252
293
  let activationMono: number | undefined;
@@ -297,7 +338,7 @@ export function createCollectorLedger(
297
338
  };
298
339
 
299
340
  const remainingMs = (clock: CollectorClock): number => {
300
- if (deadlineMono === undefined) return COLLECTOR_ELIGIBILITY_MS;
341
+ if (deadlineMono === undefined) return config.waitWindowMs;
301
342
  return Math.max(0, deadlineMono - monoNowOrThrow(clock));
302
343
  };
303
344
 
@@ -380,7 +421,9 @@ export function createCollectorLedger(
380
421
  };
381
422
 
382
423
  const commitActivationWindow = (nextActivation: Date, nextDeadline: Date): void => {
424
+ // D6: once open, PR updates / second open must not reset the window.
383
425
  if (activationTime !== undefined) return;
426
+ sessionReady = true;
384
427
  activationTime = nextActivation;
385
428
  deadlineTime = nextDeadline;
386
429
  bindDeadlineMonoFromWall();
@@ -520,6 +563,8 @@ export function createCollectorLedger(
520
563
  if (entry.customType === COLLECTOR_ACTIVATION_ENTRY_TYPE) {
521
564
  if (typeof data.activationTime === "string" && typeof data.deadlineTime === "string") {
522
565
  commitActivationWindow(new Date(data.activationTime), new Date(data.deadlineTime));
566
+ } else if (data.sessionReady === true) {
567
+ sessionReady = true;
523
568
  }
524
569
  continue;
525
570
  }
@@ -581,7 +626,7 @@ export function createCollectorLedger(
581
626
  return outputCandidate || pendingOutputCallId !== undefined;
582
627
  },
583
628
  get activationRecorded() {
584
- return activationTime !== undefined;
629
+ return sessionReady;
585
630
  },
586
631
  get activationTime() {
587
632
  return activationTime;
@@ -620,16 +665,38 @@ export function createCollectorLedger(
620
665
  latchFatal,
621
666
  assertNotFatal,
622
667
 
623
- recordActivation(clock) {
668
+ recordActivation(_clock) {
669
+ assertNotFatal();
670
+ if (sessionReady) return;
671
+ sessionReady = true;
672
+ // Session ready only — wait window opens later at a work step (#678 D4).
673
+ appendJournal(COLLECTOR_ACTIVATION_ENTRY_TYPE, {
674
+ sessionReady: true,
675
+ });
676
+ },
677
+
678
+ openWaitWindow(clock, options) {
624
679
  assertNotFatal();
625
- if (activationTime !== undefined) return;
626
- activationTime = clock.wallNow();
627
- activationMono = clock.monoNow();
628
- deadlineTime = new Date(activationTime.getTime() + COLLECTOR_ELIGIBILITY_MS);
629
- deadlineMono = activationMono + COLLECTOR_ELIGIBILITY_MS;
680
+ if (!sessionReady) {
681
+ throw latchFatal("通进司开启等待窗需要先激活会话");
682
+ }
683
+ if (activationTime !== undefined) return; // D6: do not reset
684
+ const startedAt = options?.startedAt ?? clock.wallNow();
685
+ if (!(startedAt instanceof Date) || Number.isNaN(startedAt.getTime())) {
686
+ throw new Error("通进司等待窗 startedAt 须为有效时间");
687
+ }
688
+ const nextDeadline = new Date(startedAt.getTime() + config.waitWindowMs);
689
+ // Bind mono relative to current clock so a past startedAt shortens remaining correctly.
690
+ activationTime = startedAt;
691
+ deadlineTime = nextDeadline;
692
+ const wallNow = clock.wallNow().getTime();
693
+ const monoNow = clock.monoNow();
694
+ activationMono = monoNow - (wallNow - startedAt.getTime());
695
+ deadlineMono = monoNow + (nextDeadline.getTime() - wallNow);
630
696
  appendJournal(COLLECTOR_ACTIVATION_ENTRY_TYPE, {
631
697
  activationTime: activationTime.toISOString(),
632
698
  deadlineTime: deadlineTime.toISOString(),
699
+ waitWindowMs: config.waitWindowMs,
633
700
  });
634
701
  },
635
702
 
@@ -733,7 +800,7 @@ export function createCollectorLedger(
733
800
 
734
801
  async observe(transport, clock, signal) {
735
802
  assertNotFatal();
736
- if (activationTime === undefined) {
803
+ if (!sessionReady) {
737
804
  throw latchFatal("通进司观察需要激活");
738
805
  }
739
806
  if (signal?.aborted) {
@@ -841,6 +908,7 @@ export function createCollectorLedger(
841
908
  prNumber: requireBoundPr(),
842
909
  prState: pr.state,
843
910
  headOid: pr.headOid,
911
+ ...(pr.createdAt === undefined ? {} : { prCreatedAt: pr.createdAt }),
844
912
  complete: true,
845
913
  evidenceIds: storedIds,
846
914
  pageDiagnostics,
@@ -877,7 +945,7 @@ export function createCollectorLedger(
877
945
 
878
946
  async request(input, transport, clock, signal) {
879
947
  assertNotFatal();
880
- if (activationTime === undefined || deadlineTime === undefined) {
948
+ if (!sessionReady) {
881
949
  throw latchFatal("通进司请求需要激活");
882
950
  }
883
951
  if (ledger.unresolvedTransportFailure) {
@@ -918,7 +986,8 @@ export function createCollectorLedger(
918
986
  }
919
987
  if (pastCutoff(clock)) {
920
988
  finalObservationRequired = true;
921
- throw latchFatal("通进司请求不在资格截止前");
989
+ // #678: do not latch fatal — materials must still seal after the window ends.
990
+ throw new CollectorWaitWindowClosedError("request");
922
991
  }
923
992
 
924
993
  // Caller manifest body wins when requestId is configured; otherwise role body.
@@ -1035,23 +1104,23 @@ export function createCollectorLedger(
1035
1104
 
1036
1105
  async wait(input, clock, signal) {
1037
1106
  assertNotFatal();
1038
- if (activationTime === undefined) {
1107
+ if (!sessionReady) {
1039
1108
  throw latchFatal("通进司等待需要激活");
1040
1109
  }
1110
+ // #678: wait never invents a window start. Work-step open (create-success / trigger-end)
1111
+ // must be explicit — silent "now" would break the new-PR create-success clock.
1112
+ if (activationTime === undefined || deadlineTime === undefined) {
1113
+ throw new CollectorWaitWindowClosedError("wait-before-open");
1114
+ }
1041
1115
  // durationMs shape authority = collectorWaitArgsSchema (host parameters).
1042
1116
  if (pastCutoff(clock)) {
1043
1117
  finalObservationRequired = true;
1044
- throw latchFatal("通进司等待不在资格截止前");
1118
+ throw new CollectorWaitWindowClosedError("wait");
1045
1119
  }
1046
1120
 
1047
1121
  const remaining = remainingMs(clock);
1048
- // Single-wait runtime cadence cap (v2 §6 / §9); schema max stays 15m.
1049
- const COLLECTOR_SINGLE_WAIT_MAX_MS = 300_000;
1050
- const effectiveMs = Math.min(
1051
- input.durationMs,
1052
- remaining,
1053
- COLLECTOR_SINGLE_WAIT_MAX_MS,
1054
- );
1122
+ // Cap only by remaining window — no package-local single-wait ceiling (ADR 0035 / #678).
1123
+ const effectiveMs = Math.min(input.durationMs, remaining);
1055
1124
  const startedAt = clock.wallNow().toISOString();
1056
1125
  const waitId = sha256Text(`wait:${startedAt}:${effectiveMs}`).slice(0, 16);
1057
1126
  await clock.sleep(effectiveMs, signal);
@@ -1143,6 +1212,9 @@ function buildObserveModelView(input: {
1143
1212
  completedAt: input.snapshot.completedAt,
1144
1213
  prState: input.snapshot.prState,
1145
1214
  headOid: input.snapshot.headOid,
1215
+ ...(input.snapshot.prCreatedAt === undefined
1216
+ ? {}
1217
+ : { prCreatedAt: input.snapshot.prCreatedAt }),
1146
1218
  complete: input.snapshot.complete,
1147
1219
  evidence: relevant.map((record): ObserveEvidenceEntry => projectEvidenceEntryView(record)),
1148
1220
  requestAttempts: input.attempts.map((attempt) => ({