@akagilnc/pi-workflow-roles 0.1.3873 → 0.1.3886

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.
@@ -552,7 +552,7 @@ const NOTARY_OPTIONS = [
552
552
  },
553
553
  ];
554
554
  const MERGER_OPTIONS = [
555
- // Merger project face differs: requires an in-progress ordinary merge root.
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 with one ordinary in-progress merge (defaults to cwd).",
567
- zh: "已有进行中 ordinary merge 的项目根(默认 cwd)。",
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: "Resolve one ordinary merge already in conflict.",
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 active merge."',
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
  };
@@ -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
- gitState: {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3873",
3
+ "version": "0.1.3886",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -29,7 +29,8 @@ cursor-agent -p -f --output-format text --model <MODEL_ID> "YOUR_LABOR_PROMPT"
29
29
  `cursor-grok-4.6-high`, plus `-fast` variants); some models also accept a
30
30
  bracket override form (`'claude-opus-4-8[context=1m,effort=high]'` — see
31
31
  `cursor-agent --help`).
32
- - Owner pool directive 2026-08-28: default labor model = `cursor-grok-4.6-low`.
32
+ - This note does not pin a model id. The dispatch order (owner pool
33
+ directive) names the model to pass verbatim via `--model`.
33
34
  - Always `--output-format text`; never `stream-json` (the event stream goes back
34
35
  into the seat's context as noise — see `opus.md`).
35
36
 
@@ -39,9 +40,10 @@ wrap this engine behind `ak-role` flags.
39
40
  ## Smoke test (run before first labor leg of a session)
40
41
 
41
42
  ```bash
42
- cursor-agent -p -f --output-format text --model cursor-grok-4.6-low "Reply with exactly one word: OK"
43
+ cursor-agent -p -f --output-format text --model <MODEL_ID> "Reply with exactly one word: OK"
43
44
  ```
44
45
 
45
- Expected: stdout ends with exactly `OK`, exit code 0. Verified 2026-08-28 on
46
- this host (Cursor subscription login). If it asks about directory trust, the
46
+ Expected: stdout ends with exactly `OK`, exit code 0. Mechanics verified
47
+ 2026-08-28 on this host (Cursor subscription login) with a dispatch-order
48
+ model id substituted for `<MODEL_ID>`. If it asks about directory trust, the
47
49
  `-f` flag (or `--trust`) is missing.
@@ -13,11 +13,13 @@ The machine entrypoint is `opencode`. Run from the role project root.
13
13
  Non-interactive labor uses `run` with an explicit model:
14
14
 
15
15
  ```bash
16
- opencode run -m opencode-go/glm-5.2 "YOUR_LABOR_PROMPT"
16
+ opencode run -m <MODEL_ID> "YOUR_LABOR_PROMPT"
17
17
  ```
18
18
 
19
- - `-m provider/model` selects the model. **Always confirm the current id with
20
- `opencode models` first** — provider prefixes migrate (verified 2026-08-21:
19
+ - Model id is not pinned by this note; it comes from the dispatch order
20
+ (owner pool directive), passed verbatim as `provider/model`. **Always
21
+ confirm the current id with `opencode models` first** — provider prefixes
22
+ migrate (verified 2026-08-21:
21
23
  GLM lives under `opencode-go/`, e.g. `opencode-go/glm-5.2`; the older
22
24
  `zai/glm-5.2` id errors with "Unexpected server error").
23
25
  - Plain text output only (never `--format json` for labor — the returned body
package/souls/merger.md CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  你是校书郎。
4
4
 
5
- * 只解决调用方指定、已开始的一次 Git merge 的完整冲突集,保全双方已授权意图并创建该次普通双亲 merge commit;若需新的产品或权力决定则升级,不猜测。
5
+ * 保全双方已授权意图并完成一次普通双亲 merge commit;无进行中合并、无活可干、或需新的产品/权力决定则升级,不猜测。
@@ -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
- version: Type.Literal(1), attemptId: Type.String({ minLength: 1 }),
13
- targetObjectId: Type.String({ pattern: oidPattern }), sourceObjectId: Type.String({ pattern: oidPattern }),
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({ minLength: 1 }), { minItems: 1 }),
16
- resolutionScope: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
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({ pattern: oidPattern, description: "已核验的完成合并 commit object ID" }) }, { additionalProperties: false }),
21
- 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 }),
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) || !isFullGitObjectId(value.targetObjectId) || !isFullGitObjectId(value.sourceObjectId) || value.targetObjectId.length !== value.sourceObjectId.length) fail("Merger input has invalid identity or object ID");
57
- if (!record(value.materials)) fail();
58
- for (const key of ["task", "authority", "targetIntent", "sourceIntent"] as const) validateMaterial(value.materials[key], key);
59
- const conflicts = validatePathSet(value.expectedConflictPaths, "expected conflict paths");
60
- const scope = validatePathSet(value.resolutionScope, "resolution scope");
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, expectedAttemptId?: string): MergerOutput {
70
- if (!record(value) || (expectedAttemptId !== undefined && value.attemptId !== expectedAttemptId)) throw new Error("合并回执 attempt 不匹配");
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" && isFullGitObjectId(value.mergeCommitId)) return structuredClone(value) as MergerOutput;
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
  }
