@akagilnc/pi-workflow-roles 0.1.3837 → 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.
Files changed (59) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +3 -3
  3. package/dist/acp-host/production-host.js +22736 -22720
  4. package/dist/engine-detour-tool.js +2 -2
  5. package/dist/engine-detour.js +23 -7
  6. package/dist/headless-host/production-host.js +4926 -4926
  7. package/dist/host-descriptions.js +5 -2
  8. package/dist/merger-contracts.js +23 -42
  9. package/dist/merger-git-state.js +69 -42
  10. package/dist/merger-role.js +30 -41
  11. package/dist/pi/role-turn-host.js +20 -92
  12. package/dist/public-cli/invocation.js +12 -39
  13. package/dist/public-cli/load-production-acp-host.js +1 -1
  14. package/dist/public-cli/load-production-external-host.js +10 -0
  15. package/dist/public-cli/load-production-headless-host.js +1 -1
  16. package/dist/public-cli/main.js +314 -433
  17. package/dist/public-cli/option-definitions.js +5 -5
  18. package/dist/public-cli/run-lifecycle.js +0 -2
  19. package/dist/public-role-summons.js +7 -49
  20. package/dist/reviewer-agent.js +1 -0
  21. package/dist/reviewer-child-executor.js +3 -2
  22. package/dist/role-activation-flags.js +50 -0
  23. package/dist/role-runtime.js +9 -16
  24. package/extensions/role-runtime.ts +0 -3
  25. package/package.json +1 -1
  26. package/scripts/build-package.mjs +2 -2
  27. package/souls/merger.md +1 -1
  28. package/src/acp-host/production-host.ts +31 -94
  29. package/src/acp-host/role-turn-host.ts +178 -263
  30. package/src/diagnostic-tail.ts +18 -0
  31. package/src/engine-detour-tool.ts +5 -4
  32. package/src/engine-detour.ts +44 -9
  33. package/src/external-host-turn-loop.ts +172 -0
  34. package/src/headless-host/production-host.ts +10 -7
  35. package/src/headless-host/role-turn-host.ts +271 -263
  36. package/src/host-descriptions.ts +5 -2
  37. package/src/host-session-record.ts +41 -0
  38. package/src/merger-contracts.ts +24 -38
  39. package/src/merger-git-state.ts +67 -37
  40. package/src/merger-role.ts +52 -48
  41. package/src/pi/role-turn-host.ts +18 -93
  42. package/src/prepared-role-turn.ts +68 -0
  43. package/src/public-cli/cli.ts +11 -20
  44. package/src/public-cli/invocation.ts +13 -44
  45. package/src/public-cli/load-production-acp-host.ts +2 -1
  46. package/src/public-cli/load-production-external-host.ts +17 -0
  47. package/src/public-cli/load-production-headless-host.ts +2 -1
  48. package/src/public-cli/merger-run.ts +2 -113
  49. package/src/public-cli/option-definitions.ts +5 -5
  50. package/src/public-cli/run-lifecycle.ts +0 -2
  51. package/src/public-role-summons.ts +12 -69
  52. package/src/reviewer-agent.ts +12 -1
  53. package/src/reviewer-child-executor.ts +10 -2
  54. package/src/role-activation-flags.ts +53 -0
  55. package/src/{acp-host/role-envelope.ts → role-envelope.ts} +28 -84
  56. package/src/role-runtime-dependencies.ts +89 -0
  57. package/src/role-runtime.ts +16 -17
  58. package/src/{acp-host/session-identity.ts → session-identity.ts} +6 -6
  59. /package/src/{acp-host/mcp-relay.mjs → mcp-relay.mjs} +0 -0
