@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,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
|
+
}
|
package/dist/collector-ledger.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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("
|
|
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("
|
|
433
|
+
throw new Error("通进司绑定目标要求正安全整数 PR 号");
|
|
453
434
|
}
|
|
454
435
|
if (config.prNumber !== undefined && config.prNumber !== prNumber) {
|
|
455
|
-
throw new Error(
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
|
670
|
+
configuredBody,
|
|
674
671
|
manifestDigest: config.manifest.digest,
|
|
675
|
-
requestId
|
|
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 的已认证请求 "${
|
|
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
|
-
|
|
690
|
+
requestId,
|
|
694
691
|
].join("|");
|
|
695
692
|
if (attemptKeys.has(attemptKey)) {
|
|
696
|
-
throw new Error(`通进司进程内请求 "${
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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("通进司等待不在资格截止前");
|
package/dist/collector-role.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 ? "
|
|
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
|
-
]
|
|
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(
|
|
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("
|
|
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(
|
|
233
|
+
throw new CollectorTargetBindError(`${activation.repository.canonical} 的 issue #${issueNumber} 无关联 PR;请改用明确 --pr 或其他 issueNumber`);
|
|
194
234
|
}
|
|
195
235
|
if (associated.length > 1) {
|
|
196
|
-
throw new CollectorTargetBindError(`
|
|
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}
|
|
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: "
|
|
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(
|
|
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({
|
|
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)" }),
|