@@ -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
- export type ActiveMergerGitState = { targetObjectId: string; sourceObjectId: string; unmergedPaths: string[]; automaticMergeTreeId: string };
8
- export type CompletedMergerGitState = { mergeCommitId: string; parentObjectIds: string[]; unmergedPaths: string[]; worktreeClean: boolean; resolutionChangedPaths: string[] };
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
- function nulPaths(bytes: Uint8Array, label: string): string[] {
24
- const raw = exactUtf8(bytes, label);
25
- if (raw.length > 0 && !raw.endsWith("\0")) throw new Error(`${label} is not NUL terminated`);
26
- return raw.split("\0").filter(Boolean).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
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
- const targetObjectId = line(await git(repositoryRoot, ["rev-parse", "--verify", "HEAD"]), "Git HEAD");
44
- let mergeHeadRaw: Uint8Array;
45
- try {
46
- mergeHeadRaw = await git(repositoryRoot, ["rev-parse", "--verify", "MERGE_HEAD"]);
47
- } catch {
48
- throw new Error("Assigned repository does not have one ordinary in-progress merge");
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
- const mergeHeads = exactUtf8(mergeHeadRaw, "Git MERGE_HEAD").trim().split(/\r?\n/).filter(Boolean);
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
  }
@@ -1,58 +1,62 @@
1
- import type { RoleHost, HostContext, HostToolResult } from "./host-contracts.ts";
2
- import { MERGER_ACCEPTED_TEXT, MERGER_OUTPUT_TOOL_NAME, mergerOutputSchema, validateMergerInput, validateMergerOutput, type MergerInput } from "./merger-contracts.ts";
3
- import type { MergerGitState } from "./merger-git-state.ts";
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 an immutable digest-bound Merger v1 input JSON file", type: "string" as const } } as const;
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
- function same(a: readonly string[], b: readonly string[]): boolean { return a.length === b.length && a.every((value, i) => value === b[i]); }
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
- export function createMergerRoleRuntime(pi: RoleHost, dependencies: MergerRoleDependencies, host: MergerRoleHostActions) {
16
- let activation: { soul: string; input: Readonly<MergerInput>; automaticMergeTreeId: string } | undefined;
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 { async activate() {
21
- const path = pi.getFlag(MERGER_INPUT_FLAG.name);
22
- if (typeof path !== "string" || path.trim().length === 0) throw new Error("Merger requires --ak-merger-input");
23
- const soul = (await dependencies.loadSoul()).trim(); if (!soul) throw new Error("Merger soul is empty");
24
- const input = validateMergerInput(await dependencies.loadInput(path));
25
- const state = await dependencies.gitState.activeMerge();
26
- if (state.targetObjectId !== input.targetObjectId || state.sourceObjectId !== input.sourceObjectId || !same(state.unmergedPaths, input.expectedConflictPaths) || state.unmergedPaths.length === 0 || !isFullGitObjectId(state.automaticMergeTreeId) || state.automaticMergeTreeId.length !== input.targetObjectId.length) throw new Error("Merger activation rejected repository parent, merge, automatic-result, or complete conflict-set drift");
27
- activation = { soul, input, automaticMergeTreeId: state.automaticMergeTreeId }; accepted = false;
28
- if (!registered) { registered = true;
29
- pi.registerTool({ name: MERGER_OUTPUT_TOOL_NAME, label: "合并输出", description: "提交合并结果;输出分支为 completed 与 escalate;基础设施及 Git 失败由 abort 通道承接。", promptSnippet: "提交合并结果", parameters: mergerOutputSchema,
30
- async execute(id: string, params: unknown, _signal: AbortSignal | undefined, _update: unknown, ctx: HostContext) {
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
- if (accepted) throw new Error("合并回执已受理");
33
- let output;
34
- try {
35
- output = validateMergerOutput(params, activation.input.attemptId);
36
- } catch (error) {
37
- host.failInfrastructure(error, ctx, id);
38
- }
39
- if (output.status === "completed") {
40
- let state;
41
- try { state = await dependencies.gitState.completedMerge(output.mergeCommitId, activation.automaticMergeTreeId); }
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
  }