@akagilnc/pi-workflow-roles 0.1.3771 → 0.1.3789

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 (40) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +793 -393
  4. package/dist/atomic-write.js +23 -0
  5. package/dist/collector-handbook.js +142 -0
  6. package/dist/collector-ledger.js +39 -47
  7. package/dist/collector-role.js +103 -14
  8. package/dist/collector-tool-schemas.js +25 -1
  9. package/dist/headless-host/description.js +77 -0
  10. package/dist/headless-host/mcp-relay.mjs +119 -0
  11. package/dist/headless-host/production-host.js +26841 -0
  12. package/dist/host-descriptions.js +44 -3
  13. package/dist/public-cli/load-production-external-host.js +19 -0
  14. package/dist/public-cli/load-production-headless-host.js +36 -0
  15. package/dist/public-cli/main.js +244 -234
  16. package/dist/public-cli/option-definitions.js +2 -2
  17. package/dist/public-role-summons.js +40 -5
  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/scripts/build-package.mjs +28 -0
  23. package/src/acp-host/production-host.ts +4 -0
  24. package/src/acp-host/role-envelope.ts +141 -84
  25. package/src/acp-host/role-turn-host.ts +12 -0
  26. package/src/collector-handbook.ts +194 -0
  27. package/src/collector-ledger.ts +42 -62
  28. package/src/collector-role.ts +117 -11
  29. package/src/collector-tool-schemas.ts +25 -1
  30. package/src/headless-host/description.ts +123 -0
  31. package/src/headless-host/production-host.ts +75 -0
  32. package/src/headless-host/role-turn-host.ts +424 -0
  33. package/src/host-descriptions.ts +53 -6
  34. package/src/public-cli/cli.ts +2 -2
  35. package/src/public-cli/load-production-external-host.ts +29 -0
  36. package/src/public-cli/load-production-headless-host.ts +48 -0
  37. package/src/public-cli/main.ts +5 -0
  38. package/src/public-cli/option-definitions.ts +2 -2
  39. package/src/public-role-summons.ts +62 -9
  40. package/src/role-runtime.ts +5 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Same-directory temp + rename atomic file replace.
