@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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +793 -393
- package/dist/atomic-write.js +23 -0
- package/dist/collector-handbook.js +142 -0
- package/dist/collector-ledger.js +39 -47
- package/dist/collector-role.js +103 -14
- package/dist/collector-tool-schemas.js +25 -1
- package/dist/headless-host/description.js +77 -0
- package/dist/headless-host/mcp-relay.mjs +119 -0
- package/dist/headless-host/production-host.js +26841 -0
- package/dist/host-descriptions.js +44 -3
- package/dist/public-cli/load-production-external-host.js +19 -0
- package/dist/public-cli/load-production-headless-host.js +36 -0
- package/dist/public-cli/main.js +244 -234
- package/dist/public-cli/option-definitions.js +2 -2
- package/dist/public-role-summons.js +40 -5
- package/dist/role-runtime.js +3 -0
- package/extensions/role-runtime.ts +2 -0
- package/package.json +1 -1
- package/resources/collector-bot-handbook.md +33 -0
- package/scripts/build-package.mjs +28 -0
- package/src/acp-host/production-host.ts +4 -0
- package/src/acp-host/role-envelope.ts +141 -84
- package/src/acp-host/role-turn-host.ts +12 -0
- package/src/collector-handbook.ts +194 -0
- package/src/collector-ledger.ts +42 -62
- package/src/collector-role.ts +117 -11
- package/src/collector-tool-schemas.ts +25 -1
- package/src/headless-host/description.ts +123 -0
- package/src/headless-host/production-host.ts +75 -0
- package/src/headless-host/role-turn-host.ts +424 -0
- package/src/host-descriptions.ts +53 -6
- package/src/public-cli/cli.ts +2 -2
- package/src/public-cli/load-production-external-host.ts +29 -0
- package/src/public-cli/load-production-headless-host.ts +48 -0
- package/src/public-cli/main.ts +5 -0
- package/src/public-cli/option-definitions.ts +2 -2
- package/src/public-role-summons.ts +62 -9
- package/src/role-runtime.ts +5 -0
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
|
|
12
|
+
import { writeFileAtomically } from "./atomic-write.ts";
|
|
13
|
+
import {
|
|
14
|
+
activationBookDirectory,
|
|
15
|
+
assertLedgerFileInsideHome,
|
|
16
|
+
ensureRealDirectoryTree,
|
|
17
|
+
resolveActivationLedgerHomeForPath,
|
|
18
|
+
} from "./activation-ledger-topology.ts";
|
|
19
|
+
import { COLLECTOR_HANDBOOK_MAX_BYTES } from "./collector-tool-schemas.ts";
|
|
20
|
+
|
|
21
|
+
export type CollectorHandbookScope = "general" | "repo";
|
|
22
|
+
|
|
23
|
+
export type CollectorHandbookRead = {
|
|
24
|
+
readonly general: string;
|
|
25
|
+
readonly repo: string;
|
|
26
|
+
readonly generalSource: "book" | "seed" | "empty";
|
|
27
|
+
readonly repoSource: "book" | "empty";
|
|
28
|
+
readonly generalPath: string;
|
|
29
|
+
readonly repoPath: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type CollectorHandbookWriteResult = {
|
|
33
|
+
readonly scope: CollectorHandbookScope;
|
|
34
|
+
readonly path: string;
|
|
35
|
+
readonly byteLength: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type CollectorHandbookStore = {
|
|
39
|
+
readonly root: string;
|
|
40
|
+
readonly repositoryCanonical: string;
|
|
41
|
+
read(): Promise<CollectorHandbookRead>;
|
|
42
|
+
write(scope: CollectorHandbookScope, body: string): Promise<CollectorHandbookWriteResult>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type CollectorHandbookPlacement = {
|
|
46
|
+
readonly ledgerHome: string;
|
|
47
|
+
readonly bookKey: string;
|
|
48
|
+
readonly root: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Derive handbook root from an admitted session/run path under
|
|
53
|
+
* `.ak-roles/books/<bookKey>/...`. Fails closed when topology is absent.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveCollectorHandbookRoot(sessionPath: string): CollectorHandbookPlacement {
|
|
56
|
+
if (typeof sessionPath !== "string" || sessionPath.trim().length === 0) {
|
|
57
|
+
throw new Error("通进司手册要求非空 session 路径,且位于 books/<bookKey>/");
|
|
58
|
+
}
|
|
59
|
+
// Platform path segments only — never rewrite non-separator characters (POSIX `\\` is literal).
|
|
60
|
+
const segments = sessionPath.split(sep);
|
|
61
|
+
let bookKey: string | undefined;
|
|
62
|
+
for (let i = 0; i + 3 < segments.length; i += 1) {
|
|
63
|
+
if (segments[i] === ".ak-roles" && segments[i + 1] === "books") {
|
|
64
|
+
const candidate = segments[i + 2]!;
|
|
65
|
+
// Require a following segment so bookKey is a true path component under books/.
|
|
66
|
+
if (candidate.length > 0) {
|
|
67
|
+
bookKey = candidate;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (bookKey === undefined) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`通进司手册要求 session 位于 books/<bookKey>/;收到 ${sessionPath}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (bookKey === "." || bookKey === "..") {
|
|
78
|
+
throw new Error(`通进司手册拒绝不安全 bookKey ${JSON.stringify(bookKey)}`);
|
|
79
|
+
}
|
|
80
|
+
const ledgerHome = resolveActivationLedgerHomeForPath(sessionPath);
|
|
81
|
+
const root = join(activationBookDirectory(ledgerHome, bookKey), "collector-handbook");
|
|
82
|
+
return { ledgerHome, bookKey, root };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Flat repo file name under handbook/repos/ — avoids nested owner/repo dirs. */
|
|
86
|
+
function collectorHandbookRepoFileName(repositoryCanonical: string): string {
|
|
87
|
+
if (!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(repositoryCanonical)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`通进司手册仓库文件要求规范 owner/repo,收到 ${JSON.stringify(repositoryCanonical)}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return `${repositoryCanonical.replaceAll("/", "__")}.md`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createCollectorHandbookStore(input: {
|
|
96
|
+
readonly ledgerHome: string;
|
|
97
|
+
readonly handbookRoot: string;
|
|
98
|
+
readonly repositoryCanonical: string;
|
|
99
|
+
readonly seedGeneral?: string;
|
|
100
|
+
}): CollectorHandbookStore {
|
|
101
|
+
const generalPath = join(input.handbookRoot, "general.md");
|
|
102
|
+
const repoDir = join(input.handbookRoot, "repos");
|
|
103
|
+
const repoPath = join(repoDir, collectorHandbookRepoFileName(input.repositoryCanonical));
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* ADR 0038: confine at the real read seam. Parent dirs must be physical under
|
|
107
|
+
* ledger home; the leaf must not be a pre-existing symlink (even in-home).
|
|
108
|
+
*/
|
|
109
|
+
/** Sole UTF-8 budget seam — write and read share COLLECTOR_HANDBOOK_MAX_BYTES. */
|
|
110
|
+
const assertHandbookBudget = (body: string, label: string): number => {
|
|
111
|
+
const byteLength = Buffer.byteLength(body, "utf8");
|
|
112
|
+
if (byteLength > COLLECTOR_HANDBOOK_MAX_BYTES) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`通进司手册${label} UTF-8 至多 ${COLLECTOR_HANDBOOK_MAX_BYTES} 字节,收到 ${byteLength}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return byteLength;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const readOptional = async (path: string, parentDir: string): Promise<string | undefined> => {
|
|
121
|
+
ensureRealDirectoryTree(input.ledgerHome, parentDir);
|
|
122
|
+
assertLedgerFileInsideHome(path, input.ledgerHome);
|
|
123
|
+
try {
|
|
124
|
+
const body = await readFile(path, "utf8");
|
|
125
|
+
assertHandbookBudget(body, "正文");
|
|
126
|
+
return body;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (isNotFound(error)) return undefined;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
root: input.handbookRoot,
|
|
135
|
+
repositoryCanonical: input.repositoryCanonical,
|
|
136
|
+
async read() {
|
|
137
|
+
const bookGeneral = await readOptional(generalPath, input.handbookRoot);
|
|
138
|
+
const bookRepo = await readOptional(repoPath, repoDir);
|
|
139
|
+
if (bookGeneral !== undefined) {
|
|
140
|
+
return {
|
|
141
|
+
general: bookGeneral,
|
|
142
|
+
repo: bookRepo ?? "",
|
|
143
|
+
generalSource: "book",
|
|
144
|
+
repoSource: bookRepo === undefined ? "empty" : "book",
|
|
145
|
+
generalPath,
|
|
146
|
+
repoPath,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const seed = input.seedGeneral;
|
|
150
|
+
if (typeof seed === "string" && seed.length > 0) {
|
|
151
|
+
assertHandbookBudget(seed, "正文");
|
|
152
|
+
return {
|
|
153
|
+
general: seed,
|
|
154
|
+
repo: bookRepo ?? "",
|
|
155
|
+
generalSource: "seed",
|
|
156
|
+
repoSource: bookRepo === undefined ? "empty" : "book",
|
|
157
|
+
generalPath,
|
|
158
|
+
repoPath,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
general: "",
|
|
163
|
+
repo: bookRepo ?? "",
|
|
164
|
+
generalSource: "empty",
|
|
165
|
+
repoSource: bookRepo === undefined ? "empty" : "book",
|
|
166
|
+
generalPath,
|
|
167
|
+
repoPath,
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
async write(scope, body) {
|
|
171
|
+
// body string shape = collectorHandbookWriteArgsSchema; byte budget is business (UTF-8).
|
|
172
|
+
const byteLength = assertHandbookBudget(body, "正文");
|
|
173
|
+
ensureRealDirectoryTree(input.ledgerHome, input.handbookRoot);
|
|
174
|
+
const path = scope === "general" ? generalPath : repoPath;
|
|
175
|
+
if (scope === "repo") {
|
|
176
|
+
ensureRealDirectoryTree(input.ledgerHome, repoDir);
|
|
177
|
+
}
|
|
178
|
+
assertLedgerFileInsideHome(path, input.ledgerHome);
|
|
179
|
+
await writeFileAtomically(path, body);
|
|
180
|
+
return {
|
|
181
|
+
scope,
|
|
182
|
+
path,
|
|
183
|
+
byteLength,
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isNotFound(error: unknown): boolean {
|
|
190
|
+
return typeof error === "object"
|
|
191
|
+
&& error !== null
|
|
192
|
+
&& "code" in error
|
|
193
|
+
&& (error as { code?: unknown }).code === "ENOENT";
|
|
194
|
+
}
|
package/src/collector-ledger.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import Value from "typebox/value";
|
|
2
|
-
|
|
3
1
|
import type { CollectorManifest, CollectorRepository } from "./collector-config.ts";
|
|
4
2
|
import {
|
|
5
3
|
applyEvidenceVersionHistory,
|
|
@@ -26,13 +24,6 @@ import {
|
|
|
26
24
|
type GitHubPullRequest,
|
|
27
25
|
} from "./collector-github.ts";
|
|
28
26
|
import { CollectorNonOpenRequestError } from "./collector-identity.ts";
|
|
29
|
-
import {
|
|
30
|
-
collectorObserveArgsSchema,
|
|
31
|
-
collectorOutputArgsSchema,
|
|
32
|
-
collectorReadArgsSchema,
|
|
33
|
-
collectorRequestArgsSchema,
|
|
34
|
-
collectorWaitArgsSchema,
|
|
35
|
-
} from "./collector-tool-schemas.ts";
|
|
36
27
|
import { COLLECTOR_OUTPUT_TOOL } from "./package-contracts/collector-output.ts";
|
|
37
28
|
|
|
38
29
|
export const COLLECTOR_OBSERVE_TOOL = "ak_collector_observe";
|
|
@@ -41,6 +32,8 @@ export const COLLECTOR_REQUEST_TOOL = "ak_collector_request";
|
|
|
41
32
|
export const COLLECTOR_WAIT_TOOL = "ak_collector_wait";
|
|
42
33
|
/** #676 A: role-decided target bind — business tool, ledger-booked. */
|
|
43
34
|
export const COLLECTOR_BIND_TARGET_TOOL = "ak_collector_bind_target";
|
|
35
|
+
/** #677: opaque handbook write — business tool, ledger-booked. */
|
|
36
|
+
export const COLLECTOR_HANDBOOK_WRITE_TOOL = "ak_collector_handbook_write";
|
|
44
37
|
export { COLLECTOR_OUTPUT_TOOL };
|
|
45
38
|
|
|
46
39
|
export const COLLECTOR_OPERATIONAL_TOOLS = [
|
|
@@ -49,6 +42,7 @@ export const COLLECTOR_OPERATIONAL_TOOLS = [
|
|
|
49
42
|
COLLECTOR_READ_TOOL,
|
|
50
43
|
COLLECTOR_REQUEST_TOOL,
|
|
51
44
|
COLLECTOR_WAIT_TOOL,
|
|
45
|
+
COLLECTOR_HANDBOOK_WRITE_TOOL,
|
|
52
46
|
] as const;
|
|
53
47
|
|
|
54
48
|
/**
|
|
@@ -57,7 +51,7 @@ export const COLLECTOR_OPERATIONAL_TOOLS = [
|
|
|
57
51
|
* (evidenceId + htmlUrl) and is preserved in the volume, never unconditionally
|
|
58
52
|
* transcribed into model context or receipt.
|
|
59
53
|
*/
|
|
60
|
-
|
|
54
|
+
const COLLECTOR_OBSERVE_BODY_HEAD_BYTES = 512;
|
|
61
55
|
|
|
62
56
|
function utf8SafePrefix(text: string, maxBytes: number): string {
|
|
63
57
|
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
@@ -108,7 +102,7 @@ export type ObserveModelView = {
|
|
|
108
102
|
* (collector-side; provider-visible observe details carry only this bounded
|
|
109
103
|
* projection), never unconditionally transcribed into model context.
|
|
110
104
|
*/
|
|
111
|
-
|
|
105
|
+
function projectObserveContextView(modelView: ObserveModelView): ObserveModelView {
|
|
112
106
|
return {
|
|
113
107
|
...modelView,
|
|
114
108
|
evidence: modelView.evidence.map((entry) =>
|
|
@@ -121,9 +115,9 @@ export function projectObserveContextView(modelView: ObserveModelView): ObserveM
|
|
|
121
115
|
export type CollectorOperationalTool = (typeof COLLECTOR_OPERATIONAL_TOOLS)[number];
|
|
122
116
|
|
|
123
117
|
export const COLLECTOR_ACTIVATION_ENTRY_TYPE = "ak-collector-activation" as const;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
118
|
+
const COLLECTOR_SNAPSHOT_ENTRY_TYPE = "ak-collector-snapshot" as const;
|
|
119
|
+
const COLLECTOR_REQUEST_ENTRY_TYPE = "ak-collector-request" as const;
|
|
120
|
+
const COLLECTOR_WAIT_ENTRY_TYPE = "ak-collector-wait" as const;
|
|
127
121
|
|
|
128
122
|
export type CollectorRequestAttempt = {
|
|
129
123
|
attemptId: string;
|
|
@@ -217,7 +211,7 @@ export type CollectorLedger = {
|
|
|
217
211
|
}>;
|
|
218
212
|
|
|
219
213
|
request(
|
|
220
|
-
input: { requestId: string; snapshotId: string },
|
|
214
|
+
input: { requestId: string; snapshotId: string; body?: string },
|
|
221
215
|
transport: CollectorGitHubTransport,
|
|
222
216
|
clock: CollectorClock,
|
|
223
217
|
signal?: AbortSignal,
|
|
@@ -243,29 +237,6 @@ function isOperationalTool(name: string): name is CollectorOperationalTool {
|
|
|
243
237
|
return (COLLECTOR_OPERATIONAL_TOOLS as readonly string[]).includes(name);
|
|
244
238
|
}
|
|
245
239
|
|
|
246
|
-
/** Shared Check owner for Collector tool argument shapes (schema seam, not batch law). */
|
|
247
|
-
export function collectorToolArgumentsValid(
|
|
248
|
-
name: string,
|
|
249
|
-
args: unknown,
|
|
250
|
-
): boolean {
|
|
251
|
-
// Residual envelope: reject missing/null args before schema check.
|
|
252
|
-
if (args === undefined || args === null) return false;
|
|
253
|
-
switch (name) {
|
|
254
|
-
case COLLECTOR_OBSERVE_TOOL:
|
|
255
|
-
return Value.Check(collectorObserveArgsSchema, args);
|
|
256
|
-
case COLLECTOR_READ_TOOL:
|
|
257
|
-
return Value.Check(collectorReadArgsSchema, args);
|
|
258
|
-
case COLLECTOR_REQUEST_TOOL:
|
|
259
|
-
return Value.Check(collectorRequestArgsSchema, args);
|
|
260
|
-
case COLLECTOR_WAIT_TOOL:
|
|
261
|
-
return Value.Check(collectorWaitArgsSchema, args);
|
|
262
|
-
case COLLECTOR_OUTPUT_TOOL:
|
|
263
|
-
return Value.Check(collectorOutputArgsSchema, args);
|
|
264
|
-
default:
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
240
|
export function createCollectorLedger(
|
|
270
241
|
config: CollectorConfigState,
|
|
271
242
|
options?: CollectorLedgerOptions,
|
|
@@ -335,9 +306,7 @@ export function createCollectorLedger(
|
|
|
335
306
|
|
|
336
307
|
const requireBoundPr = (): number => {
|
|
337
308
|
if (config.prNumber === undefined) {
|
|
338
|
-
throw new Error(
|
|
339
|
-
"Collector PR target is unbound; call ak_collector_bind_target with the role-decided issue/PR or pass --pr",
|
|
340
|
-
);
|
|
309
|
+
throw new Error("通进司 PR 目标未绑定");
|
|
341
310
|
}
|
|
342
311
|
return config.prNumber;
|
|
343
312
|
};
|
|
@@ -675,11 +644,11 @@ export function createCollectorLedger(
|
|
|
675
644
|
throw new Error("通进司已产出输出候选,本局不再受理目标绑定");
|
|
676
645
|
}
|
|
677
646
|
if (!Number.isSafeInteger(prNumber) || prNumber < 1) {
|
|
678
|
-
throw new Error("
|
|
647
|
+
throw new Error("通进司绑定目标要求正安全整数 PR 号");
|
|
679
648
|
}
|
|
680
649
|
if (config.prNumber !== undefined && config.prNumber !== prNumber) {
|
|
681
650
|
throw new Error(
|
|
682
|
-
|
|
651
|
+
`通进司目标已绑定 PR ${config.prNumber},不可改绑为 ${prNumber}`,
|
|
683
652
|
);
|
|
684
653
|
}
|
|
685
654
|
config.prNumber = prNumber;
|
|
@@ -915,9 +884,25 @@ export function createCollectorLedger(
|
|
|
915
884
|
throw latchFatal("通进司请求时存在未恢复的传输失败");
|
|
916
885
|
}
|
|
917
886
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
887
|
+
// Sole request identity seam: trim once, then use everywhere (lookup/marker/attemptKey).
|
|
888
|
+
// Schema pattern rejects leading/trailing whitespace at host; trim still collapses
|
|
889
|
+
// direct ledger callers and any residual padding into one stable id (#677 D2).
|
|
890
|
+
if (typeof input.requestId !== "string") {
|
|
891
|
+
throw new Error("通进司 requestId 须为字符串");
|
|
892
|
+
}
|
|
893
|
+
const requestId = input.requestId.trim();
|
|
894
|
+
if (requestId.length === 0) {
|
|
895
|
+
throw new Error("通进司 requestId 须为非空(首尾空白不构成身份)");
|
|
896
|
+
}
|
|
897
|
+
const configured = config.manifest.requests.find((item) => item.id === requestId);
|
|
898
|
+
const roleBody = typeof input.body === "string" ? input.body : undefined;
|
|
899
|
+
if (configured === undefined) {
|
|
900
|
+
// #677: role-decided trigger from handbook/field activity — body required when not in manifest.
|
|
901
|
+
if (roleBody === undefined || roleBody.trim().length === 0) {
|
|
902
|
+
throw new Error(
|
|
903
|
+
`未知通进司 requestId "${requestId}" 且未提供 body;请提交角色判定的请求正文或使用 request-manifest`,
|
|
904
|
+
);
|
|
905
|
+
}
|
|
921
906
|
}
|
|
922
907
|
|
|
923
908
|
const snapshot = snapshots.find((item) => item.snapshotId === input.snapshotId);
|
|
@@ -936,10 +921,12 @@ export function createCollectorLedger(
|
|
|
936
921
|
throw latchFatal("通进司请求不在资格截止前");
|
|
937
922
|
}
|
|
938
923
|
|
|
924
|
+
// Caller manifest body wins when requestId is configured; otherwise role body.
|
|
925
|
+
const configuredBody = configured?.requestBody ?? roleBody!;
|
|
939
926
|
const { body, marker } = buildCollectorRequestBody({
|
|
940
|
-
configuredBody
|
|
927
|
+
configuredBody,
|
|
941
928
|
manifestDigest: config.manifest.digest,
|
|
942
|
-
requestId
|
|
929
|
+
requestId,
|
|
943
930
|
headOid: snapshot.headOid,
|
|
944
931
|
});
|
|
945
932
|
const existingMarker = snapshot.evidenceIds.some((id) => {
|
|
@@ -951,7 +938,7 @@ export function createCollectorLedger(
|
|
|
951
938
|
});
|
|
952
939
|
if (existingMarker) {
|
|
953
940
|
throw new Error(
|
|
954
|
-
`通进司在此 HEAD 已有同 marker 的已认证请求 "${
|
|
941
|
+
`通进司在此 HEAD 已有同 marker 的已认证请求 "${requestId}"`,
|
|
955
942
|
);
|
|
956
943
|
}
|
|
957
944
|
|
|
@@ -960,11 +947,11 @@ export function createCollectorLedger(
|
|
|
960
947
|
config.repository.canonical,
|
|
961
948
|
String(boundPr),
|
|
962
949
|
snapshot.headOid,
|
|
963
|
-
|
|
950
|
+
requestId,
|
|
964
951
|
].join("|");
|
|
965
952
|
if (attemptKeys.has(attemptKey)) {
|
|
966
953
|
throw new Error(
|
|
967
|
-
`通进司进程内请求 "${
|
|
954
|
+
`通进司进程内请求 "${requestId}" 在 HEAD ${snapshot.headOid} 的 attempt 已用`,
|
|
968
955
|
);
|
|
969
956
|
}
|
|
970
957
|
|
|
@@ -972,7 +959,7 @@ export function createCollectorLedger(
|
|
|
972
959
|
const attemptId = sha256Text(`${attemptKey}:${startedAt}`).slice(0, 16);
|
|
973
960
|
const attempt: CollectorRequestAttempt = {
|
|
974
961
|
attemptId,
|
|
975
|
-
requestId
|
|
962
|
+
requestId,
|
|
976
963
|
observedHead: snapshot.headOid,
|
|
977
964
|
snapshotId: snapshot.snapshotId,
|
|
978
965
|
marker,
|
|
@@ -1017,7 +1004,7 @@ export function createCollectorLedger(
|
|
|
1017
1004
|
return {
|
|
1018
1005
|
status: "succeeded",
|
|
1019
1006
|
attemptId,
|
|
1020
|
-
requestId
|
|
1007
|
+
requestId,
|
|
1021
1008
|
observedHead: snapshot.headOid,
|
|
1022
1009
|
marker,
|
|
1023
1010
|
commentEvidenceId: record.evidenceId,
|
|
@@ -1032,7 +1019,7 @@ export function createCollectorLedger(
|
|
|
1032
1019
|
return {
|
|
1033
1020
|
status: "ambiguous_loss",
|
|
1034
1021
|
attemptId,
|
|
1035
|
-
requestId
|
|
1022
|
+
requestId,
|
|
1036
1023
|
observedHead: snapshot.headOid,
|
|
1037
1024
|
marker,
|
|
1038
1025
|
diagnostics: result.diagnostics,
|
|
@@ -1051,14 +1038,7 @@ export function createCollectorLedger(
|
|
|
1051
1038
|
if (activationTime === undefined) {
|
|
1052
1039
|
throw latchFatal("通进司等待需要激活");
|
|
1053
1040
|
}
|
|
1054
|
-
|
|
1055
|
-
throw new Error("通进司等待 durationMs 须为正安全整数");
|
|
1056
|
-
}
|
|
1057
|
-
if (input.durationMs > COLLECTOR_ELIGIBILITY_MS) {
|
|
1058
|
-
throw new Error(
|
|
1059
|
-
`通进司等待 durationMs 至多为 ${COLLECTOR_ELIGIBILITY_MS}`,
|
|
1060
|
-
);
|
|
1061
|
-
}
|
|
1041
|
+
// durationMs shape authority = collectorWaitArgsSchema (host parameters).
|
|
1062
1042
|
if (pastCutoff(clock)) {
|
|
1063
1043
|
finalObservationRequired = true;
|
|
1064
1044
|
throw latchFatal("通进司等待不在资格截止前");
|
package/src/collector-role.ts
CHANGED
|
@@ -25,8 +25,14 @@ import {
|
|
|
25
25
|
listPullRequestNumbersByTicket,
|
|
26
26
|
type CollectorGitHubTransport,
|
|
27
27
|
} from "./collector-github.ts";
|
|
28
|
+
import {
|
|
29
|
+
createCollectorHandbookStore,
|
|
30
|
+
resolveCollectorHandbookRoot,
|
|
31
|
+
type CollectorHandbookStore,
|
|
32
|
+
} from "./collector-handbook.ts";
|
|
28
33
|
import {
|
|
29
34
|
COLLECTOR_BIND_TARGET_TOOL,
|
|
35
|
+
COLLECTOR_HANDBOOK_WRITE_TOOL,
|
|
30
36
|
COLLECTOR_OBSERVE_TOOL,
|
|
31
37
|
COLLECTOR_OUTPUT_TOOL,
|
|
32
38
|
COLLECTOR_READ_TOOL,
|
|
@@ -42,6 +48,7 @@ import {
|
|
|
42
48
|
} from "./collector-receipt.ts";
|
|
43
49
|
import {
|
|
44
50
|
collectorBindTargetArgsSchema,
|
|
51
|
+
collectorHandbookWriteArgsSchema,
|
|
45
52
|
collectorObserveArgsSchema,
|
|
46
53
|
collectorOutputArgsSchema,
|
|
47
54
|
collectorReadArgsSchema,
|
|
@@ -76,6 +83,7 @@ export class CollectorTargetBindError extends CorrectableSubmissionError {
|
|
|
76
83
|
|
|
77
84
|
export {
|
|
78
85
|
COLLECTOR_BIND_TARGET_TOOL,
|
|
86
|
+
COLLECTOR_HANDBOOK_WRITE_TOOL,
|
|
79
87
|
COLLECTOR_OBSERVE_TOOL,
|
|
80
88
|
COLLECTOR_OUTPUT_TOOL,
|
|
81
89
|
COLLECTOR_READ_TOOL,
|
|
@@ -89,6 +97,7 @@ export const COLLECTOR_REQUIRED_TOOLS = [
|
|
|
89
97
|
COLLECTOR_READ_TOOL,
|
|
90
98
|
COLLECTOR_REQUEST_TOOL,
|
|
91
99
|
COLLECTOR_WAIT_TOOL,
|
|
100
|
+
COLLECTOR_HANDBOOK_WRITE_TOOL,
|
|
92
101
|
COLLECTOR_OUTPUT_TOOL,
|
|
93
102
|
] as const;
|
|
94
103
|
|
|
@@ -125,12 +134,14 @@ const readSchema = collectorReadArgsSchema;
|
|
|
125
134
|
const requestSchema = collectorRequestArgsSchema;
|
|
126
135
|
const waitSchema = collectorWaitArgsSchema;
|
|
127
136
|
const bindSchema = collectorBindTargetArgsSchema;
|
|
137
|
+
const handbookWriteSchema = collectorHandbookWriteArgsSchema;
|
|
128
138
|
const outputSchema = collectorOutputArgsSchema;
|
|
129
139
|
|
|
130
140
|
type RequestParams = Static<typeof requestSchema>;
|
|
131
141
|
type ReadParams = Static<typeof readSchema>;
|
|
132
142
|
type WaitParams = Static<typeof waitSchema>;
|
|
133
143
|
type BindParams = Static<typeof bindSchema>;
|
|
144
|
+
type HandbookWriteParams = Static<typeof handbookWriteSchema>;
|
|
134
145
|
type OutputParams = Static<typeof outputSchema>;
|
|
135
146
|
|
|
136
147
|
export type CollectorRoleDependencies = {
|
|
@@ -138,6 +149,8 @@ export type CollectorRoleDependencies = {
|
|
|
138
149
|
createTransport(): CollectorGitHubTransport;
|
|
139
150
|
createClock?(): CollectorClock;
|
|
140
151
|
createLedger(config: CollectorConfigState, clock: CollectorClock, ctx: HostContext): CollectorLedger;
|
|
152
|
+
/** Optional packaged seed for first-use general handbook (#677). */
|
|
153
|
+
loadHandbookSeed?(): Promise<string>;
|
|
141
154
|
};
|
|
142
155
|
|
|
143
156
|
export type CollectorRoleHostActions = {
|
|
@@ -151,25 +164,51 @@ export type CollectorActivation = {
|
|
|
151
164
|
ledger: CollectorLedger;
|
|
152
165
|
transport: CollectorGitHubTransport;
|
|
153
166
|
clock: CollectorClock;
|
|
167
|
+
handbook: CollectorHandbookStore;
|
|
168
|
+
/** Loaded at activate; refreshed after handbook write for same-session materials. */
|
|
169
|
+
handbookView: {
|
|
170
|
+
general: string;
|
|
171
|
+
repo: string;
|
|
172
|
+
generalSource: "book" | "seed" | "empty";
|
|
173
|
+
repoSource: "book" | "empty";
|
|
174
|
+
generalPath: string;
|
|
175
|
+
repoPath: string;
|
|
176
|
+
};
|
|
154
177
|
};
|
|
155
178
|
|
|
156
179
|
function buildMethodContext(activation: CollectorActivation): string {
|
|
157
180
|
const pr = activation.ledger.config.prNumber;
|
|
158
|
-
|
|
181
|
+
const handbook = activation.handbookView;
|
|
182
|
+
const lines = [
|
|
159
183
|
"<collector_method>",
|
|
160
184
|
`host: github.com`,
|
|
161
185
|
`repository: ${activation.repository.canonical}`,
|
|
162
|
-
`prNumber: ${pr === undefined ? "
|
|
186
|
+
`prNumber: ${pr === undefined ? "未绑定" : String(pr)}`,
|
|
163
187
|
`requests: ${JSON.stringify(activation.manifest.requests.map((request) => ({ id: request.id })))}`,
|
|
188
|
+
`handbookGeneralSource: ${handbook.generalSource}`,
|
|
189
|
+
`handbookRepoSource: ${handbook.repoSource}`,
|
|
164
190
|
"</collector_method>",
|
|
165
|
-
]
|
|
191
|
+
];
|
|
192
|
+
// Opaque working memory only — no directional instructions (ADR 0073).
|
|
193
|
+
// JSON + `<` → \u003c keeps bodies from forging the delivery close tag (#677).
|
|
194
|
+
if (handbook.general.length > 0 || handbook.repo.length > 0) {
|
|
195
|
+
const payload = JSON.stringify({
|
|
196
|
+
general: handbook.general,
|
|
197
|
+
repo: handbook.repo,
|
|
198
|
+
generalSource: handbook.generalSource,
|
|
199
|
+
repoSource: handbook.repoSource,
|
|
200
|
+
repository: activation.repository.canonical,
|
|
201
|
+
}).replaceAll("<", "\\u003c");
|
|
202
|
+
lines.push("", "<collector_handbook>", payload, "</collector_handbook>");
|
|
203
|
+
}
|
|
204
|
+
return lines.join("\n");
|
|
166
205
|
}
|
|
167
206
|
|
|
168
207
|
function parsePositiveTicket(raw: unknown, label: string): number | undefined {
|
|
169
208
|
if (raw === undefined || raw === null) return undefined;
|
|
170
209
|
if (typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 1) return raw;
|
|
171
210
|
if (typeof raw === "string" && /^[1-9]\d*$/.test(raw.trim())) return Number(raw.trim());
|
|
172
|
-
throw new CollectorTargetBindError(
|
|
211
|
+
throw new CollectorTargetBindError(`通进司绑定 ${label} 须为正安全整数`);
|
|
173
212
|
}
|
|
174
213
|
|
|
175
214
|
/**
|
|
@@ -228,6 +267,26 @@ export function createCollectorRoleRuntime(
|
|
|
228
267
|
ctx,
|
|
229
268
|
);
|
|
230
269
|
|
|
270
|
+
// #677: handbook under admitted book topology (session path → books/<key>/collector-handbook).
|
|
271
|
+
const sessionPath = ctx.sessionManager?.getSessionFile?.()
|
|
272
|
+
?? ctx.sessionManager?.getSessionDir?.();
|
|
273
|
+
if (typeof sessionPath !== "string" || sessionPath.length === 0) {
|
|
274
|
+
throw new Error("通进司手册要求 session 路径位于 books/<bookKey>/");
|
|
275
|
+
}
|
|
276
|
+
const placement = resolveCollectorHandbookRoot(sessionPath);
|
|
277
|
+
const seedGeneral = dependencies.loadHandbookSeed === undefined
|
|
278
|
+
? undefined
|
|
279
|
+
: (await dependencies.loadHandbookSeed()).trim();
|
|
280
|
+
const handbook = createCollectorHandbookStore({
|
|
281
|
+
ledgerHome: placement.ledgerHome,
|
|
282
|
+
handbookRoot: placement.root,
|
|
283
|
+
repositoryCanonical: repository.canonical,
|
|
284
|
+
...(seedGeneral === undefined || seedGeneral.length === 0
|
|
285
|
+
? {}
|
|
286
|
+
: { seedGeneral }),
|
|
287
|
+
});
|
|
288
|
+
const handbookView = await handbook.read();
|
|
289
|
+
|
|
231
290
|
return {
|
|
232
291
|
soul,
|
|
233
292
|
repository,
|
|
@@ -235,6 +294,8 @@ export function createCollectorRoleRuntime(
|
|
|
235
294
|
ledger,
|
|
236
295
|
transport,
|
|
237
296
|
clock,
|
|
297
|
+
handbook,
|
|
298
|
+
handbookView,
|
|
238
299
|
};
|
|
239
300
|
},
|
|
240
301
|
|
|
@@ -297,7 +358,7 @@ export function createCollectorRoleRuntime(
|
|
|
297
358
|
const issueNumber = parsePositiveTicket(params.issueNumber, "issueNumber");
|
|
298
359
|
if (prNumber === undefined && issueNumber === undefined) {
|
|
299
360
|
throw new CollectorTargetBindError(
|
|
300
|
-
"
|
|
361
|
+
"通进司绑定须由角色判定 prNumber 与/或 issueNumber",
|
|
301
362
|
);
|
|
302
363
|
}
|
|
303
364
|
|
|
@@ -310,18 +371,18 @@ export function createCollectorRoleRuntime(
|
|
|
310
371
|
});
|
|
311
372
|
if (associated.length === 0) {
|
|
312
373
|
throw new CollectorTargetBindError(
|
|
313
|
-
|
|
374
|
+
`${activation.repository.canonical} 的 issue #${issueNumber} 无关联 PR;请改用明确 --pr 或其他 issueNumber`,
|
|
314
375
|
);
|
|
315
376
|
}
|
|
316
377
|
if (associated.length > 1) {
|
|
317
378
|
throw new CollectorTargetBindError(
|
|
318
|
-
`
|
|
379
|
+
`issue #${issueNumber} 关联多个 PR:${associated.join("、")};请改用明确 prNumber 或 --pr`,
|
|
319
380
|
);
|
|
320
381
|
}
|
|
321
382
|
const fromIssue = associated[0]!;
|
|
322
383
|
if (prNumber !== undefined && prNumber !== fromIssue) {
|
|
323
384
|
throw new CollectorTargetBindError(
|
|
324
|
-
`prNumber ${prNumber}
|
|
385
|
+
`prNumber ${prNumber} 与 issue #${issueNumber} 关联 PR ${fromIssue} 冲突`,
|
|
325
386
|
);
|
|
326
387
|
}
|
|
327
388
|
bound = fromIssue;
|
|
@@ -422,8 +483,8 @@ export function createCollectorRoleRuntime(
|
|
|
422
483
|
pi.registerTool({
|
|
423
484
|
name: COLLECTOR_REQUEST_TOOL,
|
|
424
485
|
label: "通进司请求",
|
|
425
|
-
description: "
|
|
426
|
-
promptSnippet: "
|
|
486
|
+
description: "在所引最新快照 HEAD 发一次请求。requestId 可取配置清单,或角色依手册/现场判定的稳定 id;后者须同时提供 body。",
|
|
487
|
+
promptSnippet: "发一次评审请求",
|
|
427
488
|
parameters: requestSchema,
|
|
428
489
|
async execute(toolCallId: string, params: RequestParams, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
|
|
429
490
|
const activation = getActivation();
|
|
@@ -431,7 +492,11 @@ export function createCollectorRoleRuntime(
|
|
|
431
492
|
try {
|
|
432
493
|
activation.ledger.beginOperational(COLLECTOR_REQUEST_TOOL, toolCallId);
|
|
433
494
|
const details = await activation.ledger.request(
|
|
434
|
-
|
|
495
|
+
{
|
|
496
|
+
requestId: params.requestId,
|
|
497
|
+
snapshotId: params.snapshotId,
|
|
498
|
+
...(typeof params.body === "string" ? { body: params.body } : {}),
|
|
499
|
+
},
|
|
435
500
|
activation.transport,
|
|
436
501
|
activation.clock,
|
|
437
502
|
signal,
|
|
@@ -451,6 +516,47 @@ export function createCollectorRoleRuntime(
|
|
|
451
516
|
},
|
|
452
517
|
});
|
|
453
518
|
|
|
519
|
+
pi.registerTool({
|
|
520
|
+
name: COLLECTOR_HANDBOOK_WRITE_TOOL,
|
|
521
|
+
label: "通进司手册写入",
|
|
522
|
+
description: "写入通用手册或当前仓库差异全文(整份替换)。",
|
|
523
|
+
promptSnippet: "更新 bot 手册",
|
|
524
|
+
parameters: handbookWriteSchema,
|
|
525
|
+
async execute(toolCallId: string, params: HandbookWriteParams, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
|
|
526
|
+
const activation = getActivation();
|
|
527
|
+
if (activation === undefined) throw new Error("通进司未激活");
|
|
528
|
+
try {
|
|
529
|
+
activation.ledger.beginOperational(COLLECTOR_HANDBOOK_WRITE_TOOL, toolCallId);
|
|
530
|
+
// scope/body shape authority = collectorHandbookWriteArgsSchema (host parameters).
|
|
531
|
+
const details = await activation.handbook.write(params.scope, params.body);
|
|
532
|
+
activation.handbookView = await activation.handbook.read();
|
|
533
|
+
activation.ledger.completeOperational(toolCallId);
|
|
534
|
+
return {
|
|
535
|
+
content: [{
|
|
536
|
+
type: "text" as const,
|
|
537
|
+
text: `手册已写入:${details.scope}`,
|
|
538
|
+
}],
|
|
539
|
+
details: {
|
|
540
|
+
scope: details.scope,
|
|
541
|
+
path: details.path,
|
|
542
|
+
byteLength: details.byteLength,
|
|
543
|
+
generalSource: activation.handbookView.generalSource,
|
|
544
|
+
repoSource: activation.handbookView.repoSource,
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
} catch (error) {
|
|
548
|
+
if (isCorrectableExecuteError(error)) throw error;
|
|
549
|
+
hostActions.failInfrastructure(error, ctx, toolCallId);
|
|
550
|
+
} finally {
|
|
551
|
+
try {
|
|
552
|
+
activation.ledger.completeOperational(toolCallId);
|
|
553
|
+
} catch {
|
|
554
|
+
// already completed or not begun
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
|
|
454
560
|
pi.registerTool({
|
|
455
561
|
name: COLLECTOR_WAIT_TOOL,
|
|
456
562
|
label: "通进司等待",
|