@@ -68,6 +68,7 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
68
68
  /**
69
69
  * Headless CLI family (#645). Claude is the first row; codex (#646) adds another.
70
70
  * fixedArgs: print mode, isolation without `--bare` (OAuth stays), full permissions.
71
+ * stream-json + verbose: live host events for sitian records (#811); result is last line.
71
72
  * `--setting-sources` empty = load no user/project/local CLAUDE.md/hooks/skills
72
73
  * (role envelope is delivered via `--system-prompt` wholesale replace).
73
74
  * `--strict-mcp-config` with no `--mcp-config` drops operator MCP + claude.ai connectors.
@@ -77,8 +78,10 @@ export const HEADLESS_HOST_DESCRIPTIONS: Readonly<Record<string, HeadlessHostDes
77
78
  binaryFromHome: Object.freeze([".local", "bin", "claude"]),
78
79
  sessionBindingFile: "claude-headless-session.json",
79
80
  fixedArgs: Object.freeze([
80
- // One result envelope (not stream-json): typed receipt only; no event-stream copy.
81
- "--output-format", "json",
81
+ // Live NDJSON events → sitian host-session records (#811); last line is the result receipt.
82
+ "--output-format", "stream-json",
83
+ // Intermediate assistant/tool/system events require verbose with stream-json.
84
+ "--verbose",
82
85
  "--permission-mode", "bypassPermissions",
83
86
  // Empty sources: no user/project/local operator surface (envelope owns materials).
84
87
  "--setting-sources", "",
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Non-pi host turn events → sitian sole entry (ADR 0065 / 0077 / #811).
3
+ * Adapters capture host-native structured events; this helper only packages
4
+ * the common sitianReport call. session.jsonl stays header-only (#617 DK-4).
5
+ * Write failures propagate (SitianInfrastructureError) — not best-effort.
6
+ */
7
+ import { sitianReport } from "./sitian-facade.ts";
8
+
9
+ /** Volume category under `<run>/session/<kind>/records.jsonl`. */
10
+ export const HOST_SESSION_RECORD_KIND = "host-session" as const;
11
+
12
+ function eventField(event: unknown, key: string): string | undefined {
13
+ if (typeof event !== "object" || event === null || Array.isArray(event)) return undefined;
14
+ const value = (event as Record<string, unknown>)[key];
15
+ return typeof value === "string" && value.length > 0 ? value : undefined;
16
+ }
17
+
18
+ /** Live write of one host-emitted structured event. Throws on sitian persistence failure. */
19
+ export function reportHostSessionEvent(input: {
20
+ readonly host: string;
21
+ readonly cwd: string;
22
+ readonly sessionParent: string;
23
+ readonly source: string;
24
+ readonly event: unknown;
25
+ readonly identity?: string;
26
+ readonly timestamp?: string;
27
+ }): void {
28
+ const identity = input.identity ?? eventField(input.event, "uuid");
29
+ const timestamp = input.timestamp ?? eventField(input.event, "timestamp");
30
+ sitianReport({
31
+ level: "event",
32
+ kind: HOST_SESSION_RECORD_KIND,
33
+ host: input.host,
34
+ cwd: input.cwd,
35
+ sessionParent: input.sessionParent,
36
+ source: input.source,
37
+ payload: input.event,
38
+ ...(identity === undefined ? {} : { identity }),
39
+ ...(timestamp === undefined ? {} : { timestamp }),
40
+ });
41
+ }
@@ -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
  }
@@ -15,7 +15,6 @@ import type {
15
15
  DurablePrincipal,
16
16
  DurablePrincipalAuthority,
17
17
  MethodBinding,
18
- RoleTurnActivation,
19
18
  RoleTurnHost,
20
19
  RoleTurnKnownFailure,
21
20
  RoleTurnModelConfig,
@@ -24,6 +23,7 @@ import type {
24
23
  } from "../host-contracts.ts";
25
24
  import { ExplicitInternalActivationError } from "../host-contracts.ts";
26
25
  import { applyEngineChildEnv } from "../engine-detour.ts";
26
+ import { projectActivationFlags } from "../role-activation-flags.ts";
27
27
 
28
28
 
29
29
  /** Package-relative Internal role entrypoint (ADR 0052; same path as public-cli registry). */
@@ -66,95 +66,18 @@ function buildSeatModelCliArgs(model: RoleTurnModelConfig | undefined): string[]
66
66
  ];
67
67
  }
68
68
 