3
+ * Shared primitive for ledger-adjacent typed pages (analyst metrics, etc.).
4
+ * Does not open/truncate an existing destination inode, so hard-linked twins
5
+ * keep prior bytes until the directory entry is swapped.
6
+ * Parent directory must already exist — callers that write under the package
7
+ * ledger home own confinement via ensureRealDirectoryTree (ADR 0038).
8
+ */
9
+ import { randomUUID } from "node:crypto";
10
+ import { rename, rm, writeFile } from "node:fs/promises";
11
+ import { dirname, join } from "node:path";
12
+ export async function writeFileAtomically(destination, contents) {
13
+ const parent = dirname(destination);
14
+ const temporary = join(parent, `.atomic-write-${randomUUID()}.tmp`);
15
+ try {
16
+ await writeFile(temporary, contents);
17
+ await rename(temporary, destination);
18
+ }
19
+ catch (error) {
20
+ await rm(temporary, { force: true }).catch(() => undefined);
21
+ throw error;
22
+ }
23
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Collector bot handbook — opaque working memory under the book topology.
3
+ * Runtime stores and delivers UTF-8 text only; never parses free text into
4
+ * code state rules (parent #673 D3 / #677).
5
+ *
6
+ * Placement reuses the machine ledger home (ADR 0048) when the admitted
7
+ * session already sits under books/<bookKey>/ — no parallel persistence frame.
8
+ */
9
+ import { readFile } from "node:fs/promises";
10
+ import { join, sep } from "node:path";
11
+ import { writeFileAtomically } from "./atomic-write.js";
12
+ import { activationBookDirectory, assertLedgerFileInsideHome, ensureRealDirectoryTree, resolveActivationLedgerHomeForPath, } from "./activation-ledger-topology.js";
13
+ import { COLLECTOR_HANDBOOK_MAX_BYTES } from "./collector-tool-schemas.js";
14
+ /**
15
+ * Derive handbook root from an admitted session/run path under
16
+ * `.ak-roles/books/<bookKey>/...`. Fails closed when topology is absent.
17
+ */
18
+ export function resolveCollectorHandbookRoot(sessionPath) {
19
+ if (typeof sessionPath !== "string" || sessionPath.trim().length === 0) {
20
+ throw new Error("通进司手册要求非空 session 路径,且位于 books/<bookKey>/");
21
+ }
22
+ // Platform path segments only — never rewrite non-separator characters (POSIX `\\` is literal).
23
+ const segments = sessionPath.split(sep);
24
+ let bookKey;
25
+ for (let i = 0; i + 3 < segments.length; i += 1) {
26
+ if (segments[i] === ".ak-roles" && segments[i + 1] === "books") {
27
+ const candidate = segments[i + 2];
28
+ // Require a following segment so bookKey is a true path component under books/.
29
+ if (candidate.length > 0) {
30
+ bookKey = candidate;
31
+ break;
32
+ }
33
+ }
34
+ }
35
+ if (bookKey === undefined) {
36
+ throw new Error(`通进司手册要求 session 位于 books/<bookKey>/;收到 ${sessionPath}`);
37
+ }
38
+ if (bookKey === "." || bookKey === "..") {
39
+ throw new Error(`通进司手册拒绝不安全 bookKey ${JSON.stringify(bookKey)}`);
40
+ }
41
+ const ledgerHome = resolveActivationLedgerHomeForPath(sessionPath);
42
+ const root = join(activationBookDirectory(ledgerHome, bookKey), "collector-handbook");
43
+ return { ledgerHome, bookKey, root };
44
+ }
45
+ /** Flat repo file name under handbook/repos/ — avoids nested owner/repo dirs. */
46
+ function collectorHandbookRepoFileName(repositoryCanonical) {
47
+ if (!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(repositoryCanonical)) {
48
+ throw new Error(`通进司手册仓库文件要求规范 owner/repo,收到 ${JSON.stringify(repositoryCanonical)}`);
49
+ }
50
+ return `${repositoryCanonical.replaceAll("/", "__")}.md`;
51
+ }
52
+ export function createCollectorHandbookStore(input) {
53
+ const generalPath = join(input.handbookRoot, "general.md");
54
+ const repoDir = join(input.handbookRoot, "repos");
55
+ const repoPath = join(repoDir, collectorHandbookRepoFileName(input.repositoryCanonical));
56
+ /**
57
+ * ADR 0038: confine at the real read seam. Parent dirs must be physical under
58
+ * ledger home; the leaf must not be a pre-existing symlink (even in-home).
59
+ */
60
+ /** Sole UTF-8 budget seam — write and read share COLLECTOR_HANDBOOK_MAX_BYTES. */
61
+ const assertHandbookBudget = (body, label) => {
62
+ const byteLength = Buffer.byteLength(body, "utf8");
63
+ if (byteLength > COLLECTOR_HANDBOOK_MAX_BYTES) {
64
+ throw new Error(`通进司手册${label} UTF-8 至多 ${COLLECTOR_HANDBOOK_MAX_BYTES} 字节,收到 ${byteLength}`);
65
+ }
66
+ return byteLength;
67
+ };
68
+ const readOptional = async (path, parentDir) => {
69
+ ensureRealDirectoryTree(input.ledgerHome, parentDir);
70
+ assertLedgerFileInsideHome(path, input.ledgerHome);
71
+ try {
72
+ const body = await readFile(path, "utf8");
73
+ assertHandbookBudget(body, "正文");
74
+ return body;
75
+ }
76
+ catch (error) {
77
+ if (isNotFound(error))
78
+ return undefined;
79
+ throw error;
80
+ }
81
+ };
82
+ return {
83
+ root: input.handbookRoot,
84
+ repositoryCanonical: input.repositoryCanonical,
85
+ async read() {
86
+ const bookGeneral = await readOptional(generalPath, input.handbookRoot);
87
+ const bookRepo = await readOptional(repoPath, repoDir);
88
+ if (bookGeneral !== undefined) {
89
+ return {
90
+ general: bookGeneral,
91
+ repo: bookRepo ?? "",
92
+ generalSource: "book",
93
+ repoSource: bookRepo === undefined ? "empty" : "book",
94
+ generalPath,
95
+ repoPath,
96
+ };
97
+ }
98
+ const seed = input.seedGeneral;
99
+ if (typeof seed === "string" && seed.length > 0) {
100
+ assertHandbookBudget(seed, "正文");
101
+ return {
102
+ general: seed,
103
+ repo: bookRepo ?? "",
104
+ generalSource: "seed",
105
+ repoSource: bookRepo === undefined ? "empty" : "book",
106
+ generalPath,
107
+ repoPath,
108
+ };
109
+ }
110
+ return {
111
+ general: "",
112
+ repo: bookRepo ?? "",
113
+ generalSource: "empty",
114
+ repoSource: bookRepo === undefined ? "empty" : "book",
115
+ generalPath,
116
+ repoPath,
117
+ };
118
+ },
119
+ async write(scope, body) {
120
+ // body string shape = collectorHandbookWriteArgsSchema; byte budget is business (UTF-8).
121
+ const byteLength = assertHandbookBudget(body, "正文");
122
+ ensureRealDirectoryTree(input.ledgerHome, input.handbookRoot);
123
+ const path = scope === "general" ? generalPath : repoPath;
124
+ if (scope === "repo") {
125
+ ensureRealDirectoryTree(input.ledgerHome, repoDir);
126
+ }
127
+ assertLedgerFileInsideHome(path, input.ledgerHome);
128
+ await writeFileAtomically(path, body);
129
+ return {
130
+ scope,
131
+ path,
132
+ byteLength,
133
+ };
134
+ },
135
+ };
136
+ }
137
+ function isNotFound(error) {
138
+ return typeof error === "object"
139
+ && error !== null
140
+ && "code" in error
141
+ && error.code === "ENOENT";
142
+ }
@@ -1,8 +1,6 @@
1
- import Value from "typebox/value";
2
1
  import { applyEvidenceVersionHistory, assignWindowRelations, COLLECTOR_ELIGIBILITY_MS, measureNormalizedBytes, normalizeAuthenticatedUserEvidence, normalizeIssueCommentEvidence, normalizePullRequestEvidence, normalizePullRequestReactionEvidence, normalizeReviewCommentEvidence, normalizeReviewEvidence, sha256Text, } from "./collector-evidence.js";
3
2
  import { buildCollectorRequestBody, } from "./collector-github.js";
4
3
  import { CollectorNonOpenRequestError } from "./collector-identity.js";
5
- import { collectorObserveArgsSchema, collectorOutputArgsSchema, collectorReadArgsSchema, collectorRequestArgsSchema, collectorWaitArgsSchema, } from "./collector-tool-schemas.js";
6
4
  import { COLLECTOR_OUTPUT_TOOL } from "./package-contracts/collector-output.js";
7
5
  export const COLLECTOR_OBSERVE_TOOL = "ak_collector_observe";
8
6
  export const COLLECTOR_READ_TOOL = "ak_collector_read";
@@ -10,6 +8,8 @@ export const COLLECTOR_REQUEST_TOOL = "ak_collector_request";
10
8
  export const COLLECTOR_WAIT_TOOL = "ak_collector_wait";
11
9
  /** #676 A: role-decided target bind — business tool, ledger-booked. */
12
10
  export const COLLECTOR_BIND_TARGET_TOOL = "ak_collector_bind_target";
11
+ /** #677: opaque handbook write — business tool, ledger-booked. */
12
+ export const COLLECTOR_HANDBOOK_WRITE_TOOL = "ak_collector_handbook_write";
13
13
  export { COLLECTOR_OUTPUT_TOOL };
14
14
  export const COLLECTOR_OPERATIONAL_TOOLS = [
15
15
  COLLECTOR_BIND_TARGET_TOOL,
@@ -17,6 +17,7 @@ export const COLLECTOR_OPERATIONAL_TOOLS = [
17
17
  COLLECTOR_READ_TOOL,
18
18
  COLLECTOR_REQUEST_TOOL,
19
19
  COLLECTOR_WAIT_TOOL,
20
+ COLLECTOR_HANDBOOK_WRITE_TOOL,
20
21
  ];
21
22
  /**
22
23
  * #641 chain①: observe context projects at most this many bytes of each
@@ -24,7 +25,7 @@ export const COLLECTOR_OPERATIONAL_TOOLS = [
24
25
  * (evidenceId + htmlUrl) and is preserved in the volume, never unconditionally
25
26
  * transcribed into model context or receipt.
26
27
  */
27
- export const COLLECTOR_OBSERVE_BODY_HEAD_BYTES = 512;
28
+ const COLLECTOR_OBSERVE_BODY_HEAD_BYTES = 512;
28
29
  function utf8SafePrefix(text, maxBytes) {
29
30
  if (Buffer.byteLength(text, "utf8") <= maxBytes)
30
31
  return text;
@@ -41,7 +42,7 @@ function utf8SafePrefix(text, maxBytes) {
41
42
  * (collector-side; provider-visible observe details carry only this bounded
42
43
  * projection), never unconditionally transcribed into model context.
43
44
  */
44
- export function projectObserveContextView(modelView) {
45
+ function projectObserveContextView(modelView) {
45
46
  return {
46
47
  ...modelView,
47
48
  evidence: modelView.evidence.map((entry) => entry.body === undefined
@@ -50,32 +51,12 @@ export function projectObserveContextView(modelView) {
50
51
  };
51
52
  }
52
53
  export const COLLECTOR_ACTIVATION_ENTRY_TYPE = "ak-collector-activation";
53
- export const COLLECTOR_SNAPSHOT_ENTRY_TYPE = "ak-collector-snapshot";
54
- export const COLLECTOR_REQUEST_ENTRY_TYPE = "ak-collector-request";
55
- export const COLLECTOR_WAIT_ENTRY_TYPE = "ak-collector-wait";
54
+ const COLLECTOR_SNAPSHOT_ENTRY_TYPE = "ak-collector-snapshot";
55
+ const COLLECTOR_REQUEST_ENTRY_TYPE = "ak-collector-request";
56
+ const COLLECTOR_WAIT_ENTRY_TYPE = "ak-collector-wait";
56
57
  function isOperationalTool(name) {
57
58
  return COLLECTOR_OPERATIONAL_TOOLS.includes(name);
58
59
  }
59
- /** Shared Check owner for Collector tool argument shapes (schema seam, not batch law). */
60
- export function collectorToolArgumentsValid(name, args) {
61
- // Residual envelope: reject missing/null args before schema check.
62
- if (args === undefined || args === null)
63
- return false;
64
- switch (name) {
65
- case COLLECTOR_OBSERVE_TOOL:
66
- return Value.Check(collectorObserveArgsSchema, args);
67
- case COLLECTOR_READ_TOOL:
68
- return Value.Check(collectorReadArgsSchema, args);
69
- case COLLECTOR_REQUEST_TOOL:
70
- return Value.Check(collectorRequestArgsSchema, args);
71
- case COLLECTOR_WAIT_TOOL:
72
- return Value.Check(collectorWaitArgsSchema, args);
73
- case COLLECTOR_OUTPUT_TOOL:
74
- return Value.Check(collectorOutputArgsSchema, args);
75
- default:
76
- return false;
77
- }
78
- }
79
60
  export function createCollectorLedger(config, options) {
80
61
  const clock = options?.clock;
81
62
  const journal = options?.journal;
@@ -135,7 +116,7 @@ export function createCollectorLedger(config, options) {
135
116
  const prIdentity = (pr) => `${pr.state}|${pr.headOid}|${pr.updatedAt ?? ""}`;
136
117
  const requireBoundPr = () => {
137
118
  if (config.prNumber === undefined) {
138
- throw new Error("Collector PR target is unbound; call ak_collector_bind_target with the role-decided issue/PR or pass --pr");
119
+ throw new Error("通进司 PR 目标未绑定");
139
120
  }
140
121
  return config.prNumber;
141
122
  };
@@ -449,10 +430,10 @@ export function createCollectorLedger(config, options) {
449
430
  throw new Error("通进司已产出输出候选,本局不再受理目标绑定");
450
431
  }
451
432
  if (!Number.isSafeInteger(prNumber) || prNumber < 1) {
452
- throw new Error("Collector bind target requires a positive safe-integer PR number");
433
+ throw new Error("通进司绑定目标要求正安全整数 PR 号");
453
434
  }
454
435
  if (config.prNumber !== undefined && config.prNumber !== prNumber) {
455
- throw new Error(`Collector target already bound to PR ${config.prNumber}; cannot rebind to ${prNumber}`);
436
+ throw new Error(`通进司目标已绑定 PR ${config.prNumber},不可改绑为 ${prNumber}`);
456
437
  }
457
438
  config.prNumber = prNumber;
458
439
  appendJournal("ak-collector-target-bound", {
@@ -650,9 +631,23 @@ export function createCollectorLedger(config, options) {
650
631
  if (ledger.unresolvedTransportFailure) {
651
632
  throw latchFatal("通进司请求时存在未恢复的传输失败");
652
633
  }
653
- const request = config.manifest.requests.find((item) => item.id === input.requestId);
654
- if (request === undefined) {
655
- throw new Error(`未知通进司 requestId "${input.requestId}"`);
634
+ // Sole request identity seam: trim once, then use everywhere (lookup/marker/attemptKey).
635
+ // Schema pattern rejects leading/trailing whitespace at host; trim still collapses
636
+ // direct ledger callers and any residual padding into one stable id (#677 D2).
637
+ if (typeof input.requestId !== "string") {
638
+ throw new Error("通进司 requestId 须为字符串");
639
+ }
640
+ const requestId = input.requestId.trim();
641
+ if (requestId.length === 0) {
642
+ throw new Error("通进司 requestId 须为非空(首尾空白不构成身份)");
643
+ }
644
+ const configured = config.manifest.requests.find((item) => item.id === requestId);
645
+ const roleBody = typeof input.body === "string" ? input.body : undefined;
646
+ if (configured === undefined) {
647
+ // #677: role-decided trigger from handbook/field activity — body required when not in manifest.
648
+ if (roleBody === undefined || roleBody.trim().length === 0) {
649
+ throw new Error(`未知通进司 requestId "${requestId}" 且未提供 body;请提交角色判定的请求正文或使用 request-manifest`);
650
+ }
656
651
  }
657
652
  const snapshot = snapshots.find((item) => item.snapshotId === input.snapshotId);
658
653
  if (snapshot === undefined) {
@@ -669,10 +664,12 @@ export function createCollectorLedger(config, options) {
669
664
  finalObservationRequired = true;
670
665
  throw latchFatal("通进司请求不在资格截止前");
671
666
  }
667
+ // Caller manifest body wins when requestId is configured; otherwise role body.
668
+ const configuredBody = configured?.requestBody ?? roleBody;
672
669
  const { body, marker } = buildCollectorRequestBody({
673
- configuredBody: request.requestBody,
670
+ configuredBody,
674
671
  manifestDigest: config.manifest.digest,
675
- requestId: request.id,
672
+ requestId,
676
673
  headOid: snapshot.headOid,
677
674
  });
678
675
  const existingMarker = snapshot.evidenceIds.some((id) => {
@@ -683,23 +680,23 @@ export function createCollectorLedger(config, options) {
683
680
  record.body.includes(marker);
684
681
  });
685
682
  if (existingMarker) {
686
- throw new Error(`通进司在此 HEAD 已有同 marker 的已认证请求 "${input.requestId}"`);
683
+ throw new Error(`通进司在此 HEAD 已有同 marker 的已认证请求 "${requestId}"`);
687
684
  }
688
685
  const boundPr = requireBoundPr();
689
686
  const attemptKey = [
690
687
  config.repository.canonical,
691
688
  String(boundPr),
692
689
  snapshot.headOid,
693
- request.id,
690
+ requestId,
694
691
  ].join("|");
695
692
  if (attemptKeys.has(attemptKey)) {
696
- throw new Error(`通进司进程内请求 "${request.id}" 在 HEAD ${snapshot.headOid} 的 attempt 已用`);
693
+ throw new Error(`通进司进程内请求 "${requestId}" 在 HEAD ${snapshot.headOid} 的 attempt 已用`);
697
694
  }
698
695
  const startedAt = clock.wallNow().toISOString();
699
696
  const attemptId = sha256Text(`${attemptKey}:${startedAt}`).slice(0, 16);
700
697
  const attempt = {
701
698
  attemptId,
702
- requestId: request.id,
699
+ requestId,
703
700
  observedHead: snapshot.headOid,
704
701
  snapshotId: snapshot.snapshotId,
705
702
  marker,
@@ -741,7 +738,7 @@ export function createCollectorLedger(config, options) {
741
738
  return {
742
739
  status: "succeeded",
743
740
  attemptId,
744
- requestId: request.id,
741
+ requestId,
745
742
  observedHead: snapshot.headOid,
746
743
  marker,
747
744
  commentEvidenceId: record.evidenceId,
@@ -755,7 +752,7 @@ export function createCollectorLedger(config, options) {
755
752
  return {
756
753
  status: "ambiguous_loss",
757
754
  attemptId,
758
- requestId: request.id,
755
+ requestId,
759
756
  observedHead: snapshot.headOid,
760
757
  marker,
761
758
  diagnostics: result.diagnostics,
@@ -772,12 +769,7 @@ export function createCollectorLedger(config, options) {
772
769
  if (activationTime === undefined) {
773
770
  throw latchFatal("通进司等待需要激活");
774
771
  }
775
- if (!Number.isSafeInteger(input.durationMs) || input.durationMs < 1) {
776
- throw new Error("通进司等待 durationMs 须为正安全整数");
777
- }
778
- if (input.durationMs > COLLECTOR_ELIGIBILITY_MS) {
779
- throw new Error(`通进司等待 durationMs 至多为 ${COLLECTOR_ELIGIBILITY_MS}`);
780
- }
772
+ // durationMs shape authority = collectorWaitArgsSchema (host parameters).
781
773
  if (pastCutoff(clock)) {
782
774
  finalObservationRequired = true;
783
775
  throw latchFatal("通进司等待不在资格截止前");
@@ -1,9 +1,10 @@
1
1
  import { emptyCollectorManifest, loadCollectorManifest, parseCollectorPrNumber, parseCollectorRepository, } from "./collector-config.js";
2
2
  import { createSystemCollectorClock, } from "./collector-evidence.js";
3
3
  import { createGhApiRunner, listPullRequestNumbersByTicket, } from "./collector-github.js";
4
- import { COLLECTOR_BIND_TARGET_TOOL, COLLECTOR_OBSERVE_TOOL, COLLECTOR_OUTPUT_TOOL, COLLECTOR_READ_TOOL, COLLECTOR_REQUEST_TOOL, COLLECTOR_WAIT_TOOL, projectEvidenceEntryView, } from "./collector-ledger.js";
4
+ import { createCollectorHandbookStore, resolveCollectorHandbookRoot, } from "./collector-handbook.js";
5
+ import { COLLECTOR_BIND_TARGET_TOOL, COLLECTOR_HANDBOOK_WRITE_TOOL, COLLECTOR_OBSERVE_TOOL, COLLECTOR_OUTPUT_TOOL, COLLECTOR_READ_TOOL, COLLECTOR_REQUEST_TOOL, COLLECTOR_WAIT_TOOL, projectEvidenceEntryView, } from "./collector-ledger.js";
5
6
  import { buildCollectorReceipt, } from "./collector-receipt.js";
6
- import { collectorBindTargetArgsSchema, collectorObserveArgsSchema, collectorOutputArgsSchema, collectorReadArgsSchema, collectorRequestArgsSchema, collectorWaitArgsSchema, } from "./collector-tool-schemas.js";
7
+ import { collectorBindTargetArgsSchema, collectorHandbookWriteArgsSchema, collectorObserveArgsSchema, collectorOutputArgsSchema, collectorReadArgsSchema, collectorRequestArgsSchema, collectorWaitArgsSchema, } from "./collector-tool-schemas.js";
7
8
  import { COLLECTOR_ACCEPTED_TEXT } from "./package-contracts/collector-output.js";
8
9
  import { CorrectableSubmissionError, isCorrectableExecuteError } from "./submission-correctable-error.js";
9
10
  import { CollectorUnknownEvidenceError } from "./collector-identity.js";
@@ -26,13 +27,14 @@ export class CollectorTargetBindError extends CorrectableSubmissionError {
26
27
  this.name = "CollectorTargetBindError";
27
28
  }
28
29
  }
29
- export { COLLECTOR_BIND_TARGET_TOOL, COLLECTOR_OBSERVE_TOOL, COLLECTOR_OUTPUT_TOOL, COLLECTOR_READ_TOOL, COLLECTOR_REQUEST_TOOL, COLLECTOR_WAIT_TOOL, };
30
+ export { COLLECTOR_BIND_TARGET_TOOL, COLLECTOR_HANDBOOK_WRITE_TOOL, COLLECTOR_OBSERVE_TOOL, COLLECTOR_OUTPUT_TOOL, COLLECTOR_READ_TOOL, COLLECTOR_REQUEST_TOOL, COLLECTOR_WAIT_TOOL, };
30
31
  export const COLLECTOR_REQUIRED_TOOLS = [
31
32
  COLLECTOR_BIND_TARGET_TOOL,
32
33
  COLLECTOR_OBSERVE_TOOL,
33
34
  COLLECTOR_READ_TOOL,
34
35
  COLLECTOR_REQUEST_TOOL,
35
36
  COLLECTOR_WAIT_TOOL,
37
+ COLLECTOR_HANDBOOK_WRITE_TOOL,
36
38
  COLLECTOR_OUTPUT_TOOL,
37
39
  ];
38
40
  /** Envelope-owned transport flags (ADR 0018 / #676 E). */
@@ -64,17 +66,34 @@ const readSchema = collectorReadArgsSchema;
64
66
  const requestSchema = collectorRequestArgsSchema;
65
67
  const waitSchema = collectorWaitArgsSchema;
66
68
  const bindSchema = collectorBindTargetArgsSchema;
69
+ const handbookWriteSchema = collectorHandbookWriteArgsSchema;
67
70
  const outputSchema = collectorOutputArgsSchema;
68
71
  function buildMethodContext(activation) {
69
72
  const pr = activation.ledger.config.prNumber;
70
- return [
73
+ const handbook = activation.handbookView;
74
+ const lines = [
71
75
  "<collector_method>",
72
76
  `host: github.com`,
73
77
  `repository: ${activation.repository.canonical}`,
74
- `prNumber: ${pr === undefined ? "unbound — call ak_collector_bind_target with the role-decided issue/PR before observe" : String(pr)}`,
78
+ `prNumber: ${pr === undefined ? "未绑定" : String(pr)}`,
75
79
  `requests: ${JSON.stringify(activation.manifest.requests.map((request) => ({ id: request.id })))}`,
80
+ `handbookGeneralSource: ${handbook.generalSource}`,
81
+ `handbookRepoSource: ${handbook.repoSource}`,
76
82
  "</collector_method>",
77
- ].join("\n");
83
+ ];
84
+ // Opaque working memory only — no directional instructions (ADR 0073).
85
+ // JSON + `<` → \u003c keeps bodies from forging the delivery close tag (#677).
86
+ if (handbook.general.length > 0 || handbook.repo.length > 0) {
87
+ const payload = JSON.stringify({
88
+ general: handbook.general,
89
+ repo: handbook.repo,
90
+ generalSource: handbook.generalSource,
91
+ repoSource: handbook.repoSource,
92
+ repository: activation.repository.canonical,
93
+ }).replaceAll("<", "\\u003c");
94
+ lines.push("", "<collector_handbook>", payload, "</collector_handbook>");
95
+ }
96
+ return lines.join("\n");
78
97
  }
79
98
  function parsePositiveTicket(raw, label) {
80
99
  if (raw === undefined || raw === null)
@@ -83,7 +102,7 @@ function parsePositiveTicket(raw, label) {
83
102
  return raw;
84
103
  if (typeof raw === "string" && /^[1-9]\d*$/.test(raw.trim()))
85
104
  return Number(raw.trim());
86
- throw new CollectorTargetBindError(`ak_collector_bind_target ${label} must be a positive safe integer`);
105
+ throw new CollectorTargetBindError(`通进司绑定 ${label} 须为正安全整数`);
87
106
  }
88
107
  /**
89
108
  * Shared envelope installs business tools and material callback after lifecycle gates.
@@ -118,6 +137,25 @@ export function createCollectorRoleRuntime(pi, dependencies, hostActions) {
118
137
  const clock = dependencies.createClock?.() ?? createSystemCollectorClock();
119
138
  const transport = dependencies.createTransport();
120
139
  const ledger = dependencies.createLedger({ repository, prNumber, manifest }, clock, ctx);
140
+ // #677: handbook under admitted book topology (session path → books/<key>/collector-handbook).
141
+ const sessionPath = ctx.sessionManager?.getSessionFile?.()
142
+ ?? ctx.sessionManager?.getSessionDir?.();
143
+ if (typeof sessionPath !== "string" || sessionPath.length === 0) {
144
+ throw new Error("通进司手册要求 session 路径位于 books/<bookKey>/");
145
+ }
146
+ const placement = resolveCollectorHandbookRoot(sessionPath);
147
+ const seedGeneral = dependencies.loadHandbookSeed === undefined
148
+ ? undefined
149
+ : (await dependencies.loadHandbookSeed()).trim();
150
+ const handbook = createCollectorHandbookStore({
151
+ ledgerHome: placement.ledgerHome,
152
+ handbookRoot: placement.root,
153
+ repositoryCanonical: repository.canonical,
154
+ ...(seedGeneral === undefined || seedGeneral.length === 0
155
+ ? {}
156
+ : { seedGeneral }),
157
+ });
158
+ const handbookView = await handbook.read();
121
159
  return {
122
160
  soul,
123
161
  repository,
@@ -125,6 +163,8 @@ export function createCollectorRoleRuntime(pi, dependencies, hostActions) {
125
163
  ledger,
126
164
  transport,
127
165
  clock,
166
+ handbook,
167
+ handbookView,
128
168
  };
129
169
  },
130
170
  assembleMaterials(activation, baseSystemPrompt) {
@@ -180,7 +220,7 @@ export function createCollectorRoleRuntime(pi, dependencies, hostActions) {
180
220
  const prNumber = parsePositiveTicket(params.prNumber, "prNumber");
181
221
  const issueNumber = parsePositiveTicket(params.issueNumber, "issueNumber");
182
222
  if (prNumber === undefined && issueNumber === undefined) {
183
- throw new CollectorTargetBindError("ak_collector_bind_target requires role-decided prNumber and/or issueNumber");
223
+ throw new CollectorTargetBindError("通进司绑定须由角色判定 prNumber 与/或 issueNumber");
184
224
  }
185
225
  let bound = prNumber;
186
226
  if (issueNumber !== undefined) {
@@ -190,14 +230,14 @@ export function createCollectorRoleRuntime(pi, dependencies, hostActions) {
190
230
  ticketNumber: issueNumber,
191
231
  });
192
232
  if (associated.length === 0) {
193
- throw new CollectorTargetBindError(`no PR associated with issue #${issueNumber} in ${activation.repository.canonical}; pass an explicit --pr or a different issueNumber`);
233
+ throw new CollectorTargetBindError(`${activation.repository.canonical} 的 issue #${issueNumber} 无关联 PR;请改用明确 --pr 或其他 issueNumber`);
194
234
  }
195
235
  if (associated.length > 1) {
196
- throw new CollectorTargetBindError(`multiple PRs associated with issue #${issueNumber}: ${associated.join(", ")}; pass an explicit prNumber or --pr`);
236
+ throw new CollectorTargetBindError(`issue #${issueNumber} 关联多个 PR:${associated.join("、")};请改用明确 prNumber 或 --pr`);
197
237
  }
198
238
  const fromIssue = associated[0];
199
239
  if (prNumber !== undefined && prNumber !== fromIssue) {
200
- throw new CollectorTargetBindError(`prNumber ${prNumber} conflicts with issue #${issueNumber} association PR ${fromIssue}`);
240
+ throw new CollectorTargetBindError(`prNumber ${prNumber} 与 issue #${issueNumber} 关联 PR ${fromIssue} 冲突`);
201
241
  }
202
242
  bound = fromIssue;
203
243
  }
@@ -297,8 +337,8 @@ export function createCollectorRoleRuntime(pi, dependencies, hostActions) {
297
337
  pi.registerTool({
298
338
  name: COLLECTOR_REQUEST_TOOL,
299
339
  label: "通进司请求",
300
- description: "按配置请求体与关联标记,在所引最新快照 HEAD 发一次请求。",
301
- promptSnippet: "按配置发一次请求",
340
+ description: "在所引最新快照 HEAD 发一次请求。requestId 可取配置清单,或角色依手册/现场判定的稳定 id;后者须同时提供 body。",
341
+ promptSnippet: "发一次评审请求",
302
342
  parameters: requestSchema,
303
343
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
304
344
  const activation = getActivation();
@@ -306,7 +346,11 @@ export function createCollectorRoleRuntime(pi, dependencies, hostActions) {
306
346
  throw new Error("通进司未激活");
307
347
  try {
308
348
  activation.ledger.beginOperational(COLLECTOR_REQUEST_TOOL, toolCallId);
309
- const details = await activation.ledger.request(params, activation.transport, activation.clock, signal);
349
+ const details = await activation.ledger.request({
350
+ requestId: params.requestId,
351
+ snapshotId: params.snapshotId,
352
+ ...(typeof params.body === "string" ? { body: params.body } : {}),
353
+ }, activation.transport, activation.clock, signal);
310
354
  activation.ledger.completeOperational(toolCallId);
311
355
  return {
312
356
  content: [{
@@ -323,6 +367,51 @@ export function createCollectorRoleRuntime(pi, dependencies, hostActions) {
323
367
  }
324
368
  },
325
369
  });
370
+ pi.registerTool({
371
+ name: COLLECTOR_HANDBOOK_WRITE_TOOL,
372
+ label: "通进司手册写入",
373
+ description: "写入通用手册或当前仓库差异全文(整份替换)。",
374
+ promptSnippet: "更新 bot 手册",
375
+ parameters: handbookWriteSchema,
376
+ async execute(toolCallId, params, _signal, _onUpdate, ctx) {
377
+ const activation = getActivation();
378
+ if (activation === undefined)
379
+ throw new Error("通进司未激活");
380
+ try {
381
+ activation.ledger.beginOperational(COLLECTOR_HANDBOOK_WRITE_TOOL, toolCallId);
382
+ // scope/body shape authority = collectorHandbookWriteArgsSchema (host parameters).
383
+ const details = await activation.handbook.write(params.scope, params.body);
384
+ activation.handbookView = await activation.handbook.read();
385
+ activation.ledger.completeOperational(toolCallId);
386
+ return {
387
+ content: [{
388
+ type: "text",
389
+ text: `手册已写入:${details.scope}`,
390
+ }],
391
+ details: {
392
+ scope: details.scope,
393
+ path: details.path,
394
+ byteLength: details.byteLength,
395
+ generalSource: activation.handbookView.generalSource,
396
+ repoSource: activation.handbookView.repoSource,
397
+ },
398
+ };
399
+ }
400
+ catch (error) {
401
+ if (isCorrectableExecuteError(error))
402
+ throw error;
403
+ hostActions.failInfrastructure(error, ctx, toolCallId);
404
+ }
405
+ finally {
406
+ try {
407
+ activation.ledger.completeOperational(toolCallId);
408
+ }
409
+ catch {
410
+ // already completed or not begun
411
+ }
412
+ }
413
+ },
414
+ });
326
415
  pi.registerTool({
327
416
  name: COLLECTOR_WAIT_TOOL,
328
417
  label: "通进司等待",
@@ -3,9 +3,33 @@ import { COLLECTOR_ELIGIBILITY_MS } from "./collector-evidence.js";
3
3
  import { openToolObject } from "./open-tool-schema.js";
4
4
  import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.js";
5
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)?$/;
6
11
  export const collectorRequestArgsSchema = Type.Object({
7
- 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
+ }),
8
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
+ }),
9
33
  }, { additionalProperties: false });
10
34
  export const collectorReadArgsSchema = Type.Object({
11
35
  evidenceId: Type.String({ minLength: 1, description: "observe 返回的材料证据 id(evidenceId)" }),