@akagilnc/pi-workflow-roles 0.1.3873 → 0.1.3884
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 +2 -2
- package/README.zh-CN.md +3 -3
- package/dist/acp-host/production-host.js +313 -371
- package/dist/headless-host/production-host.js +301 -359
- package/dist/merger-contracts.js +23 -42
- package/dist/merger-git-state.js +69 -42
- package/dist/merger-role.js +30 -41
- package/dist/public-cli/invocation.js +12 -39
- package/dist/public-cli/main.js +219 -331
- package/dist/public-cli/option-definitions.js +5 -5
- package/dist/public-cli/run-lifecycle.js +0 -2
- package/dist/role-runtime.js +1 -13
- package/extensions/role-runtime.ts +0 -3
- package/package.json +1 -1
- package/souls/merger.md +1 -1
- package/src/merger-contracts.ts +24 -38
- package/src/merger-git-state.ts +67 -37
- package/src/merger-role.ts +52 -48
- package/src/public-cli/invocation.ts +13 -44
- package/src/public-cli/merger-run.ts +2 -113
- package/src/public-cli/option-definitions.ts +5 -5
- package/src/public-cli/run-lifecycle.ts +0 -2
- package/src/role-runtime-dependencies.ts +0 -2
- package/src/role-runtime.ts +2 -13
|
@@ -552,7 +552,7 @@ const NOTARY_OPTIONS = [
|
|
|
552
552
|
},
|
|
553
553
|
];
|
|
554
554
|
const MERGER_OPTIONS = [
|
|
555
|
-
// Merger project face
|
|
555
|
+
// Merger project face: worktree whose Git materials are handed to the role.
|
|
556
556
|
{
|
|
557
557
|
id: "project",
|
|
558
558
|
owner: "merger",
|
|
@@ -563,8 +563,8 @@ const MERGER_OPTIONS = [
|
|
|
563
563
|
repeatable: false,
|
|
564
564
|
form: "option",
|
|
565
565
|
description: {
|
|
566
|
-
en: "Project root
|
|
567
|
-
zh: "
|
|
566
|
+
en: "Project root whose Git merge materials are read (defaults to cwd).",
|
|
567
|
+
zh: "读取 Git merge 材料的项目根(默认 cwd)。",
|
|
568
568
|
},
|
|
569
569
|
},
|
|
570
570
|
bindOwner("merger", SHARED_ATTACH_SEMANTICS),
|
|
@@ -1003,10 +1003,10 @@ const ROLE_COMMAND_HELP = {
|
|
|
1003
1003
|
},
|
|
1004
1004
|
merger: {
|
|
1005
1005
|
command: "merger",
|
|
1006
|
-
summary: "
|
|
1006
|
+
summary: "Reconcile merge materials in a worktree (escalate when nothing to merge).",
|
|
1007
1007
|
usage: ["ak-role merger [options] <instruction>"],
|
|
1008
1008
|
examples: [
|
|
1009
|
-
'ak-role merger --project /path/to/worktree "Reconcile the
|
|
1009
|
+
'ak-role merger --project /path/to/worktree "Reconcile the merge."',
|
|
1010
1010
|
],
|
|
1011
1011
|
},
|
|
1012
1012
|
inspector: {
|
|
@@ -997,7 +997,6 @@ async function loadResumableRunRecord(home, runId, authority) {
|
|
|
997
997
|
const d = record.derived;
|
|
998
998
|
if (typeof d.targetObjectId === "string" &&
|
|
999
999
|
typeof d.sourceObjectId === "string" &&
|
|
1000
|
-
typeof d.automaticMergeTreeId === "string" &&
|
|
1001
1000
|
Array.isArray(d.expectedConflictPaths) &&
|
|
1002
1001
|
Array.isArray(d.resolutionScope) &&
|
|
1003
1002
|
d.expectedConflictPaths.every((p) => typeof p === "string") &&
|
|
@@ -1005,7 +1004,6 @@ async function loadResumableRunRecord(home, runId, authority) {
|
|
|
1005
1004
|
derived = {
|
|
1006
1005
|
targetObjectId: d.targetObjectId,
|
|
1007
1006
|
sourceObjectId: d.sourceObjectId,
|
|
1008
|
-
automaticMergeTreeId: d.automaticMergeTreeId,
|
|
1009
1007
|
expectedConflictPaths: d.expectedConflictPaths,
|
|
1010
1008
|
resolutionScope: d.resolutionScope,
|
|
1011
1009
|
};
|
package/dist/role-runtime.js
CHANGED
|
@@ -1176,19 +1176,12 @@ export function createRoleRuntimeExtension(dependencies) {
|
|
|
1176
1176
|
return dependencies.loadDiaristSoul();
|
|
1177
1177
|
},
|
|
1178
1178
|
});
|
|
1179
|
-
let sessionMergerGitState = dependencies.mergerGitState;
|
|
1180
1179
|
const merger = createMergerRoleRuntime(roleHost, {
|
|
1181
1180
|
async loadSoul() { if (!dependencies.loadMergerSoul)
|
|
1182
1181
|
throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerSoul(); },
|
|
1183
1182
|
async loadInput(path) { if (!dependencies.loadMergerInput)
|
|
1184
1183
|
throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerInput(path); },
|
|
1185
|
-
|
|
1186
|
-
activeMerge() { if (!sessionMergerGitState)
|
|
1187
|
-
throw new Error("Merger runtime dependencies are not configured"); return sessionMergerGitState.activeMerge(); },
|
|
1188
|
-
completedMerge(mergeCommitId, automaticMergeTreeId) { if (!sessionMergerGitState)
|
|
1189
|
-
throw new Error("Merger runtime dependencies are not configured"); return sessionMergerGitState.completedMerge(mergeCommitId, automaticMergeTreeId); },
|
|
1190
|
-
},
|
|
1191
|
-
}, hostActions);
|
|
1184
|
+
});
|
|
1192
1185
|
// #676 E: shared envelope owns collector lifecycle (mode/fork, tool surface,
|
|
1193
1186
|
// event gates). Role module supplies business activate/tools/materials only.
|
|
1194
1187
|
let activeCollector;
|
|
@@ -1468,11 +1461,6 @@ export function createRoleRuntimeExtension(dependencies) {
|
|
|
1468
1461
|
auditor,
|
|
1469
1462
|
diarist,
|
|
1470
1463
|
merger: async () => {
|
|
1471
|
-
if (dependencies.mergerGitState === undefined) {
|
|
1472
|
-
sessionMergerGitState = dependencies.createMergerGitState?.(ctx.cwd);
|
|
1473
|
-
}
|
|
1474
|
-
if (sessionMergerGitState === undefined)
|
|
1475
|
-
throw new Error("Merger runtime dependencies are not configured");
|
|
1476
1464
|
await merger.activate();
|
|
1477
1465
|
},
|
|
1478
1466
|
};
|
|
@@ -32,7 +32,6 @@ import { loadPackagedCanonicalSkillBinding } from "../src/package-resources/meth
|
|
|
32
32
|
import { JUDGE_OUTPUT_TOOL_NAME } from "../src/package-contracts/judge-output.ts";
|
|
33
33
|
import { readOAuthKeepaliveProviders } from "../src/oauth-keepalive.ts";
|
|
34
34
|
import {
|
|
35
|
-
createProductionMergerGitState,
|
|
36
35
|
formatNavigatorRoleHelp,
|
|
37
36
|
} from "../src/role-runtime.ts";
|
|
38
37
|
import { loadAuditorSoulFromSubjectInput } from "../src/auditor-soul.ts";
|
|
@@ -160,8 +159,6 @@ export default function roleRuntime(pi: ExtensionAPI): void {
|
|
|
160
159
|
},
|
|
161
160
|
loadMergerSoul: () => loadMainRoleSessionMaterials("merger"),
|
|
162
161
|
loadMergerInput: async (path) => JSON.parse(await readFile(path, "utf8")),
|
|
163
|
-
createMergerGitState: (repositoryRoot) =>
|
|
164
|
-
createProductionMergerGitState(repositoryRoot),
|
|
165
162
|
async loadCanonicalSkillBinding(name) {
|
|
166
163
|
// Coder TDD (#109) and Reviewer code-review (#111) are package-owned.
|
|
167
164
|
// Optional Fixer diagnosing-bugs is available via --skill without this binding.
|
package/package.json
CHANGED
package/souls/merger.md
CHANGED
package/src/merger-contracts.ts
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
1
|
import { Type, type Static } from "typebox";
|
|
2
|
-
import { isFullGitObjectId } from "./git-object-id.ts";
|
|
3
2
|
import { exactUtf8 } from "./exact-utf8.ts";
|
|
4
|
-
import { sha256Hex } from "./sha256.ts";
|
|
5
3
|
import { openToolObjectFromUnion } from "./open-tool-schema.ts";
|
|
6
4
|
import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
|
|
7
5
|
|
|
8
|
-
const oidPattern = "^(?:[0-9a-f]{40}|[0-9a-f]{64})$";
|
|
9
6
|
const materialSchema = Type.Object({ bytesBase64: Type.String(), sha256: Type.String() }, { additionalProperties: false });
|
|
10
7
|
const checkSchema = Type.Object({ name: Type.String({ minLength: 1 }), argv: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }) }, { additionalProperties: false });
|
|
11
8
|
export const mergerInputSchema = Type.Object({
|
|
12
|
-
|
|
13
|
-
targetObjectId: Type.String({
|
|
9
|
+
attemptId: Type.String({ minLength: 1, description: "对账 attempt 身份(transport)" }),
|
|
10
|
+
targetObjectId: Type.String({ description: "材料:target parent OID;可空" }),
|
|
11
|
+
sourceObjectId: Type.String({ description: "材料:source parent OID;可空" }),
|
|
14
12
|
materials: Type.Object({ task: materialSchema, authority: materialSchema, targetIntent: materialSchema, sourceIntent: materialSchema }, { additionalProperties: false }),
|
|
15
|
-
expectedConflictPaths: Type.Array(Type.String(
|
|
16
|
-
resolutionScope: Type.Array(Type.String(
|
|
13
|
+
expectedConflictPaths: Type.Array(Type.String(), { description: "材料:当前未合并路径;可空" }),
|
|
14
|
+
resolutionScope: Type.Array(Type.String(), { description: "材料:解析范围提示;可空" }),
|
|
17
15
|
authorizedChecks: Type.Array(checkSchema),
|
|
18
16
|
}, { additionalProperties: false });
|
|
19
17
|
const mergerOutputVariants = Type.Union([
|
|
20
|
-
Type.Object({ status: Type.Literal("completed", { description: "completed — 形状指引,非 schema 闸" }), attemptId: Type.String({ minLength: 1, description: "已受理合并 attempt 身份" }), report: Type.String({ minLength: 1, description: "如实结果报告" }), mergeCommitId: Type.String({
|
|
21
|
-
Type.Object({ status: Type.Literal("escalate", { description: "escalate — 形状指引,非 schema 闸" }), attemptId: Type.String({ minLength: 1, description: "已受理合并 attempt 身份" }), diagnosis: Type.String({ minLength: 1, description: "
|
|
18
|
+
Type.Object({ status: Type.Literal("completed", { description: "completed — 形状指引,非 schema 闸" }), attemptId: Type.String({ minLength: 1, description: "已受理合并 attempt 身份" }), report: Type.String({ minLength: 1, description: "如实结果报告" }), mergeCommitId: Type.String({ description: "完成合并 commit object ID" }) }, { additionalProperties: false }),
|
|
19
|
+
Type.Object({ status: Type.Literal("escalate", { description: "escalate — 形状指引,非 schema 闸" }), attemptId: Type.String({ minLength: 1, description: "已受理合并 attempt 身份" }), diagnosis: Type.String({ minLength: 1, description: "合并无法或不应由本席完成的原因(含无进行中合并、无活可干、需新的产品/权力决定)" }), report: Type.String({ minLength: 1, description: "如实结果报告" }) }, { additionalProperties: false }),
|
|
22
20
|
]);
|
|
23
|
-
export const mergerOutputSchema = withInfrastructureFailureDeclaration(
|
|
24
|
-
openToolObjectFromUnion(mergerOutputVariants),
|
|
25
|
-
);
|
|
21
|
+
export const mergerOutputSchema = withInfrastructureFailureDeclaration(openToolObjectFromUnion(mergerOutputVariants));
|
|
26
22
|
|
|
27
23
|
export type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T extends readonly (infer U)[] ? readonly DeepReadonly<U>[] : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
|
|
28
24
|
export type MergerMaterial = DeepReadonly<Static<typeof materialSchema>>;
|
|
@@ -39,37 +35,27 @@ export class MergerInputContractError extends Error {
|
|
|
39
35
|
constructor(message = "Merger input violates its exact contract") { super(message); this.name = "MergerInputContractError"; }
|
|
40
36
|
}
|
|
41
37
|
function fail(message = "Merger input violates its exact contract"): never { throw new MergerInputContractError(message); }
|
|
42
|
-
function canonicalPath(path: unknown): path is string { return typeof path === "string" && path.length > 0 && !path.startsWith("/") && !path.includes("\0") && path.split("/").every(part => part !== "" && part !== "." && part !== ".."); }
|
|
43
|
-
function validatePathSet(value: unknown, label: string): string[] {
|
|
44
|
-
if (!Array.isArray(value) || value.length === 0 || !value.every(canonicalPath)) fail(`Merger ${label} must be a non-empty canonical path set`);
|
|
45
|
-
return value as string[];
|
|
46
|
-
}
|
|
47
|
-
function validateMaterial(value: unknown, label: string): void {
|
|
48
|
-
if (!record(value) || typeof value.bytesBase64 !== "string" || typeof value.sha256 !== "string") fail(`Merger ${label} material is malformed`);
|
|
49
|
-
const bytes = Buffer.from(value.bytesBase64, "base64");
|
|
50
|
-
exactUtf8(bytes, `Merger ${label} material`);
|
|
51
|
-
if (sha256Hex(bytes) !== value.sha256) fail(`Merger ${label} material digest mismatch`);
|
|
52
|
-
}
|
|
53
38
|
function deepFreeze<T>(value: T): T { if (value && typeof value === "object") { for (const child of Object.values(value as object)) deepFreeze(child); Object.freeze(value); } return value; }
|
|
39
|
+
const asString = (v: unknown): string => typeof v === "string" ? v : "";
|
|
40
|
+
const asStringArray = (v: unknown): string[] => Array.isArray(v) ? v.filter((item): item is string => typeof item === "string") : [];
|
|
41
|
+
|
|
42
|
+
function readMaterial(value: unknown, label: string): MergerMaterial {
|
|
43
|
+
if (!record(value) || typeof value.bytesBase64 !== "string") fail(`Merger ${label} material is malformed`);
|
|
44
|
+
exactUtf8(Buffer.from(value.bytesBase64, "base64"), `Merger ${label} material`);
|
|
45
|
+
return { bytesBase64: value.bytesBase64, sha256: typeof value.sha256 === "string" ? value.sha256 : "" };
|
|
46
|
+
}
|
|
54
47
|
|
|
55
48
|
export function validateMergerInput(value: unknown): MergerInput {
|
|
56
|
-
if (!record(value) || blank(value.attemptId)
|
|
57
|
-
if (!record(value.materials)) fail();
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
if (!conflicts.every(path => scope.includes(path))) fail("Merger resolution scope must contain the complete conflict set");
|
|
62
|
-
if (!Array.isArray(value.authorizedChecks)) fail("Merger authorized checks are malformed");
|
|
63
|
-
for (const check of value.authorizedChecks) {
|
|
64
|
-
if (!record(check) || !Array.isArray(check.argv) || check.argv.length === 0 || check.argv.some(blank)) fail("Merger authorized check is malformed");
|
|
65
|
-
}
|
|
66
|
-
return deepFreeze(structuredClone(value) as MergerInput);
|
|
49
|
+
if (!record(value) || blank(value.attemptId)) fail("Merger input requires a non-empty attemptId for accounting");
|
|
50
|
+
if (!record(value.materials)) fail("Merger materials are missing");
|
|
51
|
+
const materials = { task: readMaterial(value.materials.task, "task"), authority: readMaterial(value.materials.authority, "authority"), targetIntent: readMaterial(value.materials.targetIntent, "targetIntent"), sourceIntent: readMaterial(value.materials.sourceIntent, "sourceIntent") };
|
|
52
|
+
const authorizedChecks = Array.isArray(value.authorizedChecks) ? value.authorizedChecks.filter((check): check is Record<string, unknown> => record(check)).map(c => ({ name: asString(c.name), argv: asStringArray(c.argv).filter(a => a.trim().length > 0) })).filter(c => c.name.trim().length > 0 && c.argv.length > 0) : [];
|
|
53
|
+
return deepFreeze(structuredClone({ attemptId: (value.attemptId as string).trim(), targetObjectId: asString(value.targetObjectId), sourceObjectId: asString(value.sourceObjectId), materials, expectedConflictPaths: asStringArray(value.expectedConflictPaths), resolutionScope: asStringArray(value.resolutionScope), authorizedChecks }) as MergerInput);
|
|
67
54
|
}
|
|
68
55
|
|
|
69
|
-
export function validateMergerOutput(value: unknown
|
|
70
|
-
if (!record(value)
|
|
56
|
+
export function validateMergerOutput(value: unknown): MergerOutput {
|
|
57
|
+
if (!record(value)) throw new Error("合并回执无已识别的执行判别");
|
|
71
58
|
const status = typeof value.status === "string" ? value.status : undefined;
|
|
72
|
-
if (status === "completed"
|
|
73
|
-
if (status === "escalate") return structuredClone(value) as MergerOutput;
|
|
59
|
+
if (status === "completed" || status === "escalate") return structuredClone(value) as MergerOutput;
|
|
74
60
|
throw new Error("合并回执无已识别的执行判别");
|
|
75
61
|
}
|
package/src/merger-git-state.ts
CHANGED
|
@@ -1,30 +1,60 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants } from "node:fs";
|
|
4
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
5
|
import { promisify } from "node:util";
|
|
3
6
|
import { exactUtf8 } from "./exact-utf8.ts";
|
|
4
7
|
import { isFullGitObjectId } from "./git-object-id.ts";
|
|
5
8
|
|
|
6
9
|
const execFileAsync = promisify(execFile);
|
|
7
|
-
|
|
8
|
-
export type
|
|
10
|
+
|
|
11
|
+
export type ActiveMergerGitState = {
|
|
12
|
+
targetObjectId: string;
|
|
13
|
+
sourceObjectId: string;
|
|
14
|
+
unmergedPaths: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
9
17
|
export interface MergerGitState {
|
|
10
18
|
activeMerge(): Promise<ActiveMergerGitState>;
|
|
11
|
-
completedMerge(mergeCommitId: string, automaticMergeTreeId: string): Promise<CompletedMergerGitState>;
|
|
12
19
|
}
|
|
13
20
|
|
|
21
|
+
type GitExecError = { code?: unknown };
|
|
22
|
+
|
|
14
23
|
async function git(cwd: string, args: string[]): Promise<Uint8Array> {
|
|
15
24
|
const { stdout } = await execFileAsync("git", args, { cwd, encoding: "buffer", maxBuffer: 16 * 1024 * 1024 });
|
|
16
25
|
return new Uint8Array(stdout);
|
|
17
26
|
}
|
|
27
|
+
|
|
18
28
|
function line(bytes: Uint8Array, label: string): string {
|
|
19
29
|
const value = exactUtf8(bytes, label).trim();
|
|
20
30
|
if (!value) throw new Error(`${label} is empty`);
|
|
21
31
|
return value;
|
|
22
32
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
|
|
34
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
35
|
+
try { await access(path, fsConstants.F_OK); return true; }
|
|
36
|
+
catch (error) { if ((error as { code?: unknown }).code === "ENOENT") return false; throw error; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function requireInsideWorkTree(cwd: string): Promise<void> {
|
|
40
|
+
const stdout = await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
41
|
+
if (exactUtf8(stdout, "Git worktree check").trim() !== "true") throw new Error("Assigned path is not inside a Git work tree");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function tryResolveCommit(cwd: string, rev: string): Promise<string | undefined> {
|
|
45
|
+
let stdout: Uint8Array;
|
|
46
|
+
try {
|
|
47
|
+
const res = await execFileAsync("git", ["rev-parse", "--verify", "--quiet", `${rev}^{commit}`], { cwd, encoding: "buffer", maxBuffer: 16 * 1024 * 1024 });
|
|
48
|
+
stdout = new Uint8Array(res.stdout);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if ((error as GitExecError).code === 1) return undefined;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
const oid = line(stdout, `Git ${rev}`);
|
|
54
|
+
if (!isFullGitObjectId(oid)) throw new Error(`Git ${rev} identity is unavailable or invalid`);
|
|
55
|
+
return oid;
|
|
27
56
|
}
|
|
57
|
+
|
|
28
58
|
async function unmerged(cwd: string): Promise<string[]> {
|
|
29
59
|
const raw = exactUtf8(await git(cwd, ["ls-files", "-u", "-z"]), "Git unmerged index");
|
|
30
60
|
const paths = new Set<string>();
|
|
@@ -37,40 +67,40 @@ async function unmerged(cwd: string): Promise<string[]> {
|
|
|
37
67
|
return [...paths].sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
|
|
38
68
|
}
|
|
39
69
|
|
|
70
|
+
async function resolveHeadMaterial(cwd: string): Promise<string> {
|
|
71
|
+
const commit = await tryResolveCommit(cwd, "HEAD");
|
|
72
|
+
if (commit !== undefined) return commit;
|
|
73
|
+
try {
|
|
74
|
+
await execFileAsync("git", ["rev-parse", "--verify", "--quiet", "HEAD"], { cwd, encoding: "buffer", maxBuffer: 16 * 1024 * 1024 });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if ((error as GitExecError).code === 1) return "";
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
throw new Error("Git HEAD identity is unavailable or invalid");
|
|
80
|
+
}
|
|
81
|
+
|
|
40
82
|
export function createProductionMergerGitState(repositoryRoot = process.cwd()): MergerGitState {
|
|
41
83
|
return {
|
|
42
84
|
async activeMerge() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
85
|
+
await requireInsideWorkTree(repositoryRoot);
|
|
86
|
+
const targetObjectId = await resolveHeadMaterial(repositoryRoot);
|
|
87
|
+
const mergeHeadReported = line(await git(repositoryRoot, ["rev-parse", "--git-path", "MERGE_HEAD"]), "Git MERGE_HEAD path");
|
|
88
|
+
const mergeHeadPath = isAbsolute(mergeHeadReported) ? mergeHeadReported : resolve(repositoryRoot, mergeHeadReported);
|
|
89
|
+
let sourceObjectId = "";
|
|
90
|
+
if (await pathExists(mergeHeadPath)) {
|
|
91
|
+
const raw = exactUtf8(await readFile(mergeHeadPath), "Git MERGE_HEAD");
|
|
92
|
+
const mergeHeads = raw.trim().split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
|
|
93
|
+
if (mergeHeads.length === 0) throw new Error("Git MERGE_HEAD is empty");
|
|
94
|
+
if (mergeHeads.length !== 1) throw new Error("Assigned repository does not have one ordinary in-progress merge");
|
|
95
|
+
const source = mergeHeads[0]!;
|
|
96
|
+
if (!isFullGitObjectId(source)) throw new Error("Git MERGE_HEAD identity is unavailable or invalid");
|
|
97
|
+
const resolved = await tryResolveCommit(repositoryRoot, source);
|
|
98
|
+
if (resolved === undefined || (targetObjectId !== "" && resolved.length !== targetObjectId.length)) {
|
|
99
|
+
throw new Error("Git MERGE_HEAD identity is unavailable or invalid");
|
|
100
|
+
}
|
|
101
|
+
sourceObjectId = resolved;
|
|
49
102
|
}
|
|
50
|
-
|
|
51
|
-
if (mergeHeads.length !== 1 || !isFullGitObjectId(targetObjectId) || !isFullGitObjectId(mergeHeads[0]) || targetObjectId.length !== mergeHeads[0]!.length) throw new Error("Assigned repository does not have one ordinary in-progress merge");
|
|
52
|
-
let automaticMergeTreeRaw: Uint8Array;
|
|
53
|
-
try {
|
|
54
|
-
automaticMergeTreeRaw = await git(repositoryRoot, ["rev-parse", "--verify", "AUTO_MERGE^{tree}"]);
|
|
55
|
-
} catch {
|
|
56
|
-
throw new Error("Git automatic merge tree identity is unavailable or invalid");
|
|
57
|
-
}
|
|
58
|
-
const automaticMergeTreeId = line(automaticMergeTreeRaw, "Git automatic merge tree");
|
|
59
|
-
if (!isFullGitObjectId(automaticMergeTreeId) || automaticMergeTreeId.length !== targetObjectId.length) throw new Error("Git automatic merge tree identity is unavailable or invalid");
|
|
60
|
-
return { targetObjectId, sourceObjectId: mergeHeads[0]!, unmergedPaths: await unmerged(repositoryRoot), automaticMergeTreeId };
|
|
61
|
-
},
|
|
62
|
-
async completedMerge(mergeCommitId, automaticMergeTreeId) {
|
|
63
|
-
if (!isFullGitObjectId(mergeCommitId) || !isFullGitObjectId(automaticMergeTreeId) || mergeCommitId.length !== automaticMergeTreeId.length) throw new Error("Merger completion object ID is invalid");
|
|
64
|
-
const identity = exactUtf8(await git(repositoryRoot, ["show", "-s", "--format=%H%x00%P", mergeCommitId]), "Git merge commit").trimEnd().split("\0");
|
|
65
|
-
if (identity.length !== 2 || identity[0] !== mergeCommitId) throw new Error("Git merge completion identity drifted");
|
|
66
|
-
const parentObjectIds = identity[1]!.split(" ").filter(Boolean);
|
|
67
|
-
const currentHead = line(await git(repositoryRoot, ["rev-parse", "--verify", "HEAD"]), "Git HEAD");
|
|
68
|
-
const status = await git(repositoryRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
|
|
69
|
-
const frozenTree = line(await git(repositoryRoot, ["rev-parse", "--verify", `${automaticMergeTreeId}^{tree}`]), "Git frozen automatic merge tree");
|
|
70
|
-
if (frozenTree !== automaticMergeTreeId) throw new Error("Git frozen automatic merge tree identity drifted");
|
|
71
|
-
const mergeTree = line(await git(repositoryRoot, ["rev-parse", "--verify", `${mergeCommitId}^{tree}`]), "Git completed merge tree");
|
|
72
|
-
const resolutionChangedPaths = nulPaths(await git(repositoryRoot, ["diff-tree", "--no-commit-id", "--name-only", "-r", "-z", "--no-renames", automaticMergeTreeId, mergeTree]), "Git resolution path delta");
|
|
73
|
-
return { mergeCommitId: currentHead, parentObjectIds, unmergedPaths: await unmerged(repositoryRoot), worktreeClean: status.byteLength === 0 && currentHead === mergeCommitId, resolutionChangedPaths };
|
|
103
|
+
return { targetObjectId, sourceObjectId, unmergedPaths: await unmerged(repositoryRoot) };
|
|
74
104
|
},
|
|
75
105
|
};
|
|
76
106
|
}
|
package/src/merger-role.ts
CHANGED
|
@@ -1,58 +1,62 @@
|
|
|
1
|
-
import type { RoleHost, HostContext
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import type { RoleHost, HostContext } from "./host-contracts.ts";
|
|
2
|
+
import {
|
|
3
|
+
MERGER_ACCEPTED_TEXT,
|
|
4
|
+
MERGER_OUTPUT_TOOL_NAME,
|
|
5
|
+
mergerOutputSchema,
|
|
6
|
+
validateMergerInput,
|
|
7
|
+
type MergerInput,
|
|
8
|
+
} from "./merger-contracts.ts";
|
|
4
9
|
import { exactUtf8 } from "./exact-utf8.ts";
|
|
5
|
-
import { isFullGitObjectId } from "./git-object-id.ts";
|
|
6
10
|
|
|
7
11
|
export { MERGER_OUTPUT_TOOL_NAME };
|
|
8
|
-
export const MERGER_INPUT_FLAG = { name: "ak-merger-input", definition: { description: "Path to
|
|
9
|
-
export type MergerRoleDependencies = { loadSoul(): Promise<string>; loadInput(path: string): Promise<unknown>; gitState: MergerGitState };
|
|
10
|
-
export type MergerRoleHostActions = { failInfrastructure(error: unknown, ctx: HostContext, toolCallId?: string): never };
|
|
12
|
+
export const MERGER_INPUT_FLAG = { name: "ak-merger-input", definition: { description: "Path to Merger assignment materials JSON", type: "string" as const } } as const;
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
function materialText(input: MergerInput, key: keyof MergerInput["materials"]): string { return exactUtf8(Buffer.from(input.materials[key].bytesBase64, "base64"), `Merger ${key}`); }
|
|
14
|
+
export type MergerRoleDependencies = { loadSoul(): Promise<string>; loadInput(path: string): Promise<unknown>; };
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
function materialText(input: MergerInput, key: keyof MergerInput["materials"]): string {
|
|
17
|
+
return exactUtf8(Buffer.from(input.materials[key].bytesBase64, "base64"), `Merger ${key}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Merger (校书郎) role runtime.
|
|
22
|
+
* Code admits one submission and terminates the round (第 0 条).
|
|
23
|
+
* Git state / completion verification are not attendance or rejection gates (#827).
|
|
24
|
+
*/
|
|
25
|
+
export function createMergerRoleRuntime(pi: RoleHost, dependencies: MergerRoleDependencies) {
|
|
26
|
+
let activation: { soul: string; input: Readonly<MergerInput> } | undefined;
|
|
17
27
|
let registered = false;
|
|
18
28
|
let accepted = false;
|
|
19
29
|
pi.registerFlag(MERGER_INPUT_FLAG.name, MERGER_INPUT_FLAG.definition);
|
|
20
|
-
return {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
return {
|
|
31
|
+
async activate() {
|
|
32
|
+
const path = pi.getFlag(MERGER_INPUT_FLAG.name);
|
|
33
|
+
if (typeof path !== "string" || path.trim().length === 0) throw new Error("Merger requires --ak-merger-input");
|
|
34
|
+
const soul = (await dependencies.loadSoul()).trim();
|
|
35
|
+
if (!soul) throw new Error("Merger soul is empty");
|
|
36
|
+
const input = validateMergerInput(await dependencies.loadInput(path));
|
|
37
|
+
activation = { soul, input };
|
|
38
|
+
accepted = false;
|
|
39
|
+
if (!registered) {
|
|
40
|
+
registered = true;
|
|
41
|
+
pi.registerTool({
|
|
42
|
+
name: MERGER_OUTPUT_TOOL_NAME, label: "合并输出", description: "提交合并结果;输出分支为 completed 与 escalate。", promptSnippet: "提交合并结果", parameters: mergerOutputSchema,
|
|
43
|
+
async execute(_id: string, params: unknown, _signal: AbortSignal | undefined, _update: unknown, _ctx: HostContext) {
|
|
44
|
+
if (!activation) throw new Error("校书郎未激活");
|
|
45
|
+
if (accepted) throw new Error("合并回执已受理");
|
|
46
|
+
accepted = true;
|
|
47
|
+
return { content: [{ type: "text" as const, text: MERGER_ACCEPTED_TEXT }], details: params, terminate: true as const };
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
pi.on("before_agent_start", (event) => {
|
|
31
51
|
if (!activation) throw new Error("校书郎未激活");
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
catch (error) { host.failInfrastructure(error, ctx, id); }
|
|
43
|
-
const scope = new Set(activation.input.resolutionScope);
|
|
44
|
-
if (state.mergeCommitId !== output.mergeCommitId || !same(state.parentObjectIds, [activation.input.targetObjectId, activation.input.sourceObjectId]) || state.unmergedPaths.length !== 0 || !state.worktreeClean || state.resolutionChangedPaths.some(path => !scope.has(path))) host.failInfrastructure(new Error("Merger completed-state verification failed"), ctx, id);
|
|
45
|
-
}
|
|
46
|
-
accepted = true;
|
|
47
|
-
const acceptedDetails = output;
|
|
48
|
-
return { content: [{ type: "text" as const, text: MERGER_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true as const };
|
|
49
|
-
} });
|
|
50
|
-
pi.on("before_agent_start", (event) => { if (!activation) throw new Error("校书郎未激活"); const admitted = { attemptId: activation.input.attemptId, targetObjectId: activation.input.targetObjectId, sourceObjectId: activation.input.sourceObjectId, task: materialText(activation.input, "task"), authority: materialText(activation.input, "authority"), targetIntent: materialText(activation.input, "targetIntent"), sourceIntent: materialText(activation.input, "sourceIntent"), expectedConflictPaths: activation.input.expectedConflictPaths, resolutionScope: activation.input.resolutionScope, authorizedChecks: activation.input.authorizedChecks }; return { systemPrompt: `${event.systemPrompt}\n\n<merger_soul>\n${activation.soul}\n</merger_soul>\n\n<merger_assignment>\n${JSON.stringify(admitted)}\n</merger_assignment>` }; });
|
|
51
|
-
}
|
|
52
|
-
// Host-neutral AK output only (ADR 0018 fail-closed); no host-builtin name hard-require.
|
|
53
|
-
const all = pi.getAllTools().map(tool => tool.name);
|
|
54
|
-
if (all.filter(item => item === MERGER_OUTPUT_TOOL_NAME).length !== 1) {
|
|
55
|
-
throw new Error(`Merger required tool collision or missing: ${MERGER_OUTPUT_TOOL_NAME}`);
|
|
56
|
-
}
|
|
57
|
-
} };
|
|
52
|
+
const admitted = { attemptId: activation.input.attemptId, targetObjectId: activation.input.targetObjectId, sourceObjectId: activation.input.sourceObjectId, task: materialText(activation.input, "task"), authority: materialText(activation.input, "authority"), targetIntent: materialText(activation.input, "targetIntent"), sourceIntent: materialText(activation.input, "sourceIntent"), expectedConflictPaths: activation.input.expectedConflictPaths, resolutionScope: activation.input.resolutionScope, authorizedChecks: activation.input.authorizedChecks };
|
|
53
|
+
return { systemPrompt: `${event.systemPrompt}\n\n<merger_soul>\n${activation.soul}\n</merger_soul>\n\n<merger_assignment>\n${JSON.stringify(admitted)}\n</merger_assignment>` };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const all = pi.getAllTools().map((tool) => tool.name);
|
|
57
|
+
if (all.filter((item) => item === MERGER_OUTPUT_TOOL_NAME).length !== 1) {
|
|
58
|
+
throw new Error(`Merger required tool collision or missing: ${MERGER_OUTPUT_TOOL_NAME}`);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
58
62
|
}
|
|
@@ -211,11 +211,10 @@ export type AdmittedReviewerInvocation = AdmittedRoleInvocationBase & {
|
|
|
211
211
|
readonly authorityRefs: readonly string[];
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
-
/** Mechanical
|
|
214
|
+
/** Mechanical materials read from current Git state (may be empty). */
|
|
215
215
|
export type DerivedMergerEnvelope = {
|
|
216
216
|
readonly targetObjectId: string;
|
|
217
217
|
readonly sourceObjectId: string;
|
|
218
|
-
readonly automaticMergeTreeId: string;
|
|
219
218
|
readonly expectedConflictPaths: readonly string[];
|
|
220
219
|
readonly resolutionScope: readonly string[];
|
|
221
220
|
};
|
|
@@ -810,17 +809,6 @@ export type ParseAnalystArgvResult =
|
|
|
810
809
|
| ParseAnalystSweepArgv
|
|
811
810
|
| ParseAnalystCohortArgv;
|
|
812
811
|
|
|
813
|
-
/** Honest activation-class failure while deriving the active-merge envelope. */
|
|
814
|
-
export class MergerEnvelopeDerivationError extends Error {
|
|
815
|
-
readonly code = "merger-envelope-derivation" as const;
|
|
816
|
-
/** Typed cause for #107 classifyPostAdmissionFailure (isTypedActivationError). */
|
|
817
|
-
readonly knownCause = "activation" as const;
|
|
818
|
-
constructor(message: string, options?: { cause?: unknown }) {
|
|
819
|
-
super(message, options);
|
|
820
|
-
this.name = "MergerEnvelopeDerivationError";
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
812
|
/** Reject missing/blank path values so empty overrides cannot silently degrade. */
|
|
825
813
|
function requireOptionPath(
|
|
826
814
|
flag: string,
|
|
@@ -2868,7 +2856,7 @@ export function parseMergerArgv(args: readonly string[]): ParseMergerArgvResult
|
|
|
2868
2856
|
token.startsWith("--resolutionScope=")
|
|
2869
2857
|
) {
|
|
2870
2858
|
throw new CliUsageError(
|
|
2871
|
-
"merger does not accept public packet fields; the adapter
|
|
2859
|
+
"merger does not accept public packet fields; the adapter reads Git merge materials",
|
|
2872
2860
|
);
|
|
2873
2861
|
}
|
|
2874
2862
|
if (token.startsWith("-") && token !== "-") {
|
|
@@ -2894,36 +2882,20 @@ function mergerMaterialFromUtf8(text: string): MergerInput["materials"]["task"]
|
|
|
2894
2882
|
}
|
|
2895
2883
|
|
|
2896
2884
|
/**
|
|
2897
|
-
*
|
|
2898
|
-
*
|
|
2899
|
-
*
|
|
2885
|
+
* Read merger materials from current Git state.
|
|
2886
|
+
* No in-progress merge / empty conflict set still yields materials (possibly empty);
|
|
2887
|
+
* code does not gate attendance on merge state (#827).
|
|
2900
2888
|
*/
|
|
2901
2889
|
export async function deriveMergerEnvelopeFromActiveMerge(
|
|
2902
2890
|
projectRoot: string,
|
|
2903
2891
|
gitState: MergerGitState = createProductionMergerGitState(projectRoot),
|
|
2904
2892
|
): Promise<DerivedMergerEnvelope> {
|
|
2905
|
-
|
|
2906
|
-
try {
|
|
2907
|
-
state = await gitState.activeMerge();
|
|
2908
|
-
} catch (error) {
|
|
2909
|
-
const message =
|
|
2910
|
-
error instanceof Error && error.message.trim() !== ""
|
|
2911
|
-
? error.message
|
|
2912
|
-
: "Assigned repository does not have one ordinary in-progress merge";
|
|
2913
|
-
throw new MergerEnvelopeDerivationError(message, { cause: error });
|
|
2914
|
-
}
|
|
2915
|
-
if (state.unmergedPaths.length === 0) {
|
|
2916
|
-
throw new MergerEnvelopeDerivationError(
|
|
2917
|
-
"Assigned repository does not have one ordinary in-progress merge with a complete conflict set",
|
|
2918
|
-
);
|
|
2919
|
-
}
|
|
2893
|
+
const state = await gitState.activeMerge();
|
|
2920
2894
|
const expectedConflictPaths = Object.freeze([...state.unmergedPaths]);
|
|
2921
|
-
// Scope is derived as the complete conflict set; the role may not broaden it.
|
|
2922
2895
|
const resolutionScope = Object.freeze([...state.unmergedPaths]);
|
|
2923
2896
|
return Object.freeze({
|
|
2924
2897
|
targetObjectId: state.targetObjectId,
|
|
2925
2898
|
sourceObjectId: state.sourceObjectId,
|
|
2926
|
-
automaticMergeTreeId: state.automaticMergeTreeId,
|
|
2927
2899
|
expectedConflictPaths,
|
|
2928
2900
|
resolutionScope,
|
|
2929
2901
|
});
|
|
@@ -2945,8 +2917,9 @@ export type AdmitMergerInvocationOptions = {
|
|
|
2945
2917
|
|
|
2946
2918
|
/**
|
|
2947
2919
|
* Admit a Merger Role run on the common Invocation request.
|
|
2948
|
-
*
|
|
2949
|
-
*
|
|
2920
|
+
* Git materials (parents, conflicts, scope) are read from the worktree and
|
|
2921
|
+
* handed to the role as assignment materials — including when empty (#827).
|
|
2922
|
+
* Callers never supply public packet fields for those facts.
|
|
2950
2923
|
*/
|
|
2951
2924
|
export async function admitMergerInvocation(
|
|
2952
2925
|
options: AdmitMergerInvocationOptions,
|
|
@@ -2960,8 +2933,6 @@ export async function admitMergerInvocation(
|
|
|
2960
2933
|
}
|
|
2961
2934
|
|
|
2962
2935
|
const projectRoot = resolve(options.project ?? options.cwd);
|
|
2963
|
-
// Derive mechanical envelope before placing a run identity so no-merge/drift
|
|
2964
|
-
// fails honestly without orphan ledger rows or guessed packet fields.
|
|
2965
2936
|
const derived = await deriveMergerEnvelopeFromActiveMerge(
|
|
2966
2937
|
projectRoot,
|
|
2967
2938
|
options.gitState ?? createProductionMergerGitState(projectRoot),
|
|
@@ -2988,18 +2959,18 @@ export async function admitMergerInvocation(
|
|
|
2988
2959
|
const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
|
|
2989
2960
|
|
|
2990
2961
|
// Intent materials seed primary-source investigation; the method owns the work.
|
|
2962
|
+
const targetLabel = derived.targetObjectId === "" ? "(none observed)" : derived.targetObjectId;
|
|
2963
|
+
const sourceLabel = derived.sourceObjectId === "" ? "(none observed)" : derived.sourceObjectId;
|
|
2991
2964
|
const targetIntent = mergerMaterialFromUtf8(
|
|
2992
|
-
`Investigate primary sources for target parent ${
|
|
2965
|
+
`Investigate primary sources for target parent ${targetLabel}. Do not invent intent.`,
|
|
2993
2966
|
);
|
|
2994
2967
|
const sourceIntent = mergerMaterialFromUtf8(
|
|
2995
|
-
`Investigate primary sources for source parent ${
|
|
2968
|
+
`Investigate primary sources for source parent ${sourceLabel}. Do not invent intent.`,
|
|
2996
2969
|
);
|
|
2997
2970
|
const taskMaterial = mergerMaterialFromUtf8(instruction);
|
|
2998
2971
|
const authorityMaterial = mergerMaterialFromUtf8(instruction);
|
|
2999
2972
|
|
|
3000
|
-
// Validate merger envelope before placing admitted identity.
|
|
3001
2973
|
const mergerInput = validateMergerInput({
|
|
3002
|
-
version: 1,
|
|
3003
2974
|
attemptId: runId,
|
|
3004
2975
|
targetObjectId: derived.targetObjectId,
|
|
3005
2976
|
sourceObjectId: derived.sourceObjectId,
|
|
@@ -3011,7 +2982,6 @@ export async function admitMergerInvocation(
|
|
|
3011
2982
|
},
|
|
3012
2983
|
expectedConflictPaths: [...derived.expectedConflictPaths],
|
|
3013
2984
|
resolutionScope: [...derived.resolutionScope],
|
|
3014
|
-
// Authorized checks remain available on the assignment; default none.
|
|
3015
2985
|
authorizedChecks: [],
|
|
3016
2986
|
});
|
|
3017
2987
|
|
|
@@ -3035,7 +3005,6 @@ export async function admitMergerInvocation(
|
|
|
3035
3005
|
derived: {
|
|
3036
3006
|
targetObjectId: derived.targetObjectId,
|
|
3037
3007
|
sourceObjectId: derived.sourceObjectId,
|
|
3038
|
-
automaticMergeTreeId: derived.automaticMergeTreeId,
|
|
3039
3008
|
expectedConflictPaths: [...derived.expectedConflictPaths],
|
|
3040
3009
|
resolutionScope: [...derived.resolutionScope],
|
|
3041
3010
|
},
|