69
- function buildActivationFlagArgs(activation: RoleTurnActivation): string[] {
70
- switch (activation.role) {
71
- case "judge":
72
- return ["--ak-role", "judge"];
73
- case "coder":
74
- return [
75
- "--ak-role",
76
- "coder",
77
- "--ak-coder-phase",
78
- activation.phase,
79
- "--ak-coder-task",
80
- activation.taskPath,
81
- ];
82
- case "fixer":
83
- return [
84
- "--ak-role",
85
- "fixer",
86
- "--ak-fixer-phase",
87
- activation.phase,
88
- "--ak-fix-packet",
89
- activation.packetPath,
90
- ...(activation.prerequisitesPath === undefined
91
- ? []
92
- : ["--ak-fixer-prerequisites", activation.prerequisitesPath]),
93
- ];
94
- case "reviewer":
95
- return [
96
- "--ak-role",
97
- "reviewer",
98
- "--ak-review-base",
99
- activation.baseRevision,
100
- ...(activation.authorityRefs.length === 0
101
- ? []
102
- : ["--ak-review-authority-refs", JSON.stringify([...activation.authorityRefs])]),
103
- ...(activation.ticketNumber === undefined
104
- ? []
105
- : ["--ak-review-ticket-number", String(activation.ticketNumber)]),
106
- ];
107
- case "merger":
108
- return ["--ak-role", "merger", "--ak-merger-input", activation.inputPath];
109
- case "collector":
110
- return [
111
- "--ak-role",
112
- "collector",
113
- "--ak-collector-repo",
114
- activation.repo,
115
- ...(activation.pr === undefined ? [] : ["--ak-collector-pr", activation.pr]),
116
- ...(activation.requestManifestPath === undefined
117
- ? []
118
- : ["--ak-collector-request-manifest", activation.requestManifestPath]),
119
- ...(activation.waitMs === undefined ? [] : ["--ak-collector-wait-ms", activation.waitMs]),
120
- ];
121
- case "doctor":
122
- return ["--ak-role", "doctor", "--ak-doctor-case", activation.casePath];
123
- case "notary":
124
- return [
125
- "--ak-role",
126
- "notary",
127
- "--ak-notary-source-run",
128
- activation.sourceRun,
129
- ...(activation.ticketNumber === undefined
130
- ? []
131
- : ["--ak-notary-ticket-number", String(activation.ticketNumber)]),
132
- ];
133
- case "countersign":
134
- // ticketNumber rides activation/admission/invocation only (#632: no private flag).
135
- return ["--ak-role", "countersign"];
136
- case "gleaner-left":
137
- return [
138
- "--ak-role",
139
- "gleaner-left",
140
- "--ak-gleaner-left-base",
141
- activation.baseRevision,
142
- ];
143
- case "inspector":
144
- return ["--ak-role", "inspector"];
145
- case "gatekeeper":
146
- return ["--ak-role", "gatekeeper"];
147
- case "navigator":
148
- return ["--ak-role", "navigator"];
149
- case "auditor":
150
- return ["--ak-role", "auditor"];
151
- case "diarist":
152
- return ["--ak-role", "diarist"];
153
- default: {
154
- const _exhaustive: never = activation;
155
- return _exhaustive;
156
- }
69
+ /**
70
+ * Pi last hop only: shared envelope flags → controlled-session argv pairs (#819).
71
+ * Flag membership/values stay in projectActivationFlags (middle layer).
72
+ */
73
+ function activationFlagsToPiArgv(flags: ReadonlyMap<string, boolean | string>): string[] {
74
+ const args: string[] = [];
75
+ for (const [name, value] of flags) {
76
+ if (value === false) continue;
77
+ args.push(`--${name}`);
78
+ if (value !== true) args.push(String(value));
157
79
  }
80
+ return args;
158
81
  }
159
82
 
160
83
  function buildMethodArgs(methods: readonly MethodBinding[]): string[] {
@@ -193,9 +116,10 @@ export function applyPiNativeSkillInvocation(
193
116
  }
194
117
 
195
118
  /**
196
- * Translate a closed RoleTurnRequest into Pi argv after `--no-extensions -e entry`.
197
- * Controlled session constants and session coordinates are adapter-internal.
198
- * Single forced method → Pi-native `/skill:` on the argv prompt (coder/reviewer/merger).
119
+ * Pi last-hop argv after `--no-extensions -e entry` (#819).
120
+ * Activation flag membership comes from middle-layer projectActivationFlags;
121
+ * this function only renders session coords, controlled constants, and pairs.
122
+ * Single forced method → Pi-native `/skill:` on the argv prompt (#822).
199
123
  */
200
124
  export function buildPiTurnExtraArgs(
201
125
  request: RoleTurnRequest,
@@ -222,7 +146,8 @@ export function buildPiTurnExtraArgs(
222
146
  "--session-dir",
223
147
  sessionDirectory,
224
148
  ...extraPiArgs,
225
- ...buildActivationFlagArgs(request.activation),
149
+ // Envelope assembly = projectActivationFlags; pi only renders argv pairs.
150
+ ...activationFlagsToPiArgv(projectActivationFlags(request)),
226
151
  "--mode",
227
152
  "json",
228
153
  ...buildSeatModelCliArgs(request.model),
@@ -0,0 +1,68 @@
1
+ import type { RoleTurnKnownFailure, RoleTurnRequest } from "./host-contracts.ts";
2
+ import { renderAgentStartMaterials } from "./agent-start-materials.ts";
3
+
4
+ /** The shared envelope, prepared before session/new (systemPrompt delivery) and
5
+ * able to observe the host's real builtin tool surface once it arrives post-session. */
6
+ export type PreparedRoleTurn = Readonly<{
7
+ mcpServers: readonly Readonly<Record<string, unknown>>[];
8
+ /**
9
+ * Structured system-prompt authority. `body` is the provider-facing prompt
10
+ * bytes; `materials` are typed agent-start reading materials (e.g. Notary
11
+ * session bound) that the adapter folds into the override at the provider
12
+ * boundary. This structure is the authoritative production input of the
13
+ * send path — never a test-only parallel face.
14
+ */
15
+ systemPrompt: { readonly body: string; readonly materials: readonly unknown[] };
16
+ /** Effective user prompt after host-side input transform (canonical Skill invocation). */
17
+ prompt: string;
18
+ /**
19
+ * Host abort signal armed only by typed infrastructure failure (envelope
20
+ * rememberInfrastructureFailure / non-correctable MCP catch). Lawful
21
+ * context.abort() (seal / non-sole) does not arm it. executeTurn races
22
+ * session/prompt against this so infra declarations terminate even when
23
+ * ACP never resolves (#593).
24
+ */
25
+ abortSignal?: AbortSignal;
26
+ /** Shared ledger consumes the complete ACP round after session/prompt resolves. */
27
+ closeRound(): Promise<
28
+ | { readonly accepted: true }
29
+ | {
30
+ readonly accepted: false;
31
+ /** Shared envelope provides officer/correctable text; adapters only deliver it (#813). */
32
+ readonly retry: {
33
+ readonly code: string;
34
+ readonly toolCallIds: readonly string[];
35
+ readonly message: string;
36
+ };
37
+ }
38
+ | { readonly accepted: false; readonly failure: RoleTurnKnownFailure }
39
+ >;
40
+ dispose?(): Promise<void>;
41
+ /**
42
+ * Headless CLI family (#645): role terminating-tool schema for host-native
43
+ * `--json-schema`. Present for every prepared turn; ACP ignores it.
44
+ */
45
+ jsonSchema: Readonly<Record<string, unknown>>;
46
+ /** Terminating tool name whose schema is `jsonSchema`. */
47
+ terminatingToolName: string;
48
+ /**
49
+ * Headless CLI family: feed host-native `structured_output` through the same
50
+ * terminating-tool path the MCP relay uses (ledger + gates). ACP ignores it.
51
+ */
52
+ ingestStructuredOutput(params: unknown): Promise<void>;
53
+ }>;
54
+
55
+ /** Fold structured system-prompt authority into the provider-visible ACP override. */
56
+ export function renderSystemPromptOverride(authority: {
57
+ readonly body: string;
58
+ readonly materials: readonly unknown[];
59
+ }): string {
60
+ return renderAgentStartMaterials(authority.body, authority.materials);
61
+ }
62
+
63
+ export type SessionIdentityAuthority = Readonly<{
64
+ load(principal: RoleTurnRequest["principal"]): Promise<string | undefined>;
65
+ bind(principal: RoleTurnRequest["principal"], sessionId: string): Promise<void>;
66
+ /** Durable principal session path for layout ownership / isAvailable — not a rebuild source (#617 DK-4). */
67
+ resolveSessionFile(principal: RoleTurnRequest["principal"]): string;
68
+ }>;